diff --git a/.eslintignore b/.eslintignore index fcbea07f9..a82960313 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,8 @@ -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 -*.test.js + +# Needs to be ignored because it uses ESM export/import +apps/gipy/pkg/gps.js +apps/gipy/pkg/gps.d.ts +apps/gipy/pkg/gps_bg.wasm.d.ts + +# Needs to be ignored because it includes broken JS +apps/health/chart.min.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..b7590a77e --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,250 @@ +const lintExemptions = require("./apps/lint_exemptions.js"); +const fs = require("fs"); +const path = require("path"); + +function findGeneratedJS(roots) { + function* listFiles(dir, allow) { + for (const f of fs.readdirSync(dir)) { + const filepath = path.join(dir, f); + const stat = fs.statSync(filepath); + + if (stat.isDirectory()) { + yield* listFiles(filepath, allow); + } else if(allow(filepath)) { + yield filepath; + } + } + } + + return roots.flatMap(root => + [...listFiles(root, f => f.endsWith(".ts"))] + .map(f => f.replace(/\.ts$/, ".js")) + ); +} + +module.exports = { + "env": { + // TODO: "espruino": false + // TODO: "banglejs": false + // For a prototype of the above, see https://github.com/espruino/BangleApps/pull/3237 + }, + "extends": "eslint:recommended", + "globals": { + // Methods and Fields at https://banglejs.com/reference + "Array": "readonly", + "ArrayBuffer": "readonly", + "ArrayBufferView": "readonly", + "Bangle": "readonly", + "BluetoothDevice": "readonly", + "BluetoothRemoteGATTCharacteristic": "readonly", + "BluetoothRemoteGATTServer": "readonly", + "BluetoothRemoteGATTService": "readonly", + "Boolean": "readonly", + "console": "readonly", + "DataView": "readonly", + "Date": "readonly", + "E": "readonly", + "Error": "readonly", + "Flash": "readonly", + "Float32Array": "readonly", + "Float64Array": "readonly", + "Function": "readonly", + "Graphics": "readonly", + "I2C": "readonly", + "Int16Array": "readonly", + "Int32Array": "readonly", + "Int8Array": "readonly", + "InternalError": "readonly", + "JSON": "readonly", + "Math": "readonly", + "Modules": "readonly", + "NRF": "readonly", + "Number": "readonly", + "Object": "readonly", + "OneWire": "readonly", + "Pin": "readonly", + "process": "readonly", + "Promise": "readonly", + "ReferenceError": "readonly", + "RegExp": "readonly", + "Serial": "readonly", + "SPI": "readonly", + "StorageFile": "readonly", + "String": "readonly", + "SyntaxError": "readonly", + "TFMicroInterpreter": "readonly", + "TypeError": "readonly", + "Uint16Array": "readonly", + "Uint24Array": "readonly", + "Uint32Array": "readonly", + "Uint8Array": "readonly", + "Uint8ClampedArray": "readonly", + "Unistroke": "readonly", + "Waveform": "readonly", + // Methods and Fields at https://banglejs.com/reference + "__FILE__": "readonly", + "analogRead": "readonly", + "analogWrite": "readonly", + "arguments": "readonly", + "atob": "readonly", + "Bluetooth": "readonly", + "BTN": "readonly", + "BTN1": "readonly", + "BTN2": "readonly", + "BTN3": "readonly", + "BTN4": "readonly", + "BTN5": "readonly", + "btoa": "readonly", + "changeInterval": "readonly", + "clearInterval": "readonly", + "clearTimeout": "readonly", + "clearWatch": "readonly", + "decodeURIComponent": "readonly", + "digitalPulse": "readonly", + "digitalRead": "readonly", + "digitalWrite": "readonly", + "dump": "readonly", + "echo": "readonly", + "edit": "readonly", + "encodeURIComponent": "readonly", + "eval": "readonly", + "getPinMode": "readonly", + "getSerial": "readonly", + "getTime": "readonly", + "global": "readonly", + "globalThis": "readonly", + "HIGH": "readonly", + "I2C1": "readonly", + "Infinity": "readonly", + "isFinite": "readonly", + "isNaN": "readonly", + "LED": "readonly", + "LED1": "readonly", + "LED2": "readonly", + "load": "readonly", + "LoopbackA": "readonly", + "LoopbackB": "readonly", + "LOW": "readonly", + "NaN": "readonly", + "parseFloat": "readonly", + "parseInt": "readonly", + "peek16": "readonly", + "peek32": "readonly", + "peek8": "readonly", + "pinMode": "readonly", + "poke16": "readonly", + "poke32": "readonly", + "poke8": "readonly", + "print": "readonly", + "require": "readonly", + "reset": "readonly", + "save": "readonly", + "Serial1": "readonly", + "setBusyIndicator": "readonly", + "setInterval": "readonly", + "setSleepIndicator": "readonly", + "setTime": "readonly", + "setTimeout": "readonly", + "setWatch": "readonly", + "shiftOut": "readonly", + "SPI1": "readonly", + "Terminal": "readonly", + "trace": "readonly", + "VIBRATE": "readonly", + // Aliases and not defined at https://banglejs.com/reference + "g": "readonly", + "WIDGETS": "readonly", + "module": "readonly", + "exports": "writable", + "D0": "readonly", + "D1": "readonly", + "D2": "readonly", + "D3": "readonly", + "D4": "readonly", + "D5": "readonly", + "D6": "readonly", + "D7": "readonly", + "D8": "readonly", + "D9": "readonly", + "D10": "readonly", + "D11": "readonly", + "D12": "readonly", + "D13": "readonly", + "D14": "readonly", + "D15": "readonly", + "D16": "readonly", + "D17": "readonly", + "D18": "readonly", + "D19": "readonly", + "D20": "readonly", + "D21": "readonly", + "D22": "readonly", + "D23": "readonly", + "D24": "readonly", + "D25": "readonly", + "D26": "readonly", + "D27": "readonly", + "D28": "readonly", + "D29": "readonly", + "D30": "readonly", + "D31": "readonly", + + "bleServiceOptions": "writable", // available in boot.js code that's called ad part of bootupdate + }, + "parserOptions": { + "ecmaVersion": 11 + }, + "rules": { + "indent": [ + "off", + 2, + { + "SwitchCase": 1 + } + ], + "no-constant-condition": "off", + "no-delete-var": "off", + "no-empty": ["warn", { "allowEmptyCatch": true }], + "no-global-assign": "off", + "no-inner-declarations": "off", + "no-prototype-builtins": "off", + "no-redeclare": "off", + "no-unreachable": "warn", + "no-cond-assign": "warn", + "no-useless-catch": "warn", + "no-undef": "warn", + "no-unused-vars": ["warn", { "args": "none" } ], + "no-useless-escape": "off", + "no-control-regex" : "off" + }, + overrides: [ + { + files: ["*.ts"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + rules: { + "no-delete-var": "off", + "no-empty": ["error", { "allowEmptyCatch": true }], + "no-prototype-builtins": "off", + "prefer-const": "off", + "prefer-rest-params": "off", + "no-control-regex" : "off", + "@typescript-eslint/no-delete-var": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-var-requires": "off", + } + }, + ...Object.entries(lintExemptions).map(([filePath, {rules}]) => ({ + files: [filePath], + rules: Object.fromEntries(rules.map(rule => [rule, "off"])), + })), + ], + ignorePatterns: findGeneratedJS(["apps/", "modules/"]), + reportUnusedDisableDirectives: true, +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..345bce54f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: espruino +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['http://www.espruino.com/Donate']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml b/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml index 484b3ba85..045a6e18a 100644 --- a/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml +++ b/.github/ISSUE_TEMPLATE/bangle-bug-report-custom-form.yaml @@ -58,3 +58,7 @@ body: validations: required: true + - type: textarea + id: apps + attributes: + label: Installed apps \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a20c7ed7c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 + +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" + reviewers: + - "gfwilliams" diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1eb009153..bebe18748 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,4 +1,4 @@ -name: Node CI +name: build on: [push, pull_request] @@ -6,29 +6,22 @@ jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16.x] - steps: - name: Checkout repository and submodules - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: recursive - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + - name: Use Node.js 18.x + uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node-version }} - - name: install testing dependencies - run: npm i - - name: test all apps and widgets - run: npm run test - - name: install typescript dependencies + node-version: 18.x + - name: Install testing dependencies + run: npm ci + - name: Test all apps and widgets + run: npm test + - name: Install typescript dependencies working-directory: ./typescript run: npm ci - - name: build types + - name: Build all TS apps and widgets working-directory: ./typescript - run: npm run build:types - - name: build all TS apps and widgets - working-directory: ./typescript - run: npm run build \ No newline at end of file + run: npm run build diff --git a/.gitignore b/.gitignore index 231851dd6..7687a770a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .htaccess node_modules -package-lock.json .DS_Store *.js.bak appdates.csv @@ -12,3 +11,7 @@ tests/Layout/testresult.bmp apps.local.json _site .jekyll-cache +.owncloudsync.log +Desktop.ini +.sync_*.db* +*.swp diff --git a/.gitmodules b/.gitmodules index fd6663d2a..c2c1104c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "EspruinoAppLoaderCore"] path = core url = https://github.com/espruino/EspruinoAppLoaderCore.git +[submodule "webtools"] + path = webtools + url = https://github.com/espruino/EspruinoWebTools.git 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 b3da9f685..b9b770b37 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ Bangle.js App Loader (and Apps) ================================ -[![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps) +[![Build Status](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml/badge.svg)](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml) * 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. @@ -98,7 +100,7 @@ This is the best way to test... **Note:** It's a great idea to get a local copy of the repository on your PC, then run `bin/sanitycheck.js` - it'll run through a bunch of common issues -that there might be. +that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule update --init`. Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect. @@ -191,7 +193,7 @@ widget bar at the top of the screen they can add themselves to the global ``` WIDGETS["mywidget"]={ - area:"tl", // tl (top left), tr (top right) + area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) sortorder:0, // (Optional) determines order of widgets in the same corner width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout draw:draw // called to draw the widget @@ -251,12 +253,15 @@ 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 + // 'module' - this provides a module that can be used with 'require'. + // 'provides_modules' should be used if type:module is specified // 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js' // 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js' + // 'clkinfo' - Provides a 'myapp.clkinfo.js' file that can be used to display info in clocks - see modules/clock_info.js // 'RAM' - code that runs and doesn't upload anything to storage // 'launch' - replacement 'Launcher' // 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle @@ -265,11 +270,28 @@ and which gives information about the app for the Launcher. // 'notify' - provides 'notify' library for showing notifications // 'locale' - provides 'locale' library for language-specific date/distance/etc // (a version of 'locale' is included in the firmware) - "tags": "", // comma separated tag list for searching + // 'defaultconfig' - a set of apps that will can be installed and will wipe out all previously installed apps + "tags": "", // comma separated tag list for searching (don't include uppercase or spaces) + // common types are: + // 'clock' - it's a clock + // 'widget' - it is (or provides) a widget + // 'outdoors' - useful for outdoor activities + // 'tool' - a useful utility (timer, calculator, etc) + // 'game' - a game + // 'bluetooth' - uses Bluetooth LE + // 'system' - used by the system + // 'clkinfo' - provides or uses clock_info module for data on your clock face or clocks that support it (see apps/clock_info/README.md) + // 'health' - e.g. heart rate monitors or step counting "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above) "dependencies" : { "messages":"app" } // optional, depend on a specific app ID // for instance this will use notify/notifyfs is they exist, or will pull in 'notify' + "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules + "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets + "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' + "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message' + "provides_features" : ["welcome"] // optional, this app provides some feature, used to ensure two aren't installed at once. Currently just 'welcome' + "default" : true, // set if an app is the default implementer of something (a widget/module/etc) "readme": "README.md", // if supplied, a link to a markdown-style text file // that contains more information about this app (usage, etc) // A 'Read more...' link will be added under the app @@ -282,7 +304,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 @@ -310,9 +332,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" @@ -324,7 +346,7 @@ and which gives information about the app for the Launcher. ``` * name, icon and description present the app in the app loader. -* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. +* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher`, `bluetooth` or empty. * storage is used to identify the app files and how to handle them * data is used to clean up files when the app is uninstalled @@ -385,7 +407,7 @@ in an iframe. - +
Loading...
+ + - @@ -183,6 +219,9 @@ + + + diff --git a/apps/1button/ChangeLog b/apps/1button/ChangeLog index 4c21f3ace..f49841cf1 100644 --- a/apps/1button/ChangeLog +++ b/apps/1button/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Minor code improvements diff --git a/apps/1button/metadata.json b/apps/1button/metadata.json index 6cfcb9310..917e5d8f7 100644 --- a/apps/1button/metadata.json +++ b/apps/1button/metadata.json @@ -1,7 +1,7 @@ { "id": "1button", "name": "One-Button-Tracker", - "version": "0.01", + "version": "0.02", "description": "A widget that turns BTN1 into a tracker, records time of button press/release.", "icon": "widget.png", "type": "widget", diff --git a/apps/1button/widget.js b/apps/1button/widget.js index cce099309..414d3e29f 100644 --- a/apps/1button/widget.js +++ b/apps/1button/widget.js @@ -22,7 +22,7 @@ console.log("Button let go"); digitalWrite(LED2,0); var unpress_time = new Date(); - recFile = require("Storage").open("one_button_presses.csv","a"); + const recFile = require("Storage").open("one_button_presses.csv","a"); recFile.write([press_time.getTime(),unpress_time.getTime()].join(",")+"\n"); }, BTN1, { repeat: true, edge: 'falling', debounce: 50 }); diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js index 9163aaf3a..7c1ed4dba 100644 --- a/apps/2047pp/2047pp.app.js +++ b/apps/2047pp/2047pp.app.js @@ -1,140 +1,142 @@ -class TwoK { - constructor() { - this.b = Array(4).fill().map(() => Array(4).fill(0)); - this.score = 0; - this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"}; - } - drawBRect(x1, y1, x2, y2, th, c, cf, fill) { - g.setColor(c); - for (i=0; i 4) g.setColor(1, 1, 1); - else g.setColor(0, 0, 0); - g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7)); - if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh); - } - } - shift(d) { // +/-1: shift x, +/- 2: shift y - var crc = E.CRC32(this.b.toString()); - if (d==-1) { // shift x left - for (y=0; y<4; ++y) { - for (x=2; x>=0; x--) - if (this.b[y][x]==0) { - for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1]; - this.b[y][3] = 0; - } - for (x=0; x<3; ++x) - if (this.b[y][x]==this.b[y][x+1]) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y][x+1]; - for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1]; +{ // wrap app in scope to prevent minifier from removing the class definition completely + class TwoK { + constructor() { + this.b = Array(4).fill().map(() => Array(4).fill(0)); + this.score = 0; + this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"}; + } + drawBRect(x1, y1, x2, y2, th, c, cf, fill) { + g.setColor(c); + for (i=0; i 4) g.setColor(1, 1, 1); + else g.setColor(0, 0, 0); + g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7)); + if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh); + } + } + shift(d) { // +/-1: shift x, +/- 2: shift y + var crc = E.CRC32(this.b.toString()); + if (d==-1) { // shift x left + for (y=0; y<4; ++y) { + for (x=2; x>=0; x--) + if (this.b[y][x]==0) { + for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1]; this.b[y][3] = 0; - } + } + for (x=0; x<3; ++x) + if (this.b[y][x]==this.b[y][x+1]) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y][x+1]; + for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1]; + this.b[y][3] = 0; + } + } } - } - else if (d==1) { // shift x right - for (y=0; y<4; ++y) { - for (x=1; x<4; x++) - if (this.b[y][x]==0) { - for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1]; - this.b[y][0] = 0; - } - for (x=3; x>0; --x) - if (this.b[y][x]==this.b[y][x-1]) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y][x-1] ; - for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1]; - this.b[y][0] = 0; - } + else if (d==1) { // shift x right + for (y=0; y<4; ++y) { + for (x=1; x<4; x++) + if (this.b[y][x]==0) { + for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1]; + this.b[y][0] = 0; + } + for (x=3; x>0; --x) + if (this.b[y][x]==this.b[y][x-1]) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y][x-1] ; + for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1]; + this.b[y][0] = 0; + } + } } - } - else if (d==-2) { // shift y down - for (x=0; x<4; ++x) { - for (y=1; y<4; y++) - if (this.b[y][x]==0) { - for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x]; - this.b[0][x] = 0; - } - for (y=3; y>0; y--) - if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y-1][x]; - for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x]; - this.b[0][x] = 0; - } + else if (d==-2) { // shift y down + for (x=0; x<4; ++x) { + for (y=1; y<4; y++) + if (this.b[y][x]==0) { + for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x]; + this.b[0][x] = 0; + } + for (y=3; y>0; y--) + if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y-1][x]; + for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x]; + this.b[0][x] = 0; + } + } } - } - else if (d==2) { // shift y up - for (x=0; x<4; ++x) { - for (y=2; y>=0; y--) - if (this.b[y][x]==0) { - for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x]; - this.b[3][x] = 0; - } - for (y=0; y<3; ++y) - if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y+1][x]; - for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x]; - this.b[3][x] = 0; - } + else if (d==2) { // shift y up + for (x=0; x<4; ++x) { + for (y=2; y>=0; y--) + if (this.b[y][x]==0) { + for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x]; + this.b[3][x] = 0; + } + for (y=0; y<3; ++y) + if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y+1][x]; + for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x]; + this.b[3][x] = 0; + } + } } + return (E.CRC32(this.b.toString())!=crc); + } + addDigit() { + var d = Math.random()>0.9 ? 4 : 2; + var id = Math.floor(Math.random()*16); + while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16); + this.b[Math.floor(id/4)][id%4] = d; } - return (E.CRC32(this.b.toString())!=crc); } - addDigit() { - var d = Math.random()>0.9 ? 4 : 2; - var id = Math.floor(Math.random()*16); - while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16); - this.b[Math.floor(id/4)][id%4] = d; - } -} -function dragHandler(e) { - if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) { - var res = false; - if (Math.abs(e.dx)>Math.abs(e.dy)) { - if (e.dx>0) res = twok.shift(1); - if (e.dx<0) res = twok.shift(-1); + function dragHandler(e) { + if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) { + var res = false; + if (Math.abs(e.dx)>Math.abs(e.dy)) { + if (e.dx>0) res = twok.shift(1); + if (e.dx<0) res = twok.shift(-1); + } + else { + if (e.dy>0) res = twok.shift(-2); + if (e.dy<0) res = twok.shift(2); + } + if (res) twok.addDigit(); + twok.render(); } - else { - if (e.dy>0) res = twok.shift(-2); - if (e.dy<0) res = twok.shift(2); - } - if (res) twok.addDigit(); - twok.render(); } -} -function swipeHandler() { - -} + /*function swipeHandler() { + + }*/ -function buttonHandler() { - -} + /*function buttonHandler() { + + }*/ -var twok = new TwoK(); -twok.addDigit(); twok.addDigit(); -twok.render(); -if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); -if (process.env.HWVERSION==1) { - Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); }); - setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true}); - setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true}); -} + var twok = new TwoK(); + twok.addDigit(); twok.addDigit(); + twok.render(); + if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); + if (process.env.HWVERSION==1) { + Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); }); + setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true}); + setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true}); + } +} \ No newline at end of file diff --git a/apps/2047pp/ChangeLog b/apps/2047pp/ChangeLog index a1f88e5ec..2c55af1e5 100644 --- a/apps/2047pp/ChangeLog +++ b/apps/2047pp/ChangeLog @@ -1,2 +1,4 @@ 0.01: New app! 0.02: Better support for watch themes +0.03: Workaround minifier bug +0.04: Minor code improvements diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json index 033354ac6..ebc043e6c 100644 --- a/apps/2047pp/metadata.json +++ b/apps/2047pp/metadata.json @@ -2,7 +2,7 @@ "name": "2047pp", "shortName":"2047pp", "icon": "app.png", - "version":"0.02", + "version": "0.04", "description": "Bangle version of a tile shifting game", "supports" : ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, diff --git a/apps/2ofthemclk/ChangeLog b/apps/2ofthemclk/ChangeLog new file mode 100644 index 000000000..7727f3cc4 --- /dev/null +++ b/apps/2ofthemclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor code improvements diff --git a/apps/2ofthemclk/README.md b/apps/2ofthemclk/README.md new file mode 100644 index 000000000..7ac2cf779 --- /dev/null +++ b/apps/2ofthemclk/README.md @@ -0,0 +1,11 @@ +# two of them clock + +You can now wear teh memez on your wrist. + +![](screenshot.png) + +Also serves as an example of displaying seconds only when unlocked or charging and only refreshing on the minute otherwise. +Widgets not supported + +## Creator +- [Kilrah](https://github.com/kilrah) diff --git a/apps/2ofthemclk/app-icon.js b/apps/2ofthemclk/app-icon.js new file mode 100644 index 000000000..9bfb8a550 --- /dev/null +++ b/apps/2ofthemclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgZC/AH4ADkAPOgVJkgEBAQQAJiQRByEJgmQCJWSpMEAQMkyQJCpASHhAOBpAmBJJgjBCIUJCRg4CCIJxFMQ2SoARCkmACI0EBAJHCCIMLj4RFiUBskAgIXBEAU5A4P34CtCiEJsEJ/AHBCgOBAoQAEi0H////HciQsBwywICIXWzkG4A+BEY0gif46dt6/cgnIgkWnHfLIP/MoUWwHbpvC/kAjEEj0HNYQCCkEfGgP/64RB2EAifHLwMAjg1CCIMD/0H/0B8EAh+HgeAkARCE4IjC/4jBYIMPLIcIAYUPB4OBCIQABhu/AoShCHYIRBx6QBDgUw2//8OHPwcJ39//ILBCIU9LgMBSQgsBJAYRBkE/CIIABgRHD3wRFkk/2zBDAYU//3b/oRB8ARBj6ABgEE7YREEYf+oMkSwINCyClCn//z//+4RBgMkgU3EgUcwFJgEeboOXCIP2EYJCDAAVJkkGWoIuBgf2EYQPDkECCIOGd4ffyEJkgFBAAcSoEkwQCBhw+BwQaByVAkGAKwIFBBANLkEQgAyBCIVIkBpBgmSBYOQoApBgcgiQRCAQIyCCgsSjIFBCIcgRgJNCCgQyBpAgDAQT2BCgIOBBAQUCCIpfBCIwCKP4QRNpCSDCLyJBCIbjBTwYRLboJ0BCI4QD")) diff --git a/apps/2ofthemclk/app.js b/apps/2ofthemclk/app.js new file mode 100644 index 000000000..693789fb7 --- /dev/null +++ b/apps/2ofthemclk/app.js @@ -0,0 +1,90 @@ +const img = require("heatshrink").decompress(atob("2GwgZC/ADEIAQMBgEQAgMChEEAoQA4wACEAHKDBAQYA9X/YACgQFGkBH1HAQFCwEIgkAAQVAH2GAAwtBQwcQgMAwQ/vGQI4CAQRHCwFAkACCQYTRFAEqzBAopHCgMEAQiSHAEsQAYT1ChBEEPoMQBAMQAoQRCX9BEDOgR6BHwkgwVBQw4aEIlKGCPoWCI4REBBAMEQYUCH856CNwJ9CO4YCBHwOSgCGBRIREBJQICBAAOAH0UQGQJ0CHYTCDAQOQpKABZAICCZAREBQccgIgJHBoJrBXIQFBIISDFwALBKwTEkd4RrBPobIBHAMkIgICBQYoODSoIfBRIYmEACjjBMoIgBwAsEF4YFDkmSAoUBBATgDDQIjEADSqBEwI4BgJxCPoUEiVIkmCpI7BoLCBpBTEC4MChAjEfy6hCL4J3EAQMJHwLCDBwJEBQYMkiQODI4JcBQAICDEwKMCRK4dBYoJrCHYMgyB3BHYWShKGCBweAJoLXDQAJiBZAQAVC4ZZBU4KADPQR3BQYLCByA+BBAWCJoabCUIR6BcwQIFaIQAPC4KABwC8CoC/DPQSDDoICBHwREBJQabBHwRHBNYSqDZYQAMCIQCBQYT7DAQKADQwVJAQY7BAoJEBHAJZCQwYmBEANANATXBI4IvBPpJQFiEAPoYCBWwg7CBYIFCAQIFGcAYCBX4ImBIgQaBBwLDPwCDCoMAEYS/BfwJECoJEDAQuCSQLOBBwJ9DQYY7BHwaDDABYRBQwIgBDQMSVoMSHwK5BIgKDDIIyACKwMEyDLChKGEZwckIgWCIhwXBYQY7BEwJHBHZICHSoSeCMogCCQYYOBYho+BTwQmBQYQvCQAUSIJyACZAKDCMobsDEANAH5hTDTYYmDF4ICBQaICCLgSeBEAKACJohKCRgJBIwCeBC4IdBEALsBQAYCCHx4XBLIRlBkkQRgRBBdgMEJoSDMgJEBKwRcCU4J9SAQYaCGoI4BAoUQVobFBJQL4BGoICBAA4UDL4QsCiRBVAQjdBiS8CAQMShLRDfAUQH44+BC4sgyAjBHzC/CGoKnCySqCagJuCiEIQYNAQZIRCyAXBQALFXQxBHEaIQIBQYZNBH4sSQYVAKwIgBIgQ+ZMQYFBHwMBVQiGCGoILBIAsEgBNBTwWCZASAdMQkSVQaDBKAOCXIVBZALLBIIY+BwBWBQYbCfQYbCBEwRHBRIRxBQwMCQgiYDCIOShBcCQbxEBEYJHChIIBAQMQQYK5CwFAH4JHBiCYCQYg+eUgaDBAQYLCeoR6BAQUBQYcCQYJNCCgRBgQAZrBBAZ0BYoUQAQMAQYaJBKwLaDDQgCdQA6MCBYI4BiEJIgKDCYoVAHwIOBCgRBggTvChLCDRIMEGQTFBAQKDCgAOBhI+kyVIj4mBgkCRgzFGgRHBQYRKBKAJcCIkP/+UIJpB3BJoMQAoOQoBBBgCYDQclJgF+YQZEIkkQKAKDBgGAQAKSBQYNBIMfgh0BHY2SXgI1BQANIgjLCQYKPBLJZBcgPBggIDoI+BHYL4CgUBBAI/BwDIBKYUSIMsAnApFfYQABHwOQoEEiACCZYKPBQctIGwSDEfYIAEX4L7BiQLCQYTFmpEDwAuBgSAByVAIIoABhMAYoKDBH0xBHGoJ6BABMkYoJTBAoTFqgf+H5QABkGQYQOAQdNPGQUcvxBMgGAoMEiVBkmQExYOBILkfwP4jhAKgmShEEYRxNBCIJEBiVJkBKSo4zDv//8CDLFgTdBkmCExWCC4gFBIIM5IK8fwDHMEx0EDBEB/5BQoED+EAgbFBQZIjBAYK2CVozRBAoMSLRUPDoJBPvEAvw1FEZJNBgI4DAQa2CAoKeKgf+g/kIJ84//+IIX4AYMHgfx4Ech0HjgqFiUJDol/8AFBkANBUJDsC8ECIJuT/H/MQ0H8f+n//x/AFQwmFyE4j4FBL4PHKwwAB/gmBJoNBTAYCIyP4CgJBG//wn/j+CvIgLFFgFwIIMAnAUHHwU4AQMIQZmf/5BIIgjyJII0OAQMPTA8B/4CBBYjILv//cZAIBMQRBNyAHCn41B/AXEMQMP/AcHhJBIk//KwIAJQwRBRKwN/4BBC8Fx4Hj/5BBJoJBNpEDCgQAWII8AMgP4j+PAgIADYQMPOIsBQZEfwE4IMEfHooAD/EDwE/dIqDI/g4QEYJBLg//+ARBIJKDBAA8EII1HcAZBN/kBEwxBFMQSDKDQSzEuACBIIySDIJsHgEOQZkcIJeOn4XCGQhHBiRBGOgZALuAaBIJeSg4vCSoJBHwP48F/45xDvxBKDoKCMTwQAHEAeRaIccIIuP/5rBg///x0DgeB44EBIIpKBv//8EPfAwAGQZeTDQN/DoU///x44+Bn6tCj/+cwJBCwAFCIImRQAMPIIIaBH5UHgf+IJf4jggDg59C8EOAoKhBj47DEwYCBhIgE/4RBn4gEIJKnBIJkfXwL4C/kBJQJBB8fwCwSJDAAK8BIIrmBIIIdBBgP/DQYAGBZDmIF4XgIIMD+E4XgIKBSQJuFHAMBIIn4/4UBAQf/45oCAAb1BVQRBJyCtCNwQCBuE48eABAQgCFYJHCAAMcgBBEyE/OIUfWwIVBLIxiBWwOOYpZZBhwJBgPAgF+JQU/IIopDMoK8CEAeAagUHgf/4BBCGw0B/5BNDAIODHwN+AgNxLgI1B8CDJUgoLDMoZ6BGwxoBj6VBIJORLYXAfwMHHAPAjgjBHYMHB4RBFEAwLFh6kBgJBDZAhBSNwMH/l/HwP/QYKtEIIk/KwIgEiE4BYM4YoP48DXBEwM/8Y7Dn//IJ74BhxBBh4IDAAPHHwIgBIIUB47RCgiDDHwIvCUIIvCL4P/YoqJEIJn/+I+FAAPwZwMBQwK5EIIxxCx0AU4Q0HQYjmLyAOBIJXgZwILBZwIgEBYJBDkGAg68BgDFBIIMB/BBIQZhBEbod/IIZfDAARBEgYFBkCDDg6/EHwMBYQhHDeQJBLwYwC4ED/wMBh4ICEwYOCIIg4DQYYvCa4JfB/CGBUIQmCIIU4IJmBDAP+O4SkCIIhHDIIs4IJH/xyDCMoP/46YD+IpBQZ2AHwPxX5H4AQofHIIcEIIQgBHwKGCHYQABuBZCn6MDII4gBIIahC//wC4KhBJYZBBMQomBgIgCQYMAUgQPDgLIBTYbgGQZJBCx40BRIQmCZAYAEFIyDDEALCC/8cgLmCQYhBQBwRBDHwLIHAAakGh0BIIQHCIIX/wLaDI4IFD8FwSQhBGpBBC8ZBB/kOHYSkCIIccv4CBDokcuADBQYjjDMQP8QY4IBI4ZBHSQZiEfwaDD+BfCFgIgHkCDEgEPIIUAbRDOKIIOQAofjHoWOAwJ6BYov8uLFHAALFCQYL1Bh/4L4Uf+A7Hv5BPL4UAv0HfwcBAgQADgEONgxBCAgI7BUIYADLIrjFIIyYDnEfQALpDPoStCNYTODcxCDBeohBFboKPBAQLpBIJRnD/ED/wUB+IgDjkOn7RDQYTwHIIQgC8FwIIOP45BEgCGBFgJ3DIJQoDDYPwC4OPHAIICJoStHIIsgCgPgagMfGoV/Rgf8v4eBIKK8Cx/AHAUD+E/JQM4BAXjU4yDFC4LXCQwIQEcYRfIB4cgA4pWCEYImCIIKtBwBEBJQaDKgE4GoPHGQ6hDIBIABIIk/gfxfYl+BQNx4DIDQZNJII6hBwLCDUghALQY3jGgZZCL4OPEwKPEjggGhJBEn4gBHwaDFDQ4gGIIcOYooAB+ChD/gLEj+AIJVwa4SbBACpfCIIkPEQLIEAA/HVQ7FDkkOQYP+n/gEwRBZcYI+LR4ofEgRBDkBBB8EB//gHR1wAomCIIgODX4YAJR4TFGQYihB/hBBYipiCA4hfBnBALF4SDCOgcSQYhBCAQPj+P4HRcfMQkEL4QOEOgPAgF/IJMDAYWAnDFIQYRQBg/jx4jBABUDIJAIEGIRxC8YmBIAnAAgaDBAAYgCAQSSEgeOXxn8AohBDyQHCHAYRFI4ILBPoMfx4PBwPAYpBlBn6kDABscAogdDyVIA4IgBMQICBn7sB444B45KBIgPxYoU/EIZBHa4JBEuA/HBAV+AwUBIIlJgg4C/yGD8F/YgjLFwP4IJTCB/4OEIJFxEALUDII0gh4wC/ACCEwIABX4MfIwgOCageCIImQnEcLgMcHxDCCv4vBWwIABhJBFkkBF4SGBGQI7Dd4JAGAAR6BgAgFyUHgBlCGQYAHBwMcAwYdFpIIBWwaADAAWOn4FD+ILFgEEIK8AnAEDgRBGQYLCD8AjC/w+BYoIAC/jXDAAXAQY9ADoZBMAAhBHgDjBAQSzBRIYAFeoQADGQKDHgH8gBcCMoI9KgbjCwRBGkBfBUgX8uEHj+BX4f8v0AJAxBJpBBBL4JBBQZ8SII0EgIsDAQUDX4fHRgMAj5BF/0AcwyDBAAQUBIJ5fHQYKhBO4j1BXgXwnEcNwRBHkAjGo62DIIPjMoLFJ+EBL45BD/+BHwfgZYLpCjkP/DFIEY8/GYYXBfYYAJIJMkBgLmBEAQ0DIITCGIIcBU42TX4kBMoIAMhJBLEAg0DJQM4j/wYoXHB4IFBQZGBIIgAOiA+HAQSbBj6eEIQfx/8AuEHRIUAhwOBIJGRIKcCIJWQg7FDX4wsBv5EBJouAU5E/KYYAOHxBBDn5BFgb7DgEcPoJNGIJMAIIsggFAHw0B4EEIJeSQYx9C/+OnAIE/BNCCgIjJHYL7FpCMGgP4HxSkD//+QY/x4AJDh7IBJoPgiQjKiAVCBAVIYhCDOgeAII4IFwPH/+P/DpMHYRQDIJDgIAQtATYx6BjgHEIIP4SoRlNI45BVpDyBAAf8boLgBX4IADg8f/0BHyBrDIA0CLJ5BEg4FDh/AEQrPB5JBbgBBPYocOgEfwAFDII3+HyICCDgsHgESCJFBBApBEg8cAgMgQY0H//yIKl4IQvgIJICGHYUAgPH/gXBglwQY0cdJ4CFo5fF8C/KIIy8Dg/kIIMn+CnHHyQCCC4IdFwTFPn+OC4U/GoUjII0AghBbnAdKoAIGxwaBv/4JQZBHHyYCCp/gd4fACJOQYo0kh//gfwgIICng/FgPBIK1AIIcAhIRKNY+R/EfwAXDn+AEIccuPJIK1INAIeBuBBJkGQYo4aBkECBAZBBnCEEHygmDgFx4BrBBxFBkkAIJACBiQFDkaDCh/AjgLEASsCgEgHAQLFyCDBYpCVIP4RBdyDCJOgNBgiDLAQkncwQABjg+XaJpNBfYMQQaH+H4MB4HgIMgsBiUJgGAQZ8kxxBBh0AghBjQAICBoDFBQZ8kx/Aj/+gKAjkmCoKABAQMAhAXPyZBB+DCkAQMJkkCgEgwSDQyVIkA+kpMEiEIgACBkCDRAVB9BgMEYYMAQwJB4QYNAQYUEgTLBQfGAQIQABwA+2AQNIkiDCgEIZYMIIO8JQIkBgjC3AQNBgkQIISDByBB3HAKDEoKA3AQOCgEgIAWChJB6hCDEhBB5iFAQf6DDgMAgSD7YYcEiVJkBB3kGAIIaDBYvMgH4cIkGCaIpBypBBEAAI7zAQtBH4kBgkSYocgIOdIQQrFBoIOEI4YCuoBAEoKDBpMEBwTLzQY5BBHeICFIAqDByAODkBBywRAEgUBgi/zAQmQoBBDiFAPosEIOaDFhI7zAQpAEgkSH26DIBw4JBhEEiFIkGQAQRBmpA/EAAVAkGAhEAwUAA==")); + +var battery = E.getBattery(); + +// Positions on screen +const timeX = 88, timeY = 52; +const dateX = 5, dateY = 180; +const battX = 172, battY = 175; + +// Draw on every second if unlocked or charging, minute otherwise, start at with seconds on load +var drawTimeout; +var drawInterval = 1000; + +// schedule a draw for the next interval +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, drawInterval - (Date.now() % drawInterval)); +} + +// Update display and timeout on lock/unlock and charge state change +Bangle.on('lock',on=>{ + draw(); +}); + +Bangle.on('charging',charging=>{ + draw(); +}); + +function draw() { + // work out how to display the current time + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + var time = (" "+h).substr(-2) + ":" + ("0"+m).substr(-2); + var seconds = ("0"+d.getSeconds()).substr(-2); + + // g.clear(); // Unneeded if background image takes the whole screen + + // Draw background + g.drawImage(img); + g.setColor(1, 1, 1); + + // draw the current time + g.setFontAlign(1,1); // align right bottom + g.setFont("6x15",3); + g.drawString(time, timeX, timeY, false); + + // Draw battery % + g.setFont("6x15",1); + var battStr = ""; + if(Bangle.isCharging()) { + battStr = "+"; + } + g.drawString(battStr + battery + "%", battX, battY, false); + + // Draw date + g.setFontAlign(-1,1); // align left bottom + g.setFont("6x15",2); + var dateStr = require("locale").date(d)+" "; + g.drawString(dateStr, dateX, dateY, false); + + // draw the seconds only if unlocked, set next timeout + if(!Bangle.isLocked() || Bangle.isCharging()) { + drawInterval = 1000; + g.setFont("6x15",2); + g.drawString(seconds, timeX+2, timeY-4, false); + } + else + drawInterval = 60000; + + // Schedule next draw + queueDraw(); + // console.log("Draw " + time + ":" + seconds); +} + +function refreshBattery() { + battery = E.getBattery(); +} + +// Only update displayed battery level every minute as it fluctuates a lot +setInterval(refreshBattery, 60000); + +Bangle.setUI("clock"); +Bangle.setLocked(false); +// Clear the screen once, at startup +g.clear(); +// draw immediately at first +draw(); diff --git a/apps/2ofthemclk/app.png b/apps/2ofthemclk/app.png new file mode 100644 index 000000000..d304f27d9 Binary files /dev/null and b/apps/2ofthemclk/app.png differ diff --git a/apps/2ofthemclk/bg.png b/apps/2ofthemclk/bg.png new file mode 100644 index 000000000..5f65ca3c7 Binary files /dev/null and b/apps/2ofthemclk/bg.png differ diff --git a/apps/2ofthemclk/metadata.json b/apps/2ofthemclk/metadata.json new file mode 100644 index 000000000..bdaa6e150 --- /dev/null +++ b/apps/2ofthemclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "2ofthemclk", + "name": "two of them clock", + "version": "0.02", + "description": "You can now wear teh memez on your wrist.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"2ofthemclk.app.js","url":"app.js"}, + {"name":"2ofthemclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/2ofthemclk/screenshot.png b/apps/2ofthemclk/screenshot.png new file mode 100644 index 000000000..b9a80a2c5 Binary files /dev/null and b/apps/2ofthemclk/screenshot.png differ diff --git a/apps/3dclock/README.md b/apps/3dclock/README.md new file mode 100644 index 000000000..4efb43901 --- /dev/null +++ b/apps/3dclock/README.md @@ -0,0 +1,6 @@ +A simple clock with perspective scaling. +Battery drainer, performance tester, show-off piece work-in-progress. + +Demo. + + diff --git a/apps/3dclock/app-icon.js b/apps/3dclock/app-icon.js new file mode 100644 index 000000000..ac09d6113 --- /dev/null +++ b/apps/3dclock/app-icon.js @@ -0,0 +1 @@ +atob("MDAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmZCZmZAAAAAAAAAAAAAAAAAAAAAAAAAACZAAkJkJAAAAAAAAAAAAAAAAAAAAAACQkJmZmQCZkAAAAAAAAAAAAAAAAAAAAACQAAAAAJmQmZAAAAAAAAAAAAAAAAAAAAAAAAAAAJkAAJkAAAAAAAAAAAAAAAAAAAAAAAAAAJCZkACQAAAAAAAAAAAAAAAJAAAAAAAAAACZAACZAAAAAAAAAAAAAAAAAAAAAAAAAAmZAJmZkAAAAAAAAAAAAAAAAAAAAACQmQkAmQAJmQAAAAAAAAAAAAAAAAAAAAmZAJkAAAkJmQAAAAAAAAAAAAAAAAkAAAAA///wAAAJmf///wAAAAmQAAAAAAAJAAD/////8AAJD/////AAAAAAAAAJAAAJCQD//////wCZ//////AAAJAAAAAJAAD//w//8AAP/wCf//kAD/AAAAkAAAAAAAD5CQ8J8AAP/wCZkJkAAPAAAJCQAAAAAJn//w//mQAP/wAAmQCQAPAAAAAAAA//CQ///w8J+ZAP/wAACQCQD/AAAACQAP//CQDw/58P/////wAACQAA//AAAJCQAAD/AAAP+QDwn///8AAAAAAP//AACQAAAAD/AAAP8JCZn////wAAmZ///wAAAACZAJD/AACfCQmZkJmf/wAAAP//8AAAAJkAkAD/AACfAJCQmZCf/wkJD//wAAAAAAAJAP//8ADwCQCZmZmf/wAA//8AAAAAAACQkAAJAJAACQ//mZmf/wAA//CQAAAAAAkAmQAACQAACQn//////5mf//////AAAAkACQAACQAACQn/////8AkP//////AAAAAJkACQAJAJkJAJ////AJmf//////AAAAmQAAAAmZCZmZmZkJmZkJAJCZAAAAAAAAAAAACZCZmZmZmQCZmQmQAJAAAAAAAAAACQkAAJmZmZmZmZmZmZkJmZkAAAAAAAAAAJkJCZkJmZmZmZmQkJCZmQAAAAAAAAAAAAAJAACZmZmZmZmQmZkJmZAAAAAAAAAAAACQkJmZmZmZmZmZmZCZkAAAAAAAAAAAAAAAAAkJmZmZmZkJmQkJkAAAAAAAAAAAAAAJmQmZCZmQCZmZmZAAAAAAAAAAAAAAAAAACQmZmZmZAJkJkJAAAAAAAAAAAAAAAAAAAJmQmZkJkACQkAAAAAAAAAAAAAAAAAAAAAAACZmZmQmZkAAAAAAAAAAAAAAAAAAAAAAAAJkAAJkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") \ No newline at end of file diff --git a/apps/3dclock/app.js b/apps/3dclock/app.js new file mode 100644 index 000000000..89275c84f --- /dev/null +++ b/apps/3dclock/app.js @@ -0,0 +1,70 @@ +// 3d clock by gm / ainegil based on Anton Clock + + +{ // 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 +let drawTimeout; + + var moonimg = { + width : 176, height : 167, bpp : 3, + buffer : atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJJJJJJJJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJJJJJJJJJJJJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJJJJIJJIBIIJJJJJJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIBJAJJJIJIJBJJJJJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJBJABJIIAIIBBBAIJJJJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIBBAABABBBABIAJJIJJJJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBAAABBBBAIAJIIBBBBIBJJJJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAIJAAIBIIJIAABIJBJJJJIJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIIAAJAJIJABJJBBAAAIIIABAJAIJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJJJIJIJIBJABAJABIBBBABJJJIJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJJJABJBAIIJJJBJJJBJAAABBJJJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBIBIAAAAJIAAAIABIJIAABIABIBJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAIAIAAABAAAABABBBABIJAIAJIBAIIJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAJAAAAAAAAABAIJJBBBABIBJJJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAJBIAAAAAAAAAAAAAAIAJJBABAIIJBJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABABIABAAAAAAABAAABAAJBJIIIJABAIAJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAJJIJBJJAAAAAJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAJAJJAAIAJBAAJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIIJBABIAAJBAJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJBJBIAJBBIAAAJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIABJJABBBIAIAIBJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAABABIIJJJJJJJBIAABJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAIAAAAAIAABJJJAIJJIBJJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABJIBBJJJBIBAIIBJBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAAAAAAAAABAAAIIJIJBAIBJJIBIIAJIIJIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAABIAAAAAAAAAAAAAAAAAAAAABABBBAAAJBJBBBBBAIAJIJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAABABJIBJJJAAJAIBJJJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAJBAAAAAAAAAAAAAAAAAAAAAAAAAIAIJJJIBJBIBIABIJJAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAIAIAAAAAAAAAAAAAAAAIAAAAAIIABBJIIIJIAJJIIJJJIJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBJABBJJBBAJBBJBIBBJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAIAAIJIBAAIAAIAABJAIABABIJJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAJAAABAAAIIIIIAAAAABIBBBABJJIAJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAAIJIIBABAABAAIAIBAIBABJJJAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAIAAIAAAAAAAAAAIABAJABIIAAIAAIIBBBBBAJIAJAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIBBBAIABAAAAAABBAAABBAJJJJIAIAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAABAAAAABAAAAAAAAABAAABBBAIBIIABAAAAAAAAAAIIJJJJAABAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAABAAAAAAAAABAAAAABABIJJBBAIIBAAAAAAAAAABIAIJAIBJBAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAAAAABAAAAAIAIIAAAAIAAAAABIAAAAAAAAIAAAAAAIBBAIJJJJAIIAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAABBBAAAAABAAAAAIAIAIAIBAAAABABAAAJIAIJBAJJIIAAAAAAAAAAAAAAAAAAAAAAAJIAAAAAAAAAAAAAAIAAAAAAAIABAABAAAAAAABJBAIJAAAAAAAAAAAJJIIBJJJIIJAAAAAAAAAAAAAAAAAAAAAAAIAIAAAAAAAAAAAAAAAAABJIAIAAAAABAAAAAAJBJJJIIAAAAAAAAAAIJAJJJJBIJJAAAAAAAAAAAAAAAAAAAAAAABBAAAAAAAAAAAABAAAAAIJBAIBIIBBIBBAAAABJBBIJAAAAAAAAIAAAAJJBJIJJJIIAAAAAAAAAAAAAAAAAAAAAJIAAAAAAAAAAAAAAIAAAAIJABBABBIIAAAAAIJIABBIAAAAAAAAAAAAAJJJIJBJJJJIAAAAAAAAAAAAAAAAAAAAAJJBAAAAAAAAAAAAAIAAAABAAAIBJAABIAIAABIAIBBIIAAAAAAAAAAAABIJJBBBAJBIAAAAAAAAAAAAAAAAAAAAABABAAAAAAAAAAAABIIAAABABBBIAABJIJAIBIIABJAIIAAAAAAAIAAAAJAJIIJIJJJBAAAAAAAAAAAAAAAAAAAAAJIJIAAAAAAAAAABBIBAAAAJAAABJBBAAAAABAJIABAAAAAAAAAIAAAAABJJBBIJJJJAAAAAAAAAAAAAAAAAAAAABJIAAAAAAAAAAAAABIIAAAAIAJIJIIAIIIAAAABBBABAAAAAAAAAAAAAABJIJBJIBJIJAAAAAAAAAAAAAAAAAAAABAIAAAAAAAAAAAABBAAIABAAJIJJJABJBAAAABAAIAAIAIAAAAAAAAAAAJJJJJJJIJJAIAAAAAAAAAAAAAAAAAAAIBIBAAAAAAAAAAAAAJAAAIAAIBJJIBAJIAABAAAAAABAIAIAAAAAAAAABABJJJBJJJIJAAAAAAAAAAAAAAAAAAAAAIJBAAAAAAAAAAAABIAAAAAAABJJBAAAABAAAAABAAAAAAAAAAAAAAAAAAAJJJIBJJJAIAAAAAAAAAAAAAAAAAABBJABAAAAAAAAAAAAAIAAAJIJAIBJBAAAIIAAAAAAAAAAAAIAAAAIAAAAAAAIJBBJAJBABAAAAAAAAAAAAAAAAAABIAJAAAAAAAAAAAAABAABABAAABIAAAJIABAAAAAAAAAABIIAAAAAAAAAAAJIJJJIJIBJJAAAAAAAAAAAAAAAAAAABIAIAAAAAAAAAAAAIAABAAAJAAIBAIAIIAAAAAAAAAAABIAIAIAAAAAAAAIIJBJIJABJJIAAAAAAAAAAAAAAAAAJIJIAAAAAAAAAAAAAAABAAAIIJAIIAAAAAAAAAAAAAAAAAJAAAJIAAAAABJJJJJJBIIBJAIAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAAAAAAAAAAAIBAABAAIIIAAAAIBBAAAAAIAAAIIIAAAAAAJIBJIAAJJJJIAAAAAAAAAAAAAAAAAIAJBIAAAAAAAAAAAAAAAAIABBBIAAAAAAAAAAIBBBBAAABBAAIBBBAAAAAAAIBIAJJBIJJBAAAAAAAAAAAAAAAAAIIJAAAAAAAAAAAAAAAAAIAABAAIAIAAAAAAAAIIIJAAAAAAAAAIIAAIAAAIIBBJJJJJJIAJAAAAAAAAAAAAAAAAAAAIAIAAAAAAAAAAAAAAAAAAAIIBIAAAAAAAAABBIJAIAAAAABBIAAAAAAAAAAAJJIJABIBJAAAAAAAAAAAAAAAABIIAABAAAAAAAAAAAAAAAAAIIBBBBBAAIAAAABBJBJIBIAAIAABAAAAAAAAAABAAJIIIAABJAAAAAAAAAAAAAAAABBAAAIAAAAAAAAAAAAAAAAAAAIABIAAABAAAAAAJIIJBAAIBJBJAAAAAAAAAAAABBIIIIAIBAAAAAAAAAAAAAAAAAJIABJAAAAAAAAAAAAAABAAIAAAAIAAAAAAAAAIBBIJAAIABJIBAAAAAAAAAAAAABJBAAAIBAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAIAAAAAJABAAAIAAAIJJIAJJABJJAAAAAAAAAAABAABJJJIIIAJAAAAAAAAAAAAAAAAABBAAAAAAAAAAAAAAAAAAAIAAAABABAAIAAAIAIAJBJJIBIIAIJAAAAAAAAAAAAIAJJAAABJIAAAAAAAAAAAAAAAABIAAJAAAAAAAAAAAAAAAAAAAAAABBAIAIABABAIBAJJBJIIJBIAAAAAIAAAAAABIBJIAAAAAAAAAAAAAAAAAAAAIBBAJAAAAAAAAAAAAAAAAIAAAAAJIABAAAJAIAABABIABJBIBAAAAAAAAAAAAAAAAJIAIAAAIAAAAAAAAAAAAAAABIAAIJAAAAAAAAAAAAAAAAJAAAABAAAABBBJJIAAAABBJBJIJBBAAAAAAAAAAAABBJIIAAAAIAAAAAAAAAAAAAAAAIJAJAAAAAAAAAAAAAAAJBAAABBBIJABBBIAAAIBBAIJJJJBIJAAAAAAAAAAAAAAJBJAAAAAJAAAAAAAAAAAAAAAAJIBAIIAAAAAAAAAAAAABJAAABABAAAJBIIJABAABBJBJJJJABAAAAAAAAAAAAAAJJJAAAABAAAAAAAAAAAAAAAAIIIJBBJAAAAAAAAAAAABABAAAAAAAIIAJJBJBAIJJJJJJJJJJIIAAAAAAAAAAAAAABJAAAAAIAAAAAAAAAAAAAAAABIIABIAAAAAAAAAAAAABIAAAABAAIABJJABIJIBIBJIJJJJJJIIAAAAAAAAAAAAABBBAAAAIAAAAAAAAAAAAAAABIIAABIIIAAAAAAAAAAAAAAAAAAAJBBABJBJJJBAABBJJJJJIJBAAAAAAAAAAAAABBIBAAAIIAAAAAAAAAAAAAAABAAAAAIIAAAAAABAAAAAAAAAAAABBIABJIJJJJJJJJIJJJJBJAIIAAAAAAAAAAABBBJIAAAIAAAAAAAAAAAAAAAAIIIIBBABBAIIABIAAAAAAAAAAAABJAAABIJJJJBBJJJJBBBBBAABAAAAAAAAAABIAJBJIAAIBAAAAAAAAAAAAAAAJBAAAAIAIAABABAIAAAAAAAAAAAIAIAABBIJIJJIJBJJIBBBJIAIAAAAAAAAAAAAIAJBIABBAAAAAAAAAAAAAAAAAIBAJAAAAAAAIABAAAAAAAAAAAAAJAAAABBAJIBIIJJJBBBIJJBIAAAAAAAAAAAABBBIIIJAIAAAAAAAAAAAAAAAAAAIIJAAAIIAAIBAIAAAAAAAAAAIJAAAAJBJABJBIJJJJJJBJIJJBAAAAIAAAAAAIAIBBIIJAAAAAAAAAAAAAAAAABBIJBABABAABIAAAAAAAAAAAAABAAIAAIJAJJJJJBJJBJJJJJIJJAAAAAIAAAIABABJIJIAAAAAAAAAAAAAAAAABBBIIAAAIIBAIIIAAAAAAABABAIAAABAIJBJJJAIIBJJJJBBJJJJBIAAAAIJAAAIBABJBABJIAAAAAAAAAAAAAAAABJBBJABBBBAAAAAAAAAAAAAAAAAIAAAIJBJBABIIJJJIJIJJJJJJIIBABJAAABBAAAJJIABAAAAAAAAAAAAAAAABAABJJJBBABJAAAAAAAAAIAAAAAAAAAAAIJBIABBJJJJJJJBBJJJJIAABBJABAIIAAAJIAIAAAAAAAAAAAAAAAAABAIJJABIAIIAAAAAAAAAABBAAAABAIAABABJAIJJIJJJJJIIJJJJIIAABBJIAABAAAAAIJAAAAAAAAAAAAAAAAAAAJABJJBIAAAAAAAAAAIIIBAAAAAAAAAIABBJAJJBBJJJBBJBJJJJIIAAJIIAIBAAAAAAABIIAAAAAAAAAAAAAAAABABJBIIAIBIAAAAAAAIIBAAAABAAAAAAAAJIBJIIIJJIJJJJJJJJIAAIJBAJJIAAAAABJAIBAAAAAAAAAAAAAAAAAAABJJJBAIIAAAAAAAJAIAABAAAABAABBBBJBIBBJJJIJJJIJJJAIABBBBIJJAAAAAAAAJIJAAAAAAAAAAAAAAAABAAABIBBAABAAAAAAAAAIAAIAIAAAAAAAJBAIBJJJBIBJBJJJJJIABIBAAAIIAAAAAAAABBAAAAAAAAAAAAAAAAAAIIIAJBAJIBAAAAAABAAAAAAAAAAAAAABJJIJAJBIJIJJBJJJJIJJBAABJJIIBAAAAAAABBBAAAAAAAAAAAAAAAAABABJIBABIAAAAAAAAABAAAIAAAAAAABJBAIJJJJIIBJJJJJJJJBJAAAIBJJJIAAAAAAAAABAAAAAAAAAAAAAAAAAAAABJJIBIJAAAAAAABAAAAAIAAAAAABJJJBBIJJJJJJJJJJJJJJBBABJAIAIBAAAAAABABBAAAAAAAAAAAAAAAAAABIABBBBBIIAAAAAAAAAIAAABAAAIAAJBIJJJJAJJJIBJJJBJJJJJAJJJJJAAAIAAAABABAAAAAAAAAAAAAAAAAABBBBBAJJIAAAAAAAAAAAAIAAAIAAAABIIJIJJIIIJIJJIJJJJJJIJBJJJJIIBAAAAAAAIAAAAAAAAAAAAAAAAAAAJBBABAAIJBJBAAAAAAJAAAAAAAAAABBJJBIIIJJBIJJJJJJJJJJJJIJBBJJJAAAAAAAIBIAAAAAAAAAAAAAAAAAAIBBBIAIIJJIIAAAAIAIAAIAAAAAAAAAIJBABJIJJJJJJJBIIJJBJIJABJJJBAAAAAAIAIIAAAAAAAAAAAAAAAAAABBIAIABABAJAAAAAAAIAAIAAAAAAAAAJBJIIJJJBAIJIJBBJJJJJIBAAABJBAAAAAAABAIAAAAAAAAAAAAAAAAAAABAIIBBIBBJAAAIAIIAAAAAAAAAAAABAJJBBBJJJIJJBJBJBJJJJBBAIAJJABAAAAAAIJBAAAAAAAAAAAAAAAAAAABBABBAJIJAIBAAIBAABABBAAAAAABIBJJJJJJIIJAIJJJJJJJJJIIIBAJJIBAAAAAAAAAAAAAAAAAAAAAAAAAAAABBJAAABAJIBAAAAJABAIJAAAAAAAABJJAAJJBBIJIIJJIJIJJJJJAABABJAAAAAABIIAAAAAAAAAAAAAAAAAAAAAJAABAIIIIIBIBBAAAAIJBAIAAAAAAJJJBJBJBJJBJBJJBJBJJJAIIBAIAJJABAAABIBBIAAAAAAAAAAAAAAAAAAABBBAIAIAIBABIIJAAAAABBIAAAIIABJJJJJIJBJJIJJBBJJBJJJJBAAABAAAAAAAJBBAAAAAAAAAAAAAAAAAAAAAAABIBBIIBAABABBAAAAABIJABAABJJJBJJIBIJJJIJBBJIJJJJIABAAIJJBIBAIBAIAIAAAAAAAAAAAAAAAAAAAAABAABJBAAAIAIIJIAJAAAJAIIAJJBJJJJJJBJBJJJJJJIIIJJIJIAAABAJBIBAAAJAAIAAAAAAAAAAAAAAAAAAAAAIIIAJIABAAAIBABAAAABJJJJJJJJJJBJJIJJJJBJJIIJJJIIJIAAAABJIBJJABBJJBIAAAAAAAAAAAAAAAAAAAAABIIABBBABIIAJAAIAIJJJJBIJJJJJJJJJJIJJJJBJJJBJJBIIJBAAABAABJBJAIIJIIAAAAAAAAAAAAAAAAAAAAABABBAABABBBIJIBIBIJJBJAJBJJJJJJJJJJJJJJJJBIJBBBJJAIABBJIJIBBABBJIIAAAAAAAAAAAAAAAAAAAAAAABIIBAIIIAAIJBJIIJJJAJJJJJJJJJJJJJJJJJIJAJJJJJJJBJAIAABJIAJAAAIJBBAAAAAAAAAAAAAAAAAAAAAAABIAIIAAIIJBJAIIIJJIIJJJJJJJJJJJJJJIJJJAIJBIJJJJIJIBABIBIIAAABBAIAAAAAAAAAAAAAAAAAAAAAAAAAIBBBAAJAIAJJIIIIJBJJBJJJJJJJJBJJJBIIBJBBJJJJJJIJJJJAJBAABAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIBAIBBJABBJJJJJJJJJJJJJJJJJBIIBIIJIJJJAIJIBIIAAIAIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAIABAAAAAJIAJJBIIJJJJJJJJJJJJJJJJJJIJJJJJJJJBJJJJJAJAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAJAAAJBJBJBAJJIJJJJJJJJJJJJJIJJAJJJJJJIIJIJJIAJABIBAAJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAIAJBAIAAAIIJJJJJJJJJJJJJJJJJJJJJIJJJBJIIJIJIJJJBBJJBIBBJIJBAAAAAAAAAAAAAAAAAAAAAAAAAAAABIIIAAABAAJBJBJJAJJJJJJJJJJJJJJJJBJJJJIIJJBBJIIIABBAIIIAIABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAIIIIBAIIJBJJIJBJIJJJJJJJJJJJJJJJJJJJIJAJJBABJJJJIAJIAAIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAIAABABJAJBBJJJJJJJJJJJJJJBJJJJJJJBJJJIJBBJIBJJJBIJJJAIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIIBABIAABBJAJJBJJJJJJJJJJJJJJJJJJJJJIJBBJIIJBAAJAJJIBAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAIBIAAJAABBJJJIJJJJJJJJJJJIIIJJIIIBJIIJIAIBJBJBJABIJBABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAIJAAIBJIBJBBJJJJJJJJJJJJJJBIJJJBBJJJJJJBJJJJJBJJJJIBABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAAAIAIBBJBBJBJJJBBJJJJJJJJJJJJJJIBBJJBAIIIJJAJJBJJIIJAIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABIIAIIABAIIJIJJJJJJJJJJJJJBIJJJJJABBIJABBIJAJJJBBIJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIBAIBBJIAIIJBJJJJJJJJJJJJBJJIJJIJJIJAJJBBJBJBJIJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIIABIABBIJAIJIJJJJJJJJJJJJJJJJBBJJJBJJAJJJJJJBJIJBAJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIIBJAAABJJJJIJJJJJJJJJJJJBBBJJJJJJIJJABAJJAJJJJJIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBAIAAABBIIBJJBJJJJJJJJJBJIJAJAJJBJAAIJJIJIBBJIJJABBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAJBAJIBJIIJJJJJJJJJJJJIBJBBJIBJJJBIJAAJAJJJJBBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBIIJJAIIIBJIBBJJJJJJJJJJJJJBJBIBJIIJJBJAJJBJJJJJIIAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAJABAIJABIJJJJJJJJJJBJBJJIJJJJBJIJIJIIBIJABIJBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBJBJIJIJJJJJJJIJJJJJJJJJJBIAJBIJIJBJIJJIJJJBBIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBJAIIBBJJJJJJJJJBAJJBJJBJIBIAJJJBJJJJBBJAJJJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBABABIBIJJJJBJJIIJJJBIJJIJJJJAJJJJJJJIIJJBJIJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJJJIAAJBJJJIJJJJJJJJBBIJJIJJIJJJIJIJIJJAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBBJBAAIBJJJJJJBBJIJJIJJIJBJJAIIJJJIJIAJABABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAJBJJJBIJJJJBJBIBJBAAIJJIJJIJBJJJBJBJAAAIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBJIBIBIJJJJJAJAJJJJIIIIJBBJJAJJBIAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJJAAJJJJJJJJBIIJJJBBJBBBBIJJIBAIAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJBIBIJJJBJIJIJJJBBJIIJBJIJBABIBBABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAJAIJBIJIIJBJJJJBBIJIIJJJJJJBJBIJJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBJJJJBJJBBBBJJABIJIIJIJAIIIBBBIJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAIAJIJJIBBJJBJIJJJBJIIJJJIIBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAIJAJIBJBBBIJIJIIBJJJJJBAJBAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJIJJJAJJBIIJJIIIIIJJJBAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAIIJJJJJJJJIJABJIJIBJJIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIIIJJIJJAJBJAJJBAJJIJBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAJJBIJJJJJIJBBBIIJIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJJJJIAJBIAIAJBIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJBAJJIIJBJJBIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJIIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") +}; +g.reset().clearRect(0,0,175,175); // clear whole background (w/o widgets) +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + var date = new Date(); + var hour = date.getHours(); + var minute = date.getMinutes(); + var second = date.getSeconds(); + var millisec = date.getMilliseconds(); + var depth = 600; + var startd = 60; + var focal = 350; + var basesize = 24; + var poszs = startd + depth * millisec / 1000; + var posys = -12; + var poszm = startd + depth-depth * second / 60; + var poszh = startd + startd + depth * minute / 180; + var scales = focal / poszs; + var scalem = focal / poszm; + var scaleh = focal / poszh; + var ys = posys * scales; + var fsizes = basesize * scales; + var fsizem = basesize * scalem; + var fsizeh = basesize * scaleh; + + g.drawImage(moonimg,0,0); + g.setColor(0,0,0); + g.fillRect(0,175-32,175,175); + // g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + g.setColor(1,1,1); + g.setFontAlign(1, 0).setFont("Vector",fsizeh).drawString(hour, x, y); + g.setFontAlign(-1, 0).setFont("Vector",fsizem).drawString(minute, x, y); + g.setFontAlign(0,0).setFont("Vector",fsizes).drawString(second, x, y+ys); + + //g.setFontAlign(0, 0).setFont("6x8", 2).drawString("blahblah", x, y+48); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 50 - (Date.now() % 50)); +}; + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + }}); +// Load widgets +Bangle.loadWidgets(); +draw(); +setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/3dclock/app.png b/apps/3dclock/app.png new file mode 100644 index 000000000..934ffdaea Binary files /dev/null and b/apps/3dclock/app.png differ diff --git a/apps/3dclock/metadata.json b/apps/3dclock/metadata.json new file mode 100644 index 000000000..a32a80a0d --- /dev/null +++ b/apps/3dclock/metadata.json @@ -0,0 +1,16 @@ +{ "id": "3dclock", + "name": "3D Clock", + "shortName":"3DClock", + "icon": "app.png", + "version":"0.01", + "description": "This is a simple 3D scaling demo based on Anton Clock", + "screenshots" : [ { "url":"screenshot.png" }], + "type":"clock", + "tags": "clock", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"3dclock.app.js","url":"app.js"}, + {"name":"3dclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/3dclock/screenshot.png b/apps/3dclock/screenshot.png new file mode 100644 index 000000000..ebe675d8f Binary files /dev/null and b/apps/3dclock/screenshot.png differ diff --git a/apps/7x7dotsclock/7x7dotsclock.app.js b/apps/7x7dotsclock/7x7dotsclock.app.js index aa174b2d2..0ff96d5c9 100644 --- a/apps/7x7dotsclock/7x7dotsclock.app.js +++ b/apps/7x7dotsclock/7x7dotsclock.app.js @@ -149,11 +149,11 @@ function drawHSeg(x1,y1,x2,y2,Num,Color,Size) { if (Color == "fg") { g.setColor(g.theme.fg); } else { - g.setColor(mColor[0],mColor[1],mColor[2]); + g.setColor(mColor[0],mColor[1],mColor[2]); } g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size); } else { - g.setColor(bColor[0],bColor[1],bColor[2]); + g.setColor(bColor[0],bColor[1],bColor[2]); g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1); } } @@ -166,7 +166,7 @@ function drawSSeg(x1,y1,x2,y2,Num,Color,Size) { for (let j = 1; j < 8; j++) { if (Font[Num][j-1][i-1] == 1) { if (Color == "fg") { - g.setColor(sColor[0],sColor[1],sColor[2]); + g.setColor(sColor[0],sColor[1],sColor[2]); } else { g.setColor(g.theme.fg); //g.setColor(0.7,0.7,0.7); @@ -253,8 +253,8 @@ function actions(v){ if(BTN1.read() === true) { print("BTN pressed"); Bangle.showLauncher(); - } - + } + if(v==-1){ print("up swipe event"); if(settings.swupApp != "") load(settings.swupApp); @@ -269,7 +269,7 @@ function actions(v){ } // Get Messages status -var messages = require("Storage").readJSON("messages.json",1)||[]; +var messages_installed = require("Storage").read("messages") !== undefined; //var BTconnected = NRF.getSecurityStatus().connected; //NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected); @@ -282,34 +282,34 @@ function drawWidgeds() { //print(BluetoothDevice.connected); var x1Bt = 160; var y1Bt = 0; - var x2Bt = x1Bt + 30; + //var x2Bt = x1Bt + 30; var y2Bt = y2Bt; if (NRF.getSecurityStatus().connected) g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); else g.setColor(g.theme.dark ? "#666" : "#999"); - g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt); + - //Battery //print(E.getBattery()); //print(Bangle.isCharging()); - + var x1B = 130; var y1B = 2; var x2B = x1B + 20; var y2B = y1B + 15; - + g.setColor(g.theme.bg); g.clearRect(x1B,y1B,x2B,y2B); - + g.setColor(g.theme.fg); g.drawRect(x1B,y1B,x2B,y2B); g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B); g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3); - + //Messages @@ -318,25 +318,25 @@ function drawWidgeds() { var x2M = x1M + 25; var y2M = y2B; - if (messages.some(m=>m.new)) { + if (messages_installed && require("messages").status() == "new") { g.setColor(g.theme.fg); g.fillRect(x1M,y1M,x2M,y2M); g.setColor(g.theme.bg); g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2); g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M); } - + var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var d = new Date(); var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear(); print(strDow[dow] + ' ' + day + '.' + month + ' ' + year); - + g.setColor(g.theme.fg); g.setFontAlign(-1, -1,0); g.setFont("Vector", 20); g.drawString(strDow[dow] + ' ' + day, 0, 0, true); - + } @@ -354,7 +354,7 @@ function SetFull(on) { } else { Ys = 30; Bangle.setUI("updown",actions); - Bangle.on('swipe', function(direction) { + Bangle.on('swipe', function(direction) { switch (direction) { case 1: print("swipe left event"); @@ -362,7 +362,7 @@ function SetFull(on) { print(settings.swleftApp); break; case -1: - print("swipe right event"); + print("swipe right event"); if(settings.swrightApp != "") load(settings.swrightApp); print(settings.swrightApp); break; @@ -374,7 +374,7 @@ function SetFull(on) { SegH = (Ye-Ys)/2; Dy = SegH/16; - + draw(); if (on != true) { @@ -391,4 +391,4 @@ Bangle.on('lock', function(on) { SetFull(Bangle.isLocked()); -var secondInterval = setInterval(draw, 1000); +/*var secondInterval =*/ setInterval(draw, 1000); diff --git a/apps/7x7dotsclock/ChangeLog b/apps/7x7dotsclock/ChangeLog index d2c98a472..cdc4ffcc5 100644 --- a/apps/7x7dotsclock/ChangeLog +++ b/apps/7x7dotsclock/ChangeLog @@ -1,2 +1,4 @@ 0.01: Initial version for upload -0.02: better theme support, configurable colors, small improvements +0.02: Better theme support, configurable colors, small improvements +0.03: Use `messages` library to check for new messages +0.04: Minor code improvements diff --git a/apps/7x7dotsclock/metadata.json b/apps/7x7dotsclock/metadata.json index 41f0836d3..c4edc1ae8 100644 --- a/apps/7x7dotsclock/metadata.json +++ b/apps/7x7dotsclock/metadata.json @@ -1,7 +1,7 @@ { "id": "7x7dotsclock", "name": "7x7 Dots Clock", "shortName":"7x7 Dots Clock", - "version":"0.02", + "version": "0.04", "description": "A clock with a big 7x7 dots Font", "icon": "dotsfontclock.png", "tags": "clock", diff --git a/apps/8ball/8ball.png b/apps/8ball/8ball.png new file mode 100644 index 000000000..72344261a Binary files /dev/null and b/apps/8ball/8ball.png differ diff --git a/apps/8ball/ChangeLog b/apps/8ball/ChangeLog new file mode 100644 index 000000000..3bcffb19b --- /dev/null +++ b/apps/8ball/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/8ball/app-icon.js b/apps/8ball/app-icon.js new file mode 100644 index 000000000..399dbef21 --- /dev/null +++ b/apps/8ball/app-icon.js @@ -0,0 +1 @@ +atob("MDCBAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/4AAAAB4AeAAAAHgAHgAAAOAABwAAAcAAA4AAA4AMAcAABwAMAOAABgA/AGAADAAeADAAHAAMADgAGAAAABgAGAAAABgAMAAAAAwAMBAAAAwAMDAAAAwAMHwAAAwAMHwAAAwAMDAACAwAMBAADAwAMAAAPgwAMAAAPgwAMAAADAwAGAAACBgAGAAAABgAHAAAADgADAAAADAADgAAAHAAB////+AAB////+AABgAAAGAABgAAAGAABgAAAGAADAAAADAADAAAADAADAAAADAAGAAAABgAGAAAABgAH/////gAP/////wAYAAAAAYAYAAAAAYAf/////4AP/////wAAAAAAAAAAAAAAAAA==") diff --git a/apps/8ball/app.js b/apps/8ball/app.js new file mode 100644 index 000000000..8a3ee427e --- /dev/null +++ b/apps/8ball/app.js @@ -0,0 +1,92 @@ +var keyboard = "textinput"; +var Name = ""; +Bangle.setLCDTimeout(0); +var menuOpen = 1; +var answers = new Array("no", "yes","WHAT????","What do you think", "That was a bad question", "YES!!!", "NOOOOO!!", "nope","100%","yup","why should I answer that?","think for yourself","ask again later, I'm busy", "what Was that horrible question","how dare you?","you wanted to hear yes? okay, yes", "Don't get angry when I say no","you are 100% wrong","totally, for sure","hmmm... I'll ponder it and get back to you later","wow, you really have a lot of questions", "NOPE","is the sky blue, hmmm...","I don't have time to answer","How many more questions before you change my name?","theres this thing called wikipedia","hmm... I don't seem to be able to reach the internet right now","if you phrase it like that, yes","Huh, never thought so hard in my life","The winds of time say no"); +var consonants = new Array("b","c","d","f","g","h","j","k","l","m","n","p","q","r","s","t","v","w","x","y","z"); +var vowels = new Array("a","e","i","o","u"); +try {keyboard = require(keyboard);} catch(e) {keyboard = null;} +function generateName() +{ + Name = ""; + var nameLength = Math.round(Math.random()*5); + for(var i = 0; i < nameLength; i++){ + var cosonant = consonants[Math.round(Math.random()*consonants.length/2)]; + var vowel = vowels[Math.round(Math.random()*vowels.length/2)]; + Name = Name + cosonant + vowel; + if(Name == "") + { + generateName(); + } + } +} +generateName(); +function menu() +{ + g.clear(); + E.showMenu(); + menuOpen = 1; + E.showMenu({ + "" : { title : Name }, + "< Back" : () => menu(), + "Start" : () => { + E.showMenu(); + g.clear(); + menuOpen = 0; + Drawtext("ask " + Name + " a yes or no question"); + }, + "regenerate name" : () => { + menu(); + generateName(); + }, + "show answers" : () => { + var menu = new Array([]); + for(var i = 0; i < answers.length; i++){ + menu.push({title : answers[i]}); + } + E.showMenu(menu); + + + }, + + "Add answer" : () => { + E.showMenu(); + keyboard.input({}).then(result => {if(result != ""){answers.push(result);} menu();}); + }, + "Edit name" : () => { + E.showMenu(); + keyboard.input({}).then(result => {if(result != ""){Name = result;} menu();}); + + }, + "Exit" : () => load(), + }); +} +menu(); + + var answer; +function Drawtext(text) +{ + g.clear(); + g.setFont("Vector", 20); + g.drawString(g.wrapString(text, g.getWidth(), -20).join("\n")); +} +function WriteAnswer() +{ + if (menuOpen == 0) + { + var randomnumber = Math.round(Math.random()*answers.length); + answer = answers[randomnumber]; + Drawtext(answer); + setTimeout(function() { + Drawtext("ask " + Name + " a yes or no question"); +}, 3000); + + } + +} +setWatch(function() { + menu(); + +}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true, edge:"falling"}); + + Bangle.on('touch', function(button, xy) { WriteAnswer(); }); diff --git a/apps/8ball/metadata.json b/apps/8ball/metadata.json new file mode 100644 index 000000000..da387d3d6 --- /dev/null +++ b/apps/8ball/metadata.json @@ -0,0 +1,19 @@ +{ "id": "8ball", + "name": "Magic 8 ball", + "shortName":"8ball", + "icon": "8ball.png", + "version":"0.01", + "screenshots": [ + {"url":"screenshot.png"}, + {"url":"screenshot-1.png"}, + {"url":"screenshot-2.png"} + ], + "allow_emulator": true, + "description": "A very sarcastic magic 8ball", + "tags": "game", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"8ball.app.js","url":"app.js"}, + {"name":"8ball.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/8ball/screenshot-1.png b/apps/8ball/screenshot-1.png new file mode 100644 index 000000000..edf1a4695 Binary files /dev/null and b/apps/8ball/screenshot-1.png differ diff --git a/apps/8ball/screenshot-2.png b/apps/8ball/screenshot-2.png new file mode 100644 index 000000000..c5c607089 Binary files /dev/null and b/apps/8ball/screenshot-2.png differ diff --git a/apps/8ball/screenshot.png b/apps/8ball/screenshot.png new file mode 100644 index 000000000..f1f888cf3 Binary files /dev/null and b/apps/8ball/screenshot.png differ diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog index feb008f5f..9718a652d 100644 --- a/apps/90sclk/ChangeLog +++ b/apps/90sclk/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! -0.02: Fullscreen settings. \ No newline at end of file +0.02: Fullscreen settings. +0.03: Tell clock widgets to hide. +0.04: Use widget_utils. diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js index 6babbfec2..63a48b27a 100644 --- a/apps/90sclk/app.js +++ b/apps/90sclk/app.js @@ -1,6 +1,7 @@ const SETTINGS_FILE = "90sclk.setting.json"; const locale = require('locale'); const storage = require('Storage'); +const widget_utils = require('widget_utils'); /* @@ -109,12 +110,15 @@ function draw() { // Draw widgets if not fullscreen if(settings.fullscreen){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } } +// Show launcher when middle button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); // Clear the screen once, at startup @@ -140,5 +144,3 @@ Bangle.on('lock', function(isLocked) { }); -// Show launcher when middle button pressed -Bangle.setUI("clock"); diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json index fb2824a6f..b4b320a1b 100644 --- a/apps/90sclk/metadata.json +++ b/apps/90sclk/metadata.json @@ -1,7 +1,7 @@ { "id": "90sclk", "name": "90s Clock", - "version": "0.02", + "version": "0.04", "description": "A 90s style watch-face", "readme": "README.md", "icon": "app.png", @@ -14,5 +14,6 @@ {"name":"90sclk.app.js","url":"app.js"}, {"name":"90sclk.img","url":"app-icon.js","evaluate":true}, {"name":"90sclk.settings.js","url":"settings.js"} - ] + ], + "data": [{"name":"90sclk.setting.json"}] } diff --git a/apps/90sclk/settings.js b/apps/90sclk/settings.js index 8f97cd317..74241d603 100644 --- a/apps/90sclk/settings.js +++ b/apps/90sclk/settings.js @@ -21,7 +21,6 @@ '< Back': back, 'Full Screen': { value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), onchange: () => { settings.fullscreen = !settings.fullscreen; save(); diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index 1c18ca59b..712a52a37 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -4,3 +4,4 @@ 0.04: Set 00:00 to 12:00 for 12 hour time 0.05: Display time, even on Thursday 0.06: Fix light theme issue, where widgets would end up on a light strip +0.07: Minor code improvements diff --git a/apps/93dub/app.js b/apps/93dub/app.js index f970eec5d..c9f670993 100644 --- a/apps/93dub/app.js +++ b/apps/93dub/app.js @@ -8,7 +8,7 @@ var imgBg = require("heatshrink").decompress(atob("2GwgJC/AH4A/AH4A/AH4A/AH4A/AC // reg number first char 48 28 by 41 var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // tiny font for percentage first char 48 6 by 8 -var fontTiny = atob("AH6BgYF+ACFB/wEBAGGDhYlxAEKBkZFuAAx0hP8EAPqRkZGOAH6RkZFOAICHmKDAAG6RkZFuAHKJiYl+AAAAAAAAAAAAAAAA"); +//var fontTiny = atob("AH6BgYF+ACFB/wEBAGGDhYlxAEKBkZFuAAx0hP8EAPqRkZGOAH6RkZFOAICHmKDAAG6RkZFuAHKJiYl+AAAAAAAAAAAAAAAA"); // date font first char 48 12 by 15 var fontDate = atob("AAAAAfv149wAeADwAeADwAeADvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAPHn9/AAAAAAP0A9wweGDwweGDwweGDvAL8AAAAAAAAAAAgwOGDwweGDwweGDvHp98AAAAA/gB6AAwAGAAwAGAAwAGAPHj9/AAAAAfgF6BwweGDwweGDwweGDgHoB+AAAAAfv169wweGDwweGDwweGDgHoB+AAAAAAAAAAgAGAAwAGAAwAGAAvHh9/AAAAAfv169wweGDwweGDwweGDvHr9+AAAAAfgF6BwweGDwweGDwweGDvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); @@ -25,7 +25,7 @@ var imgSun = E.toArrayBuffer(atob("Ig8BwHf7D7Ac/MHD/z8wMP/PzMQ/8/M/D/z8z8QPf7f6A // define icons var imgSep = E.toArrayBuffer(atob("BhsBAAAAAA///////////////AAAAAAA")); -var imgPercent = E.toArrayBuffer(atob("BwcBuq7ffbqugA==")); +//var imgPercent = E.toArrayBuffer(atob("BwcBuq7ffbqugA==")); var img24hr = E.toArrayBuffer(atob("EwgBj7vO53na73tcDtu9uDev7vA93g==")); var imgPM = E.toArrayBuffer(atob("EwgB+HOfdnPu1X3ar4dV9+q+/bfftg==")); diff --git a/apps/93dub/metadata.json b/apps/93dub/metadata.json index 524780792..4b0b7bd21 100644 --- a/apps/93dub/metadata.json +++ b/apps/93dub/metadata.json @@ -3,7 +3,7 @@ "shortName":"93 Dub", "icon": "93dub.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.06", + "version": "0.07", "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", "tags": "clock", "type": "clock", diff --git a/apps/BLEcontroller/ChangeLog b/apps/BLEcontroller/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/BLEcontroller/ChangeLog +++ b/apps/BLEcontroller/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/BLEcontroller/app-ex2.js b/apps/BLEcontroller/app-ex2.js index 27e629d5d..8076fc357 100644 --- a/apps/BLEcontroller/app-ex2.js +++ b/apps/BLEcontroller/app-ex2.js @@ -25,7 +25,7 @@ declare global variables for the toggle button statuses; if you add an additional toggle button you should declare it and initiase it here */ -var status_spk = {value: true}; +//var status_spk = {value: true}; var status_face = {value: true}; var status_iris_light = {value: false}; var status_iris = {value: false}; diff --git a/apps/BLEcontroller/metadata.json b/apps/BLEcontroller/metadata.json index bb28b2360..de3a58ba1 100644 --- a/apps/BLEcontroller/metadata.json +++ b/apps/BLEcontroller/metadata.json @@ -2,7 +2,7 @@ "id": "BLEcontroller", "name": "BLE Customisable Controller with Joystick", "shortName": "BLE Controller", - "version": "0.01", + "version": "0.02", "description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.", "icon": "BLEcontroller.png", "tags": "tool,bluetooth", diff --git a/apps/Tyreid/ChangeLog b/apps/Tyreid/ChangeLog new file mode 100644 index 000000000..2f792c8de --- /dev/null +++ b/apps/Tyreid/ChangeLog @@ -0,0 +1 @@ +0.01: Change log created diff --git a/apps/Tyreid/README.md b/apps/Tyreid/README.md new file mode 100644 index 000000000..5c205ab57 --- /dev/null +++ b/apps/Tyreid/README.md @@ -0,0 +1,18 @@ +Tyreid + +Tyreid is a Bluetooth war-driving app for the Bangle.js 2. + +Menu options: +- Start: This turns on the Bluetooth and starts logging Bluetooth packets with time, latitude, and longitude information to a CSV file. +- Pause/Continue: These functions pause the capture and then allow it to resume. +- Devices: When paused this menu option will display the MAC addresses of discovered Bluetooth devices. Selecting a device will then display the MAC, Manufacturer code, the time it was first seen, and the RSSI of the first sighting. +- Marker: This command adds a 'marker' to the CSV log, which consists of the time and location information, but the Bluetooth packet information is replaced with the word MARKER. Markers can also be added by pressing the watch's button. +- Exit: This exits the app. + +The current number of discovered devices is displayed in the top left corner. +This value is displayed in green when the GPS has a fix, or red otherwise. + +To retrieve the CSV file, connect to the watch through the Espruino web IDE (https://www.espruino.com/ide/). From there the files stored on the watch can be downloaded by clicking the storage icon in the IDE's central column. + + + diff --git a/apps/Tyreid/app-icon.js b/apps/Tyreid/app-icon.js new file mode 100644 index 000000000..503a87b59 --- /dev/null +++ b/apps/Tyreid/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAB/QAAAAAAAAAAAAAD/0AAAAAAAAAAAAAH+9AAAAAAAAAAAAAPtfQAAAAAAAAAAAAudH0AAAAAAAAAAAB8tB9AAAAAAAAAAAD0tAfQAAAAAAAAAAHgtAH0AAAAAAAAAAPAtAB9AAAAAAAAAAtAtAAfQAAAAAAAAB8AtAAH0AAAAAAAAD0AtAAB9AAAAAAAALgAtAAAfQAAAAAAAfAAtAAAH0AAAAAAA9AAtAAAB9AAAAAAC4AAtAAAAfQAAAAADwAAtAAAALwAAAAAAQAAtAAAAvgAAAAAAAAAsAAAC9AAAAAAAAAAsAAAP0AAAAAAAAAAsAAB/AAAAAAAAAAAsAAH4AAAAAAAAAAAsAAvQAAAAAAAAAAAsAD9AAAAAAAAAAAAsAD0AAAAAAAAAAAAsAB8AAAAAAAAAAAAsAAtAAAAAAAAAAAAsAAPQAAAAAAAAAAAsAAHwAAAAAAAAAAAsAAC4AAAAAAAAAAAsAAA9AAAAAAAAAAAsAAAPAAAAAAAAAAAsAAAHgAAAAAAAAAA8AAAC0AAAAAAAAAA8AAAA8AAAAAAAAAA8AAAAEAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) diff --git a/apps/Tyreid/app.js b/apps/Tyreid/app.js new file mode 100644 index 000000000..8ab63cabd --- /dev/null +++ b/apps/Tyreid/app.js @@ -0,0 +1,272 @@ +//---------------------------- Tyreid ----------------------------// +// +// Bluetooth war-driving app for the Bangle.js 2 +// +// TH10111 2023 +// + +Bangle.loadWidgets(); +Bangle.drawWidgets(); // <-- for development only (shouldn't need for a real app) + +// Global variables +var gpsFix_flag = 0; +var bt_id_arr = []; +var num_bt_devices = 0; +var running_flag = 0; // 0 = stopped, 1 = running, 2 = paused + +// Log file +var file = require("Storage").open("tyreid_log.csv","w"); + +// Logo +var logo = { + width : 176, height : 176, bpp : 2, + buffer : atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC////QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf////QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv/////QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///v//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///+f//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+/4f//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//b/gf//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/8v+Af//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//C/4Af//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4L/gAf//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC//Av+AAb//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/0C/0AAb//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AP/QAAb//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wA/9AAAb//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9AD/0AAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv/gAP/QAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AA/9AAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//QAD/0AAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/8AAP/QAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAA/9AAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AAD/0AAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAP/QAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/0AAA/9AAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/+AAAD/0AAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAAP/QAAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9AAAA/9AAAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv/wAAAD/0AAAAAAAL//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAP/QAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAA/9AAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAD/0AAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//QAAAAP/QAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/4AAAAA/9AAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAAAD/0AAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/0AAAAAP/QAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC//AAAAAA/9AAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAD/0AAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9AAAAAAP/QAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAAAAA/9AAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9AAAAAAD/0AAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv/gAAAAAAP/QAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAA/9AAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//QAAAAAAD/0AAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/8AAAAAAAP/QAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAAAAAA/8AAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AAAAAAAD/wAAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAAAAAAP/AAAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/0AAAAAAAA/8AAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAD/wAAAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAAAAAAAP/AAAAAAAAAAAAAAAH//QAAAAAAAAAAAAAAAAAAAAAAAAAAAv9AAAAAAAAA/8AAAAAAAAAAAAAAAv/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAAD/wAAAAAAAAAAAAAAP//0AAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAP/AAAAAAAAAAAAAAH//+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAAAAAAB///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAAAAAv//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAAAAAAAP//5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAAAAAH//9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAAAB///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAAAAAAv//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAAAAL//5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAAD//9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAAAAB///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAAAf//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAL//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAAAD//9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAAB///QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAf//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAAH//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAC//9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAABv/+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAAf//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAL//4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAH//9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAv/+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAC//kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAH/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AAAAAAH/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAL/5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAf/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAf/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAv/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAv/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAB//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAB//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAC/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAG/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAH/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAH/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAL/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAf/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAf/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAv/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAv/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAB//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAC/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAH/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAH/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAL/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAL/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAf/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAv/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAv/QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAC/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/8AAAAAAAAAAAAAH/0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGqQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") +}; + + +// +// Functions +// + +function gpsTick(fix) { // GPS fix callback + //console.log(fix); // print GPS Fix info to console + if (fix.fix > 0) { + gpsFix_flag = 1; + } else { + gpsFix_flag = 0; + } + WIDGETS["widTyreid"].draw(WIDGETS["widTyreid"]); // update widget with gps tick + Bangle.drawWidgets(); // Not sure why I need this here, but otherwise the widget draws over itself without clearing first! +} + +function gpsON() { // Turn GPS on + Bangle.on('GPS',gpsTick); + Bangle.setGPSPower(true); +} + +function gpsOFF() { // Turn GPS off + Bangle.setGPSPower(false); +} + +function btPacket(packet) { // BT packet callback + let latest_fix = Bangle.getGPSFix(); + let mac = packet.id.substring(0,17); + let mac_info = packet.id.substring(18); + +// console.log(" "); +// console.log(packet); // print packet info + //console.log(mac); + //console.log(5*latest_fix.hdop); // print latest GPS fix info +// console.log(" "); + + // Compile the values to be stored + var new_data = [ + latest_fix.time, + latest_fix.lat, + latest_fix.lon, + latest_fix.alt, + latest_fix.hdop*5, + packet.name, + mac, + mac_info, + packet.manufacturer, + packet.rssi, + packet.services, + packet.data, + packet.serviceData, + packet.manufacturerData + ]; + // Write data to the file (including a new line) + file.write(new_data.join(",")+"\n"); + + if (num_bt_devices < 99) { + if (!bt_id_arr.includes(mac)) { // if device id has not been recorded + bt_id_arr[num_bt_devices] = mac; // note the id + num_bt_devices++; // increment the number of devices found + // Add the new device to the devices menu, with information... + //console.log(" "); + //console.log(mac); + //console.log(packet.manufacturer); + //console.log(latest_fix.time); + //console.log(packet.rssi); + //console.log(" "); + // Add the new device to the devices menu, with information... + device_menu[mac] = () => { + E.showPrompt([mac,mac_info],{title:"MAC",buttons:{"Ok":true}}).then(function(v){ + E.showPrompt(packet.manufacturer,{title:"Manufacturer",buttons:{"Ok":true}}).then(function(v){ + E.showPrompt(latest_fix.time,{title:"First Seen",buttons:{"Ok":true}}).then(function(v){ + E.showPrompt(packet.rssi,{title:"RSSI",buttons:{"Ok":true}}).then(function(v){ + E.showMenu(device_menu); + }); + }); + }); + }); + }; + } + } +} + +function headings() { // Add headings to the file + // Compile the values to be stored + var new_data = [ + "Time", + "Latitude", + "Longitude", + "Altitude", + "Accuracy", + "Name", + "MAC", + "MAC Info", + "Manufacturer", + "RSSI", + "Services", + "Data", + "Service Data", + "Manufacturer Data" + ]; + // Write data to the file (including a new line) + file.write(new_data.join(",")+"\n"); +} + +function btON() { // Turn Bluetooth on + NRF.setScan(btPacket); +} + +function btOFF() { // Turn Bluetooth off + NRF.setScan(); +} + +function start() { // Start the application + bt_id_arr = []; + headings(); + num_bt_devices = 0; + btON(); + running_flag = 1; + E.showMenu(running_menu); +} + +function exit() { // Exit the application + gpsOFF(); + btOFF(); + running_flag = 0; + load(); +} + +function pause() { // Pause the application + btOFF(); + running_flag = 2; + //console.log(bt_id_arr); + E.showMenu(pause_menu); +} + +function resume() { // Continue after pause + btON(); + running_flag = 1; + E.showMenu(running_menu); +} + +function marker() { // add a marker packet to the log + let latest_fix = Bangle.getGPSFix(); + +// console.log(" "); +// console.log("MARKER"); // print packet info +// console.log(" "); + + // Compile the values to be stored + var new_data = [ + latest_fix.time, + latest_fix.lat, + latest_fix.lon, + latest_fix.alt, + latest_fix.hdop, + "MARKER" + ]; + + // Write data to the file (including a new line) + file.write(new_data.join(",")+"\n"); + + // Indicate that the marker has been added + E.showMessage("Marker Added."); + + // Back to the menu + if (running_flag == 0) { + E.showMenu(init_menu); + } else if (running_flag == 1) { + E.showMenu(running_menu); + } else { + E.showMenu(pause_menu); + } +} + +setWatch(() => { // If the button is pressed, then add a marker + marker(); +}, BTN1, {repeat:true}); + +WIDGETS["widTyreid"]={ + area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + width: 24, // width of the widget + draw: function() { + let disp_dev_val = "-"; + + if (num_bt_devices < 99) { + disp_dev_val = num_bt_devices.toString(); + } else { + disp_dev_val = "99+"; + } + + g.setFont("6x8",3); + if (gpsFix_flag == 1) { + g.setColor(0,1,0).drawString(disp_dev_val, this.x+24/2, this.y); // green + } else { + g.setColor(1,0,0).drawString(disp_dev_val, this.x+24/2, this.y); // red + } + } +}; + + +let init_menu = { + "": { "title": "Tyreid" }, + "Start": function() { start(); }, + "Marker": function() { marker(); }, + "Exit": function() { exit(); }, +}; + +let running_menu = { + "": { "title": "[Running]" }, + "Pause": function() { pause(); }, + "Marker": function() { marker(); }, + "Exit": function() { exit(); }, +}; + +let pause_menu = { + "": { "title": "[Paused]" }, + "Continue": function() { resume(); }, + "Devices": function() { E.showMenu(device_menu); }, + "Marker": function() { marker(); }, + "Exit": function() { exit(); }, +}; + +let device_menu = { + "": { "title": "Devices:" }, + "Back": function() { + if (running_flag == 0) { + E.showMenu(init_menu); + } else if (running_flag == 1) { + E.showMenu(running_menu); + } else { + E.showMenu(pause_menu); + } + }, +}; + + +// Main +gpsON(); // turn GPS on straight away to start trying for a fix +g.setColor(1,1,1).fillRect(0,0,176,176); +g.drawImage(logo,0,0); // splash screen +// 2sec wait before starting initial menu +setTimeout(function () { + E.showMenu(init_menu); +}, 2*1000); + + diff --git a/apps/Tyreid/metadata.json b/apps/Tyreid/metadata.json new file mode 100644 index 000000000..4663287e8 --- /dev/null +++ b/apps/Tyreid/metadata.json @@ -0,0 +1,14 @@ +{ "id": "Tyreid", + "name": "Tyreid", + "shortName":"Tyreid", + "version":"0.01", + "description": "Bluetooth war-driving app for Bangle.js 2", + "icon": "small_logo.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"Tyreid.app.js","url":"app.js"}, + {"name":"Tyreid.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/Tyreid/small_logo.png b/apps/Tyreid/small_logo.png new file mode 100644 index 000000000..8b705cd3f Binary files /dev/null and b/apps/Tyreid/small_logo.png differ diff --git a/apps/UI4swatch/Changelog b/apps/UI4swatch/ChangeLog similarity index 57% rename from apps/UI4swatch/Changelog rename to apps/UI4swatch/ChangeLog index d81132fb6..e6883d3d6 100644 --- a/apps/UI4swatch/Changelog +++ b/apps/UI4swatch/ChangeLog @@ -1 +1,2 @@ 0.01: 1st ver, defining a common UI/UX +0.02: Minor code improvements diff --git a/apps/UI4swatch/app.js b/apps/UI4swatch/app.js index 8cf3891b4..af1f5e3e8 100644 --- a/apps/UI4swatch/app.js +++ b/apps/UI4swatch/app.js @@ -5,7 +5,7 @@ identify device and dimensions max printable position max_x-1 i.e 239 */ - var colbackg='#111111';//black + //var colbackg='#111111';//black var colorange='#e56e06'; //RGB format rrggbb var v_color_lines=0xFFFF; //White hex format var v_color_b_area='#111111'; @@ -13,13 +13,13 @@ max printable position max_x-1 i.e 239 var v_font1size=10; //out of quotes var v_font2size=12; var v_font_banner_size=30; - var v_clicks=0; + //var v_clicks=0; //pend identify widget area dinamically 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') { diff --git a/apps/UI4swatch/metadata.json b/apps/UI4swatch/metadata.json index 379d173c3..5b4abf406 100644 --- a/apps/UI4swatch/metadata.json +++ b/apps/UI4swatch/metadata.json @@ -2,10 +2,10 @@ "id": "UI4swatch", "name": "UI 4 swatch", "shortName": "UI 4 swatch", - "version": "0.01", + "version": "0.02", "description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.", "icon": "app.png", - "tags": "Color,input,buttons,touch,UI", + "tags": "color,input,buttons,touch,ui", "supports": ["BANGLEJS"], "readme": "README.md", "screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}], diff --git a/apps/Uke/ChangeLog b/apps/Uke/ChangeLog new file mode 100644 index 000000000..56fcafa19 --- /dev/null +++ b/apps/Uke/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Increased Legibility, GUI rework +0.03: 13 new chords +0.04: Minor code improvements diff --git a/apps/Uke/README.md b/apps/Uke/README.md new file mode 100644 index 000000000..b6236e307 --- /dev/null +++ b/apps/Uke/README.md @@ -0,0 +1,12 @@ +# Uke Chords + +An app that simply describes finger placements on a Ukulele to form common chords. + +## Usage + +Select a chord to view. +Use the button to return to the chord selection menu. + +## Creator + +NovaDawn999 diff --git a/apps/Uke/app-icon.js b/apps/Uke/app-icon.js new file mode 100644 index 000000000..17b77ab32 --- /dev/null +++ b/apps/Uke/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgtqiAXWiMRDKsBolBCqcQilEoQwTiMUoMkkJGUiQwUFwVCGCcUoVEkJ5SgJ2CAQMROyIsBoVDoIXQgMSiJ2EPB4uBdwMieCMBCoIZCDoJdQAAMSUYUBLqIXBIhxCBCAJdDIZwPBTgIAEFxrOCAAIuTCwVELoQuToIuRgIuDCoUiFxjNCFwq7BC5YWBFoZdDAQIXLCwpdEogXKLYgWBXYZ9BC5SKDCwQYCkIHBC5IuFFQIYBiQhCC5JdFCoIYBBIYXJIwlEFwUUBIYXOLgIYDA4ReJC4i4BI4RODOxj/CAQIyBFwSOMoIYCagQ4BCxQXEigrBiS7CLpRHGAIMiMwYXMQoYwCSogXKU4gwCC6gwCC6ApEUoIFDRxR4Fd4QXReAgcEC5hIFLyAwJFxwwIiIWODATbDCyIYCAAQWSACY")) diff --git a/apps/Uke/app.js b/apps/Uke/app.js new file mode 100644 index 000000000..975ca8aaa --- /dev/null +++ b/apps/Uke/app.js @@ -0,0 +1,259 @@ +const stringInterval = 30; +const stringLength = 131; +const fretHeight = 35; +const fingerOffset = 17; +const x = 44; +const y = 32; + +//chords +const cc = [ + "C", + "x", + "x", + "x", + "33" +]; + +const dd = [ + "D", + "22", + "23", + "24", + "x" +]; + +var ee = [ + "E", + "33", + "32", + "34", + "11" +]; + +const ff = [ + "F", + "22", + "x", + "11", + "x" +]; + +const gg = [ + "G", + "x", + "21", + "33", + "22", +]; + +const aa = [ + "A", + "22", + "11", + "x", + "x" +]; + +const bb = [ + "B", + "42", + "43", + "44", + "21" +]; + +const cm = [ + "Cm", + "11", + "x", + "12", + "34" +]; + +const dm = [ + "Dm", + "x", + "22", + "33", + "11" +]; + +const em = [ + "Em", + "x", + "43", + "32", + "21" +]; + +const fm = [ + "Fm", + "33", + "11", + "11", + "11" +]; + +const gm = [ + "Gm", + "x", + "22", + "33", + "11" +]; + +const am = [ + "Am", + "22", + "23", + "11", + "x" +]; + +const bm = [ + "Bm", + "x", + "43", + "32", + "21" +]; + +const c7 = [ + "C7", + "22", + "33", + "11", + "x" +]; + +const d7 = [ + "D7", + "x", + "22", + "11", + "23" +]; + +const e7 = [ + "E7", + "x", + "11", + "x", + "x" +]; + +const f7 = [ + "F7", + "11", + "22", + "11", + "11" +]; + +const g7 = [ + "G7", + "x", + "x", + "x", + "11" +]; + +const a7 = [ + "A7", + "21", + "21", + "21", + "32" +]; + +const b7 = [ + "B7", + "11", + "22", + "x", + "23" +]; + + + +var menu = { + "" : { "title" : "Uke Chords" }, + "C" : function() { draw(cc); }, + "D" : function() { draw(dd); }, + "E" : function() { draw(ee); }, + "F" : function() { draw(ff); }, + "G" : function() { draw(gg); }, + "A" : function() { draw(aa); }, + "B" : function() { draw(bb); }, + "C7" : function() { draw(c7); }, + "D7" : function() { draw(d7); }, + "E7" : function() { draw(e7); }, + "F7" : function() { draw(f7); }, + "G7" : function() { draw(g7); }, + "A7" : function() { draw(a7); }, + "B7" : function() { draw(b7); }, + "Cm" : function() { draw(cm); }, + "Dm" : function() { draw(dm); }, + "Em" : function() { draw(em); }, + "Fm" : function() { draw(fm); }, + "Gm" : function() { draw(gm); }, + "Am" : function() { draw(am); }, + "Bm" : function() { draw(bm); }, + "About" : function() { + E.showMessage( + "Created By:\nNovaDawn999", { + title:"About" + } + ); + } +}; + + + +function drawBase() { + for (let i = 0; i < 4; i++) { + g.drawLine(x + i * stringInterval, y, x + i * stringInterval, y + stringLength); + g.fillRect(x- 1, y + i * fretHeight - 1, x + stringInterval * 3 + 1, y + i * fretHeight + 1); + } +} + +function drawChord(chord) { + g.drawString(chord[0], g.getWidth() * 0.5 - (chord[0].length * 5), 16); + for (let i = 0; i < chord.length; i++) { + if (i === 0 || chord[i][0] === "x") { + continue; + } + if (chord[i][0] === "0") { + g.drawString(chord[i][1], x + (i - 1) * stringInterval - 5, y + fretHeight * chord[i][0] + 2, true); + g.drawCircle(x + (i - 1) * stringInterval -1, y + fretHeight * chord[i][0], 10); + } + else { + g.drawString(chord[i][1], x + (i - 1) * stringInterval -5, y -fingerOffset + fretHeight * chord[i][0] + 2, true); + g.drawCircle(x + (i - 1) * stringInterval -1, y -fingerOffset + fretHeight * chord[i][0], 10); + } + } +} + +function buttonPress() { + setWatch(() => { + buttonPress(); + }, BTN); + E.showMenu(menu); +} + +function draw(chord) { + g.clear(); + drawBase(); + drawChord(chord); +} + + + +function main() { + E.showMenu(menu); + setWatch(() => { + buttonPress(); + }, BTN); +} + +main(); diff --git a/apps/Uke/app.png b/apps/Uke/app.png new file mode 100644 index 000000000..7a6dd67ea Binary files /dev/null and b/apps/Uke/app.png differ diff --git a/apps/Uke/metadata.json b/apps/Uke/metadata.json new file mode 100644 index 000000000..1185c7202 --- /dev/null +++ b/apps/Uke/metadata.json @@ -0,0 +1,14 @@ +{ "id": "Uke", + "name": "Uke Chords", + "shortName":"Uke", + "version": "0.04", + "description": "Wrist mounted ukulele chords", + "icon": "app.png", + "tags": "uke,chords", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"Uke.app.js","url":"app.js"}, + {"name":"Uke.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/_example_app/README.md b/apps/_example_app/README.md index dc139bc9a..eff3919c6 100644 --- a/apps/_example_app/README.md +++ b/apps/_example_app/README.md @@ -1,5 +1,10 @@ # App Name +More information on making apps: + +* http://www.espruino.com/Bangle.js+First+App +* http://www.espruino.com/Bangle.js+App+Loader + Describe the app... Add screen shots (if possible) to the app folder and link then into this file with ![](.png) diff --git a/apps/_example_app/metadata.json b/apps/_example_app/metadata.json index e0d664338..42dfca2b8 100644 --- a/apps/_example_app/metadata.json +++ b/apps/_example_app/metadata.json @@ -5,7 +5,7 @@ "description": "A detailed description of my great app", "icon": "app.png", "tags": "", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"7chname.app.js","url":"app.js"}, diff --git a/apps/_example_clkinfo/ChangeLog b/apps/_example_clkinfo/ChangeLog new file mode 100644 index 000000000..78ba28f3b --- /dev/null +++ b/apps/_example_clkinfo/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock Info! diff --git a/apps/_example_clkinfo/README.md b/apps/_example_clkinfo/README.md new file mode 100644 index 000000000..3d5970e39 --- /dev/null +++ b/apps/_example_clkinfo/README.md @@ -0,0 +1,27 @@ +# Clock Info Name + +More info on making Clock Infos and what they are: http://www.espruino.com/Bangle.js+Clock+Info + +Describe the clock info... + +Add screen shots (if possible) to the app folder and link then into this file with ![](.png) + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_clkinfo/clkinfo.js b/apps/_example_clkinfo/clkinfo.js new file mode 100644 index 000000000..b653709ce --- /dev/null +++ b/apps/_example_clkinfo/clkinfo.js @@ -0,0 +1,16 @@ +(function() { + return { + name: "Bangle", + // img: 24x24px image for this list of items. The default "Bangle" list has its own image so this is not needed + items: [ + { name : "Item1", + get : function() { return { text : "TextOfItem1", + // v : 10, min : 0, max : 100, - optional + img : atob("GBiBAAAAAAAAAAAYAAD/AAOBwAYAYAwAMAgAEBgAGBAACBCBCDHDjDCBDBAACBAACBhCGAh+EAwYMAYAYAOBwAD/AAAYAAAAAAAAAA==") }}, + show : function() {}, + hide : function() {}, + // run : function() {} optional (called when tapped) + } + ] + }; +}) // must not have a semi-colon! \ No newline at end of file diff --git a/apps/_example_widget/widget.png b/apps/_example_clkinfo/icon.png similarity index 100% rename from apps/_example_widget/widget.png rename to apps/_example_clkinfo/icon.png diff --git a/apps/_example_clkinfo/metadata.json b/apps/_example_clkinfo/metadata.json new file mode 100644 index 000000000..83b8184d8 --- /dev/null +++ b/apps/_example_clkinfo/metadata.json @@ -0,0 +1,14 @@ +{ "id": "7chname", + "name": "My clock info's human readable name", + "shortName":"Short Name", + "version":"0.01", + "description": "A detailed description of my clock info", + "icon": "icon.png", + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"7chname.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/_example_clock/ChangeLog b/apps/_example_clock/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/_example_clock/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/_example_clock/README.md b/apps/_example_clock/README.md new file mode 100644 index 000000000..5d750a965 --- /dev/null +++ b/apps/_example_clock/README.md @@ -0,0 +1,25 @@ +# Clock Name + +More info on making Clock Faces: https://www.espruino.com/Bangle.js+Clock + +Describe the Clock... + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_clock/app-icon.js b/apps/_example_clock/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/_example_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/_example_clock/app.js b/apps/_example_clock/app.js new file mode 100644 index 000000000..a5d114b3a --- /dev/null +++ b/apps/_example_clock/app.js @@ -0,0 +1,50 @@ +{ + // timeout used to update every minute + let drawTimeout; + + // schedule a draw for the next minute + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function() { + // queue next draw in one minute + queueDraw(); + // Work out where to draw... + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date); + // draw time + g.setFontAlign(0,0).setFont("Vector",48); + g.clearRect(0,y-20,g.getWidth(),y+25); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 30; + g.setFontAlign(0,0).setFont("6x8"); + g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background + g.drawString(dateStr,x,y); + }; + + // Clear the screen once, at startup + g.clear(); + // draw immediately at first, queue update + draw(); + + // Show launcher when middle button pressed + Bangle.setUI({mode:"clock", remove:function() { + // free any memory we allocated to allow fast loading + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + }}); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} \ No newline at end of file diff --git a/apps/_example_clock/icon.png b/apps/_example_clock/icon.png new file mode 100644 index 000000000..582cb2e08 Binary files /dev/null and b/apps/_example_clock/icon.png differ diff --git a/apps/_example_clock/metadata.json b/apps/_example_clock/metadata.json new file mode 100644 index 000000000..d9150af3c --- /dev/null +++ b/apps/_example_clock/metadata.json @@ -0,0 +1,16 @@ +{ "id": "7chname", + "name": "My clock human readable name", + "shortName":"Short Name", + "version":"0.01", + "description": "A detailed description of my clock", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"7chname.app.js","url":"app.js"}, + {"name":"7chname.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/_example_clock/screenshot.png b/apps/_example_clock/screenshot.png new file mode 100644 index 000000000..32d910ef5 Binary files /dev/null and b/apps/_example_clock/screenshot.png differ diff --git a/apps/_example_widget/README.md b/apps/_example_widget/README.md index a909e9e7e..c786d3be8 100644 --- a/apps/_example_widget/README.md +++ b/apps/_example_widget/README.md @@ -1,5 +1,7 @@ # Widget Name +More info on making Widgets and what they are: http://www.espruino.com/Bangle.js+Widgets + Describe the app... Add screen shots (if possible) to the app folder and link then into this file with ![](.png) diff --git a/apps/_example_widget/icon.png b/apps/_example_widget/icon.png new file mode 100644 index 000000000..582cb2e08 Binary files /dev/null and b/apps/_example_widget/icon.png differ diff --git a/apps/_example_widget/metadata.json b/apps/_example_widget/metadata.json index ad4b7537d..8dc7c75c6 100644 --- a/apps/_example_widget/metadata.json +++ b/apps/_example_widget/metadata.json @@ -3,7 +3,7 @@ "shortName":"Short Name", "version":"0.01", "description": "A detailed description of my great widget", - "icon": "widget.png", + "icon": "icon.png", "type": "widget", "tags": "widget", "supports" : ["BANGLEJS2"], diff --git a/apps/_example_widget/widget.js b/apps/_example_widget/widget.js index f7aed6991..198be7b07 100644 --- a/apps/_example_widget/widget.js +++ b/apps/_example_widget/widget.js @@ -1,16 +1,14 @@ -/* run widgets in their own function scope so they don't interfere with -currently-running apps */ +/* run widgets in their own function scope if they need to define local +variables so they don't interfere with currently-running apps */ (() => { - function draw() { - g.reset(); // reset the graphics context to defaults (color/font/etc) - // add your code - g.drawString("X", this.x, this.y); - } - // add your widget WIDGETS["mywidget"]={ - area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right), be aware that not all apps support widgets at the bottom of the screen width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout - draw:draw // called to draw the widget + draw:function() { + g.reset(); // reset the graphics context to defaults (color/font/etc) + // add your code + g.drawString("X", this.x, this.y); + } // called to draw the widget }; })() diff --git a/apps/a_clock_timer/ChangeLog b/apps/a_clock_timer/ChangeLog index c01ad2077..0e717fee7 100644 --- a/apps/a_clock_timer/ChangeLog +++ b/apps/a_clock_timer/ChangeLog @@ -1 +1,5 @@ 0.01: Beta version for Bangle 2 (2021/11/28) +0.02: Shows night time on the map (2022/12/28) +0.03: Add 1 minute timer with upper taps (2023/01/05) +1.00: Page to set up custom time zones (2023/01/06) +1.01: Minor code improvements diff --git a/apps/a_clock_timer/README.md b/apps/a_clock_timer/README.md index e8e2647a9..4e199344a 100644 --- a/apps/a_clock_timer/README.md +++ b/apps/a_clock_timer/README.md @@ -2,14 +2,17 @@ * Works with Bangle 2 * Timer - * Right tap: start/increase by 10 minutes; Left tap: decrease by 5 minutes + * Top Right tap: increase by 1 minute + * Top Left tap: decrease by 1 minute + * Bottom Right tap: increase by 10 minutes + * Bottom Left tap: decrease by 5 minutes * Short buzz at T-30, T-20, T-10 ; Double buzz at T * Other time zones - * Currently hardcoded to Paris and Tokyo (this will be customizable in a future version) + * Showing Paris and Tokyo by default, but you can customize this using the dedicated configuration page on the app store * World Map - * The yellow line shows the position of the sun + * The map shows day and night on Earth and the position of the Sun (yellow line) -![](screenshot.png) +![](screenshot-1.png) ![](screenshot.png) ## Creator [@alainsaas](https://github.com/alainsaas) diff --git a/apps/a_clock_timer/app.js b/apps/a_clock_timer/app.js index 5f9a3a468..6cb56c67f 100644 --- a/apps/a_clock_timer/app.js +++ b/apps/a_clock_timer/app.js @@ -2,7 +2,7 @@ function getImg() { return require("heatshrink").decompress(atob("2FRgP/ABnxBRP5BJH+gEfBZHghnAv4JFmA+Bj0PBIn3//4h3An4oDAQJWEEIf8AwMEuFOCofAh/QjAWEg4VEwEAnw2DDoKEHEYPwAoUBmgrDhgUHS4XgAwUD/gVC/g+FAAZgEwEf4YqC/EQFQ4NDFgV/4Z3C/EcCo1974VCLAV/V4d7Co9/Co0PCoX+vk4Ko/HCosCRYX5nwTFkEAr/nCokICoL+B/aCGCoMHCoq3EdoraGCosPz4HBcILEJCocBwEHOwQrIgQrHgoHCFYMEgwVJYoMBsEnCofAnkMNQJXH4D4EbQMPkF/xwrEj+/HIkAoAVDj8QueHCoorDCoUDLwd96J0BKwgrHh4VDv+9CosDx6QCCo4HB//8VwvvXgQVDJIYSBCo/sBwaZBgF/NoYVHgH8V4qYDAwUYlAVFEYbFDDgwAGConogf9Zg8DCpP4cIh0Dg0BGAgVE+gVIgUA+AVI+wVE/xAEh5HDEgn+CpEAbgJCCHQoVBn4VJ/ED4ANDAAQVJ4EPPQPAt4VF4BeDColgj/8h/gFYwJBCpF//k//ANDCAYVIcgP+CpH/54VHCAIVB/4VIwYECCocIAwIVBx4VG9+AMITbCYAYJB34VG/UAj4VI7/9Cgw9CJYXAmBtDMAQsIfYhvCCofyvywGB4QFFgYGC/d+agYVLSgf8+ArG/APBD4QVBgh0CAwNwv/fCo4PCCo94s7VDCohnDAoI7Enlv8BZECoRCDAggAB3/3/gzDMAIVFY4IVE4IPBOoZ9DCpXwCoMvCqKxB//3bYywD4BtFAAPfDooVFFYIVGw4VFB4KZFngNE/uPCovgFYgEBuK+Fg4zFCoIrFCovwgQVF+AVFgPxEYzFEbgQVD4EDCoozBYogVCgYVE8bpGCo4HDCoPzBgoVIL4fAg4MGgAIHCofgCszND8BOHK4x2BCofwXgv4h6vGCps/Co6uDAA/7RgIjDDwTaDABPA//9FaAtDCop0FC5YVDLwoAH8//94GD/wVNCYKNECpwPBQggVPNggVBNp4VFFZwAGCquHCqnzCB4")); } -var IMAGEWIDTH = 176; +//var IMAGEWIDTH = 176; var IMAGEHEIGHT = 81; Graphics.prototype.setFontMichroma36 = function() { @@ -18,19 +18,29 @@ var timervalue = 0; var istimeron = false; var timertick; -Bangle.on('touch',t=>{ - if (t == 1) { +Bangle.on('touch',(touchside, touchdata)=>{ + if (touchside == 1) { Bangle.buzz(30); - if (timervalue < 5*60) { timervalue = 1 ; } - else { timervalue -= 5*60; } + var changevalue = 0; + if(touchdata.y > 88) { + changevalue += 60*5; + } else { + changevalue += 60*1; + } + if (timervalue < changevalue) { timervalue = 1 ; } + else { timervalue -= changevalue; } } - else if (t == 2) { + else if (touchside == 2) { Bangle.buzz(30); if (!istimeron) { istimeron = true; timertick = setInterval(countDown, 1000); } - timervalue += 60*10; + if(touchdata.y > 88) { + timervalue += 60*10; + } else { + timervalue += 60*1; + } } }); @@ -73,12 +83,13 @@ function countDown() { function showWelcomeMessage() { g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); g.setFontAlign(0, 0).setFont("6x8"); - g.drawString("Touch right to", 44, 80); + g.drawString("Tap right to", 44, 80); g.drawString("start timer", 44, 88); setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000); } // time +var offsets = require("Storage").readJSON("a_clock_timer.settings.json") || [ ["PAR",1], ["TYO",9] ]; var drawTimeout; function getGmt() { @@ -102,20 +113,34 @@ function queueNextDraw() { function draw() { g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT); - - var x_sun = 176 - (getGmt().getHours() / 24 * 176 + 4); + + var gmtHours = getGmt().getHours(); + + var x_sun = 176 - (gmtHours / 24 * 176 + 4); g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight()); g.reset(); + var x_night_start = (176 - (((gmtHours-6)%24) / 24 * 176 + 4)) % 176; + var x_night_end = 176 - (((gmtHours+6)%24) / 24 * 176 + 4); + g.setColor('#000'); + for (let x = x_night_start; x < (x_night_end < x_night_start ? 176 : x_night_end); x+=2) { + g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight()); + } + if (x_night_end < x_night_start) { + for (let x = 0; x < x_night_end; x+=2) { + g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight()); + } + } + var locale = require("locale"); - + var date = new Date(); g.setFontAlign(0,0); g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46); g.setFont("6x8"); g.drawString(locale.date(new Date(),1), 125, 68); - g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80); - g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88); + g.drawString(offsets[0][0]+" "+locale.time(getTimeFromTimezone(offsets[0][1]),1), 125, 80); + g.drawString(offsets[1][0]+" "+locale.time(getTimeFromTimezone(offsets[1][1]),1), 125, 88); queueNextDraw(); } diff --git a/apps/a_clock_timer/custom.html b/apps/a_clock_timer/custom.html new file mode 100644 index 000000000..b62226340 --- /dev/null +++ b/apps/a_clock_timer/custom.html @@ -0,0 +1,58 @@ + + + + + +

You can set the 2 additional timezones displayed by the clock.

+ + + + + +
NameUTC Offset (Hours)
+

Click

+ + + + diff --git a/apps/a_clock_timer/metadata.json b/apps/a_clock_timer/metadata.json index cc61fc57b..1d3661501 100644 --- a/apps/a_clock_timer/metadata.json +++ b/apps/a_clock_timer/metadata.json @@ -1,17 +1,19 @@ { "id": "a_clock_timer", "name": "A Clock with Timer", - "version": "0.01", + "version": "1.01", "description": "A Clock with Timer, Map and Time Zones", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-1.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], "allow_emulator": true, "readme": "README.md", + "custom": "custom.html", "storage": [ {"name":"a_clock_timer.app.js","url":"app.js"}, {"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"a_clock_timer.settings.json"}] } diff --git a/apps/a_clock_timer/screenshot-1.png b/apps/a_clock_timer/screenshot-1.png new file mode 100644 index 000000000..ede6439de Binary files /dev/null and b/apps/a_clock_timer/screenshot-1.png differ diff --git a/apps/a_clock_timer/screenshot.png b/apps/a_clock_timer/screenshot.png index 4fb3dd9f2..893daa9d0 100644 Binary files a/apps/a_clock_timer/screenshot.png and b/apps/a_clock_timer/screenshot.png differ diff --git a/apps/a_dndtoggle/ChangeLog b/apps/a_dndtoggle/ChangeLog new file mode 100644 index 000000000..2d760f914 --- /dev/null +++ b/apps/a_dndtoggle/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Add settings page; Add line break to update message \ No newline at end of file diff --git a/apps/a_dndtoggle/README.md b/apps/a_dndtoggle/README.md new file mode 100644 index 000000000..c373bc872 --- /dev/null +++ b/apps/a_dndtoggle/README.md @@ -0,0 +1,16 @@ +# a_dndtoggle - Toggle Quiet Mode of the watch + +When Quiet mode is off, just start this app to set quiet mode. Start it again to turn off quiet mode. + +Use the app settings to choose which quiet mode you prefer ("Alarms" or "Silent"). Default is "Silent". + +Work in progress. + +#ToDo +Current status indicator + +## Creator + +Hank - contact at http://forum.espruino.com + + diff --git a/apps/a_dndtoggle/a_dndtoggle.app.js b/apps/a_dndtoggle/a_dndtoggle.app.js new file mode 100644 index 000000000..207034209 --- /dev/null +++ b/apps/a_dndtoggle/a_dndtoggle.app.js @@ -0,0 +1,46 @@ + +const modeNames = [/*LANG*/"Noisy", /*LANG*/"Alarms", /*LANG*/"Silent"]; +let bSettings = require('Storage').readJSON('setting.json',true)||{}; +let current = 0|bSettings.quiet; +//0 off +//1 alarms +//2 silent + +const dndSettings = + require('Storage').readJSON("a_dndtoggle.settings.json", true) || {}; + +console.log("old: " + current); + +switch (current) { + case 0: + bSettings.quiet = dndSettings.mode || 2; + Bangle.buzz(); + setTimeout('Bangle.buzz();',500); + break; + case 1: + bSettings.quiet = 0; + Bangle.buzz(); + break; + case 2: + bSettings.quiet = 0; + Bangle.buzz(); + break; + default: + bSettings.quiet = 0; + Bangle.buzz(); +} + +console.log("new: " + bSettings.quiet); + +E.showMessage(modeNames[current] + " -> \n" + modeNames[bSettings.quiet]); +setTimeout('exitApp();', 2000); + + +function exitApp(){ + +require("Storage").writeJSON("setting.json", bSettings); +// reload clocks with new theme, otherwise just wait for user to switch apps + +load() + +} \ No newline at end of file diff --git a/apps/a_dndtoggle/a_dndtoggle.png b/apps/a_dndtoggle/a_dndtoggle.png new file mode 100644 index 000000000..4c8b74c0c Binary files /dev/null and b/apps/a_dndtoggle/a_dndtoggle.png differ diff --git a/apps/a_dndtoggle/app-icon.js b/apps/a_dndtoggle/app-icon.js new file mode 100644 index 000000000..0b08cc65b --- /dev/null +++ b/apps/a_dndtoggle/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AAl/Agf/AAUAgIFDwEHAofgh/g/0Ag/wj+AnwVB/EegEfEIN4nkAh+AgE8vgVBAoV4Aoce/EAgfADQIFcjwpFHYIFCnxBFJopZBn5ZCMopxFPoqJFSowA/gA=")) \ No newline at end of file diff --git a/apps/a_dndtoggle/metadata.json b/apps/a_dndtoggle/metadata.json new file mode 100644 index 000000000..62f0edae9 --- /dev/null +++ b/apps/a_dndtoggle/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "a_dndtoggle", + "name": "a_dndtoggle - Toggle Quiet Mode of the watch", + "shortName": "A_DND Toggle", + "version": "0.02", + "description": "Toggle Quiet Mode of the watch just by starting this app.", + "icon": "a_dndtoggle.png", + "type": "app", + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "data" : [ + {"name":"a_dndtoggle.settings.json"} + ], + "storage": [ + {"name":"a_dndtoggle.app.js","url":"a_dndtoggle.app.js"}, + {"name":"a_dndtoggle.settings.js","url":"settings.js"}, + {"name":"a_dndtoggle.img","url":"app-icon.js","evaluate":true} + ], + "readme": "README.md" +} diff --git a/apps/a_dndtoggle/settings.js b/apps/a_dndtoggle/settings.js new file mode 100644 index 000000000..483af8c97 --- /dev/null +++ b/apps/a_dndtoggle/settings.js @@ -0,0 +1,32 @@ +(function(back) { + + const settings = + require('Storage').readJSON("a_dndtoggle.settings.json", true) || {}; + + function updateSettings() { + require('Storage').writeJSON("a_dndtoggle.settings.json", settings); + } + + function buildMainMenu(){ + // 0-Noisy is only a placeholder so that the other values map to the Bangle quiet mode options + const modes = [/*LANG*/"Noisy",/*LANG*/"Alarms",/*LANG*/"Silent"]; + let mainmenu = { + '': { 'title': 'A_DND Toggle' }, + '< Back': back, + /*LANG*/"Quiet Mode": { + value: settings.mode || 2, + min: 1, // don't allow choosing 0-Noisy + max: modes.length - 1, + format: v => modes[v], + onchange: v => { + settings.mode = v; + updateSettings(); + } + } + }; + + return mainmenu; + } + + E.showMenu(buildMainMenu()); + }) diff --git a/apps/a_speech_timer/ChangeLog b/apps/a_speech_timer/ChangeLog index b3aa9e0dd..d9b45a518 100644 --- a/apps/a_speech_timer/ChangeLog +++ b/apps/a_speech_timer/ChangeLog @@ -1,2 +1,4 @@ 1.00: Release (2021/12/01) 1.01: Grey font when timer is frozen (2021/12/04) +1.02: Force light theme, since the app is not designed for dark theme (2022/12/28) +1.03: Minor code improvements diff --git a/apps/a_speech_timer/app.js b/apps/a_speech_timer/app.js index 440cd92c6..6bb5a3edf 100644 --- a/apps/a_speech_timer/app.js +++ b/apps/a_speech_timer/app.js @@ -124,7 +124,6 @@ Bangle.on('swipe',(swiperight, swipedown)=>{ } }); -var drawTimeout; var showInstructions = true; function draw() { @@ -166,6 +165,7 @@ function draw() { g.drawRect(88+8,138-24, 176-10, 138+22); } +g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); require("FontHaxorNarrow7x17").add(Graphics); g.clear(); Bangle.loadWidgets(); diff --git a/apps/a_speech_timer/metadata.json b/apps/a_speech_timer/metadata.json index 6255a6b92..91262896c 100644 --- a/apps/a_speech_timer/metadata.json +++ b/apps/a_speech_timer/metadata.json @@ -2,7 +2,7 @@ "id":"a_speech_timer", "name":"Speech Timer", "icon": "app.png", -"version":"1.01", +"version": "1.03", "description": "A timer designed to help keeping your speeches and presentations to time.", "tags": "tool,timer", "readme":"README.md", diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index f5638fdd2..57e918ad2 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -10,3 +10,6 @@ 0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021) 0.11: Bangle.js2: New pixels, btn1 to exit 0.12: Actual pixels as of 29th Nov 2021 +0.13: Bangle.js 2: Use setUI to add software back button +0.14: Add automatic translation of more strings +0.15: Minor code improvements diff --git a/apps/about/app-bangle1.js b/apps/about/app-bangle1.js index 28a292376..dd94c1e84 100644 --- a/apps/about/app-bangle1.js +++ b/apps/about/app-bangle1.js @@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4); } else { y=-(4+h); // small screen, start right at top } -g.drawString("Powered by Espruino",0,y+=4+h); -g.drawString("Version "+ENV.VERSION,0,y+=h); +g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h); +g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); function getVersion(name,file) { var j = s.readJSON(file,1); @@ -24,9 +24,9 @@ getVersion("Launcher","launch.info"); getVersion("Settings","setting.info"); y+=h; -g.drawString(MEM.total+" JS Variables available",0,y+=h); -g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); -if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h); +g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h); +g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h); +if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h); g.setFontAlign(0,-1); g.flip(); diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 978d36193..a7aa0d466 100644 --- a/apps/about/app-bangle2.js +++ b/apps/about/app-bangle2.js @@ -10,7 +10,7 @@ var img = atob("sIwDkm2S66DYwA2AAAAAHAHGSRxJEkAAgmGGBxDIADIdAFJIbAHF9HP00kBUC6Dt var imgHeight = g.imageMetrics(img).height; var imgScroll = Math.floor(Math.random()*imgHeight); -g.reset().setFont("6x15").setFontAlign(0,0); +g.clear(1).setFont("6x15").setFontAlign(0,0); g.drawString(ENV.VERSION + " " + NRF.getAddress(), g.getWidth()/2, 171); g.drawImage(img,0,24); @@ -20,11 +20,11 @@ function getVersion(name,file) { return v?(name+" "+(v?"v"+v:"Unknown")):"NO "+name; } -var versions = [ +/*var versions = [ getVersion("Bootloader","boot.info"), getVersion("Launcher","launch.info"), getVersion("Settings","setting.info") -]; +];*/ var logo = E.toArrayBuffer(atob("PBwBAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAD/w+AAAAQAHA4hAAAAQAMAMhAAAAQAYBmhAAAAQAYBGiAAAAQAQCD/H74+R4wGDhoKJCSEwEDgoKJCT8wFDgoKJCSAwHDhoKJCSEQHj/H6I+R4YHmAAAACAAYEGAAABCAAMEMAAAA8AAHA4AAAAAAAD/wAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/gAAAAAAAB/g")); var imageTop = 24; @@ -35,17 +35,17 @@ function drawInfo() { g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56); var h=8, y = 24-h; g.setFont("6x8").setFontAlign(-1,-1); - g.drawString("Powered by Espruino",0,y+=4+h); - g.drawString("Version "+ENV.VERSION,0,y+=h); + g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h); + g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); getVersion("Bootloader","boot.info"); getVersion("Launcher","launch.info"); getVersion("Settings","setting.info"); - g.drawString(MEM.total+" JS Vars",0,y+=h); - g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); - if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h); + g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h); + g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h); + if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h); imageTop = y+h; imgScroll = imgHeight-imageTop; @@ -69,4 +69,7 @@ function drawImage() { // TODO: a nice little animation before setTimeout(drawInfo, 1000); -setWatch(_=>load(), BTN1); +Bangle.setUI({ + mode : "custom", + back : load +}); diff --git a/apps/about/metadata.json b/apps/about/metadata.json index 6c22bdc56..3d9730b81 100644 --- a/apps/about/metadata.json +++ b/apps/about/metadata.json @@ -1,7 +1,7 @@ { "id": "about", "name": "About", - "version": "0.12", + "version": "0.15", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "icon": "app.png", "tags": "tool,system", diff --git a/apps/accellog/ChangeLog b/apps/accellog/ChangeLog index 80981fe27..45469da5c 100644 --- a/apps/accellog/ChangeLog +++ b/apps/accellog/ChangeLog @@ -2,3 +2,5 @@ 0.02: Use the new multiplatform 'Layout' library 0.03: Exit as first menu option, dont show decimal places for seconds 0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware +0.05: Add max G values during recording, record actual G values and magnitude to CSV +0.06: Convert Yes/No On/Off in settings to checkboxes diff --git a/apps/accellog/app.js b/apps/accellog/app.js index f4c1b3c5a..ee82f435f 100644 --- a/apps/accellog/app.js +++ b/apps/accellog/app.js @@ -1,5 +1,6 @@ var fileNumber = 0; var MAXLOGS = 9; +var logRawData = false; function getFileName(n) { return "accellog."+n+".csv"; @@ -24,6 +25,10 @@ function showMenu() { /*LANG*/"View Logs" : function() { viewLogs(); }, + /*LANG*/"Log raw data" : { + value : !!logRawData, + onchange : v => { logRawData=v; } + }, }; E.showMenu(menu); } @@ -78,6 +83,7 @@ function viewLogs() { } function startRecord(force) { + var stopped = false; if (!force) { // check for existing file var f = require("Storage").open(getFileName(fileNumber), "r"); @@ -92,39 +98,101 @@ function startRecord(force) { var Layout = require("Layout"); var layout = new Layout({ type: "v", c: [ - {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2}, - {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, - {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2}, - {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, - {type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1}, - ] - },{btns:[ // Buttons... - {label:/*LANG*/"STOP", cb:()=>{ - Bangle.removeListener('accel', accelHandler); - showMenu(); + { type: "h", c: [ + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2}, + {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2}, + {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + ]}, + { type: "h", c: [ + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max X", pad:2}, + {type:"txt", id:"maxX", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max Y", pad:2}, + {type:"txt", id:"maxY", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + { type: "v", c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Max Z", pad:2}, + {type:"txt", id:"maxZ", font:"6x8", label:" - ", pad:5, bgCol:g.theme.bg}, + ]}, + ]}, + {type:"txt", font:"6x8", label:/*LANG*/"Max G", pad:2}, + {type:"txt", id:"maxMag", font:"6x8:4", label:" - ", pad:5, bgCol:g.theme.bg}, + {type:"txt", id:"state", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1}, + ]}, + { + btns:[ // Buttons... + {id: "btnStop", label:/*LANG*/"STOP", cb:()=>{ + if (stopped) { + showMenu(); + } + else { + Bangle.removeListener('accel', accelHandler); + layout.state.label = /*LANG*/"STOPPED"; + layout.state.bgCol = /*LANG*/"#0f0"; + stopped = true; + layout.render(); + } }} ]}); layout.render(); // now start writing var f = require("Storage").open(getFileName(fileNumber), "w"); - f.write("Time (ms),X,Y,Z\n"); + f.write("Time (ms),X,Y,Z,Total\n"); var start = getTime(); var sampleCount = 0; + var maxMag = 0; + var maxX = 0; + var maxY = 0; + var maxZ = 0; function accelHandler(accel) { var t = getTime()-start; - f.write([ - t*1000, - accel.x*8192, - accel.y*8192, - accel.z*8192].map(n=>Math.round(n)).join(",")+"\n"); + if (logRawData) { + f.write([ + t*1000, + accel.x*8192, + accel.y*8192, + accel.z*8192, + accel.mag*8192, + ].map(n=>Math.round(n)).join(",")+"\n"); + } else { + f.write([ + Math.round(t*1000), + accel.x, + accel.y, + accel.z, + accel.mag, + ].join(",")+"\n"); + } + if (accel.mag > maxMag) { + maxMag = accel.mag.toFixed(2); + } + if (accel.x > maxX) { + maxX = accel.x.toFixed(2); + } + if (accel.y > maxY) { + maxY = accel.y.toFixed(2); + } + if (accel.z > maxZ) { + maxZ = accel.z.toFixed(2); + } sampleCount++; layout.samples.label = sampleCount; layout.time.label = Math.round(t)+"s"; - layout.render(layout.samples); - layout.render(layout.time); + layout.maxX.label = maxX; + layout.maxY.label = maxY; + layout.maxZ.label = maxZ; + layout.maxMag.label = maxMag; + layout.render(); } Bangle.setPollInterval(80); // 12.5 Hz - the default diff --git a/apps/accellog/metadata.json b/apps/accellog/metadata.json index fdf6cf320..907828c7f 100644 --- a/apps/accellog/metadata.json +++ b/apps/accellog/metadata.json @@ -2,7 +2,7 @@ "id": "accellog", "name": "Acceleration Logger", "shortName": "Accel Log", - "version": "0.04", + "version": "0.06", "description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC", "icon": "app.png", "tags": "outdoor", diff --git a/apps/accelrec/ChangeLog b/apps/accelrec/ChangeLog index 7327ae25f..eace81e1e 100644 --- a/apps/accelrec/ChangeLog +++ b/apps/accelrec/ChangeLog @@ -2,3 +2,6 @@ 0.02: Increase record time to 5 second Calculate the time moving in graph display Trigger on 1.04g now, and record 10 samples before trigger +0.03: Bangle.js 2 compatibility +0.04: Minor code improvements +0.05: Can record 100hz, z-axis color changed to yellow, autosave to file, no need select, delete old records diff --git a/apps/accelrec/app.js b/apps/accelrec/app.js index 65f2a63ca..eb91d77af 100644 --- a/apps/accelrec/app.js +++ b/apps/accelrec/app.js @@ -1,179 +1,253 @@ -var acc; +//var acc; var HZ = 100; -var SAMPLES = 5*HZ; // 5 seconds -var SCALE = 5000; -var THRESH = 1.04; +var SAMPLES = 6 * HZ; // 6 seconds +var SCALE = 2000; +var THRESH = 1.4; var accelx = new Int16Array(SAMPLES); var accely = new Int16Array(SAMPLES); // North var accelz = new Int16Array(SAMPLES); // Into clock face +var timestep = new Int16Array(SAMPLES); // Into clock face var accelIdx = 0; var lastAccel; -function accelHandlerTrigger(a) {"ram" - if (a.mag*2>THRESH) { // *2 because 8g mode - tStart = getTime(); - g.drawString("Recording",g.getWidth()/2,g.getHeight()/2,1); - Bangle.removeListener('accel',accelHandlerTrigger); - Bangle.on('accel',accelHandlerRecord); - lastAccel.forEach(accelHandlerRecord); - accelHandlerRecord(a); - } else { - if (lastAccel.length>10) lastAccel.shift(); - lastAccel.push(a); - } -} -function accelHandlerRecord(a) {"ram" - var i = accelIdx++; - accelx[i] = a.x*SCALE*2; - accely[i] = -a.y*SCALE*2; - accelz[i] = a.z*SCALE*2; - if (accelIdx>=SAMPLES) recordStop(); -} -function recordStart() {"ram" - Bangle.setLCDTimeout(0); // force LCD on - accelIdx = 0; - lastAccel = []; - Bangle.accelWr(0x18,0b01110100); // off, +-8g - Bangle.accelWr(0x1B,0x03 | 0x40); // 100hz output, ODR/2 filter - Bangle.accelWr(0x18,0b11110100); // +-8g - Bangle.setPollInterval(10); // 100hz input - setTimeout(function() { - Bangle.on('accel',accelHandlerTrigger); - g.clear(1).setFont("6x8",2).setFontAlign(0,0); - g.drawString("Waiting",g.getWidth()/2,g.getHeight()/2); - }, 200); -} +var timestep_start = 0; - -function recordStop() {"ram" - //console.log("Length:",getTime()-tStart); - Bangle.setPollInterval(80); // default poll interval - Bangle.accelWr(0x18,0b01101100); // off, +-4g - Bangle.accelWr(0x1B,0x0); // default 12.5hz output - Bangle.accelWr(0x18,0b11101100); // +-4g - Bangle.removeListener('accel',accelHandlerRecord); - E.showMessage("Finished"); - showData(); -} - - -function showData() { - g.clear(1); - var w = g.getWidth()-20; // width - var m = g.getHeight()/2; // middle - var s = 12; // how many pixels per G - g.fillRect(9,0,9,g.getHeight()); - g.setFontAlign(0,0); - for (var l=-8;l<=8;l++) - g.drawString(l, 5, m - l*s); - - function plot(a) { - g.moveTo(10,m - a[0]*s/SCALE); - for (var i=0;i0.1) { - if (itEnd) tEnd=i; +function accelHandlerTrigger(a) { + "ram" + if (a.mag * 2 > THRESH) { // *2 because 8g mode + timestep_start = getTime(); + g.drawString("Recording", g.getWidth() / 2, g.getHeight() / 2, 1); + Bangle.removeListener('accel', accelHandlerTrigger); + Bangle.on('accel', accelHandlerRecord); + lastAccel.forEach(accelHandlerRecord); + accelHandlerRecord(a); + } else { + if (lastAccel.length > 10) lastAccel.shift(); + lastAccel.push(a); } - if (a>maxAccel) maxAccel=a; - vel += a/HZ; - if (vel>maxVel) maxVel=vel; - } - g.reset(); - g.setFont("6x8").setFontAlign(1,0); - g.drawString("Max Y Accel: "+maxAccel.toFixed(2)+" g",g.getWidth()-14,g.getHeight()-50); - g.drawString("Max Y Vel: "+maxVel.toFixed(2)+" m/s",g.getWidth()-14,g.getHeight()-40); - g.drawString("Time moving: "+(tEnd-tStart)/HZ+" s",g.getWidth()-14,g.getHeight()-30); - //console.log("End Velocity "+vel); - g.setFont("6x8").setFontAlign(0,0,1); - g.drawString("FINISH",g.getWidth()-4,g.getHeight()/2); - setWatch(function() { - showMenu(); - }, BTN2); +} + +function accelHandlerRecord(a) { + "ram" + var i = accelIdx++; + accelx[i] = a.x * SCALE * 2; // *2 because of 8g mode + accely[i] = -a.y * SCALE * 2; + accelz[i] = a.z * SCALE * 2; + timestep[i] = (getTime() - timestep_start) * 1000; + if (accelIdx >= SAMPLES) recordStop(); +} + +function recordStart() { + "ram" + Bangle.setLCDTimeout(0); // force LCD on + accelIdx = 0; + lastAccel = []; + Bangle.accelWr(0x18, 0b01110100); // off, +-8g + Bangle.accelWr(0x1B, 0x03 | 0x40); // 100hz output, ODR/2 filter + Bangle.accelWr(0x18, 0b11110100); // +-8g + Bangle.setPollInterval(10); // 100hz input + setTimeout(function() { + Bangle.on('accel', accelHandlerTrigger); + g.clear(1).setFont("6x8", 2).setFontAlign(0, 0); + g.drawString("Waiting", g.getWidth() / 2, g.getHeight() / 2); + }, 200); +} + + +function recordStop() { + "ram" + //console.log("Length:",getTime()-tStart); + Bangle.setPollInterval(80); // default poll interval + Bangle.accelWr(0x18, 0b01101100); // off, +-4g + Bangle.accelWr(0x1B, 0x0); // default 12.5hz output + Bangle.accelWr(0x18, 0b11101100); // +-4g + Bangle.removeListener('accel', accelHandlerRecord); + E.showMessage("Finished"); + showData(true); +} + + +function showData(save_file) { + g.clear(1); + let csv_files_N = require("Storage").list(/^acc.*\.csv$/).length; + let w_full = g.getWidth(); + let h = g.getHeight(); + var w = g.getWidth() - 20; // width + var m = g.getHeight() / 2; // middle + var s = 12; // how many pixels per G + g.fillRect(9, 0, 9, g.getHeight()); + g.setFontAlign(0, 0); + for (var l = -8; l <= 8; l++) + g.drawString(l, 5, m - l * s); + + function plot(a) { + g.moveTo(10, m - a[0] * s / SCALE); + for (var i = 0; i < SAMPLES; i++) + g.lineTo(10 + i * w / SAMPLES, m - a[i] * s / SCALE); + } + g.setColor("#FFFA5F"); + plot(accelz); + g.setColor("#ff0000"); + plot(accelx); + g.setColor("#00ff00"); + plot(accely); + + // work out stats + var maxAccel = 0; + var tStart = SAMPLES, + tEnd = 0; + var max_YZ = 0; + for (var i = 0; i < SAMPLES; i++) { + var a = Math.abs(accely[i] / SCALE); + let a_yz = Math.sqrt(Math.pow(accely[i] / SCALE, 2) + Math.pow(accelz[i] / SCALE, 2)); + if (a > 0.1) { + if (i < tStart) tStart = i; + if (i > tEnd) tEnd = i; + } + if (a > maxAccel) maxAccel = a; + if (a_yz > max_YZ) max_YZ = a_yz; + } + g.reset(); + g.setFont("6x8").setFontAlign(1, 0); + g.drawString("Max X Accel: " + maxAccel.toFixed(2) + " g", g.getWidth() - 14, g.getHeight() - 50); + g.drawString("Max YZ Accel: " + max_YZ.toFixed(2) + " g", g.getWidth() - 14, g.getHeight() - 40); + g.drawString("Time moving: " + (tEnd - tStart) / HZ + " s", g.getWidth() - 14, g.getHeight() - 30); + g.setFont("6x8", 2).setFontAlign(0, 0); + g.drawString("File num: " + (csv_files_N + 1), w_full / 2, h - 20); + g.setFont("6x8").setFontAlign(0, 0, 1); + g.drawString("FINISH", g.getWidth() - 4, g.getHeight() / 2); + setWatch(function() { + if (save_file) showSaveMenu(); // when select only plot, don't ask for save option + else showMenu(); + }, global.BTN2 ? BTN2 : BTN); } function showBig(txt) { - g.clear(1); - g.setFontVector(80).setFontAlign(0,0); - g.drawString(txt,g.getWidth()/2, g.getHeight()/2); - g.flip(); + g.clear(1); + g.setFontVector(80).setFontAlign(0, 0); + g.drawString(txt, g.getWidth() / 2, g.getHeight() / 2); + g.flip(); } function countDown() { - showBig(3); - setTimeout(function() { - showBig(2); + showBig(3); setTimeout(function() { - showBig(1); - setTimeout(function() { - recordStart(); - }, 800); + showBig(2); + setTimeout(function() { + showBig(1); + setTimeout(function() { + recordStart(); + }, 800); + }, 1000); }, 1000); - }, 1000); } function showMenu() { - Bangle.setLCDTimeout(10); // set timeout for LCD in menu - var menu = { - "" : { title : "Acceleration Rec" }, - "Start" : function() { - E.showMenu(); - if (accelIdx==0) countDown(); - else E.showPrompt("Overwrite Recording?").then(ok=>{ - if (ok) countDown(); else showMenu(); - }); - }, - "Plot" : function() { - E.showMenu(); - if (accelIdx) showData(); - else E.showAlert("No Data").then(()=>{ - showMenu(); - }); - }, - "Save" : function() { - E.showMenu(); - if (accelIdx) showSaveMenu(); - else E.showAlert("No Data").then(()=>{ - showMenu(); - }); - }, - "Exit" : function() { - load(); - }, - }; - E.showMenu(menu); + Bangle.setLCDTimeout(10); // set timeout for LCD in menu + var menu = { + "": { title: "Acceleration Rec" }, + "Start": function() { + E.showMenu(); + if (accelIdx == 0) countDown(); + else E.showPrompt("Overwrite Recording?").then(ok => { + if (ok) countDown(); + else showMenu(); + }); + }, + "Plot": function() { + E.showMenu(); + if (accelIdx) showData(false); + else E.showAlert("No Data").then(() => { + showMenu(); + }); + }, + "Storage": function() { + E.showMenu(); + if (require("Storage").list(/^acc.*\.csv$/).length) + StorageMenu(); + else + E.showAlert("No Data").then(() => { + showMenu(); + }); + }, + "Exit": function() { + load(); + }, + }; + E.showMenu(menu); } function showSaveMenu() { - var menu = { - "" : { title : "Save" } - }; - [1,2,3,4,5,6].forEach(i=>{ - var fn = "accelrec."+i+".csv"; - var exists = require("Storage").read(fn)!==undefined; - menu["Recording "+i+(exists?" *":"")] = function() { - var csv = ""; - for (var i=0;i { + if (ok) + SaveFile(); + else + showMenu(); + }); } -showMenu(); +function SaveFile() { + let csv_files_N = require("Storage").list(/^acc.*\.csv$/).length; + //if (csv_files_N > 20) + // E.showMessage("Storage is full"); + // showMenu(); + let csv = ""; + let date = new Date(); + let fn = "accelrec_" + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + "_" + (csv_files_N + 1) + ".csv"; + E.showMessage("Saveing to file \n" + fn); + for (var i = 0; i < SAMPLES; i++) + csv += `${timestep[i]},${accelx[i]/SCALE},${accely[i]/SCALE},${accelz[i]/SCALE}\n`; + require("Storage").write(fn, csv); + showMenu(); +} + +//Show saved csv files +function StorageMenu() { + var menu = { + "": { + title: "Storage" + } + }; + let csv_files = require("Storage").list(/^acc.*\.csv$/); + var inx = 0; + csv_files.forEach(fn => { + inx++; + menu[inx + ". " + fn] = function() { + StorageOptions(fn); + }; + }); + menu["< Back"] = function() { + showMenu(); + }; + E.showMenu(menu); +} + +function StorageOptions(file) { + let menu = { + "": { + title: "Options" + }, + "Plot": function() { + showMenu(); + }, + "Delete": function() { + E.showMenu(); + E.showPrompt("Delete recording?").then(ok => { + if (ok) + DeleteRecord(file); + else + StorageMenu(); + }); + }, + "< Back": function() { + StorageMenu(); + }, + }; + E.showMenu(menu); +} + +function DeleteRecord(file) { + E.showMessage("Deleteing file \n" + file); + require("Storage").erase(file); + StorageMenu(); +} +showMenu(); \ No newline at end of file diff --git a/apps/accelrec/interface.html b/apps/accelrec/interface.html index ce3547387..d69b2d50c 100644 --- a/apps/accelrec/interface.html +++ b/apps/accelrec/interface.html @@ -37,7 +37,7 @@ function getData() { `; promise = promise.then(function() { document.querySelector(`.btn[fn='${fn}'][act='save']`).addEventListener("click", function() { - Util.saveCSV(fn.slice(0,-4), "X,Y,Z\n"+fileData[fn]); + Util.saveCSV(fn.slice(0,-4), "Time,X,Y,Z\n"+fileData[fn]); }); document.querySelector(`.btn[fn='${fn}'][act='delete']`).addEventListener("click", function() { Util.showModal("Deleting..."); diff --git a/apps/accelrec/metadata.json b/apps/accelrec/metadata.json index 8b082c8bc..36d700a19 100644 --- a/apps/accelrec/metadata.json +++ b/apps/accelrec/metadata.json @@ -2,11 +2,11 @@ "id": "accelrec", "name": "Acceleration Recorder", "shortName": "Accel Rec", - "version": "0.02", + "version": "0.05", "description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.", "icon": "app.png", "tags": "", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "interface": "interface.html", "storage": [ diff --git a/apps/accelsender/ChangeLog b/apps/accelsender/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/accelsender/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/accelsender/README.md b/apps/accelsender/README.md new file mode 100644 index 000000000..eb18bb88a --- /dev/null +++ b/apps/accelsender/README.md @@ -0,0 +1,19 @@ +# Accerleration Data Provider + +This app provides acceleration data via Bluetooth, which can be used in Gadgetbridge. + +## Usage + +This boot code runs in the background and has no user interface. +Currently this app is used to enable Sleep as Android tracking for your Banglejs using Gadgetbridge. + +**Please Note**: This app only listens to "accel" events and sends them to your phone using Bluetooth. + +## Creator + +[Another Stranger](https://github.com/anotherstranger) + +## Aknowledgements + +Special thanks to [José Rebelo](https://github.com/joserebelo) and [Rob Pilling](https://github.com/bobrippling) +for their Code Reviews and guidance. diff --git a/apps/accelsender/bluetooth.png b/apps/accelsender/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/accelsender/bluetooth.png differ diff --git a/apps/accelsender/boot.js b/apps/accelsender/boot.js new file mode 100644 index 000000000..b1a076e2b --- /dev/null +++ b/apps/accelsender/boot.js @@ -0,0 +1,55 @@ +(() => { + /** + * Sends a message to the gadgetbridge via Bluetooth. + * @param {Object} message - The message to be sent. + */ + function gbSend(message) { + try { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } catch (error) { + console.error("Failed to send message via Bluetooth:", error); + } + } + + var max_acceleration = { x: 0, y: 0, z: 0, diff: 0, td: 0, mag: 0 }; + var hasData = false; + + /** + * Updates the maximum acceleration if the current acceleration is greater. + * @param {Object} accel - The current acceleration object with x, y, z, and mag properties. + */ + function updateAcceleration(accel) { + hasData = true; + var current_max_raw = accel.mag; + var max_raw = max_acceleration.mag; + + if (current_max_raw > max_raw) { + max_acceleration = accel; + } + } + + /** + * Updates the acceleration data and sends it to gadgetbridge. + * Resets the maximum acceleration. + * Note: If your interval setting is too short, the last value gets sent again. + */ + function sendAccelerationData() { + var accel = hasData ? max_acceleration : Bangle.getAccel(); + + var update_data = { + t: "accel", accel: accel + }; + gbSend(update_data); + + max_acceleration = { x: 0, y: 0, z: 0, mag: 0, diff: 0, td: 0 }; + hasData = false; + } + + var config = require("Storage").readJSON("accelsender.json") || {}; + if (config.enabled) { // Gadgetbridge needs to enable and disable tracking by writing {enabled: true} to "accelsender.json" and reloading + setInterval(sendAccelerationData, config.interval); + Bangle.on("accel", updateAcceleration); // Log all acceleration events + } + +})(); diff --git a/apps/accelsender/boot.min.js b/apps/accelsender/boot.min.js new file mode 100644 index 000000000..72a336083 --- /dev/null +++ b/apps/accelsender/boot.min.js @@ -0,0 +1 @@ +(()=>{function e(a){c=!0;a.mag>b.mag&&(b=a)}function f(){var a={t:"accel",accel:c?b:Bangle.getAccel()};try{Bluetooth.println(""),Bluetooth.println(JSON.stringify(a))}catch(g){console.error("Failed to send message via Bluetooth:",g)}b={x:0,y:0,z:0,mag:0,diff:0,td:0};c=!1}var b={x:0,y:0,z:0,diff:0,td:0,mag:0},c=!1,d=require("Storage").readJSON("accelsender.json")||{};d.enabled&&(setInterval(f,d.interval),Bangle.on("accel",e))})(); diff --git a/apps/accelsender/config.json b/apps/accelsender/config.json new file mode 100644 index 000000000..70590f2f2 --- /dev/null +++ b/apps/accelsender/config.json @@ -0,0 +1,4 @@ +{ + "enabled": false, + "interval": 10000 +} \ No newline at end of file diff --git a/apps/accelsender/metadata.json b/apps/accelsender/metadata.json new file mode 100644 index 000000000..b63f7485e --- /dev/null +++ b/apps/accelsender/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "accelsender", + "name": "Acceleration Data Provider", + "shortName": "Accel Data Provider", + "version": "0.01", + "description": "This app sends accelerometer and heart rate data from your Bangle.js via Bluetooth.", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "accel", + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "accelsender.boot.js", + "url": "boot.min.js" + } + ], + "data": [ + { + "name": "accelsender.json", + "url": "config.json" + } + ] +} diff --git a/apps/acmaze/ChangeLog b/apps/acmaze/ChangeLog index b8c1ec0b5..8a19442ef 100644 --- a/apps/acmaze/ChangeLog +++ b/apps/acmaze/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Faster maze generation 0.03: Avoid clearing bottom widgets +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/acmaze/app.js b/apps/acmaze/app.js index 16a1ce561..c3157f087 100644 --- a/apps/acmaze/app.js +++ b/apps/acmaze/app.js @@ -54,7 +54,7 @@ function Maze(n) { // Abort if BTN1 pressed [grace period for menu] // (for some reason setWatch() fails inside constructor) if (ngroupsstepsCounted}; })(); diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog index d4b5100a2..df6bcaf6b 100644 --- a/apps/activityreminder/ChangeLog +++ b/apps/activityreminder/ChangeLog @@ -3,3 +3,10 @@ 0.03: Do not alarm while charging 0.04: Obey system quiet mode 0.05: Battery optimisation, add the pause option, bug fixes +0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night +0.07: Fix bug on the cutting edge firmware +0.08: Use default Bangle formatter for booleans +0.09: New app screen (instead of showing settings or the alert) and some optimisations +0.10: Add software back button via setUI +0.11: Add setting to unlock screen +0.12: Fix handling that dates can be given as ms since epoch. diff --git a/apps/activityreminder/README.md b/apps/activityreminder/README.md index 25e2c8d35..0c79b4141 100644 --- a/apps/activityreminder/README.md +++ b/apps/activityreminder/README.md @@ -11,4 +11,5 @@ Different settings can be personalized: - Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min - Pause delay: Same as Dismiss delay but longer (usefull for meetings and such). From 30 to 240 min - Min steps: Minimal amount of steps to count as an activity +- Temp Threshold: Temperature threshold to determine if the watch is worn diff --git a/apps/activityreminder/alert.js b/apps/activityreminder/alert.js new file mode 100644 index 000000000..8b359a073 --- /dev/null +++ b/apps/activityreminder/alert.js @@ -0,0 +1,43 @@ +(function () { + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + const storage = require("Storage"); + let activityreminder_data = activityreminder.loadData(); + + function run() { + E.showPrompt("Inactivity detected", { + title: "Activity reminder", + buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 } + }).then(function (v) { + if (v == 1) { + activityreminder_data.okDate = new Date(); + } + if (v == 2) { + activityreminder_data.dismissDate = new Date(); + } + if (v == 3) { + activityreminder_data.pauseDate = new Date(); + } + activityreminder.saveData(activityreminder_data); + load(); + }); + + // Obey system quiet mode: + if (!(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(400); + } + + if ((storage.readJSON('activityreminder.s.json', 1) || {}).unlock) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); + } + + setTimeout(load, 20000); + } + + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + run(); + +})(); diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js index f3d72976e..81e10d8dd 100644 --- a/apps/activityreminder/app.js +++ b/apps/activityreminder/app.js @@ -1,42 +1,58 @@ -function drawAlert() { - E.showPrompt("Inactivity detected", { - title: "Activity reminder", - buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 } - }).then(function (v) { - if (v == 1) { - activityreminder_data.okDate = new Date(); - } - if (v == 2) { - activityreminder_data.dismissDate = new Date(); - } - if (v == 3) { - activityreminder_data.pauseDate = new Date(); - } - activityreminder.saveData(activityreminder_data); - load(); - }); +(function () { + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + let activityreminder_data = activityreminder.loadData(); + let W = g.getWidth(); + // let H = g.getHeight(); - // Obey system quiet mode: - if (!(storage.readJSON('setting.json', 1) || {}).quiet) { - Bangle.buzz(400); + function getHoursMins(date){ + var h = date.getHours(); + var m = date.getMinutes(); + return (""+h).substr(-2) + ":" + ("0"+m).substr(-2); } - setTimeout(load, 20000); -} -function run() { - if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) { - drawAlert(); - } else { - eval(storage.read("activityreminder.settings.js"))(() => load()); + function drawData(name, value, y){ + g.drawString(name, 10, y); + g.drawString(value, 100, y); } -} + + function drawInfo() { + var h=18, y = h; + g.setColor(g.theme.fg); + g.setFont("Vector",h).setFontAlign(-1,-1); + // Header + g.drawLine(0,25,W,25); + g.drawLine(0,26,W,26); + + g.drawString("Current Cycle", 10, y+=h); + drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h); + drawData("Steps", getCurrentSteps(), y+=h); -const activityreminder = require("activityreminder"); -const storage = require("Storage"); -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -const activityreminder_settings = activityreminder.loadSettings(); -const activityreminder_data = activityreminder.loadData(); -run(); + /* + g.drawString("Button Press", 10, y+=h*2); + drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h); + drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h); + drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h); + */ + } + + function getCurrentSteps(){ + let health = Bangle.getHealthStatus("day"); + return health.steps - activityreminder_data.stepsOnDate; + } + + function run() { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + drawInfo(); + Bangle.setUI({ + mode : "custom", + back : load + }) + } + + run(); + +})(); diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js index 7c094f521..5a11d73b8 100644 --- a/apps/activityreminder/boot.js +++ b/apps/activityreminder/boot.js @@ -1,45 +1,81 @@ -function run() { +(function () { + // load variable before defining functions cause it can trigger a ReferenceError + const activityreminder = require("activityreminder"); + const activityreminder_settings = activityreminder.loadSettings(); + let activityreminder_data = activityreminder.loadData(); + + if (activityreminder_data.firstLoad) { + activityreminder_data.firstLoad = false; + activityreminder.saveData(activityreminder_data); + } + + function run() { if (isNotWorn()) return; let now = new Date(); let h = now.getHours(); - let health = Bangle.getHealthStatus("day"); - if (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour) { - if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed - || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch - activityreminder_data.stepsOnDate = health.steps; - activityreminder_data.stepsDate = now; - activityreminder.saveData(activityreminder_data); - /* todo in a futur release - add settimer to trigger like 10 secs after the stepsDate + minSteps - cancel all other timers of this app - */ - } - - if(activityreminder.mustAlert(activityreminder_data, activityreminder_settings)){ - load('activityreminder.app.js'); - } - } - -} - -function isNotWorn() { - // todo in a futur release check temperature and mouvement in a futur release - return Bangle.isCharging(); -} - -const activityreminder = require("activityreminder"); -const activityreminder_settings = activityreminder.loadSettings(); -if (activityreminder_settings.enabled) { - const activityreminder_data = activityreminder.loadData(); - if(activityreminder_data.firstLoad){ - activityreminder_data.firstLoad =false; + if (isDuringAlertHours(h)) { + let health = Bangle.getHealthStatus("day"); + if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed + || health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch + activityreminder_data.stepsOnDate = health.steps; + activityreminder_data.stepsDate = now; activityreminder.saveData(activityreminder_data); + /* todo in a futur release + Add settimer to trigger like 30 secs after going in this part cause the person have been walking + (pass some argument to run() to handle long walks and not triggering so often) + */ + } + + if (mustAlert(now)) { + load('activityreminder.alert.js'); + } } + + } + + function isNotWorn() { + return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature()); + } + + function isDuringAlertHours(h) { + if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight + return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour); + } else { // passing through midnight + return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour); + } + } + + function mustAlert(now) { + if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected + if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago + (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago + (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago + return true; + } + } + return false; + } + + Bangle.on('midnight', function () { + /* + Usefull trick to have the app working smothly for people using it at night + */ + let now = new Date(); + let h = now.getHours(); + if (activityreminder_settings.enabled && isDuringAlertHours(h)) { + // updating only the steps and keeping the original stepsDate on purpose + activityreminder_data.stepsOnDate = 0; + activityreminder.saveData(activityreminder_data); + } + }); + + + if (activityreminder_settings.enabled) { setInterval(run, 60000); /* todo in a futur release increase setInterval time to something that is still sensible (5 mins ?) - add settimer to trigger like 10 secs after the stepsDate + minSteps - cancel all other timers of this app + when we added a settimer */ -} + } +})(); diff --git a/apps/activityreminder/lib.js b/apps/activityreminder/lib.js index 5b7959827..583662ab1 100644 --- a/apps/activityreminder/lib.js +++ b/apps/activityreminder/lib.js @@ -1,57 +1,41 @@ -const storage = require("Storage"); - exports.loadSettings = function () { - return Object.assign({ - enabled: true, - startHour: 9, - endHour: 20, - maxInnactivityMin: 30, - dismissDelayMin: 15, - pauseDelayMin: 120, - minSteps: 50 - }, storage.readJSON("activityreminder.s.json", true) || {}); + return Object.assign({ + enabled: true, + startHour: 9, + endHour: 20, + maxInnactivityMin: 30, + dismissDelayMin: 15, + pauseDelayMin: 120, + minSteps: 50, + tempThreshold: 27 + }, require("Storage").readJSON("activityreminder.s.json", true) || {}); }; exports.writeSettings = function (settings) { - storage.writeJSON("activityreminder.s.json", settings); + require("Storage").writeJSON("activityreminder.s.json", settings); }; exports.saveData = function (data) { - storage.writeJSON("activityreminder.data.json", data); + require("Storage").writeJSON("activityreminder.data.json", data); }; exports.loadData = function () { - let health = Bangle.getHealthStatus("day"); - const data = Object.assign({ - firstLoad: true, - stepsDate: new Date(), - stepsOnDate: health.steps, - okDate: new Date(1970), - dismissDate: new Date(1970), - pauseDate: new Date(1970), - }, - storage.readJSON("activityreminder.data.json") || {}); + let health = Bangle.getHealthStatus("day"); + let data = Object.assign({ + firstLoad: true, + stepsDate: new Date(), + stepsOnDate: health.steps, + okDate: new Date(1970), + dismissDate: new Date(1970), + pauseDate: new Date(1970), + }, + + require("Storage").readJSON("activityreminder.data.json") || {}); - if(typeof(data.stepsDate) == "string") - data.stepsDate = new Date(data.stepsDate); - if(typeof(data.okDate) == "string") - data.okDate = new Date(data.okDate); - if(typeof(data.dismissDate) == "string") - data.dismissDate = new Date(data.dismissDate); - if(typeof(data.pauseDate) == "string") - data.pauseDate = new Date(data.pauseDate); + data.stepsDate = new Date(typeof data.stepsDate === 'string' ? data.stepsDate : data.stepsDate.ms); + data.okDate = new Date(typeof data.okDate === 'string' ? data.okDate : data.okDate.ms); + data.dismissDate = new Date(typeof data.dismissDate === 'string' ? data.dismissDate : data.dismissDate.ms); + data.pauseDate = new Date(typeof data.pauseDate === 'string' ? data.pauseDate : data.pauseDate.ms); - return data; + return data; }; - -exports.mustAlert = function(activityreminder_data, activityreminder_settings) { - let now = new Date(); - if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected - if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago - (now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago - (now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago - return true; - } - } - return false; -} \ No newline at end of file diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json index 15f10f2ed..1cbe4683e 100644 --- a/apps/activityreminder/metadata.json +++ b/apps/activityreminder/metadata.json @@ -3,21 +3,22 @@ "name": "Activity Reminder", "shortName":"Activity Reminder", "description": "A reminder to take short walks for the ones with a sedentary lifestyle", - "version":"0.05", + "version":"0.12", "icon": "app.png", "type": "app", - "tags": "tool,activity", + "tags": "tool,activity,health", "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name": "activityreminder.app.js", "url":"app.js"}, {"name": "activityreminder.boot.js", "url": "boot.js"}, {"name": "activityreminder.settings.js", "url": "settings.js"}, + {"name": "activityreminder.alert.js", "url": "alert.js"}, {"name": "activityreminder", "url": "lib.js"}, {"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true} ], "data": [ {"name": "activityreminder.s.json"}, - {"name": "activityreminder.data.json"} + {"name": "activityreminder.data.json", "storageFile": true} ] } diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js index 9dff61f48..28082a8a0 100644 --- a/apps/activityreminder/settings.js +++ b/apps/activityreminder/settings.js @@ -1,76 +1,93 @@ (function (back) { // Load settings const activityreminder = require("activityreminder"); - const settings = activityreminder.loadSettings(); + let settings = activityreminder.loadSettings(); + + function getMainMenu(){ + var mainMenu = { + "": { "title": "Activity Reminder" }, + "< Back": () => back(), + 'Enable': { + value: settings.enabled, + onchange: v => { + settings.enabled = v; + activityreminder.writeSettings(settings); + } + }, + 'Start hour': { + value: settings.startHour, + min: 0, max: 24, + onchange: v => { + settings.startHour = v; + activityreminder.writeSettings(settings); + } + }, + 'End hour': { + value: settings.endHour, + min: 0, max: 24, + onchange: v => { + settings.endHour = v; + activityreminder.writeSettings(settings); + } + }, + 'Max inactivity': { + value: settings.maxInnactivityMin, + min: 15, max: 120, + onchange: v => { + settings.maxInnactivityMin = v; + activityreminder.writeSettings(settings); + }, + format: x => x + "m" + }, + 'Dismiss delay': { + value: settings.dismissDelayMin, + min: 5, max: 60, + onchange: v => { + settings.dismissDelayMin = v; + activityreminder.writeSettings(settings); + }, + format: x => x + "m" + }, + 'Pause delay': { + value: settings.pauseDelayMin, + min: 30, max: 240, step: 5, + onchange: v => { + settings.pauseDelayMin = v; + activityreminder.writeSettings(settings); + }, + format: x => { + return x + "m"; + } + }, + 'Min steps': { + value: settings.minSteps, + min: 10, max: 500, step: 10, + onchange: v => { + settings.minSteps = v; + activityreminder.writeSettings(settings); + } + }, + 'Temp Threshold': { + value: settings.tempThreshold, + min: 20, max: 40, step: 0.5, + format: v => v + "°C", + onchange: v => { + settings.tempThreshold = v; + activityreminder.writeSettings(settings); + } + }, + 'Unlock on alarm': { + value: !!settings.unlock, + onchange: v => { + settings.unlock = v; + activityreminder.writeSettings(settings); + } + }, + }; + + return mainMenu; + } // Show the menu - E.showMenu({ - "": { "title": "Activity Reminder" }, - "< Back": () => back(), - 'Enable': { - value: settings.enabled, - format: v => v ? "Yes" : "No", - onchange: v => { - settings.enabled = v; - activityreminder.writeSettings(settings); - } - }, - 'Start hour': { - value: settings.startHour, - min: 0, max: 24, - onchange: v => { - settings.startHour = v; - activityreminder.writeSettings(settings); - } - }, - 'End hour': { - value: settings.endHour, - min: 0, max: 24, - onchange: v => { - settings.endHour = v; - activityreminder.writeSettings(settings); - } - }, - 'Max inactivity': { - value: settings.maxInnactivityMin, - min: 15, max: 120, - onchange: v => { - settings.maxInnactivityMin = v; - activityreminder.writeSettings(settings); - }, - format: x => { - return x + " min"; - } - }, - 'Dismiss delay': { - value: settings.dismissDelayMin, - min: 5, max: 60, - onchange: v => { - settings.dismissDelayMin = v; - activityreminder.writeSettings(settings); - }, - format: x => { - return x + " min"; - } - }, - 'Pause delay': { - value: settings.pauseDelayMin, - min: 30, max: 240, - onchange: v => { - settings.pauseDelayMin = v; - activityreminder.writeSettings(settings); - }, - format: x => { - return x + " min"; - } - }, - 'Min steps': { - value: settings.minSteps, - min: 10, max: 500, - onchange: v => { - settings.minSteps = v; - activityreminder.writeSettings(settings); - } - } - }); + E.showMenu(getMainMenu()); }) diff --git a/apps/advcasio/ChangeLog b/apps/advcasio/ChangeLog new file mode 100644 index 000000000..0bca28a1c --- /dev/null +++ b/apps/advcasio/ChangeLog @@ -0,0 +1,6 @@ +0.01: AdvCasio first version +0.02: Remove un-needed fonts to improve memory usage +0.03: Tell clock widgets to hide. +0.04: Swipe down to see widgets, step counter now just uses getHealthStatus +0.05: Report latest HRM rather than HRM 10 minutes ago (fix #2395) +0.06: Use watch temperature \ No newline at end of file diff --git a/apps/advcasio/README.md b/apps/advcasio/README.md new file mode 100644 index 000000000..bce3f71d7 --- /dev/null +++ b/apps/advcasio/README.md @@ -0,0 +1,53 @@ +# Adv Casio Clock + + + +An over-engineered clock inspired by Casio watches.
+It has a dedicated timer, a scratchpad and displays the current temperature.
+Forked from the awesome Cassio Watch.
+ +## Todo + +- Improving quality of the background images, right now it is quite blurry. +- Improving screenshots quality. +- Improving web app look. +- Improving bangle app performances (using functions for images and specialized array). + +## Functionalities + +- Current time +- Current day and month +- Footsteps +- Battery +- Simple Timer embedded +- Current temperature +- Scratchpad + +## Screenshots +Clock:
+ + + + +Web interface to update weather & scratchpad
+https://dotgreg.github.io/advCasioBangleClock + + + +## Usage + +### How to start/stop the timer +- swipe up : add time (+5min) +- swipe down : remove time (-5min) +- swipe right : start timer +- swipe left : stop timer + +## Links +### Issues, suggestions and bugtracker +https://github.com/dotgreg/advCasioBangleClock/issues + +### Code repository (bangle app and web app) +https://github.com/dotgreg/advCasioBangleClock + +### Creator +https://github.com/dotgreg diff --git a/apps/advcasio/app-icon.js b/apps/advcasio/app-icon.js new file mode 100644 index 000000000..2471ceac7 --- /dev/null +++ b/apps/advcasio/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AH4A/AGsCmUQC6kf/8wC6k///wgEv//zD4PxAQIJBABP//4QBC4IcBh/yEQIXKgP/l4rBl/yGAMP/4iBKJUC/5gBIAQVBBAMR/8gC5IQBAAMQC4IVBFoMjAYIXNmAXBgYXCPgQAJl/xHwPwj/yn5kC/55BUxSlC+JiBVgQ5BUxiDBUIIXBIQQXBcCoA/AH4ADXAUgUAUQBAkPeoTDFgIHBAALQEA4XwC4IOEAAQRBbAQBBCAIgBEYMQC4TnEC4XyeQgBDAAMwC4pIDC4kDAgJLD//xC5QIBNQISCFYIZCC4aEBAQRCDAAPyl4hBOIh3Cn53GNgMRiKxGBAR5BAoYA/AH4A/AH4A5A")) diff --git a/apps/advcasio/app.js b/apps/advcasio/app.js new file mode 100644 index 000000000..796fac2a7 --- /dev/null +++ b/apps/advcasio/app.js @@ -0,0 +1,160 @@ +const storage = require('Storage'); + +require("Font6x12").add(Graphics); +require("Font8x12").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); + +function bigThenSmall(big, small, x, y) { + g.setFont("7x11Numeric7Seg", 2); + g.drawString(big, x, y); + x += g.stringWidth(big); + g.setFont("8x12"); + g.drawString(small, x, y); +} + +function getBackgroundImage() { + return require("heatshrink").decompress(atob("2GwwkGIf4AfgMRkUiiIHCiMRiAMDAwYCCBAYVDAHMv/4ACkBIBAgPxBgM/BYXyAoICBCowA5gRADKQUDKAYMCmYCBiBXBCo4A5J4MxiMSKQUf+YBBBgSiBgc/kBXBBAMyCoK2CK/btCiUhfAJLCkBkDiMQgBXDCoUvNAJX+AAU/+MB/8wAQIAC+cQK5hoDgIEBBIQFEAYIPHBIgBBAQQIDBwZXSKIMxgJaBgEjmZYCmBXLgLBBkkAgUhiMxBIM0iMSCoMRkZECkQJEichBINDiETAgISBiQTDK6MvJAXzVIQrBBYMCK5E/K4kwGIJXFgdAMgQQBiYiCDgU0HQSlCgMikIEBEAMTDYJXQ+UikYDBj6nCAAMTWoJ6BK4oVEK4c0oQ+BK4MjAgMDJoJXHNYJXHBwa0BohcDY4QAKgJQE+LzBNwJVBkQMEkBXBCoyvFJAVAKISaBiMiHQRIDkVBoSyCK5CvBAgavNDAJAC+cQn5DCgSpBl4MDgBXBgCsBCoYoMLAKREgIKDBJIdKK5oA/AH4A/AH4A/ADUBIH4APiAFEi1mAGUADrkRKwUGK2ZXes1gK2xXfD8A3/K/4AWgxX/ACtga2AwIHLkAgCwvJw6RcDgIABK+w4cK/I4dsEGP5BXtSAQ6BV/5XSG4RX/K6Y3fK+42CK/5XTGwcGK/5XSVwY5cK+o1DAAayYsAhDsCv4K7BTBK4YeYK7CyFVzJXFFIpXtVwYiYK/rmZKYYDDELJXXG4YiaK/Y0aKgQAEK+gkdKt5XGKzqv5GTpX6ETlgK4xWrKTyxKVthXmAGRX/K/5X/AH5X/K/4gBAH4A/AFz/uAH4A/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/AHNggEGHfEAgAEHKyQXVK0qTCAggbUK+6SDAApzXK/5BRDYZX3KxBBSYqxXngyvaV25XEd4ZCSsAcBAoRZ2dQZXBLwgaQCIYeCAGirCS4YGCDSJXCC6ZaodYICBZzSw4S4I+XDgSv4K4rzCK/47RAQTMaWHI9YV3TscV3aVagByBK3SwCSqyt8AAQ+XK/4A/AH4A/AH4A3gAA/AH4AuZbdggwc3ADpX/K/5XxsEAgA+XK/o8BgBX/K64/WK/4/XK/5X/K/5XvgBX/K64cYHrw4CSTFggCuXK4oDCEQJXYDS6ScDgg4CPKyRCAAZX0HAgBDK+LlYK4oeBAwZ9aK+lgAoQGBgyvzDIIDBK66sCG4JXYCwIBDK7ADCK+xZCHwJXzGoQ8BK7DpBAAaSXSgRXZO4okCK+IaXV4oABEILSWSYjRCHSo3BDSxXEAAIcBAISvyKawcIAYIGCK/4cUH4YlaHS0AHgI1XOg5YBPrY6WHgRXfAGRXDHzBX8VoJX/K68ADjRX6sBX/K/5X/K8wdcK/UAG7B0iKzZYbK/BWDAH4A/hWpzWhIf4ASgOpzIAB0EAhhH/AB8ZzGJ1WazMA4pH/AB+pxOZxOpzVMqA2ugUzmcgD7cKVYOqzGqpnRFw8ykchK8kviEBmQFBgMiFocSCAcSkUQAgMikRsHhWqxOq0Ut4mqBw0DC4IxBD4wpBHAQMCA4cCGJIAFj8hDIQuBkMTCwU/AYQJBiUxFoPxiIVDK4kyxUz4cxl+KK5MfDQXyD4UCmMSmAEBAQQHDgMTmIxHAAqpBmaqCFwMDEYZRBgEjCQQBB+USK5E/ns/0Uzwc6K48ykYkCK4IfCc4I4CK4QHEBAYAMiICBmYuDmQEBh8iAgRXCLISvJO4MqwcklEiK5CADV4oaBV4oHEK6Eve4JNCbwRfCiMTFoMDkMRSAJXCD49azWp0UqzWayJXIQwcAO4cCkMCFIJOCA4XxK6KPBkR6DTwYyBAwYPEAggfFzORpWK1OZyAOHJ4QfERAUSEgQxIIIgAr1URWIOZzOgGtwAhgMZzWq1OaIv4ASKgOqzTkvAEmq1WgFtQA==")); +} + +function getRocketSequences() { + return { + 1: require("heatshrink").decompress(atob("qFGwkCkQAiiEBEkUgKQhPhE8ogCE8YhCiQoEE7pKEPIgncTQ4neEwpQCPoh1eJYYwCJ7QmHKAh1hZIpOjPAUBJ0ZQCTzEhExZ1lPAZ1kKDQmOJ65O2E65OPOy5O2E64mPOyxO/J2wnPJyx2QJ35O/J2khE0p2POq52PEy4nOiQnlOrEhiSfMJrEggQnLJzB1CPBQmZkInMEzBQDPBImbPBR1ZEoRMCZYImhgQgEE0BzFKAgmaDwLDFKAbqdYQwHBOrcgDgLBFJrsiiRNGYbpLBY4Ymhd4omkkUhE0pQEEwUBJjrHBd4QmCdzoiBDwYrCPLyZHF4QnagQeCE8UgJwYniJwgnIOzwfFO0wJCJzMQE4gyFEzR2FBQombkInDQI4AakAnBTYS+ZE5BMDE0LEES7YnLE0R3FAEQA=")), + 2: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYon/AA0gEAQniEwIhCAgYndEIjqBE8CaGKogmgKAp1fKAgncExBQBBQR1gKAp7BJ0IndExR4CE0idaOpYnbExqeYJxxPYEx0BJ0x2XExx2XJ20QE6xONJi5OPGwJOlBwLFkLoLFlBwJOkOwJOlE4JOkTjBOOE/52Pdi5OPEy7FnE5wmXE5xOZT5gmYEoMiiB1lgR4KTLAkDPBJ1WIAYDDKA4mWJwchDwYEDTjQiDJQh4GYLAhHFosSJy6OCTIxaEEywbBKYwjEEzMgUQxQFBogAURwZOGOjTKJdTYnOEryfHE0JQEfIpQgYQMAgJLeAgrtfTI4ndgSaFE4h0bdQkSZQpOfEAgIBO0AnEdrh2FJAb1EdbInEBIpObOwhOEEzYnFXzZ2HE4QlhE4QlDFMKcDYooniO0QnDT0YnCE0ciA")), + 3: require("heatshrink").decompress(atob("qFGwkEogAjiMUEkVAKYgnhPYolgOQIniOYZ4FOcLqBE8CaGKojpgKAomhEYUQE7gmHKAIxCE0QkCPYR1gZIgnZExR4CJ0idmE7ZONYzImNgEUJ0p3YJRh2ZJJwnXOpQhBdkpaETsMEGQhOhE7jFLUYpOfTzgmKE4hOiE4hOigEUJ0rvCEywnPEqx2OTjBOOE7ImOTsqeZE5zFYoJOmT5kBJzEAih4LdK5mBAQInKOqoYDEgR4JEypHDEYbxJOq5ABdgZ7CEzZOEJQgnGihOYEIzJFTionCKYxWGEy9ADAYnGUIYmWog/EdBFAEy7KIKAwnjKwLqWE5pMeT48CVQpQfgMjKEtEiAnfEQJQCgJSCTcB6FJzkEdYcUE8FAdQghDOzonKTjh2EZAidcDoInHJzodBOwx/BE8JxcOwsAOwQmhJgSXDObwnFEwUUO0LFGE8aeiE4YmiokQE0tE")), + 4: require("heatshrink").decompress(atob("qFGwkCkQAjiMSEkRTFE/4AGkMAgQCBE8MgEIYEDE7whDdQIngTQxVEE0ChFTjxQFE7jnFKAgxCOsBQFZgJ1gE7wmKPAROkTrTEHGAwnYiBHJFAaeXOoyXBEQZPac5AsFgJOhAoh2XJwwnFKoROdE4J9GJzwnIiQmVkInPAC0QE5AJFE64mHY5DFdE4SBEYr5JDJ0hKDJ0jCZJxoACgInmKLAmOTq5OOEy5OPTsxOYE5wmXO5wlYkAnMOqshiRNCgR4LOC8CkJCCEzxHDAgYnJOqpAEDoZ4HEyodDEQpQHdCsQOwwFHEyzoCPYzJGEy0gEwaZGA4acVEQSjHKAomXkQYEYAwlZeRKYDE8gjCYa7zJEwcCkImfKAb4FAD0hdTh4LgRSBOcR0CJz0gYYrrgN4QnEYrxOEE4bEeiAnGF4J2idL6VDE8ohBE0gnFE0J0BE4QGBiROgdIQABgJ2hJoTtjYgZSEE8ScgE4omikUQTcQADA=")), + 5: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYonhiAnjkEATIIniEwIhCAgYndEIhQFYUZVEE0BQFOr5QEeQQmiKAL1DOr5QEE7ROCDgZVEAoInZDwchFQQoDPAJOdEQYrBdrZFDOYwncEJDsDVIpOXgJxEE4pObEAgGFgJOaE48BaIhOZJ5ZObY5ROcE441CE6xOGPAwtCJzpGCJ0hHDkI1DJzwoEJzInLFg52dUo5O/J35OzE54mWOx4mXJxx1XE54mXkUhExkSJzCfMOrAlBPBiZXgQDBAQQmgJgh4JOqoYEFYwmaDoZzEFgh1YDgkiiAFEKAroXJJAGFiQmVkCNDTIz5EJy57HKAomXkQYEJoqaYeRadEJrAnJEQUAgJPiAoYmeT4cCkAnBE0BKCJkT1EkDCeJYYiDOkLDFFL5wBE4guCPDhEBEwQiDY70CkInDiQnCJzkhOwhKDdzp2Idb4nEE0B0Bdo4niE0J0CeYhOhgESUYYnidsgnEE0KeCE0gnDE0ciA")), + 6: require("heatshrink").decompress(atob("qFGwkCkQA/ABEgKQZPhEwgABEsAoGJkBxBE8JKEAowAbJIhQEgLDiPooAdKA4ncTZAndSwhQEFoInaJQkSKAwlZdgwnfSgYADE4h1ZDwInlcggnIOzAdCE8i7EY5J3XDgYhGd4pOZEI52bSYwGCOAJ2bYIodEOzZOFFAjFcEwwAIE6xOHABBO/J34ndEyx2PJ00BJ00SJ0p1XE54mXOxxO/J5wmYgQnMOrB2BPBgkWiJ1CPBbBYAYR4KiTAXRwIrFTjgZDJYZ4IEyoiEIwrDcEJJQFOqwiBDARxFFwgmXkAYDEogsBF4QmXEQJ7GUYYkBEzDKJAgYmdEQbKFEzonEKYgngJwgmfZggmjKQghgiBRGkBzeTgUikJRgc47LDErTnDEAkQJzkCJwYnEJzonEJIaddOwhJEJzgdBE4hYEJzieJADgnEE0KUCXzoAGkJLEiB2hOgQDBT0TsDT0YmlE4YmjkQ=")), + 7: require("heatshrink").decompress(atob("qFGwkCkQAhkIpBiQlhkBSEJ8InlEIIoFE7whEE8pQFE7giBJQoneI4MCTYhQDE7YdCYYondEQYnEPwZ1bE5BQCJzonHkR2ZEAkBE4pNBE7zHFYrYhFUgonaXAQeEEwruZEYcgiROHJ7AfDAwxOeAAURiAmHE65HIOzwmOJ35OPE6xOPO35O/J35O/J1gnPEyx2PEy5OOOq5OnE5xOYO5omZgJQMJrQnLiQnagR4JOq5nCDgZ1fEYRLDE5DoZkUQNoZ4GOrJKGAoomXOw7lCAwYmYDgJSEAAUBA4QDBJzB6FOQrDXJwTJFdLjJKE9jDYZRAmkKAwmhKAgmiKAYmBkApdJIgjCKYIncOQYvJYTovGE84lagR2DE4xOakBOEgJXFOjYnEJAbtdOwggEkAmbDgInDE0B0BE4QgcE5AkiXYbpCOLonGYo4nhPMYnCUEgnBY0kiA==")), + 8: require("heatshrink").decompress(atob("qFGwkCkQA/ABBSEJ8MgE4kBEsBPFE7xMCOIJ3hOYgFEE7rCGE70gE4pQBiAndYQwjBUohOZD4ZQFE7YkBE5AICYbZ2GE7sggJRCAA8iYzZOITroALE7EhExh4CAC0QExpPXOponZExx2XJ24nWdh52XdhzF/Yu5O/J35O0E55OXOx5O/J2omXE5x1XO54mYgQnMJrR4LOrciiAmiJgR4KEzIjDPBAlYiAiEeI51YkEBE4J5CD4KceTQQcBJgRQFdTZDCJIjDcNIqhGdTQmCkByFTTInDKgoAEE7ZEEJwhPdE1R1FE0InEE0R3DEwTGcDwomEE7hKFPYqafE8ROCE5DJbE5B/IEqh2ED4gnCJrMCJwgnEiB2bE4qeFEzUggQmIBQLEaEQImHLIImaE4YfcOw4lEFMLECS7onJO8wmkE4QljAAIA==")), + }; +} + +let rocketSequence = 1; +let settings = storage.readJSON("cassioWatch.settings.json", true) || {}; +let rocketSpeed = settings.rocketSpeed || 700; +delete settings; + +// schedule a draw for the next minute +let rocketInterval; +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function clearIntervals() { + if (rocketInterval) clearInterval(rocketInterval); + rocketInterval = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.clearRect(80, 57, 170, 96); + g.setColor(0, 255, 255); + g.drawRect(80, 57, 170, 96); + g.fillRect(80, 57, 170, 96); + g.setColor(0, 0, 0); + g.drawString(require("locale").time(new Date(), 1), 70, 60); + g.setFont("8x12", 2); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130); + g.setFont("8x12"); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126); + g.setFont("8x12", 2); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 78, 137); +} + +function drawBattery() { + bigThenSmall(E.getBattery(), "%", 135, 21); +} + +function drawRocket() { + let Rocket = getRocketSequences(); + g.clearRect(5, 62, 63, 115); + g.setColor(0, 255, 255); + g.drawRect(5, 62, 63, 115); + g.fillRect(5, 62, 63, 115); + g.drawImage(Rocket[rocketSequence], 5, 65, { scale: 0.7 }); + g.setColor(0, 0, 0); + rocketSequence = rocketSequence + 1; + if(rocketSequence > 8) rocketSequence = 1; +} + +function getTemperature(){ + try { + var temperature = E.getTemperature() + var formatted = require("locale").temp(temperature).replace(/[^\d-]/g, ''); + return formatted; + + } catch(ex) { + print(ex) + return "?" + } +} + +function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; +} + + +function draw() { + queueDraw(); + + g.clear(1); + g.setColor(0, 255, 255); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + let background = getBackgroundImage(); + g.drawImage(background, 0, 0, { scale: 1 }); + g.setColor(0, 0, 0); + g.setFont("6x12"); + g.drawString("Launching Process", 30, 20); + g.setFont("8x12"); + g.drawString("ACTIVATE", 40, 35); + + g.setFontAlign(0,-1); + g.setFont("8x12", 2); + g.drawString(getTemperature(), 155, 132); + g.drawString(Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm), 109, 98); + g.drawString(getSteps(), 158, 98); + + g.setFontAlign(-1,-1); + drawClock(); + drawRocket(); + drawBattery(); + + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + clearIntervals(); + } +}); + + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); + if (!locked) { + rocketInterval = setInterval(drawRocket, rocketSpeed); + } +}); + +Bangle.setUI("clock"); + +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe +g.clear(1); +draw(); diff --git a/apps/advcasio/app.png b/apps/advcasio/app.png new file mode 100644 index 000000000..a7c1b2736 Binary files /dev/null and b/apps/advcasio/app.png differ diff --git a/apps/advcasio/data.json b/apps/advcasio/data.json new file mode 100644 index 000000000..d986fa09c --- /dev/null +++ b/apps/advcasio/data.json @@ -0,0 +1 @@ +{"tasks":"", "weather":[]}; diff --git a/apps/advcasio/metadata.json b/apps/advcasio/metadata.json new file mode 100644 index 000000000..e3df73555 --- /dev/null +++ b/apps/advcasio/metadata.json @@ -0,0 +1,22 @@ +{ "id": "advcasio", + "name": "Advanced Casio Clock", + "shortName":"advcasio", + "version":"0.06", + "description": "An over-engineered clock inspired by Casio watches. It has current temperature, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.", + "icon": "app.png", + "tags": "clock", + "type": "clock", + "screenshots": [ + { "url": "screenshot-clock-1.jpg" }, + { "url": "screenshot-clock-2.jpg" }, + { "url": "screenshot-clock-3.jpg" }, + { "url": "screenshot-webapp.jpg" } + ], + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "allow_emulator":true, + "storage": [ + {"name":"advcasio.app.js","url":"app.js"}, + {"name":"advcasio.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/advcasio/screenshot-clock-1.jpg b/apps/advcasio/screenshot-clock-1.jpg new file mode 100644 index 000000000..7f6f042c9 Binary files /dev/null and b/apps/advcasio/screenshot-clock-1.jpg differ diff --git a/apps/advcasio/screenshot-clock-2.jpg b/apps/advcasio/screenshot-clock-2.jpg new file mode 100644 index 000000000..b5f1e38af Binary files /dev/null and b/apps/advcasio/screenshot-clock-2.jpg differ diff --git a/apps/advcasio/screenshot-clock-3.jpg b/apps/advcasio/screenshot-clock-3.jpg new file mode 100644 index 000000000..59389eb31 Binary files /dev/null and b/apps/advcasio/screenshot-clock-3.jpg differ diff --git a/apps/advcasio/screenshot-webapp.jpg b/apps/advcasio/screenshot-webapp.jpg new file mode 100644 index 000000000..d67bdba91 Binary files /dev/null and b/apps/advcasio/screenshot-webapp.jpg differ diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog new file mode 100644 index 000000000..03ae7894b --- /dev/null +++ b/apps/agenda/ChangeLog @@ -0,0 +1,19 @@ +0.01: Basic agenda with events from GB +0.02: Added settings page to force calendar sync +0.03: Disable past events display from settings +0.04: Added awareness of allDay field +0.05: Displaying calendar colour and name +0.06: Added clkinfo for clocks. +0.07: Clkinfo improvements. +0.08: Fix error in clkinfo (didn't require Storage & locale) + Fix clkinfo icon +0.09: Ensure Agenda supplies an image for clkinfo items +0.10: Update clock_info to avoid a redraw +0.11: Setting to use "Today" and "Yesterday" instead of dates + Added dynamic, short and range fields to clkinfo +0.12: Added color field and updating clkinfo periodically (running events) +0.13: Show day of the week in date +0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones +0.15: Minor code improvements +0.16: Correct date for all day events in negative timezones, improve locale display +0.17: Fixed "Today" and "Tomorrow" labels displaying in non-current weeks diff --git a/apps/agenda/README.md b/apps/agenda/README.md new file mode 100644 index 000000000..1a0ec9264 --- /dev/null +++ b/apps/agenda/README.md @@ -0,0 +1,30 @@ +# Agenda + +Basic agenda reading the events synchronised from GadgetBridge. + +### Functionalities + +* List all events in the next week (or whatever is synchronized) +* Optionally view past events (until GB removes them) +* Show start time and location of the events in the list +* Show the colour of the calendar in the list +* Display description, location and calendar name after tapping on events + +### Troubleshooting + +For the events sync to work, GadgetBridge needs to have the calendar permission and calendar sync should be enabled in the devices settings (gear sign in GB, also check the blacklisted calendars there, if events are missing). +Keep in mind that GadgetBridge won't synchronize all events on your calendar, just the ones in a time window of 7 days (you don't want your watch to explode), ideally every day old events get deleted since they appear out of such window. + +#### Force Sync + +If for any reason events still cannot sync or some are missing, you can try any of the following (just one, you normally don't need to do this): +1. from GB open the burger menu (side), tap debug and set time. +2. from the bangle, open settings > apps > agenda > Force calendar sync, then select not to delete the local events (this is equivalent to option 1). +3. do like option 2 but delete events, GB will synchronize a fresh database instead of patching the old one (good in case you somehow cannot get rid of older events) + +After any of the options, you may need to disconnect/force close Gadgetbridge before reconnecting and let it sync (give it some time for that too), restart the agenda app on the bangle after a while to see the changes. + +### Report a bug + +You can easily open an issue in the espruino repo, but I won't be notified and it might take time. +If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps). diff --git a/apps/agenda/agenda-icon.js b/apps/agenda/agenda-icon.js new file mode 100644 index 000000000..891543955 --- /dev/null +++ b/apps/agenda/agenda-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg1yhGIxAPMBwIPFhH//GAC5n/C4oHBC5/IGwoXBHQQAKC4OIFAWOxHv9GO9wAKI4XoC4foEIIWLC4IABC4gIBFxnuE4IqBC4gARC4ZzNAAwXaxe7ACO4C625C4m4xIJBzAeCxGbCAOIFgQOBC4pOBxe4AYIPBAYQKCAYYXE3GL/ADBx/oxb3BC4X+xG4xwOBC4uP/YDB54MBf4Po3eM/4XBx/+C4pTBGIIkBLgOYAYIvB9GJBwI6BL45zCL4aCCL4h3GU64ALdYS1CI55bBAAgXFO4mMO4QDBDIO/////YxBU53IxIVB/GfDAWYa5wtC/GPAYWIL4wXBL4oSBC4jcBC4m4QIWYSwWIIQIAG/CnMMAIAC/JLCMIIvMIwZHFJAJfLC5yPHAYIRDAoy/KCIi7BMon4d4+Od4IXBxAZBEQLtB/+YxIXDL4SLCL4WPzAXCNgRFBLIKnKLIrcEI4gXNAAp3CxGZAAzCBC5KnCKAIAICxBlBC4IAJxG/C4/4wAXLhBgD/IcD3AXMGAIqDDgRGNGAoXDFxxhEI4W4FxwwCaoYWBFx4YDAAQWRAEQ")) diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js new file mode 100644 index 000000000..f9ea6a35d --- /dev/null +++ b/apps/agenda/agenda.clkinfo.js @@ -0,0 +1,102 @@ +(function() { + function getPassedSec(date) { + var now = new Date(); + var passed = (now-date)/1000; + if(passed<0) return 0; + return passed; + } + + /* + * Returns the array [interval, switchTimeout] + * `interval` is the refresh rate (hourly or per minute) + * `switchTimeout` is the time before the refresh rate should change (or expiration) + */ + function getRefreshIntervals(ev) { + const threshold = 2 * 60 * 1000; //2 mins + const slices = 16; + var now = new Date(); + var passed = now - (ev.timestamp*1000); + var remaining = (ev.durationInSeconds*1000) - passed; + if(remaining<0) + return []; + if(passed<0) //check once it's started + return [ 2*-passed, -passed ]; + var slice = Math.round(remaining/slices); + if(slice < threshold) { //no need to refresh frequently + return [ threshold, remaining ]; + } + return [ slice, remaining ]; + } + + function _doInterval(interval) { + return setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, interval); + }, interval); + } + function _doSwitchTimeout(ev, switchTimeout) { + return setTimeout(()=>{ + this.emit("redraw"); + clearInterval(this.interval); + this.interval = undefined; + var tmp = getRefreshIntervals(ev); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, ev, switchTimeout); + }, switchTimeout); + } + + var agendaItems = { + name: "Agenda", + img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="), + dynamic: true, + items: [] + }; + var locale = require("locale"); + var now = new Date(); + var agenda = require("Storage").readJSON("android.calendar.json") + .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000) + .sort((a,b)=>a.timestamp - b.timestamp); + + agenda.forEach((entry, i) => { + + var title = entry.title.slice(0,12); + var date = new Date(entry.timestamp*1000); + var dateStr = locale.date(date).replace(/\d\d\d\d/,""); + var shortStr = ((date-now) > 86400000 || entry.allDay) ? dateStr : locale.time(date,1); + var color = "#"+(0x1000000+Number(entry.color)).toString(16).padStart(6,"0"); + dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; + shortStr = shortStr.trim().replace(" ", "\n"); + + agendaItems.items.push({ + name: "Agenda "+i, + hasRange: true, + get: () => ({ text: title + "\n" + dateStr, + img: agendaItems.img, short: shortStr, + color: color, + v: getPassedSec(date), min: 0, max: entry.durationInSeconds}), + show: function() { + var tmp = getRefreshIntervals(entry); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, entry, switchTimeout); + }, + hide: function() { + if(this.interval) + clearInterval(this.interval); + if(this.switchTimeout) + clearTimeout(this.switchTimeout); + this.interval = undefined; + this.switchTimeout = undefined; + } + }); + }); + + return agendaItems; +}) diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js new file mode 100644 index 000000000..f8fffc643 --- /dev/null +++ b/apps/agenda/agenda.js @@ -0,0 +1,168 @@ +/* CALENDAR is a list of: + {id:int, + type, + timestamp, + durationInSeconds, + title, + description, + location, + color:int, + calName, + allDay: bool, + } +*/ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +//var FILE = "android.calendar.json"; + +var Locale = require("locale"); + +//var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +//var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; + +//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?) +var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; +var settings = require("Storage").readJSON("agenda.settings.json",true)||{}; + +CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); + +function getDate(timestamp, allDay) { + // All day events are always in UTC and always start at 00:00:00, so we + // need to "undo" the timezone offsetting to make sure that the day is + // correct. + var offset = allDay ? new Date().getTimezoneOffset() * 60 : 0 + return new Date((timestamp+offset)*1000); +} + +function formatDay(date) { + let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/,*\s*\d\d\d\d/,""); + if (!settings.useToday) { + return formattedDate; + } + const today = new Date(Date.now()); + if (date.getDate() == today.getDate()) + return /*LANG*/"Today "; + else { + var tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + if (date.getDate() == tomorrow.getDate()) { + return /*LANG*/"Tomorrow "; + } + return formattedDate; + } +} +function formatDateLong(date, includeDay, allDay) { + let shortTime = Locale.time(date,1)+Locale.meridian(date); + if(allDay) shortTime = ""; + if(includeDay || allDay) { + return formatDay(date)+" "+shortTime; + } + return shortTime; +} + +function formatDateShort(date, allDay) { + return formatDay(date)+(allDay?"":" "+Locale.time(date,1)+Locale.meridian(date)); +} + +var lines = []; +function showEvent(ev) { + var bodyFont = fontBig; + if(!ev) return; + g.setFont(bodyFont); + //var lines = []; + if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10); + var titleCnt = lines.length; + var start = getDate(ev.timestamp, ev.allDay); + // All day events end at the midnight boundary of the following day. Here, we + // subtract one second for all day events so the days display correctly. + const allDayEndCorrection = ev.allDay ? 1 : 0; + var end = getDate((+ev.timestamp) + (+ev.durationInSeconds) - allDayEndCorrection, ev.allDay); + var includeDay = true; + if (titleCnt) lines.push(""); // add blank line after title + if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) + includeDay = false; + if(!includeDay && ev.allDay) { + //single day all day + lines = lines.concat( + g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10)); + } else if(includeDay || ev.allDay) { + lines = lines.concat( + /*LANG*/"Start"+":", + g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), + /*LANG*/"End"+":", + g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); + } else { + lines = lines.concat( + g.wrapString(formatDateShort(start,true), g.getWidth()-10), + g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10), + g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10)); + } + if(ev.location) + lines = lines.concat("",/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10)); + if(ev.description && ev.description.trim()) + lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10)); + if(ev.calName) + lines = lines.concat("",/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); + E.showScroller({ + h : g.getFontHeight(), // height of each menu item in pixels + c : lines.length, // number of menu items + // a function to draw a menu item + draw : function(idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx=lines.length-2) + showList(); + }, + back : () => showList() + }); +} + +function showList() { + //it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely + if(!settings.pastEvents) { + let now = new Date(); + //TODO add threshold here? + CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000); + } + if(CALENDAR.length == 0) { + E.showMessage(/*LANG*/"No events"); + return; + } + E.showScroller({ + h : 52, + c : Math.max(CALENDAR.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var ev = CALENDAR[idx]; + g.setColor(g.theme.fg); + g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); + if (!ev) return; + var isPast = false; + var x = r.x+2, title = ev.title; + var body = formatDateShort(getDate(ev.timestamp, ev.allDay),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location"); + if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; + if (title) g.setFontAlign(-1,-1).setFont(fontBig) + .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg); + g.drawString(body, x+10,r.y+20); + } + g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items + if(ev.color) { + g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0")); + g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4); + } + }, + select : idx => showEvent(CALENDAR[idx]), + back : () => load() + }); +} +showList(); diff --git a/apps/agenda/agenda.png b/apps/agenda/agenda.png new file mode 100644 index 000000000..c850b0e5d Binary files /dev/null and b/apps/agenda/agenda.png differ diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json new file mode 100644 index 000000000..c0198357d --- /dev/null +++ b/apps/agenda/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "agenda", + "name": "Agenda", + "version": "0.17", + "description": "Simple agenda", + "icon": "agenda.png", + "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], + "tags": "agenda,clkinfo", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"agenda.app.js","url":"agenda.js"}, + {"name":"agenda.settings.js","url":"settings.js"}, + {"name":"agenda.clkinfo.js","url":"agenda.clkinfo.js"}, + {"name":"agenda.img","url":"agenda-icon.js","evaluate":true} + ], + "data": [{"name":"agenda.settings.json"}] +} diff --git a/apps/agenda/screenshot_agenda_event1.png b/apps/agenda/screenshot_agenda_event1.png new file mode 100644 index 000000000..581da286b Binary files /dev/null and b/apps/agenda/screenshot_agenda_event1.png differ diff --git a/apps/agenda/screenshot_agenda_event2.png b/apps/agenda/screenshot_agenda_event2.png new file mode 100644 index 000000000..f5edcaae8 Binary files /dev/null and b/apps/agenda/screenshot_agenda_event2.png differ diff --git a/apps/agenda/screenshot_agenda_overview.png b/apps/agenda/screenshot_agenda_overview.png new file mode 100644 index 000000000..a2030d05f Binary files /dev/null and b/apps/agenda/screenshot_agenda_overview.png differ diff --git a/apps/agenda/settings.js b/apps/agenda/settings.js new file mode 100644 index 000000000..62e0c6dbd --- /dev/null +++ b/apps/agenda/settings.js @@ -0,0 +1,55 @@ +(function(back) { + function gbSend(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } + var settings = require("Storage").readJSON("agenda.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("agenda.settings.json", settings); + } + var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; + var mainmenu = { + "" : { "title" : "Agenda" }, + "< Back" : back, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Force calendar sync" : () => { + if(NRF.getSecurityStatus().connected) { + E.showPrompt(/*LANG*/"Do you want to also clear the internal database first?", { + buttons: {/*LANG*/"Yes": 1, /*LANG*/"No": 2, /*LANG*/"Cancel": 3} + }).then((v)=>{ + switch(v) { + case 1: + require("Storage").writeJSON("android.calendar.json",[]); + CALENDAR = []; + /* falls through */ + case 2: + gbSend({t:"force_calendar_sync", ids: CALENDAR.map(e=>e.id)}); + E.showAlert(/*LANG*/"Request sent to the phone").then(()=>E.showMenu(mainmenu)); + break; + case 3: + default: + E.showMenu(mainmenu); + return; + } + }); + } else { + E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu)); + } + }, + /*LANG*/"Show past events" : { + value : !!settings.pastEvents, + onchange: v => { + settings.pastEvents = v; + updateSettings(); + } + }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, + }; + E.showMenu(mainmenu); +}) diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog new file mode 100644 index 000000000..303fc7583 --- /dev/null +++ b/apps/agpsdata/ChangeLog @@ -0,0 +1,8 @@ +0.01: First, proof of concept +0.02: Load AGPS data on app start and automatically in background +0.03: Do not load AGPS data on boot + Increase minimum interval to 6 hours +0.04: Write AGPS data chunks with delay to improve reliability +0.05: Show last success date + Do not start A-GPS update automatically +0.06: Switch off gps after updating \ No newline at end of file diff --git a/apps/agpsdata/README.md b/apps/agpsdata/README.md new file mode 100644 index 000000000..57bb055a1 --- /dev/null +++ b/apps/agpsdata/README.md @@ -0,0 +1,19 @@ +# A-GPS Data + +Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge. + +Will download A-GPS data in background (if enabled in settings). + +The GNSS type can be configured in the settings. + +Make sure: +* your GadgetBridge version supports http requests +* turn on internet access in GadgetBridge settings + +Currently proof of concept on Bangle.js 2 only. + +## Creator +[@pidajo](https://github.com/pidajo) + +## Contributor +[@myxor](https://github.com/myxor) diff --git a/apps/agpsdata/agpsdata-icon.js b/apps/agpsdata/agpsdata-icon.js new file mode 100644 index 000000000..1677a2177 --- /dev/null +++ b/apps/agpsdata/agpsdata-icon.js @@ -0,0 +1 @@ +atob("MDCEAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAIiIOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiIAAAAAAAAAAAAAAAAAAAAAACIiPOIiIiIAAAAAAAAAAAAAAAAAAAAAAiIj/OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIiAAAAAAAAAAAAAAAAAAAAAiD//OIiIiIiAAAAAAAAAAAAAAAAAAAAIiP//OIiIiIiAAAAAAAAAAAAAAAAAAAAIg///OIiIiIiIAAAAAAAAAAAAAAAAAACIj///OIiIiIiIAAAAAAAAAAAAAAAAAACIP///OIiIiIiIgAAAAAAAAAAAAAAAAACI////OIiIiIiIgAAAAAAAAAAAAAAAAAiD////OIiIiIiIiAAAAAAAAAAAAAAAAAiP////OIiIiIiIiAAAAAAAAAAAAAAAAIiP////OIiIiIiIiIAAAAAAAAAAAAAAAIj/////OIiIiIiIiIAAAAAAAAAAAAAACIj/////OIiIiIiIiIgAAAAAAAAAAAAACI//////OIiIiIiIiIgAAAAAAAAAAAAAiI//////OIiIiIiIiIgAAAAAAAAAAAAAiIiIiIiIgzMzMzMziIiAAAAAAAAAAAAAiIiIiIiIj///////+IiAAAAAAAAAAAAIiIiIiIiIj////////4iIAAAAAAAAAAAIiIiIiIiIgzMzMzMzM4iIAAAAAAAAAACIP///////OIiIiIiIiIiIgAAAAAAAAACI////////OIiIiIiIiIiIgAAAAAAAAAiI////////OIiIiIiIiIiIiAAAAAAAAAiP////////OIiIiIiIiIiIiAAAAAAAAIiP////////OIiIiIiIiIiIiIAAAAAAAIj/////////OIiIiIiIiIiIiIAAAAAACIj////////ziIiIiIiIiIiIiIgAAAAACIP///////+IiIiIiIiIiIiIiIgAAAAACI//////84gYiIiIiIiIiIiIiIgAAAAAiD////84iIiAAAiIiIiIiIiIiIiAAAAAiP///ziIiIAAAAAIiIiIiIiIiIiAAAAIg/8ziIiIAAAAAAAAAIiIiIiIiIiIAAAIj/iIiIAAAAAAAAAAAAAIiIiIiIiIAACIiIiBgAAAAAAAAAAAAAAACIiIiIiIgACIiIgAAAAAAAAAAAAAAAAAAACIiIiIgACIgAAAAAAAAAAAAAAAAAAAAAAAiIiIgA==") diff --git a/apps/agpsdata/agpsdata.png b/apps/agpsdata/agpsdata.png new file mode 100644 index 000000000..a0f4de4cb Binary files /dev/null and b/apps/agpsdata/agpsdata.png differ diff --git a/apps/agpsdata/app.js b/apps/agpsdata/app.js new file mode 100644 index 000000000..48714d6d2 --- /dev/null +++ b/apps/agpsdata/app.js @@ -0,0 +1,68 @@ +function display(text1, text2) { + g.reset(); + g.clear(); + var img = require("Storage").read("agpsdata.img"); + if (img) { + g.drawImage(img, g.getWidth() - 48, g.getHeight() - 48 - 24); + } + g.setFont("Vector", 18); + g.setFontAlign(0, 1); + g.drawString(text1, g.getWidth() / 2, g.getHeight() / 3 + 24); + if (text2 != undefined) { + g.setFont("Vector", 12); + g.setFontAlign(-1, -1); + g.drawString(text2, 5, g.getHeight() / 3 + 29); + } + Bangle.drawWidgets(); +} + +// Show launcher when middle button pressed +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +let waiting = false; + +function start(restart) { + g.reset(); + g.clear(); + waiting = false; + if (!restart) { + display("Start?", "touch to start"); + } + else { + display("Retry?", "touch to retry"); + } + Bangle.on("touch", () => { updateAgps(); }); + + const file = "agpsdata.json"; + let data = require("Storage").readJSON(file, 1) || {}; + if (data.lastUpdate) { + g.setFont("Vector", 11); + g.drawString("last success:", 5, g.getHeight() - 22); + g.drawString(new Date(data.lastUpdate).toISOString(), 5, g.getHeight() - 11); + } + +} + +function updateAgps() { + g.reset(); + g.clear(); + if (!waiting) { + waiting = true; + display("Updating A-GPS...", "takes ~10 seconds"); + require("agpsdata").pull(function() { + waiting = false; + display("A-GPS updated.", "touch to close"); + Bangle.on("touch", () => { load(); }); + }, + function(error) { + waiting = false; + E.showAlert(error, "Error") + .then(() => { start(true); }); + }); + } else { + display("Waiting..."); + } +} +start(false); diff --git a/apps/agpsdata/boot.js b/apps/agpsdata/boot.js new file mode 100644 index 000000000..2b1e6819c --- /dev/null +++ b/apps/agpsdata/boot.js @@ -0,0 +1,26 @@ +(function() { + let waiting = false; + let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || { + enabled: true, + refresh: 1440 + }; + + if (settings.refresh == undefined) settings.refresh = 1440; + + function successCallback(){ + waiting = false; + } + + function errorCallback(){ + waiting = false; + } + + if (settings.enabled) { + setInterval(() => { + if (!waiting && NRF.getSecurityStatus().connected){ + waiting = true; + require("agpsdata").pull(successCallback, errorCallback); + } + }, settings.refresh * 1000 * 60); + } +})(); diff --git a/apps/agpsdata/default.json b/apps/agpsdata/default.json new file mode 100644 index 000000000..0b6e0cecf --- /dev/null +++ b/apps/agpsdata/default.json @@ -0,0 +1 @@ +{"enabled":true,"refresh":1440,"gnsstype":1} diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js new file mode 100644 index 000000000..4610331f6 --- /dev/null +++ b/apps/agpsdata/lib.js @@ -0,0 +1,96 @@ +function readSettings() { + settings = Object.assign( + require('Storage').readJSON("agpsdata.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {}); +} + +var FILE = "agpsdata.settings.json"; +var settings; +readSettings(); + +function setAGPS(b64) { + return new Promise(function(resolve, reject) { + const gnsstype = settings.gnsstype || 1; // default GPS + // What about: + // NAV-TIMEUTC (0x01 0x10) + // NAV-PV (0x01 0x03) + // or AGPS.zip uses AID-INI (0x0B 0x01) + Bangle.setGPSPower(1,"agpsdata"); // turn GPS on + Serial1.println(CASIC_CHECKSUM("$PCAS04," + gnsstype)); // set GNSS mode + + try { + writeChunks(atob(b64), ()=>{ + setTimeout(()=>{ + Bangle.setGPSPower(0,"agpsdata"); + resolve(); + }, 1000); + }); + } catch (e) { + console.log("error:", e); + Bangle.setGPSPower(0,"agpsdata"); + reject(); + } + }); +} + +var chunkI = 0; +function writeChunks(bin, resolve) { + return new Promise(function(resolve2) { + const chunkSize = 128; + setTimeout(function() { + if (chunkI < bin.length) { + var chunk = bin.substr(chunkI, chunkSize); + Serial1.write(atob(btoa(chunk))); + + chunkI += chunkSize; + writeChunks(bin, resolve); + } else { + if (resolve) + resolve(); // call outer resolve + } + }, 200); + }); +} + +function CASIC_CHECKSUM(cmd) { + var cs = 0; + for (var i = 1; i < cmd.length; i++) + cs = cs ^ cmd.charCodeAt(i); + return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0'); +} + +function updateLastUpdate() { + const file = "agpsdata.json"; + let data = require("Storage").readJSON(file, 1) || {}; + data.lastUpdate = Math.round(Date.now()); + require("Storage").writeJSON(file, data); +} + +exports.pull = function(successCallback, failureCallback) { + const uri = "https://www.espruino.com/agps/casic.base64"; + if (Bangle.http) { + Bangle.http(uri, {timeout : 10000}) + .then(event => { + setAGPS(event.resp) + .then(r => { + updateLastUpdate(); + if (successCallback) + successCallback(); + }) + .catch((e) => { + console.log("error", e); + if (failureCallback) + failureCallback(e); + }); + }) + .catch((e) => { + console.log("error", e); + if (failureCallback) + failureCallback(e); + }); + } else { + console.log("error: No http method found"); + if (failureCallback) + failureCallback(/*LANG*/ "No http method"); + } +}; diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json new file mode 100644 index 000000000..446661045 --- /dev/null +++ b/apps/agpsdata/metadata.json @@ -0,0 +1,24 @@ +{ "id": "agpsdata", + "name": "A-GPS Data Downloader App", + "shortName":"A-GPS Data", + "icon": "agpsdata.png", + "version":"0.06", + "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", + "tags": "boot,tool,assisted,gps,agps,http", + "allow_emulator":true, + "supports": ["BANGLEJS2"], + "readme":"README.md", + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ], + "storage": [ + {"name":"agpsdata.app.js","url":"app.js"}, + {"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true}, + {"name":"agpsdata.default.json","url":"default.json"}, + {"name":"agpsdata.boot.js","url":"boot.js"}, + {"name":"agpsdata","url":"lib.js"}, + {"name":"agpsdata.settings.js","url":"settings.js"} + ], + "data": [ + {"name": "agpsdata.json"}, + {"name": "agpsdata.settings.json"} + ] +} diff --git a/apps/agpsdata/screenshot.png b/apps/agpsdata/screenshot.png new file mode 100644 index 000000000..1fcb2d8ee Binary files /dev/null and b/apps/agpsdata/screenshot.png differ diff --git a/apps/agpsdata/screenshot2.png b/apps/agpsdata/screenshot2.png new file mode 100644 index 000000000..7c546e4b5 Binary files /dev/null and b/apps/agpsdata/screenshot2.png differ diff --git a/apps/agpsdata/settings.js b/apps/agpsdata/settings.js new file mode 100644 index 000000000..95b06fe55 --- /dev/null +++ b/apps/agpsdata/settings.js @@ -0,0 +1,71 @@ +(function(back) { +function writeSettings(key, value) { + var s = Object.assign( + require('Storage').readJSON(settingsDefaultFile, true) || {}, + require('Storage').readJSON(settingsFile, true) || {}); + s[key] = value; + require('Storage').writeJSON(settingsFile, s); + readSettings(); +} + +function readSettings() { + settings = Object.assign( + require('Storage').readJSON(settingsDefaultFile, true) || {}, + require('Storage').readJSON(settingsFile, true) || {}); +} + +var settingsFile = "agpsdata.settings.json"; +var settingsDefaultFile = "agpsdata.default.json"; + +var settings; +readSettings(); + +const gnsstypes = [ + "", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS", + "GPS+BDS+GLON." +]; + +function buildMainMenu() { + var mainmenu = { + '' : {'title' : 'AGPS download'}, + '< Back' : back, + "Enabled" : { + value : !!settings.enabled, + onchange : v => { writeSettings("enabled", v); } + }, + "Refresh every" : { + value : settings.refresh / 60, + min : 6, + max : 168, + step : 1, + format : v => v + "h", + onchange : v => { writeSettings("refresh", Math.round(v * 60)); } + }, + "GNSS type" : { + value : settings.gnsstype, + min : 1, + max : 7, + step : 1, + format : v => gnsstypes[v], + onchange : x => writeSettings('gnsstype', x) + }, + "Force refresh" : () => { + E.showMessage("Loading A-GPS data"); + require("agpsdata") + .pull( + function() { + E.showAlert("Success").then( + () => { E.showMenu(buildMainMenu()); }); + }, + function(error) { + E.showAlert(error, "Error") + .then(() => { E.showMenu(buildMainMenu()); }); + }); + } + }; + + return mainmenu; +} + +E.showMenu(buildMainMenu()); +}) diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog new file mode 100644 index 000000000..c8c5c75cb --- /dev/null +++ b/apps/aiclock/ChangeLog @@ -0,0 +1,9 @@ +0.01: New app! +0.02: Design improvements and fixes. +0.03: Indicate battery level through line occurrence. +0.04: Use widget_utils module. +0.05: Support for clkinfo. +0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.07: Use clock_info.addInteractive instead of a custom implementation +0.08: Use clock_info module as an app +0.09: clock_info now uses app name to maintain settings specifically for this clock face \ No newline at end of file diff --git a/apps/aiclock/README.md b/apps/aiclock/README.md new file mode 100644 index 000000000..521bd2c5e --- /dev/null +++ b/apps/aiclock/README.md @@ -0,0 +1,24 @@ +# AI Clock +This clock was designed by stable diffusion ([paper](https://arxiv.org/abs/2112.10752)) using the following prompt: + +`A rectangle banglejs watchface` + + +The original output of stable diffusion is shown here: + +![](orig.png) + +My implementation is shown below. Note that horizontal lines occur randomly, but the +probability is correlated with the battery level. So if your screen contains only +a few lines its time to charge your bangle again ;) Also note that the upper text +implements the clkinfo module and can be configured via touch and swipe left/right and up/down. + +![](impl.png) + + +# Thanks to +The great open-source community: I used an open-source diffusion model (https://github.com/CompVis/stable-diffusion) +to generate a watch face for the open-source smartwatch BangleJs. + +## Creator +- [David Peer](https://github.com/peerdavid). \ No newline at end of file diff --git a/apps/aiclock/aiclock.app.js b/apps/aiclock/aiclock.app.js new file mode 100644 index 000000000..8d76bc9fe --- /dev/null +++ b/apps/aiclock/aiclock.app.js @@ -0,0 +1,259 @@ +/************************************************ + * AI Clock + */ + const clock_info = require("clock_info"); + + + + /************************************************ + * Assets + */ +require("Font7x11Numeric7Seg").add(Graphics); +Graphics.prototype.setFontGochiHand = function(scale) { + // Actual height 27 (29 - 3) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAA8AAAAADwAAAAAPAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAA/+AAAB//4AAH///gAH///gAAf//AAAB/+AAAAH8AAAAAAAAAAAAAAAAAAAAH8AAAAB/8AAAAP/4AAAB//wAAAPx/AAAB8B+AAAHgD4AAA+AHgAADwAeAAAPAB4AAA8AHgAAD4AeAAAPgB4AAAeAPgAAB8A8AAAH4HwAAAP/+AAAAf/wAAAA/+AAAAB/wAAAAB8AAAAAAAAAAADgAAAAAfAAAAAB4AAAAAPAAAAAB8AAAAAHgAAAAA8AAAAADwAAAAAf4AAAAB//8AAAD//4AAAH//gAAAD/+AAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AHgAAHgA+AAA/AD4AAD4AfgAAfAD+AAB4Af4AAHgD/gAAeAfeAAB4D54AAHw/HgAAf/4fAAA//B8AAD/4DwAAH+APAAAHgA8AAAAADwAAAAAOAAAAAAAAABgAAAAAPAAAAAB8AAAAAHwAYAAAeAD4AAD4APwAAPA4fgAA8Hw+AADwfB4AAPh4HwAA+HgPAAB/+A8AAH/4DwAAP/weAAAf/j4AAAc//gAAAB/8AAAAD/gAAAAD8AAAAAAAAAAAAAAAAAADAAAAAA+AAAAAP4AAAAB/wAAAAP/AAAAD+8AAAAfzwAAAf8HAAAB/gcAAAH/hwAAAf//gAAA//+AAAAf//gAAAP//gAAAD/+AAAAB/4AAAAH/AAAAAeAAAAAAgAAAAAAAAAAAAcAAAB8H8AAAP4f4AAA/x/wAAD/H/gAAf+A+AAB74B4AAHnwHgAAefAfAAB58A8AAHj4DwAAePgPAAB4fA8AAHh+HgAAeD8+AAB4P/4AAHgf/AAAeA/4AAAAA+AAAAAAAAAAAAAAAAAAHgAAAAD/wAAAA//gAAAH//AAAA//+AAAD4H8AAAfA/wAAB4D/AAAHgP+AAAeB54AAB4HngAAHweeAAAfB54AAA4HngAAAAeeAAAAB/4AAAAH/AAAAAP4AAAAAfAAADwAAAAAPAAAAAA8HgAAADweAAAAPB4AAAA8HgAAADweAAAAPh4AAAA+HgAAAB4eAAAAHx4AAAAf//8AAA///wAAD//+AAAH//4AAAAeAAAAAB4AAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAD+AAAA+f+AAAH//8AAA///wAAH/4fgAAePgeAAB4+B4AAHj4HwAAePgPAAB4+A8AAHz4DwAAfngeAAA//B4AAD/+HgAAH//8AAAP//wAAAAf+AAAAA/wAAAAAYAAAAAAAAAAA/gAAAAH/AAAAA/8AAAAD34AAAAeHgAAAB4eAAAAHh4AAAA8HgAAADweAAAAPDwAAAA8PAAAADx4AAAAPvgAAAAf///AAB///8AAH///wAAP///AAA/wA4AABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOA4AAAA8DwAAADwPAAAAPA8AAAAYBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), + 46, + atob("DQoXEBQVExUUFRYUDQ=="), + 40+(scale<<8)+(1<<16) + ); + return this; +} + + +function drawBackground(start, end) { + g.setFontAlign(0,0); + g.setColor("#000"); + + var bat = E.getBattery() / 100.0; + var y = start; + while(y < end){ + // Show less lines in case of small battery level. + if(Math.random() > bat){ + y += 5; + continue; + } + + y += 3 + Math.floor(Math.random() * 10); + g.drawLine(0, y, W, y); + g.drawLine(0, y+1, W, y+1); + g.drawLine(0, y+2, W, y+2); + y += 2; + } +} + + +/************************************************ + * Set some important constants such as width, height and center + */ +var W = g.getWidth(),R=W/2; +var H = g.getHeight(); +var cx = W/2; +var cy = H/2; +var drawTimeout; + +var clkInfoY = 60; + + +/* + * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ + */ +Graphics.prototype.drawRotRect = function(w, r1, r2, angle) { + angle = angle % 360; + var w2=w/2, h=r2-r1, theta=angle*Math.PI/180; + return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0], + {x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta})); +}; + + + +function drawCircle(isLocked){ + g.setColor(g.theme.fg); + g.fillCircle(cx, cy, 12); + + var c = isLocked ? "#f00" : g.theme.bg; + g.setColor(c); + g.fillCircle(cx, cy, 6); +} + + +function drawTime(){ + // Draw digital time first + drawDigits(); + + // And now the analog time + var drawHourHand = g.drawRotRect.bind(g,8,12,R-38); + var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 ); + + g.setFontAlign(0,0); + + // Compute angles + var date = new Date(); + var m = parseInt(date.getMinutes() * 360 / 60); + var h = date.getHours(); + h = h > 12 ? h-12 : h; + h += date.getMinutes()/60.0; + h = parseInt(h*360/12); + + // Draw minute and hour fg + g.setColor(g.theme.fg); + drawHourHand(h); + drawMinuteHand(m); +} + + +function drawDigits(){ + var date = new Date(); + + g.setFontAlign(0,0); + g.setFont("7x11Numeric7Seg",3); + + var text = ("0"+date.getHours()).substr(-2) + ":" + ("0"+date.getMinutes()).substr(-2); //Bangle.getHealthStatus("day").steps; + var w = g.stringWidth(text); + g.setColor(g.theme.bg); + g.fillRect(cx-w/2-4, 120, cx+w/2+4, 140+20); + + // Draw right line as designed by stable diffusion + g.setColor(g.theme.fg); + g.drawLine(cx+w/2+5, 120, cx+w/2+5, 140+20); + g.drawLine(cx+w/2+6, 120, cx+w/2+6, 140+20); + g.drawLine(cx+w/2+7, 120, cx+w/2+7, 140+20); + + // And the 7set text + g.setColor("#BBB"); + g.drawString("88:88", cx, 140); + g.drawString("88:88", cx+1, 140); + g.drawString("88:88", cx, 141); + + g.setColor(g.theme.fg); + g.drawString(text, cx, 140); + g.drawString(text, cx+1, 140); + g.drawString(text, cx, 141); +} + + +function draw(){ + // Note that we force a redraw also of the clock info as + // we want to ensure (for design purpose) that the hands + // are above the clkinfo section. + clockInfoMenu.redraw(); +} + + +function drawMainClock(){ + // Queue draw in one minute + queueDraw(); + + g.setColor("#fff"); + g.reset().clearRect(0, clkInfoY, g.getWidth(), g.getHeight()); + + drawBackground(clkInfoY, H); + drawTime(); + drawCircle(Bangle.isLocked()); +} + + +/* + * Listeners + */ +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('lock', function(isLocked) { + drawCircle(isLocked); +}); + + +E.on("kill", function(){ + clockInfoMenu.remove(); + delete clockInfoMenu; +}); + + +/* + * Some helpers + */ +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +/************************************************ + * Clock Info + */ +let clockInfoItems = clock_info.load(); +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app : "aiclock", + x : 0, + y: 0, + w: W, + h: clkInfoY, + draw : (itm, info, options) => { + g.setFontAlign(0,0); + g.setFont("Vector", 20); + + g.setColor("#fff"); + g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h); + drawBackground(0, clkInfoY+2); + + // Set text and font + var image = info.img; + var text = String(info.text); + + var imgWidth = image == null ? 0 : 24; + var strWidth = g.stringWidth(text); + var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2); + var w = imgWidth + strWidth; + + // Draw right line as designed by stable diffusion + g.setColor(options.focus ? "#0f0" : "#fff"); + g.fillRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2) + + g.setColor("#000"); + g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2); + g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2); + g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2); + + // Draw text and image + g.drawString(text, cx+imgWidth/2, 42); + g.drawString(text, cx+1+imgWidth/2, 41); + + if(image != null) { + var scale = image.width ? imgWidth / image.width : 1; + g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale}); + } + + drawMainClock(); + } +}); + + +/* + * Lets start widgets, listen for btn etc. + */ +// Show launcher when middle button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); +/* + * we are not drawing the widgets as we are taking over the whole screen + * so we will blank out the draw() functions of each widget and change the + * area to the top bar doesn't get cleared. + */ +require('widget_utils').hide(); + +// Clear the screen once, at startup and draw clock +g.setTheme({bg:"#fff",fg:"#000",dark:false}); +draw(); + +// After drawing the watch face, we can draw the widgets +// Bangle.drawWidgets(); diff --git a/apps/aiclock/aiclock.icon.js b/apps/aiclock/aiclock.icon.js new file mode 100644 index 000000000..0033b3848 --- /dev/null +++ b/apps/aiclock/aiclock.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/ACfAEZU/ECZELIKhSR/+PAoWAv4FDhk/x/ggP+j0fx/AgP8n8PCIX8CwIFC/F/w4FBgP4gEHC4QFE//w//DC4QFB8YFC+P/8IdCAoYdBAoPxDoQAd+CiKh4dQwDhfAA4A=")) \ No newline at end of file diff --git a/apps/aiclock/aiclock.png b/apps/aiclock/aiclock.png new file mode 100644 index 000000000..104261254 Binary files /dev/null and b/apps/aiclock/aiclock.png differ diff --git a/apps/aiclock/impl.png b/apps/aiclock/impl.png new file mode 100644 index 000000000..8a9e43e2d Binary files /dev/null and b/apps/aiclock/impl.png differ diff --git a/apps/aiclock/impl_2.png b/apps/aiclock/impl_2.png new file mode 100644 index 000000000..be3519a4b Binary files /dev/null and b/apps/aiclock/impl_2.png differ diff --git a/apps/aiclock/impl_3.png b/apps/aiclock/impl_3.png new file mode 100644 index 000000000..c2a036d14 Binary files /dev/null and b/apps/aiclock/impl_3.png differ diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json new file mode 100644 index 000000000..66f5de664 --- /dev/null +++ b/apps/aiclock/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "aiclock", + "name": "AI Clock", + "shortName":"AI Clock", + "icon": "aiclock.png", + "version":"0.09", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, + "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", + "type": "clock", + "tags": "clock,clkinfo", + "screenshots": [ + {"url":"orig.png"}, + {"url":"impl.png"}, + {"url":"impl_2.png"}, + {"url":"impl_3.png"} + ], + "storage": [ + {"name":"aiclock.app.js","url":"aiclock.app.js"}, + {"name":"aiclock.img","url":"aiclock.icon.js","evaluate":true} + ] +} diff --git a/apps/aiclock/orig.png b/apps/aiclock/orig.png new file mode 100644 index 000000000..009826454 Binary files /dev/null and b/apps/aiclock/orig.png differ diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index b00055334..15afa790b 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -29,3 +29,29 @@ 0.27: New UI! 0.28: Fix bug with alarms not firing when configured to fire only once 0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday +0.30: Fix "Enable All" +0.31: Add seconds to timers +0.32: Fix wrong hidden filter + Add option for auto-delete a timer after it expires +0.33: Allow hiding timers&alarms +0.34: Add "Confirm" option to alarm/timer edit menus +0.35: Add automatic translation of more strings +0.36: alarm widget moved out of app +0.37: add message input and dated Events +0.38: Display date in locale + When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection + Display alarm label in delete prompt +0.39: Dated event repeat option +0.40: Use substring of message when it's longer than fits the designated menu entry. +0.41: Fix a menu bug affecting alarms with empty messages. +0.42: Fix date not getting saved in event edit menu when tapping Confirm +0.43: New settings: Show confirm, Show Overflow, Show Group. +0.44: Add "delete timer after expiration" setting to events. +0.45: Fix new alarm when selectedAlarm is undefined +0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it. +0.47: Fix wrap around when snoozed through midnight +0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year. +0.49: fix uncaught error if no scroller (Bangle 1). Would happen when trying + to select an alarm in the main menu. +0.50: Bangle.js 2: Long touch of alarm in main menu toggle it on/off. Touching the icon on + the right will do the same. diff --git a/apps/alarm/README.md b/apps/alarm/README.md index 741946b0c..d7b64b3c2 100644 --- a/apps/alarm/README.md +++ b/apps/alarm/README.md @@ -1,21 +1,27 @@ # Alarms & Timers -This app allows you to add/modify any alarms and timers. +This app allows you to add/modify any alarms, timers and events. + +Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered. If a datetime input app (e.g. datetime_picker) is detected, it will be used for the selection of the date+time of events. It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps. ## Menu overview - `New...` - - `New Alarm` → Configure a new alarm + - `New Alarm` → Configure a new alarm (triggered based on time and day of week) - `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely) - - `New Timer` → Configure a new timer + - `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds) + - `New Event` → Configure a new event (triggered based on time and date) + - `Repeat` → Alarm can be fired only once or repeated (every X number of _days_, _weeks_, _months_ or _years_) - `Advanced` - `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details - `Enable All` → Enable _all_ disabled alarms & timers - `Disable All` → Disable _all_ enabled alarms & timers - `Delete All` → Delete _all_ alarms & timers +On Bangle.js 2 it's possible to toggle alarms, timers and events from the main menu. This is done by clicking the indicator icons of corresponding entries. Or long pressing anywhere on them. + ## Creator - [Gordon Williams](https://github.com/gfwilliams) @@ -25,6 +31,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master - [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features - [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support - [storm64](https://github.com/storm64) - Fix redrawing in submenus +- [thyttan](https://github.com/thyttan) - Toggle alarms directly from main menu. ## Attributions diff --git a/apps/alarm/app.js b/apps/alarm/app.js index fe0f67dbb..0318be6d3 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -1,11 +1,18 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); +const settings = Object.assign({ + showConfirm : true, + showAutoSnooze : true, + showHidden : true +}, require('Storage').readJSON('alarm.json',1)||{}); // 0 = Sunday (default), 1 = Monday const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0; -const WORKDAYS = 62 +const WORKDAYS = 62; const WEEKEND = firstDayOfWeek ? 192 : 65; const EVERY_DAY = firstDayOfWeek ? 254 : 127; +const INTERVALS = ["day", "week", "month", "year"]; +const INTERVAL_LABELS = [/*LANG*/"Day", /*LANG*/"Week", /*LANG*/"Month", /*LANG*/"Year"]; const iconAlarmOn = "\0" + atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA=="); const iconAlarmOff = "\0" + (g.theme.dark @@ -21,6 +28,8 @@ const iconTimerOff = "\0" + (g.theme.dark // An array of alarm objects (see sched/README.md) var alarms = require("sched").getAlarms(); +// Fix possible wrap around in existing alarms #3281, broken alarms still needs to be saved to get fixed +alarms.forEach(e => e.t %= 86400000); // This can probably be removed in the future when we are sure there are no more broken alarms function handleFirstDayOfWeek(dow) { if (firstDayOfWeek == 1) { @@ -40,41 +49,97 @@ function handleFirstDayOfWeek(dow) { // Check the first day of week and update the dow field accordingly (alarms only!) alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); -function showMainMenu() { +function getLabel(e) { + const dateStr = getDateText(e.date); + return (e.timer + ? require("time_utils").formatDuration(e.timer) + : (dateStr ? `${dateStr}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) + ) + (e.msg ? ` ${e.msg}` : ""); +} + +function getDateText(d) { + return d && (settings.menuDateFormat === "mmdd" ? d.substring(d.startsWith(new Date().getFullYear()) ? 5 : 0) : require("locale").date(new Date(d), 1)); +} + +function trimLabel(label, maxLength) { + if(settings.showOverflow) return label; + return (label.length > maxLength + ? label.substring(0,maxLength-3) + "..." + : label.substring(0,maxLength)); +} + +function formatAlarmProperty(msg) { + if(settings.showOverflow) return msg; + if (msg == null) { + return msg; + } else if (msg.length > 7) { + return msg.substring(0,6)+"..."; + } else { + return msg.substring(0,7); + } +} + +function showMainMenu(scroll, group, scrollback) { const menu = { - "": { "title": /*LANG*/"Alarms & Timers" }, - "< Back": () => load(), - /*LANG*/"New...": () => showNewMenu() + "": { "title": group || /*LANG*/"Alarms & Timers", scroll: scroll }, + "< Back": () => group ? showMainMenu(scrollback) : load(), + /*LANG*/"New...": () => showNewMenu(group) }; + const getGroups = settings.showGroup && !group; + const groups = getGroups ? {} : undefined; + var showAlarm; + const getIcon = (e)=>{return e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff);}; alarms.forEach((e, index) => { - var label = e.timer - ? require("time_utils").formatDuration(e.timer) - : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""); - menu[label] = { - value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff), - onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index) - }; + showAlarm = !settings.showGroup || (group ? e.group === group : !e.group); + if(showAlarm) { + const label = trimLabel(getLabel(e),40); + menu[label] = { + value: e.on, + onchange: (v, touch) => { + if (touch && (2==touch.type || 145getIcon(e) + }; + } else if (getGroups) { + groups[e.group] = undefined; + } }); - menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); + if (!group) { + Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller?scroller.scroll:undefined)); + menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); + } - E.showMenu(menu); + var scroller = E.showMenu(menu).scroller; } -function showNewMenu() { - E.showMenu({ +function showNewMenu(group) { + const newMenu = { "": { "title": /*LANG*/"New..." }, - "< Back": () => showMainMenu(), - /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined), - /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined) - }); + "< Back": () => showMainMenu(group), + /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined, false, null, group), + /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined), + /*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true, null, group) + }; + + if (group) delete newMenu[/*LANG*/"Timer"]; + E.showMenu(newMenu); } -function showEditAlarmMenu(selectedAlarm, alarmIndex) { +function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate, scroll, group) { var isNew = alarmIndex === undefined; var alarm = require("sched").newDefaultAlarm(); + if (isNew && group) alarm.group = group; + if (withDate || (selectedAlarm && selectedAlarm.date)) { + alarm.del = require("sched").getSettings().defaultDeleteExpiredTimers; + } alarm.dow = handleFirstDayOfWeek(alarm.dow); if (selectedAlarm) { @@ -82,40 +147,120 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { } var time = require("time_utils").decodeTime(alarm.t); + if (withDate && !alarm.date) alarm.date = new Date().toLocalISOString().slice(0,10); + var date = alarm.date ? new Date(alarm.date) : undefined; + var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm"); + var keyboard = "textinput"; + try {keyboard = require(keyboard);} catch(e) {keyboard = null;} + var datetimeinput; + try {datetimeinput = require("datetimeinput");} catch(e) {datetimeinput = null;} const menu = { - "": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" }, + "": { "title": title }, "< Back": () => { - saveAlarm(alarm, alarmIndex, time); - showMainMenu(); + prepareAlarmForSave(alarm, alarmIndex, time, date); + saveAndReload(); + showMainMenu(scroll, group); + } + }; + + if (alarm.date && datetimeinput) { + menu[`${getDateText(date.toLocalISOString().slice(0,10))} ${require("time_utils").formatTime(time)}`] = { + value: date, + format: v => "", + onchange: v => { + setTimeout(() => { + var datetime = new Date(v.getTime()); + datetime.setHours(time.h, time.m); + datetimeinput.input({datetime}).then(result => { + time.h = result.getHours(); + time.m = result.getMinutes(); + prepareAlarmForSave(alarm, alarmIndex, time, result, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); + }); + }, 100); + } + }; + } else { + Object.assign(menu, { + /*LANG*/"Hour": { + value: time.h, + format: v => ("0" + v).substr(-2), + min: 0, + max: 23, + wrap: true, + onchange: v => time.h = v + }, + /*LANG*/"Minute": { + value: time.m, + format: v => ("0" + v).substr(-2), + min: 0, + max: 59, + wrap: true, + onchange: v => time.m = v + }, + /*LANG*/"Day": { + value: date ? date.getDate() : null, + min: 1, + max: 31, + wrap: true, + onchange: v => date.setDate(v) + }, + /*LANG*/"Month": { + value: date ? date.getMonth() + 1 : null, + format: v => require("date_utils").month(v), + onchange: v => date.setMonth((v+11)%12) + }, + /*LANG*/"Year": { + value: date ? date.getFullYear() : null, + min: new Date().getFullYear(), + max: 2100, + onchange: v => date.setFullYear(v) + } + }); + } + + Object.assign(menu, { + /*LANG*/"Message": { + value: alarm.msg, + format: formatAlarmProperty, + onchange: () => { + setTimeout(() => { + keyboard.input({text:alarm.msg}).then(result => { + alarm.msg = result; + prepareAlarmForSave(alarm, alarmIndex, time, date, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); + }); + }, 100); + } }, - /*LANG*/"Hour": { - value: time.h, - format: v => ("0" + v).substr(-2), - min: 0, - max: 23, - wrap: true, - onchange: v => time.h = v - }, - /*LANG*/"Minute": { - value: time.m, - format: v => ("0" + v).substr(-2), - min: 0, - max: 59, - wrap: true, - onchange: v => time.m = v + /*LANG*/"Group": { + value: alarm.group, + format: formatAlarmProperty, + onchange: () => { + setTimeout(() => { + keyboard.input({text:alarm.group}).then(result => { + alarm.group = result; + prepareAlarmForSave(alarm, alarmIndex, time, date, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); + }); + }, 100); + } }, /*LANG*/"Enabled": { value: alarm.on, onchange: v => alarm.on = v }, /*LANG*/"Repeat": { - value: decodeDOW(alarm), - onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => { + value: decodeRepeat(alarm), + onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => { + if (repeat) { + alarm.del = false; // do not auto delete a repeated alarm + } alarm.rp = repeat; alarm.dow = dow; - alarm.t = require("time_utils").encodeTime(time); - setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); + prepareAlarmForSave(alarm, alarmIndex, time, date, true); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); }) }, /*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v), @@ -123,19 +268,44 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { value: alarm.as, onchange: v => alarm.as = v }, - /*LANG*/"Cancel": () => showMainMenu() - }; + /*LANG*/"Delete After Expiration": { + value: alarm.del, + onchange: v => alarm.del = v + }, + /*LANG*/"Hidden": { + value: alarm.hidden || false, + onchange: v => alarm.hidden = v + }, + /*LANG*/"Cancel": () => showMainMenu(scroll, group), + /*LANG*/"Confirm": () => { + prepareAlarmForSave(alarm, alarmIndex, time, date); + saveAndReload(); + showMainMenu(scroll, group); + } + }); + + if (!keyboard) delete menu[/*LANG*/"Message"]; + if (!keyboard || !settings.showGroup) delete menu[/*LANG*/"Group"]; + if (!settings.showConfirm) delete menu[/*LANG*/"Confirm"]; + if (!settings.showAutoSnooze) delete menu[/*LANG*/"Auto Snooze"]; + if (!settings.showHidden) delete menu[/*LANG*/"Hidden"]; + if (!alarm.date) { + delete menu[/*LANG*/"Day"]; + delete menu[/*LANG*/"Month"]; + delete menu[/*LANG*/"Year"]; + delete menu[/*LANG*/"Delete After Expiration"]; + } if (!isNew) { menu[/*LANG*/"Delete"] = () => { - E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { + E.showPrompt(getLabel(alarm) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { if (confirm) { alarms.splice(alarmIndex, 1); saveAndReload(); - showMainMenu(); + showMainMenu(scroll, group); } else { alarm.t = require("time_utils").encodeTime(time); - setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate, scroll, group); } }); }; @@ -144,17 +314,18 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { E.showMenu(menu); } -function saveAlarm(alarm, alarmIndex, time) { +function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) { alarm.t = require("time_utils").encodeTime(time); alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0; + if(date) alarm.date = date.toLocalISOString().slice(0,10); - if (alarmIndex === undefined) { - alarms.push(alarm); - } else { - alarms[alarmIndex] = alarm; + if(!temp) { + if (alarmIndex === undefined) { + alarms.push(alarm); + } else { + alarms[alarmIndex] = alarm; + } } - - saveAndReload(); } function saveAndReload() { @@ -168,49 +339,82 @@ function saveAndReload() { alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); } -function decodeDOW(alarm) { +function decodeRepeat(alarm) { return alarm.rp - ? require("date_utils") - .dows(firstDayOfWeek, 2) - .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") - .join("") - .toLowerCase() - : "Once" + ? (alarm.date + ? `${alarm.rp.num}*${INTERVAL_LABELS[INTERVALS.indexOf(alarm.rp.interval)]}` + : require("date_utils") + .dows(firstDayOfWeek, 2) + .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") + .join("") + .toLowerCase()) + : /*LANG*/"Once"; } -function showEditRepeatMenu(repeat, dow, dowChangeCallback) { - var originalRepeat = repeat; - var originalDow = dow; - var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY; +function showEditRepeatMenu(repeat, day, dowChangeCallback) { + var dow; const menu = { "": { "title": /*LANG*/"Repeat Alarm" }, "< Back": () => dowChangeCallback(repeat, dow), - /*LANG*/"Once": { + /*LANG*/"Only Once": () => dowChangeCallback(false, EVERY_DAY) // The alarm will fire once. Internally it will be saved // as "fire every days" BUT the repeat flag is false so // we avoid messing up with the scheduler. - value: !repeat, - onchange: () => dowChangeCallback(false, EVERY_DAY) - }, - /*LANG*/"Workdays": { - value: repeat && dow == WORKDAYS, - onchange: () => dowChangeCallback(true, WORKDAYS) - }, - /*LANG*/"Weekends": { - value: repeat && dow == WEEKEND, - onchange: () => dowChangeCallback(true, WEEKEND) - }, - /*LANG*/"Every Day": { - value: repeat && dow == EVERY_DAY, - onchange: () => dowChangeCallback(true, EVERY_DAY) - }, - /*LANG*/"Custom": { - value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false, - onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow) - } }; + let restOfMenu; + if (typeof day === "number") { + dow = day; + var originalDow = dow; + var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY; + + restOfMenu = { + /*LANG*/"Workdays": { + value: repeat && dow == WORKDAYS, + onchange: () => dowChangeCallback(true, WORKDAYS) + }, + /*LANG*/"Weekends": { + value: repeat && dow == WEEKEND, + onchange: () => dowChangeCallback(true, WEEKEND) + }, + /*LANG*/"Every Day": { + value: repeat && dow == EVERY_DAY, + onchange: () => dowChangeCallback(true, EVERY_DAY) + }, + /*LANG*/"Custom": { + value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false, + onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, repeat, originalDow) + } + }; + } else { + // var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month + dow = EVERY_DAY; + const repeatObj = repeat || {interval: "month", num: 1}; + + restOfMenu = { + /*LANG*/"Every": { + value: repeatObj.num, + min: 1, + onchange: v => { + repeat = repeatObj; + repeat.num = v; + } + }, + /*LANG*/"Interval": { + value: INTERVALS.indexOf(repeatObj.interval), + format: v => INTERVAL_LABELS[v], + min: 0, + max: INTERVALS.length - 1, + onchange: v => { + repeat = repeatObj; + repeat.interval = INTERVALS[v]; + } + } + }; + } + + Object.assign(menu, restOfMenu); E.showMenu(menu); } @@ -221,7 +425,7 @@ function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) // If the user unchecks all the days then we assume repeat = once // and we force the dow to every day. var repeat = dow > 0; - dowChangeCallback(repeat, repeat ? dow : EVERY_DAY) + dowChangeCallback(repeat, repeat ? dow : EVERY_DAY); } }; @@ -232,7 +436,7 @@ function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) }; }); - menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback) + menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback); E.showMenu(menu); } @@ -247,11 +451,14 @@ function showEditTimerMenu(selectedTimer, timerIndex) { } var time = require("time_utils").decodeTime(timer.timer); + var keyboard = "textinput"; + try {keyboard = require(keyboard);} catch(e) {keyboard = null;} const menu = { "": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" }, "< Back": () => { - saveTimer(timer, timerIndex, time); + prepareTimerForSave(timer, timerIndex, time); + saveAndReload(); showMainMenu(); }, /*LANG*/"Hours": { @@ -268,23 +475,61 @@ function showEditTimerMenu(selectedTimer, timerIndex) { wrap: true, onchange: v => time.m = v }, + /*LANG*/"Seconds": { + value: time.s, + min: 0, + max: 59, + step: 1, + wrap: true, + onchange: v => time.s = v + }, + /*LANG*/"Message": { + value: timer.msg, + format: formatAlarmProperty, + onchange: () => { + setTimeout(() => { + keyboard.input({text:timer.msg}).then(result => { + timer.msg = result; + prepareTimerForSave(timer, timerIndex, time, true); + setTimeout(showEditTimerMenu, 10, timer, timerIndex); + }); + }, 100); + } + }, /*LANG*/"Enabled": { value: timer.on, onchange: v => timer.on = v }, + /*LANG*/"Delete After Expiration": { + value: timer.del, + onchange: v => timer.del = v + }, + /*LANG*/"Hidden": { + value: timer.hidden || false, + onchange: v => timer.hidden = v + }, /*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v), + /*LANG*/"Cancel": () => showMainMenu(), + /*LANG*/"Confirm": () => { + prepareTimerForSave(timer, timerIndex, time); + saveAndReload(); + showMainMenu(); + } }; + if (!keyboard) delete menu[/*LANG*/"Message"]; + if (!settings.showConfirm) delete menu[/*LANG*/"Confirm"]; + if (!settings.showHidden) delete menu[/*LANG*/"Hidden"]; if (!isNew) { menu[/*LANG*/"Delete"] = () => { - E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { + E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { if (confirm) { alarms.splice(timerIndex, 1); saveAndReload(); showMainMenu(); } else { timer.timer = require("time_utils").encodeTime(time); - setTimeout(showEditTimerMenu, 10, timer, timerIndex) + setTimeout(showEditTimerMenu, 10, timer, timerIndex); } }); }; @@ -293,18 +538,18 @@ function showEditTimerMenu(selectedTimer, timerIndex) { E.showMenu(menu); } -function saveTimer(timer, timerIndex, time) { +function prepareTimerForSave(timer, timerIndex, time, temp) { timer.timer = require("time_utils").encodeTime(time); - timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer; + timer.t = (require("time_utils").getCurrentTimeMillis() + timer.timer) % 86400000; timer.last = 0; - if (timerIndex === undefined) { - alarms.push(timer); - } else { - alarms[timerIndex] = timer; + if (!temp) { + if (timerIndex === undefined) { + alarms.push(timer); + } else { + alarms[timerIndex] = timer; + } } - - saveAndReload(); } function showAdvancedMenu() { @@ -327,7 +572,16 @@ function enableAll(on) { } else { E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" }).then((confirm) => { if (confirm) { - alarms.forEach(alarm => alarm.on = on); + alarms.forEach((alarm, i) => { + alarm.on = on; + if (on) { + if (alarm.timer) { + prepareTimerForSave(alarm, i, require("time_utils").decodeTime(alarm.timer)); + } else { + prepareAlarmForSave(alarm, i, require("time_utils").decodeTime(alarm.t)); + } + } + }); saveAndReload(); showMainMenu(); } else { diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index cac837b5e..17dd147e3 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,17 +2,17 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.29", + "version": "0.50", "description": "Set alarms and timers on your Bangle", "icon": "app.png", - "tags": "tool,alarm,widget", + "tags": "tool,alarm", "supports": [ "BANGLEJS", "BANGLEJS2" ], "readme": "README.md", - "dependencies": { "scheduler":"type" }, + "dependencies": { "scheduler":"type", "alarm":"widget" }, "storage": [ { "name": "alarm.app.js", "url": "app.js" }, { "name": "alarm.img", "url": "app-icon.js", "evaluate": true }, - { "name": "alarm.wid.js", "url": "widget.js" } + { "name": "alarm.settings.js", "url":"settings.js" } ], "screenshots": [ { "url": "screenshot-1.png" }, @@ -26,5 +26,6 @@ { "url": "screenshot-9.png" }, { "url": "screenshot-10.png" }, { "url": "screenshot-11.png" } - ] + ], + "data":[ {"name":"alarm.settings.json"} ] } diff --git a/apps/alarm/settings.js b/apps/alarm/settings.js new file mode 100644 index 000000000..2843fbdb1 --- /dev/null +++ b/apps/alarm/settings.js @@ -0,0 +1,51 @@ +(function(back) { + let settings = Object.assign({ + showConfirm : true, + showAutoSnooze : true, + showHidden : true + }, require('Storage').readJSON('alarm.json',1)||{}); + + const save = () => require('Storage').write('alarm.json', settings); + const DATE_FORMATS = ['default', 'mmdd']; + const DATE_FORMATS_LABELS = [/*LANG*/'Default', /*LANG*/'MMDD']; + + const appMenu = { + '': {title: 'alarm'}, '< Back': back, + /*LANG*/'Menu Date Format': { + value: DATE_FORMATS.indexOf(settings.menuDateFormat || 'default'), + format: v => DATE_FORMATS_LABELS[v], + min: 0, + max: DATE_FORMATS.length - 1, + onchange : v => { + if(v > 0) { + settings.menuDateFormat=DATE_FORMATS[v]; + } else { + delete settings.menuDateFormat; + } + save(); + } + }, + /*LANG*/'Show Menu Auto Snooze': { + value : !!settings.showAutoSnooze, + onchange : v => { settings.showAutoSnooze=v; save();} + }, + /*LANG*/'Show Menu Confirm': { + value : !!settings.showConfirm, + onchange : v => { settings.showConfirm=v; save();} + }, + /*LANG*/'Show Menu Hidden': { + value : !!settings.showHidden, + onchange : v => { settings.showHidden=v; save();} + }, + /*LANG*/'Show Menu Group': { + value : !!settings.showGroup, + onchange : v => { settings.showGroup=v; save();} + }, + /*LANG*/'Show Text Overflow': { + value : !!settings.showOverflow, + onchange : v => { settings.showOverflow=v; save();} + }, + }; + + E.showMenu(appMenu); +}) diff --git a/apps/alarm/widget.js b/apps/alarm/widget.js deleted file mode 100644 index 052ac9ebd..000000000 --- a/apps/alarm/widget.js +++ /dev/null @@ -1,8 +0,0 @@ -WIDGETS["alarm"]={area:"tl",width:0,draw:function() { - if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); - },reload:function() { - // don't include library here as we're trying to use as little RAM as possible - WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0; - } -}; -WIDGETS["alarm"].reload(); diff --git a/apps/ncfrun/ChangeLog b/apps/alarmqm/ChangeLog similarity index 100% rename from apps/ncfrun/ChangeLog rename to apps/alarmqm/ChangeLog diff --git a/apps/alarmqm/app.png b/apps/alarmqm/app.png new file mode 100644 index 000000000..ee0085206 Binary files /dev/null and b/apps/alarmqm/app.png differ diff --git a/apps/alarmqm/boot.js b/apps/alarmqm/boot.js new file mode 100644 index 000000000..cf35452c0 --- /dev/null +++ b/apps/alarmqm/boot.js @@ -0,0 +1,20 @@ +(function () { + function dismissAlarm(alarm) { + // Run only for alarms, not timers + if (!alarm.timer) { + if ("qmsched" in WIDGETS) { + require("qmsched").setMode(0); + } else { + // Code from qmsched.js, so we can work without it + require("Storage").writeJSON( + "setting.json", + Object.assign(require("Storage").readJSON("setting.json", 1) || {}, { + quiet: 0, + }) + ); + } + } + } + + Bangle.on("alarmDismiss", dismissAlarm); + })(); \ No newline at end of file diff --git a/apps/alarmqm/metadata.json b/apps/alarmqm/metadata.json new file mode 100644 index 000000000..bae4b0807 --- /dev/null +++ b/apps/alarmqm/metadata.json @@ -0,0 +1,13 @@ +{ "id": "alarmqm", + "name": "Alarm Quiet Mode", + "shortName":"AlarmQM", + "version":"0.01", + "description": "Service that turns off quiet mode after alarm dismiss", + "icon": "app.png", + "tags": "quiet,alarm", + "supports" : ["BANGLEJS2"], + "type": "bootloader", + "storage": [ + {"name":"alarmqm.boot.js","url":"boot.js"} + ] +} diff --git a/apps/alpinenav/ChangeLog b/apps/alpinenav/ChangeLog new file mode 100644 index 000000000..9e08d0e35 --- /dev/null +++ b/apps/alpinenav/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Added adjustment for Bangle.js magnetometer heading fix +0.03: Minor code improvements diff --git a/apps/alpinenav/README.md b/apps/alpinenav/README.md index d18cdfd6d..823d6c9f8 100644 --- a/apps/alpinenav/README.md +++ b/apps/alpinenav/README.md @@ -2,13 +2,26 @@ Alpine Navigator ================ App that performs GPS monitoring to track and display position relative to a given origin in realtime. -![screenshot](./sample.png) +![screenshot](./sample.png) + + [compass 5] + + altitude +[start 1] [current 2] + + distance +[from start 3] [track 4] + + +[btn1 -- screen lock] +[btn2 -- remove points] +[btn3 -- pause] Functions --------- -Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially. +Note if you've not used GPS yet, I suggest using one of the GPS apps to get your first fix and confirm, as I've found that helps initially. -The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed: +The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. All your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit, but I've kept it fairly conservative here, as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed: 1. altitude at origin, this is displayed left of the centre. 2. current altitude, displayed centre right @@ -16,12 +29,12 @@ The GPS and magnetometer will be turned on and after a few moments, when the wat 4. distance travelled, bottom right (meters) 5. compass heading, at the top -For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals. +For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time, because the waypoints will be reduced when it reaches a set threshold, so you may see the path smooth out slightly at intervals. -If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough. +If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring, but often just moving your wrist a bit is enough. The buttons do the following: -BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work. +BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! Soft and hard reset will both still work. BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route. BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen. diff --git a/apps/alpinenav/app.js b/apps/alpinenav/app.js index 29eeab0c9..7188e25bf 100644 --- a/apps/alpinenav/app.js +++ b/apps/alpinenav/app.js @@ -145,7 +145,7 @@ Bangle.setCompassPower(1); Bangle.setGPSPower(1); g.clear(); process_GPS(); -var poll_GPS = setInterval(process_GPS, 9000); +/*var poll_GPS =*/ setInterval(process_GPS, 9000); setWatch(function () { if (!button_lock) { @@ -224,7 +224,7 @@ Bangle.on('mag', function (m) { if (isNaN(m.heading)) compass_heading = "---"; else - compass_heading = 360 - Math.round(m.heading); + compass_heading = Math.round(m.heading); current_colour = g.getColor(); g.reset(); g.setColor(background_colour); diff --git a/apps/alpinenav/metadata.json b/apps/alpinenav/metadata.json index dcb56e912..f85b80e09 100644 --- a/apps/alpinenav/metadata.json +++ b/apps/alpinenav/metadata.json @@ -1,7 +1,7 @@ { "id": "alpinenav", "name": "Alpine Nav", - "version": "0.01", + "version": "0.03", "description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime", "icon": "app-icon.png", "tags": "outdoors,gps", diff --git a/apps/altimeter/ChangeLog b/apps/altimeter/ChangeLog index 29388520e..905152382 100644 --- a/apps/altimeter/ChangeLog +++ b/apps/altimeter/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Actually upload correct code +0.03: Display sea-level pressure, too, and allow calibration +0.04: Switch to using system code for pressure calibration diff --git a/apps/altimeter/app.js b/apps/altimeter/app.js index cac4e80fd..6e44161da 100644 --- a/apps/altimeter/app.js +++ b/apps/altimeter/app.js @@ -1,30 +1,66 @@ -Bangle.setBarometerPower(true, "app"); +Bangle.setBarometerPower(true, "altimeter"); g.clear(1); Bangle.loadWidgets(); Bangle.drawWidgets(); -var zero = 0; var R = Bangle.appRect; var y = R.y + R.h/2; var MEDIANLENGTH = 20; -var avr = [], median; -var value = 0; +var avr = []; + +function fmt(t) { + if ((t > -100) && (t < 1000)) + t = t.toFixed(1); + else + t = t.toFixed(0); + return t; +} Bangle.on('pressure', function(e) { while (avr.length>MEDIANLENGTH) avr.pop(); avr.unshift(e.altitude); - median = avr.slice().sort(); - g.reset().clearRect(0,y-30,g.getWidth()-10,y+30); + let median = avr.slice().sort(); + g.reset().clearRect(0,y-30,g.getWidth()-10,R.h); if (median.length>10) { var mid = median.length>>1; - value = E.sum(median.slice(mid-4,mid+5)) / 9; - g.setFont("Vector",50).setFontAlign(0,0).drawString((value-zero).toFixed(1), g.getWidth()/2, y); + var value = E.sum(median.slice(mid-4,mid+5)) / 9; + } else { + var value = median[median.length>>1]; } + t = fmt(value); + + g.setFont("Vector",50).setFontAlign(0,0).drawString(t, g.getWidth()/2, y); + + let o = Bangle.getOptions(); + let sea = o.seaLevelPressure; + t = sea.toFixed(1) + " " + e.temperature.toFixed(1); + if (0) { + print("alt raw:", value.toFixed(1)); + print("temperature:", e.temperature); + print("pressure:", e.pressure); + print("sea pressure:", sea); + } + g.setFont("Vector",25).setFontAlign(-1,0).drawString(t, 10, R.y+R.h - 35); }); +function setPressure(m, a) { + o = Bangle.getOptions(); + print(o); + o.seaLevelPressure = o.seaLevelPressure * m + a; + Bangle.setOptions(o); + avr = []; +} + +print(g.getFonts()); g.reset(); -g.setFont("6x8").setFontAlign(0,0).drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); -g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2); -setWatch(function() { - zero = value; -}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true}); +g.setFont("Vector:15"); +g.setFontAlign(0,0); +g.drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); +g.drawString(/*LANG*/"SEA L (hPa) TEMP (C)", g.getWidth()/2, y+62); +g.flip(); +g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"STD", g.getWidth()-5, g.getHeight()/2); +Bangle.setUI("updown", btn=> { + if (!btn) setPressure(0, 1013.25); + if (btn<0) setPressure(1, 1); + if (btn>0) setPressure(1, -1); +}); diff --git a/apps/altimeter/metadata.json b/apps/altimeter/metadata.json index 8bdbf3022..ff5eb9935 100644 --- a/apps/altimeter/metadata.json +++ b/apps/altimeter/metadata.json @@ -1,6 +1,6 @@ { "id": "altimeter", "name": "Altimeter", - "version":"0.02", + "version":"0.04", "description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.", "icon": "app.png", "tags": "tool,outdoors", diff --git a/apps/alyxclock/ChangeLog b/apps/alyxclock/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/alyxclock/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/alyxclock/README.md b/apps/alyxclock/README.md new file mode 100644 index 000000000..7765a878f --- /dev/null +++ b/apps/alyxclock/README.md @@ -0,0 +1,4 @@ +# Half-Life Alyx Style clock + +![](screenshot_alyxclock.png) + diff --git a/apps/alyxclock/alyxclock.app.js b/apps/alyxclock/alyxclock.app.js new file mode 100644 index 000000000..57b7d7f48 --- /dev/null +++ b/apps/alyxclock/alyxclock.app.js @@ -0,0 +1,174 @@ +const icoH = [ + [0,1,1,0,0,1,1,0], + [1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1], + [0,1,1,1,1,1,1,0], + [0,0,1,1,1,1,0,0], + [0,0,0,1,1,0,0,0], + [0,0,0,0,0,0,0,0], +] + +const icoR = [ + [0,0,0,0,1,1,1,1,0,0,0,0], + [0,0,1,1,0,0,0,0,1,1,0,0], + [0,1,1,1,1,0,0,1,1,0,1,0], + [0,1,1,0,0,0,0,0,0,0,1,0], + [1,1,1,1,1,1,1,1,0,0,0,1], + [1,1,0,0,1,0,0,0,0,0,0,1], + [1,1,1,1,1,1,1,0,1,1,0,1], + [1,1,1,1,1,1,0,0,0,0,1,1], + [0,1,1,1,1,1,1,1,1,1,1,0], + [0,1,1,1,1,1,1,1,1,1,1,0], + [0,0,1,1,1,1,1,1,1,1,0,0], + [0,0,0,0,1,1,1,1,0,0,0,0], +] + +let idTimeout = null; + +function icon (icon, x, y, size, gap) { + const color = g.getColor(); + for (let r=0; r W-40 || + (lastModified.x1 > W/2 && lastModified.y1 < 16)) clockInfoMenuB.redraw(); + } + if (lastModified.y2>W-40) { + if (lastModified.x1 < 40 || + (lastModified.x1 < W/2 && lastModified.y2>W-16)) clockInfoMenuD.redraw(); + if (lastModified.x2 > W-40 || + (lastModified.x1 > W/2 && lastModified.y2>W-16)) clockInfoMenuC.redraw(); + } + } + // draw hands + drawHands(); + lastModified = getHandBounds(); + //g.drawRect(lastModified); // debug + }; + + // Clear the screen once, at startup + background.fillRect(0, 0, W - 1, H - 1); + // draw immediately at first, queue update + draw(); + + let clockInfoMenuA, clockInfoMenuB, clockInfoMenuC, clockInfoMenuD; + // Show launcher when middle button pressed + Bangle.setUI({ + mode: "clock", + redraw : draw, + remove: function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (clockInfoMenuA) clockInfoMenuA.remove(); + if (clockInfoMenuB) clockInfoMenuB.remove(); + if (clockInfoMenuC) clockInfoMenuC.remove(); + if (clockInfoMenuD) clockInfoMenuD.remove(); + require("widget_utils").show(); // re-show widgets + } + }); + // Load widgets + Bangle.loadWidgets(); + require("widget_utils").hide(); + + // render clockinfos + let clockInfoDraw = function(itm, info, options) { + // itm: the item containing name/hasRange/etc + // info: data returned from itm.get() containing text/img/etc + // options: options passed into addInteractive + const left = options.x < 88, + top = options.y < 88, + imgx = left ? 1 : W - 28, imgy = top ? 19 : H - 42, + textx = left ? 2 : W - 1, texty = top ? 2 : H - 16; + let bg = g.theme.bg, fg = g.theme.fg; + // Clear the background + g.reset(); + background.fillRect(imgx, imgy, imgx+25, imgy+25); // erase image + background.fillRect(left?0:W/2, texty-1, left?W/2:W-1, texty+15); // erase text + // indicate focus - change colours + if (options.focus) { + bg = g.theme.fg; + fg = g.toColor("#f00"); + } + + if (info.img) + require("clock_info").drawBorderedImage(info.img,imgx,imgy); + + g.setFont("6x8:2").setFontAlign(left ? -1 : 1, -1); + g.setColor(bg).drawString(info.text, textx-2, texty). // draw the text background + drawString(info.text, textx+2, texty). + drawString(info.text, textx, texty-2). + drawString(info.text, textx, texty+2); + g.setColor(fg).drawString(info.text, textx, texty); // draw the text + // redraw hands if needed + if ((top && lastModified.x1=texty)) { + g.reset(); + drawHands(); + } + }; + + // Load the clock infos + let clockInfoItems = require("clock_info").load(); + let clockInfoItemsBangle = clockInfoItems.find(i=>i.name=="Bangle"); + // Add extra Calendar and digital clock ClockInfos + if (clockInfoItemsBangle) { + if (!clockInfoItemsBangle.items.find(i=>i.name=="Date")) { + clockInfoItemsBangle.items.push({ name : "Date", + get : () => { + let d = new Date(); + let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0); + g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17); + return { + text : require("locale").dow(d,1).toUpperCase(), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 86400000); + }, 86400000 - (Date.now() % 86400000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + }); + } + if (!clockInfoItemsBangle.items.find(i=>i.name=="Clock")) { + clockInfoItemsBangle.items.push({ name : "Clock", + get : () => { + return { + text : require("locale").time(new Date(),1), + img : atob("GBiBAAAAAAB+AAD/AAD/AAH/gAP/wAP/wAYAYAYAYAYAYAYAYAYAcAYAcAYAYAYAYAYAYAYAYAP/wAP/wAH/gAD/AAD/AAB+AAAAAA==") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 60000); + }, 60000 - (Date.now() % 60000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + }); + } + } + + + // Add the 4 clockinfos + const CLOCKINFOSIZE = 50; + clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, { + x: 0, + y: 0, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, { + x: W - CLOCKINFOSIZE, + y: 0, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuC = require("clock_info").addInteractive(clockInfoItems, { + x: W - CLOCKINFOSIZE, + y: H - CLOCKINFOSIZE, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + clockInfoMenuD = require("clock_info").addInteractive(clockInfoItems, { + x: 0, + y: H - CLOCKINFOSIZE, + w: CLOCKINFOSIZE, + h: CLOCKINFOSIZE, + draw: clockInfoDraw + }); + + /*setInterval(function() { + date.ms += 60000; draw(); + }, 500);*/ + } \ No newline at end of file diff --git a/apps/analogquadclk/icon.png b/apps/analogquadclk/icon.png new file mode 100644 index 000000000..79ded13c1 Binary files /dev/null and b/apps/analogquadclk/icon.png differ diff --git a/apps/analogquadclk/metadata.json b/apps/analogquadclk/metadata.json new file mode 100644 index 000000000..d60255717 --- /dev/null +++ b/apps/analogquadclk/metadata.json @@ -0,0 +1,16 @@ +{ "id": "analogquadclk", + "name": "Analog Quad Clock", + "shortName":"Quad Clock", + "version":"0.03", + "description": "An analog clock with clockinfos in each of the 4 corners, allowing 4 different data types to be rendered at once", + "icon": "icon.png", + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ], + "type": "clock", + "tags": "clock,clkinfo,analog,clockbg", + "supports" : ["BANGLEJS2"], + "dependencies" : { "clock_info":"module", "clockbg":"module" }, + "storage": [ + {"name":"analogquadclk.app.js","url":"app.js"}, + {"name":"analogquadclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/analogquadclk/screenshot.png b/apps/analogquadclk/screenshot.png new file mode 100644 index 000000000..f1a1dd6b5 Binary files /dev/null and b/apps/analogquadclk/screenshot.png differ diff --git a/apps/analogquadclk/screenshot2.png b/apps/analogquadclk/screenshot2.png new file mode 100644 index 000000000..a62a94f58 Binary files /dev/null and b/apps/analogquadclk/screenshot2.png differ diff --git a/apps/andark/ChangeLog b/apps/andark/ChangeLog index 341868930..fa89d5618 100644 --- a/apps/andark/ChangeLog +++ b/apps/andark/ChangeLog @@ -1,4 +1,9 @@ 0.01: Release 0.02: Rename app 0.03: Add type "clock" -0.04: changed update cylce, when locked +0.04: Changed update cylce, when locked +0.05: Fix support for dark theme + support widgets + + add settings for widgets, order of drawing and hour hand length +0.06: Fix issue showing widgets when app is fast-loaded into from launcher with widgets disabled +0.07: Enable fast loading and queue updates to the second +0.08: Restore redraw on charging event + fixup for safer fast-loading diff --git a/apps/andark/README.md b/apps/andark/README.md index 3770c1017..9034677c2 100644 --- a/apps/andark/README.md +++ b/apps/andark/README.md @@ -1,10 +1,16 @@ -# Analog Clock +# Dark Analog Clock ## Features -* second hand +* second hand (only on unlocked screen) * date -* battery percantage -* no widgets +* battery percentage (showing charge status with color) +* turned off or swipeable widgets (choose in settings) ![logo](andark_screen.png) + +## Settings + +* whether to load widgets, or not; if widgets are loaded, they are swipeable from the top; if not, NO ACTIONS of widgets are available +* date and battery can be printed both below hands (as if hands were physical) and above (more readable) +* hour hand can be made slighly shorter to improve readability when minute hand is behind a number diff --git a/apps/andark/andark_screen.png b/apps/andark/andark_screen.png index 2ac54c1cd..1f0e5b089 100644 Binary files a/apps/andark/andark_screen.png and b/apps/andark/andark_screen.png differ diff --git a/apps/andark/app.js b/apps/andark/app.js index efa00ce6f..81d757ce4 100644 --- a/apps/andark/app.js +++ b/apps/andark/app.js @@ -1,22 +1,41 @@ -const c={"x":g.getWidth()/2,"y":g.getHeight()/2}; -let zahlpos=[]; -let unlock = false; +{ +const defaultSettings = { + loadWidgets : false, + textAboveHands : false, + shortHrHand : false +}; +const settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{}); -function zeiger(len,dia,tim){ - const x =c.x+ Math.cos(tim)*len/2, - y =c.y + Math.sin(tim)*len/2, +const c={"x":g.getWidth()/2,"y":g.getHeight()/2}; + +const zahlpos=(function() { + let z=[]; + let sk=1; + for(let i=-10;i<50;i+=5){ + let win=i*2*Math.PI/60; + let xsk =c.x+2+Math.cos(win)*(c.x-10), + ysk =c.y+2+Math.sin(win)*(c.x-10); + if(sk==3){xsk-=10;} + if(sk==6){ysk-=10;} + if(sk==9){xsk+=10;} + if(sk==12){ysk+=10;} + if(sk==10){xsk+=3;} + z.push([sk,xsk,ysk]); + sk+=1; + } + return z; +})(); + +const zeiger = function(len,dia,tim) { + const x=c.x+ Math.cos(tim)*len/2, + y=c.y + Math.sin(tim)*len/2, d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)}, pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y]; return pol; +}; -} - -function draw(){ - const d=new Date(); +const drawHands = function(d) { let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds(); - //draw black rectangle in the middle to clear screen from scale and hands - g.setColor(0,0,0); - g.fillRect(10,10,2*c.x-10,2*c.x-10); g.setColor(1,1,1); if(h>12){ @@ -29,30 +48,93 @@ function draw(){ m=2*Math.PI/60*(m)-Math.PI/2; s=2*Math.PI/60*s-Math.PI/2; - g.setFontAlign(0,0); - g.setFont("Vector",10); - let dateStr = " "+require("locale").date(d)+" "; - g.drawString(dateStr, c.x, c.y+20, true); - // g.drawString(d.getDate(),1.4*c.x,c.y,true); - g.drawString(Math.round(E.getBattery()/5)*5+"%",c.x,c.y+40,true); - drawlet(); //g.setColor(1,0,0); - const hz = zeiger(100,5,h); + const hz = zeiger(settings.shortHrHand?88:100,5,h); g.fillPoly(hz,true); - // g.setColor(1,1,1); + //g.setColor(1,1,1); const minz = zeiger(150,5,m); g.fillPoly(minz,true); if (unlock){ - const sekz = zeiger(150,2,s); - g.fillPoly(sekz,true); + const sekz = zeiger(150,2,s); + g.fillPoly(sekz,true); } g.fillCircle(c.x,c.y,4); +}; +const drawText = function(d) { + g.setFont("Vector",10); + g.setBgColor(0,0,0); + g.setColor(1,1,1); + const dateStr = require("locale").date(d); + g.drawString(dateStr, c.x, c.y+20, true); + const batStr = Math.round(E.getBattery()/5)*5+"%"; + if (Bangle.isCharging()) { + g.setBgColor(1,0,0); + } + g.drawString(batStr, c.x, c.y+40, true); +}; +const drawNumbers = function() { + //draws the numbers on the screen + g.setFont("Vector",20); + g.setColor(1,1,1); + g.setBgColor(0,0,0); + for(let i = 0;i<12;i++){ + g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true); + } +}; + +let drawTimeout; +let queueMillis = 1000; +let unlock = true; + +const updateState = function() { + if (Bangle.isLCDOn()) { + if (!Bangle.isLocked()) { + queueMillis = 1000; + unlock = true; + } else { + queueMillis = 60000; + unlock = false; + } + draw(); + } else { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}; + +const queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, queueMillis - (Date.now() % queueMillis)); +}; + +const draw = function() { + // draw black rectangle in the middle to clear screen from scale and hands + g.setColor(0,0,0); + g.fillRect(10,10,2*c.x-10,2*c.x-10); + // prepare for drawing the text + g.setFontAlign(0,0); + // do drawing + drawNumbers(); + const d=new Date(); + if (settings.textAboveHands) { + drawHands(d); drawText(d); + } else { + drawText(d); drawHands(d); + } + queueDraw(); +}; -} //draws the scale once the app is startet -function drawScale(){ +const drawScale = function() { + // clear the screen + g.setBgColor(0,0,0); + g.clear(); + // draw the ticks of the scale for(let i=-14;i<47;i++){ const win=i*2*Math.PI/60; let d=2; @@ -62,64 +144,35 @@ function drawScale(){ g.fillRect(10,10,2*c.x-10,2*c.x-10); g.setColor(1,1,1); } -} +}; -//draws the numbers on the screen +//// main running sequence //// -function drawlet(){ - g.setFont("Vector",20); - for(let i = 0;i<12;i++){ - g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2]); - } -} -//calcultes the Position of the numbers when app starts and saves them in an array -function setlet(){ - let sk=1; - for(let i=-10;i<50;i+=5){ - let win=i*2*Math.PI/60; - let xsk =c.x+2+Math.cos(win)*(c.x-10), - ysk =c.y+2+Math.sin(win)*(c.x-10); - if(sk==3){xsk-=10;} - if(sk==6){ysk-=10;} - if(sk==9){xsk+=10;} - if(sk==12){ysk+=10;} - if(sk==10){xsk+=3;} - zahlpos.push([sk,xsk,ysk]); - sk+=1; - } -} -setlet(); -// Clear the screen once, at startup -g.setBgColor(0,0,0); -g.clear(); -drawScale(); -draw(); - -let secondInterval= setInterval(draw, 1000); -// Stop updates when LCD is off, restart when on - -Bangle.on('lcdPower',on=>{ - if (secondInterval) clearInterval(secondInterval); - secondInterval = undefined; - if (on) { - secondInterval = setInterval(draw, 1000); - draw(); // draw immediately - }else{ +// Show launcher when middle button pressed, and widgets that we're clock +Bangle.setUI({ + mode: "clock", + remove: function() { + Bangle.removeListener('lcdPower', updateState); + Bangle.removeListener('lock', updateState); + Bangle.removeListener('charging', draw); + // We clear drawTimout after removing all listeners, because they can add one again + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + require("widget_utils").show(); } }); -Bangle.on('lock',on=>{ - if (secondInterval) clearInterval(secondInterval); - secondInterval = undefined; - if (!on) { - secondInterval = setInterval(draw, 1000); - unlock = true; - draw(); // draw immediately - }else{ - secondInterval = setInterval(draw, 60000); - unlock = false; - draw(); - } - }); +// Load widgets if needed, and make them show swipeable +if (settings.loadWidgets) { + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); +} else if (global.WIDGETS) require("widget_utils").hide(); -// Show launcher when middle button pressed -Bangle.setUI("clock"); +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower', updateState); +Bangle.on('lock', updateState); +Bangle.on('charging', draw); // Immediately redraw when charger (dis)connected + +updateState(); +drawScale(); +draw(); +} diff --git a/apps/andark/metadata.json b/apps/andark/metadata.json index 3e2b3116e..4bd88b3f5 100644 --- a/apps/andark/metadata.json +++ b/apps/andark/metadata.json @@ -1,15 +1,18 @@ { "id": "andark", "name": "Analog Dark", "shortName":"AnDark", - "version":"0.04", + "version":"0.08", "description": "analog clock face without disturbing widgets", "icon": "andark_icon.png", "type": "clock", "tags": "clock", "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"andark_screen.png"}], "readme": "README.md", "storage": [ {"name":"andark.app.js","url":"app.js"}, + {"name":"andark.settings.js","url":"settings.js"}, {"name":"andark.img","url":"app_icon.js","evaluate":true} - ] + ], + "data": [{"name":"andark.json"}] } diff --git a/apps/andark/settings.js b/apps/andark/settings.js new file mode 100644 index 000000000..7bbceb2c2 --- /dev/null +++ b/apps/andark/settings.js @@ -0,0 +1,28 @@ +(function(back) { + const defaultSettings = { + loadWidgets : false, + textAboveHands : false, + shortHrHand : false + } + let settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{}); + + const save = () => require('Storage').write('andark.json', settings); + + const appMenu = { + '': {title: 'andark'}, '< Back': back, + /*LANG*/'Load widgets': { + value : !!settings.loadWidgets, + onchange : v => { settings.loadWidgets=v; save();} + }, + /*LANG*/'Text above hands': { + value : !!settings.textAboveHands, + onchange : v => { settings.textAboveHands=v; save();} + }, + /*LANG*/'Short hour hand': { + value : !!settings.shortHrHand, + onchange : v => { settings.shortHrHand=v; save();} + }, + }; + + E.showMenu(appMenu); +}) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index f13ccd95c..9ac11d75b 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -9,3 +9,36 @@ 0.08: Handling of alarms 0.09: Alarm vibration, repeat, and auto-snooze now handled by sched 0.10: Fix SMS bug +0.12: Use default Bangle formatter for booleans +0.13: Added Bangle.http function (see Readme file for more info) +0.14: Fix timeout of http function not being cleaned up +0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later) +0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152) +0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge +0.18: Use new message library + If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged) +0.19: Add automatic translation for a couple of strings. +0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly. +0.21: Fix broken 'Messages' button in menu +0.22: Handle connection events for GPS forwarding from phone +0.23: Handle 'act' Gadgetbridge messages for realtime activity monitoring +0.24: Handle new 'nav' event for navigation +0.25: Added option to 'ignore' an app from the message +0.26: Change handling of GPS status to depend on GPS events instead of connection events +0.27: Issue newline before GB commands (solves issue with console.log and ignored commands) +0.28: Navigation messages no longer launch the Maps view unless they're new +0.29: Support for http request xpath return format +0.30: Send firmware and hardware versions on connection + Allow alarm enable/disable +0.31: Implement API for activity fetching +0.32: Added support for loyalty cards from gadgetbridge +0.33: Fix alarms created in Gadgetbridge not repeating +0.34: Implement API for activity tracks fetching (Recorder app logs). +0.35: Implement API to enable/disable acceleration data tracking. +0.36: Move from wrapper function to {} and let - faster execution at boot + Allow `calendar-` to take an array of items to remove +0.37: Support Gadgetbridge canned responses +0.38: Don't rewrite settings file on every boot! +0.39: Move GB message handling into a library to reduce boot time from 40ms->13ms +0.40: Ensure we send health 'activity' message to gadgetbridge (added 2v26) +0.41: When using `actfetch`, fetch historical activity type too \ No newline at end of file diff --git a/apps/android/README.md b/apps/android/README.md index c10718aac..a7a539e38 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -20,6 +20,8 @@ It contains: of Gadgetbridge - making your phone make noise so you can find it. * `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js keep any messages it has received, or should it delete them? +* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS +but instead asks Gadgetbridge on the phone to use the phone's GPS * `Messages` - launches the messages app, showing a list of messages ## How it works @@ -32,6 +34,39 @@ Responses are sent back to Gadgetbridge simply as one line of JSON. More info on message formats on http://www.espruino.com/Gadgetbridge +## Functions provided + +The boot code also provides some useful functions: + +* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean. +* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown` +* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain: + * `id` - a custom (string) ID + * `timeout` - a timeout for the request in milliseconds (default 30000ms) + * `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant) + * `return` for xpath, if not specified, one result is returned. If `return:"array"` an array of results is returned. + * `method` HTTP method (default is `get`) - `get/post/head/put/patch/delete` + * `body` the body of the HTTP request + * `headers` an object of headers, eg `{HeaderOne : "headercontents"}` + +`Bangle.http` returns a promise which contains: + +```JS +{ + t:"http", + id: // the ID of this HTTP request + resp: "...." // a string containing the response +} +``` + +eg: + +```JS +Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{ + console.log("Got ",data.resp); +}); +``` + ## Testing Bangle.js can only hold one connection open at a time, so it's hard to see diff --git a/apps/android/boot.js b/apps/android/boot.js index efd7e7e46..37550cdd0 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -1,122 +1,55 @@ -(function() { - function gbSend(message) { - Bluetooth.println(""); - Bluetooth.println(JSON.stringify(message)); - } - var lastMsg; - - var settings = require("Storage").readJSON("android.settings.json",1)||{}; - //default alarm settings - if (settings.rp == undefined) settings.rp = true; - if (settings.as == undefined) settings.as = true; - if (settings.vibrate == undefined) settings.vibrate = ".."; - require('Storage').writeJSON("android.settings.json", settings); - var _GB = global.GB; - global.GB = (event) => { +/* global GB */ +{ + // settings var is deleted after this executes to save memory + let settings = Object.assign({rp:true,as:true,vibrate:".."}, + require("Storage").readJSON("android.settings.json",1)||{} + ); + let _GB = global.GB; + global.GB = e => { // feed a copy to other handlers if there were any - if (_GB) setTimeout(_GB,0,Object.assign({},event)); - - /* TODO: Call handling, fitness */ - var HANDLERS = { - // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add - "notify" : function() { - Object.assign(event,{t:"add",positive:true, negative:true}); - // Detect a weird GadgetBridge bug and fix it - // For some reason SMS messages send two GB notifications, with different sets of info - if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") { - // Mutate the other message - event.id = lastMsg.id; - } - lastMsg = event; - require("messages").pushMessage(event); - }, - // {t:"notify~",id:int, title:string} // modified - "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, - // {t:"notify-",id:int} // remove - "notify-" : function() { event.t="remove";require("messages").pushMessage(event); }, - // {t:"find", n:bool} // find my phone - "find" : function() { - if (Bangle.findDeviceInterval) { - clearInterval(Bangle.findDeviceInterval); - delete Bangle.findDeviceInterval; - } - if (event.n) // Ignore quiet mode: we always want to find our watch - Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000); - }, - // {t:"musicstate", state:"play/pause",position,shuffle,repeat} - "musicstate" : function() { - require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state}); - }, - // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} - "musicinfo" : function() { - require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"})); - }, - // {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"}) - "call" : function() { - Object.assign(event, { - t:event.cmd=="incoming"?"add":"remove", - id:"call", src:"Phone", - positive:true, negative:true, - title:event.name||"Call", body:"Incoming call\n"+event.number}); - require("messages").pushMessage(event); - }, - "alarm" : function() { - //wipe existing GB alarms - var sched; - try { sched = require("sched"); } catch (e) {} - if (!sched) return; // alarms may not be installed - var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms"); - for (var i = 0; i < gbalarms.length; i++) - sched.setAlarm(gbalarms[i].id, undefined); - var alarms = sched.getAlarms(); - var time = new Date(); - var currentTime = time.getHours() * 3600000 + - time.getMinutes() * 60000 + - time.getSeconds() * 1000; - for (var j = 0; j < event.d.length; j++) { - // prevents all alarms from going off at once?? - var dow = event.d[j].rep; - if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW - var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0; - var a = require("sched").newDefaultAlarm(); - a.id = "gb"+j; - a.appid = "gbalarms"; - a.on = true; - a.t = event.d[j].h * 3600000 + event.d[j].m * 60000; - a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format - a.last = last; - alarms.push(a); - } - sched.setAlarms(alarms); - sched.reload(); - }, - }; - var h = HANDLERS[event.t]; - if (h) h(); else console.log("GB Unknown",event); + if (_GB) setTimeout(_GB,0,Object.assign({},e)); + Bangle.emit("GB",e); + require("android").gbHandler(e); }; - + // HTTP request handling - see the readme + Bangle.http = (url,options)=>require("android").httpHandler(url,options); // Battery monitor - function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } - NRF.on("connect", () => setTimeout(sendBattery, 2000)); + let sendBattery = function() { require("android").gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } Bangle.on("charging", sendBattery); - if (!settings.keep) - NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect + NRF.on("connect", () => setTimeout(function() { + sendBattery(); + require("android").gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION}); + GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process + }, 2000)); + NRF.on("disconnect", () => { + // disable HRM/activity monitoring ('act' message) + GB({t:"act",stp:0,hrm:0,int:0}); // just call the handler to save duplication + // remove all messages on disconnect (if enabled) + var settings = require("Storage").readJSON("android.settings.json",1)||{}; + if (!settings.keep) + require("messages").clearAll(); + }); setInterval(sendBattery, 10*60*1000); - // Health tracking - Bangle.on('health', health=>{ - gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + // Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins + Bangle.on('health', h=>{ + require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement, act: h.activity }); // h.activity added in 2v26 }); // Music control Bangle.musicControl = cmd => { // play/pause/next/previous/volumeup/volumedown - gbSend({ t: "music", n:cmd }); + require("android").gbSend({ t: "music", n:cmd }); }; // Message response Bangle.messageResponse = (msg,response) => { - if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" }); - if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); + if (msg.id=="call") return require("android").gbSend({ t: "call", n:response?"ACCEPT":"REJECT" }); + if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); // error/warn here? }; + Bangle.messageIgnore = msg => { + if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:"MUTE", id: msg.id }); + }; + // GPS overwrite logic + if (settings.overwriteGps) require("android").overwriteGPS(); // remove settings object so it's not taking up RAM delete settings; -})(); +} diff --git a/apps/android/lib.js b/apps/android/lib.js new file mode 100644 index 000000000..c2b3722b3 --- /dev/null +++ b/apps/android/lib.js @@ -0,0 +1,389 @@ +exports.gbSend = function(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); +} +let lastMsg, // for music messages - may not be needed now... + gpsState = {}, // keep information on GPS via Gadgetbridge + settings = Object.assign({rp:true,as:true,vibrate:".."}, + require("Storage").readJSON("android.settings.json",1)||{} + ); + +exports.gbHandler = (event) => { + var HANDLERS = { + // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add + "notify" : function() { + print("notify",event); + Object.assign(event,{t:"add",positive:true, negative:true}); + // Detect a weird GadgetBridge bug and fix it + // For some reason SMS messages send two GB notifications, with different sets of info + if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") { + // Mutate the other message + event.id = lastMsg.id; + } + lastMsg = event; + require("messages").pushMessage(event); + }, + // {t:"notify~",id:int, title:string} // modified + "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, + // {t:"notify-",id:int} // remove + "notify-" : function() { event.t="remove";require("messages").pushMessage(event); }, + // {t:"find", n:bool} // find my phone + "find" : function() { + if (Bangle.findDeviceInterval) { + clearInterval(Bangle.findDeviceInterval); + delete Bangle.findDeviceInterval; + } + if (event.n) // Ignore quiet mode: we always want to find our watch + Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000); + }, + // {t:"musicstate", state:"play/pause",position,shuffle,repeat} + "musicstate" : function() { + require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state}); + }, + // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} + "musicinfo" : function() { + require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"})); + }, + // {"t":"call","cmd":"incoming/end/start/outgoing","name":"Bob","number":"12421312"}) + "call" : function() { + Object.assign(event, { + t:event.cmd=="incoming"?"add":"remove", + id:"call", src:"Phone", + positive:true, negative:true, + title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number}); + require("messages").pushMessage(event); + }, + "canned_responses_sync" : function() { + require("Storage").writeJSON("replies.json", event.d); + }, + // {"t":"alarm", "d":[{h:int,m:int,rep:int},... } + "alarm" : function() { + //wipe existing GB alarms + var sched; + try { sched = require("sched"); } catch (e) {} + if (!sched) return; // alarms may not be installed + var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms"); + for (var i = 0; i < gbalarms.length; i++) + sched.setAlarm(gbalarms[i].id, undefined); + var alarms = sched.getAlarms(); + var time = new Date(); + var currentTime = time.getHours() * 3600000 + + time.getMinutes() * 60000 + + time.getSeconds() * 1000; + for (var j = 0; j < event.d.length; j++) { + // prevents all alarms from going off at once?? + var dow = event.d[j].rep; + var rp = false; + if (!dow) { + dow = 127; //if no DOW selected, set alarm to all DOW + } else { + rp = true; + } + var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0; + var a = require("sched").newDefaultAlarm(); + a.id = "gb"+j; + a.appid = "gbalarms"; + a.on = event.d[j].on !== undefined ? event.d[j].on : true; + a.t = event.d[j].h * 3600000 + event.d[j].m * 60000; + a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format + a.rp = rp; + a.last = last; + alarms.push(a); + } + sched.setAlarms(alarms); + sched.reload(); + }, + //TODO perhaps move those in a library (like messages), used also for viewing events? + //add and remove events based on activity on phone (pebble-like) + // {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool + "calendar" : function() { + var cal = require("Storage").readJSON("android.calendar.json",true); + if (!cal || !Array.isArray(cal)) cal = []; + var i = cal.findIndex(e=>e.id==event.id); + if(i<0) + cal.push(event); + else + cal[i] = event; + require("Storage").writeJSON("android.calendar.json", cal); + }, + // {t:"calendar-", id:int} + "calendar-" : function() { + var cal = require("Storage").readJSON("android.calendar.json",true); + //if any of those happen we are out of sync! + if (!cal || !Array.isArray(cal)) cal = []; + if (Array.isArray(event.id)) + cal = cal.filter(e=>!event.id.includes(e.id)); + else + cal = cal.filter(e=>e.id!=event.id); + require("Storage").writeJSON("android.calendar.json", cal); + }, + //triggered by GB, send all ids + // { t:"force_calendar_sync_start" } + "force_calendar_sync_start" : function() { + var cal = require("Storage").readJSON("android.calendar.json",true); + if (!cal || !Array.isArray(cal)) cal = []; + exports.gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)}); + }, + // {t:"http",resp:"......",[id:"..."]} + "http":function() { + //get the promise and call the promise resolve + if (Bangle.httpRequest === undefined) return; + var request=Bangle.httpRequest[event.id]; + if (request === undefined) return; //already timedout or wrong id + delete Bangle.httpRequest[event.id]; + clearTimeout(request.t); //t = timeout variable + if(event.err!==undefined) //if is error + request.j(event.err); //r = reJect function + else + request.r(event); //r = resolve function + }, + // {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true } + "gps": function() { + if (!settings.overwriteGps) return; + // modify event for using it as Bangle GPS event + delete event.t; + if (!isFinite(event.satellites)) event.satellites = NaN; + if (!isFinite(event.course)) event.course = NaN; + event.fix = 1; + if (event.long!==undefined) { // for earlier Gadgetbridge implementations + event.lon = event.long; + delete event.long; + } + if (event.time){ + event.time = new Date(event.time); + } + + if (!gpsState.lastGPSEvent) { + // this is the first event, save time of arrival and deactivate internal GPS + Bangle.moveGPSPower(0); + } else { + // this is the second event, store the intervall for expecting the next GPS event + gpsState.interval = Date.now() - gpsState.lastGPSEvent; + } + gpsState.lastGPSEvent = Date.now(); + // in any case, cleanup the GPS state in case no new events arrive + if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS); + gpsState.timeoutGPS = setTimeout(()=>{ + // reset state + gpsState.lastGPSEvent = undefined; + gpsState.timeoutGPS = undefined; + gpsState.interval = undefined; + // did not get an expected GPS event but have GPS clients, switch back to internal GPS + if (Bangle.isGPSOn()) Bangle.moveGPSPower(1); + }, (gpsState.interval || 10000) + 1000); + Bangle.emit('GPS', event); + }, + // {t:"is_gps_active"} + "is_gps_active": function() { + exports.gbSend({ t: "gps_power", status: Bangle.isGPSOn() }); + }, + // {t:"act", hrm:bool, stp:bool, int:int} + "act": function() { + if (exports.actInterval) clearInterval(exports.actInterval); + exports.actInterval = undefined; + if (exports.actHRMHandler) + exports.actHRMHandler = undefined; + Bangle.setHRMPower(event.hrm,"androidact"); + if (!(event.hrm || event.stp)) return; + if (!isFinite(event.int)) event.int=1; + var lastSteps = Bangle.getStepCount(); + var lastBPM = 0; + exports.actHRMHandler = function(e) { + lastBPM = e.bpm; + }; + Bangle.on('HRM',exports.actHRMHandler); + exports.actInterval = setInterval(function() { + var steps = Bangle.getStepCount(); + exports.gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 }); + lastSteps = steps; + }, event.int*1000); + }, + // {t:"actfetch", ts:long} + "actfetch": function() { + exports.gbSend({t: "actfetch", state: "start"}); + var actCount = 0; + var actCb = function(r) { + // The health lib saves the samples at the start of the 10-minute block + // However, GB expects them at the end of the block, so let's offset them + // here to keep a consistent API in the health lib + var sampleTs = r.date.getTime() + 600000; + if (sampleTs >= event.ts) { + exports.gbSend({ + t: "act", + ts: sampleTs, + stp: r.steps, + hrm: r.bpm, + mov: r.movement, + act: r.activity + }); + actCount++; + } + } + if (event.ts != 0) { + require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb); + } else { + require("health").readFullDatabase(actCb); + } + exports.gbSend({t: "actfetch", state: "end", count: actCount}); + }, + //{t:"listRecs", id:"20230616a"} + "listRecs": function() { + let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21)); + if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back. + let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id); + if (-1 == firstNonsyncedIdx) { + recs = [] + } else { + recs = recs.slice(firstNonsyncedIdx); + } + } + exports.gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions? + }, + //{t:"fetchRec", id:"20230616a"} + "fetchRec": function() { + // TODO: Decide on what names keys should have. + if (exports.fetchRecInterval) { + clearInterval(exports.fetchRecInterval); + exports.fetchRecInterval = undefined; + } + if (event.id=="stop") { + return; + } else { + let log = require("Storage").open("recorder.log"+event.id+".csv","r"); + let lines = "init";// = log.readLine(); + let pkgcnt = 0; + exports.gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch. + let sendlines = ()=>{ + lines = log.readLine(); + for (var i = 0; i < 3; i++) { + let line = log.readLine(); + if (line) lines += line; + } + pkgcnt++; + exports.gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt}); + if (!lines && exports.fetchRecInterval) { + clearInterval(exports.fetchRecInterval); + exports.fetchRecInterval = undefined; + } + }; + exports.fetchRecInterval = setInterval(sendlines, 50); + } + }, + "nav": function() { + event.id="nav"; + if (event.instr) { + event.t="add"; + event.src="maps"; // for the icon + event.title="Navigation"; + if (require("messages").getMessages().find(m=>m.id=="nav")) + event.t = "modify"; + } else { + event.t="remove"; + } + require("messages").pushMessage(event); + }, + "cards" : function() { + // we receive all, just override what we have + if (Array.isArray(event.d)) + require("Storage").writeJSON("android.cards.json", event.d); + }, + "accelsender": function () { + require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval}); + load(); + } + }; + var h = HANDLERS[event.t]; + if (h) h(); else console.log("GB Unknown",event); +}; + +// HTTP request handling - see the readme +// options = {id,timeout,xpath} +exports.httpHandler = (url,options) => { + options = options||{}; + if (!NRF.getSecurityStatus().connected) + return Promise.reject(/*LANG*/"Not connected to Bluetooth"); + if (Bangle.httpRequest === undefined) + Bangle.httpRequest={}; + if (options.id === undefined) { + // try and create a unique ID + do { + options.id = Math.random().toString().substr(2); + } while( Bangle.httpRequest[options.id]!==undefined); + } + //send the request + var req = {t: "http", url:url, id:options.id}; + if (options.xpath) req.xpath = options.xpath; + if (options.return) req.return = options.return; // for xpath + if (options.method) req.method = options.method; + if (options.body) req.body = options.body; + if (options.headers) req.headers = options.headers; + exports.gbSend(req); + //create the promise + var promise = new Promise(function(resolve,reject) { + //save the resolve function in the dictionary and create a timeout (30 seconds default) + Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{ + //if after "timeoutMillisec" it still hasn't answered -> reject + delete Bangle.httpRequest[options.id]; + reject("Timeout"); + },options.timeout||30000)}; + }); + return promise; +}; + +exports.overwriteGPS = () => { // if the overwrite option is set, call this on init.. + const origSetGPSPower = Bangle.setGPSPower; + Bangle.moveGPSPower = (state) => { + if (Bangle.isGPSOn()){ + let orig = Bangle._PWR.GPS; + delete Bangle._PWR.GPS; + origSetGPSPower(state); + Bangle._PWR.GPS = orig; + } + }; + + // work around Serial1 for GPS not working when connected to something + let serialTimeout; + let wrap = function(f){ + return (s)=>{ + if (serialTimeout) clearTimeout(serialTimeout); + origSetGPSPower(1, "androidgpsserial"); + f(s); + serialTimeout = setTimeout(()=>{ + serialTimeout = undefined; + origSetGPSPower(0, "androidgpsserial"); + }, 10000); + }; + }; + Serial1.println = wrap(Serial1.println); + Serial1.write = wrap(Serial1.write); + + // replace set GPS power logic to suppress activation of gps (and instead request it from the phone) + Bangle.setGPSPower = ((isOn, appID) => { + let pwr; + if (!this.lastGPSEvent){ + // use internal GPS power function if no gps event has arrived from GadgetBridge + pwr = origSetGPSPower(isOn, appID); + } else { + // we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app + if (!Bangle._PWR) Bangle._PWR={}; + if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[]; + if (!appID) appID="?"; + if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID); + if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1); + pwr = Bangle._PWR.GPS.length>0; + // stop internal GPS, no clients left + if (!pwr) origSetGPSPower(0); + } + // always update Gadgetbridge on current power state + require("android").gbSend({ t: "gps_power", status: pwr }); + return pwr; + }).bind(gpsState); + // allow checking for GPS via GadgetBridge + Bangle.isGPSOn = () => { + return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0); + }; + // stop GPS on boot if not activated + setTimeout(()=>{ + if (!Bangle.isGPSOn()) require("android").gbSend({ t: "gps_power", status: false }); + },3000); +}; \ No newline at end of file diff --git a/apps/android/metadata.json b/apps/android/metadata.json index bf37b8407..2066a2133 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,19 +2,20 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.10", + "version": "0.41", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", - "dependencies": {"messages":"app"}, + "dependencies": {"messages":"module"}, "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"android.app.js","url":"app.js"}, {"name":"android.settings.js","url":"settings.js"}, {"name":"android.img","url":"app-icon.js","evaluate":true}, - {"name":"android.boot.js","url":"boot.js"} + {"name":"android.boot.js","url":"boot.js"}, + {"name":"android","url":"lib.js"} ], - "data": [{"name":"android.settings.json"}], + "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}], "sortorder": -8 } diff --git a/apps/android/settings.js b/apps/android/settings.js index 695d483c6..82883a68d 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -1,5 +1,6 @@ (function(back) { - function gb(j) { + function gbSend(j) { + Bluetooth.println(""); Bluetooth.println(JSON.stringify(j)); } var settings = require("Storage").readJSON("android.settings.json",1)||{}; @@ -9,22 +10,31 @@ var mainmenu = { "" : { "title" : "Android" }, "< Back" : back, - /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, /*LANG*/"Find Phone" : () => E.showMenu({ "" : { "title" : /*LANG*/"Find Phone" }, "< Back" : ()=>E.showMenu(mainmenu), - /*LANG*/"On" : _=>gb({t:"findPhone",n:true}), - /*LANG*/"Off" : _=>gb({t:"findPhone",n:false}), + /*LANG*/"On" : _=>gbSend({t:"findPhone",n:true}), + /*LANG*/"Off" : _=>gbSend({t:"findPhone",n:false}), }), /*LANG*/"Keep Msgs" : { value : !!settings.keep, - format : v=>v?/*LANG*/"Yes":/*LANG*/"No", onchange: v => { settings.keep = v; updateSettings(); } }, - /*LANG*/"Messages" : ()=>load("messages.app.js"), + /*LANG*/"Overwrite GPS" : { + value : !!settings.overwriteGps, + onchange: newValue => { + if (newValue) { + Bangle.setGPSPower(false, 'android'); + } + settings.overwriteGps = newValue; + updateSettings(); + } + }, + /*LANG*/"Messages" : ()=>require("messages").openGUI(), }; E.showMenu(mainmenu); }) diff --git a/apps/android/test.json b/apps/android/test.json new file mode 100644 index 000000000..429fd70fe --- /dev/null +++ b/apps/android/test.json @@ -0,0 +1,99 @@ +{ + "app" : "android", + "setup" : [{ + "id": "default", + "steps" : [ + {"t":"cmd", "js": "Bangle.setGPSPower=(isOn, appID)=>{if (!appID) appID='?';if (!Bangle._PWR) Bangle._PWR={};if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);return Bangle._PWR.GPS.length>0;};", "text": "Fake the setGPSPower"}, + {"t":"wrap", "fn": "Bangle.setGPSPower", "id": "gpspower"}, + {"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port println"}, + {"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"}, + {"t":"cmd", "js": "Bangle._PWR={}", "text": "Prepare an empty _PWR for following asserts"}, + {"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"}, + {"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"} + ] + },{ + "id": "connected", + "steps" : [ + {"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: true };}", "text": "Control the security status to be connected"} + ] + },{ + "id": "disconnected", + "steps" : [ + {"t":"cmd", "js": "NRF.getSecurityStatus = () => { return { connected: false };}", "text": "Control the security status to be disconnected"} + ] + }], + "tests" : [{ + "description": "Check setGPSPower is replaced", + "steps" : [ + {"t":"cmd", "js": "Serial1.println = () => { }", "text": "Fake the serial port"}, + {"t":"cmd", "js": "Bluetooth.println = () => { }", "text": "Fake the Bluetooth println"}, + {"t":"cmd", "js": "require('Storage').writeJSON('android.settings.json', {overwriteGps: true})", "text": "Enable GPS overwrite"}, + {"t":"cmd", "js": "eval(require('Storage').read('android.boot.js'))", "text": "Load the boot code"}, + {"t":"assert", "js": "Bangle.setGPSPower.toString().includes('native')", "is":"false", "text": "setGPSPower has been replaced"} + ] + },{ + "description": "Test switching hardware GPS on and off", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ] , "text": "internal GPS switched on"}, + {"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns falsy when switching off"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + {"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ] , "text": "internal GPS switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, internal GPS active until GB GPS event arrives", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"undefinedOrEmpty", "text": "No GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"falsy", "text": "isGPSOn shows GPS as off"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 1, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on"}, + + {"t":"gb", "obj":{"t":"gps"}}, + {"t":"assertArray", "js": "Bangle._PWR.GPS", "is":"notEmpty", "text": "GPS clients still there"}, + {"t":"assert", "js": "Bangle.isGPSOn()", "is":"truthy", "text": "isGPSOn still shows GPS as on"}, + {"t":"assertCall", "id": "gpspower", "count": 2, "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, internal stays off", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + + {"t":"gb", "obj":{"t":"gps"}}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"}, + + {"t":"assert", "js": "Bangle.setGPSPower(0, 'test')", "is":"falsy", "text": "setGPSPower returns truthy when switching on"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS still switched off"} + ] + },{ + "description": "Test switching when GB GPS is available, but no event arrives", + "steps" : [ + {"t":"setup", "id": "default"}, + {"t":"setup", "id": "connected"}, + + {"t":"assert", "js": "Bangle.setGPSPower(1, 'test')", "is":"truthy", "text": "setGPSPower returns truthy when switching on"}, + + {"t":"resetCall", "id": "gpspower"}, + {"t":"gb", "obj":{"t":"gps"}, "text": "trigger switch"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 0 } ], "text": "internal GPS switched off"}, + + {"t":"resetCall", "id": "gpspower"}, + {"t":"advanceTimers", "ms":"12000", "text": "wait for fallback"}, + {"t":"assertCall", "id": "gpspower", "argAsserts": [ { "t": "assert", "arg": "0", "is": "equal", "to": 1 } ], "text": "internal GPS switched on caused by missing GB event"} + ] + }] +} diff --git a/apps/angles/ChangeLog b/apps/angles/ChangeLog new file mode 100644 index 000000000..7727f3cc4 --- /dev/null +++ b/apps/angles/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor code improvements diff --git a/apps/angles/app.js b/apps/angles/app.js new file mode 100644 index 000000000..d124d7ce7 --- /dev/null +++ b/apps/angles/app.js @@ -0,0 +1,47 @@ +g.clear().setRotation(1); +// g.setRotation ALSO changes accelerometer axes +var avrAngle = undefined; +var history = []; + +var R = Bangle.appRect; +var relativeTo = undefined; + +function draw(v) { + if (v===undefined) v = Bangle.getAccel(); + // current angle + var d = Math.sqrt(v.y*v.y + v.z*v.z); + var ang = Math.atan2(-v.x, d)*180/Math.PI; + // Median filter + if (history.length > 10) history.shift(); // pull old reading off the start + history.push(ang); + avrAngle = history.slice().sort()[(history.length-1)>>1]; // median filter + // Render + var x = R.x + R.w/2; + var y = R.y + R.h/2; + g.reset().clearRect(R).setFontAlign(0,0); + var displayAngle = avrAngle; + g.setFont("6x15").drawString("ANGLE (DEGREES)", x, R.y2-8); + if (relativeTo!==undefined) { + g.drawString("RELATIVE TO", x,y-50); + g.setFont("Vector:30").drawString(relativeTo.toFixed(1),x,y-30); + y += 20; + displayAngle = displayAngle-relativeTo; + } + g.setFont("Vector:60").drawString(displayAngle.toFixed(1),x,y); + +} + +draw(); +Bangle.on('accel',draw); + +// Pressing the button turns relative angle on/off +Bangle.setUI({ + mode : "custom", + btn : function(n) { + if (relativeTo===undefined) + relativeTo = avrAngle; + else + relativeTo = undefined; + draw(); + } +}); \ No newline at end of file diff --git a/apps/angles/icon.js b/apps/angles/icon.js new file mode 100644 index 000000000..3f051f95f --- /dev/null +++ b/apps/angles/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA")) \ No newline at end of file diff --git a/apps/angles/icon.png b/apps/angles/icon.png new file mode 100644 index 000000000..1a4559d44 Binary files /dev/null and b/apps/angles/icon.png differ diff --git a/apps/angles/metadata.json b/apps/angles/metadata.json new file mode 100644 index 000000000..3ddee427a --- /dev/null +++ b/apps/angles/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "angles", + "name": "Angles (Spirit Level)", + "shortName": "Angles", + "version": "0.02", + "description": "Shows Angle or Relative angle in degrees (Digital Protractor/Inclinometer). Place Bangle sideways against a surface with the button facing away for best readings.", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "tags": "tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"angles.app.js","url":"app.js"}, + {"name":"angles.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/angles/screenshot.png b/apps/angles/screenshot.png new file mode 100644 index 000000000..9d631cf74 Binary files /dev/null and b/apps/angles/screenshot.png differ diff --git a/apps/animclk/ChangeLog b/apps/animclk/ChangeLog index 348448c34..00192d63b 100644 --- a/apps/animclk/ChangeLog +++ b/apps/animclk/ChangeLog @@ -1,3 +1,6 @@ 0.01: New App! 0.02: Fix bug if image clock wasn't installed 0.03: Update to use setUI +0.04: Tell clock widgets to hide. Move loadWidgets() so it only runs on +startup and not on every draw. +0.05: Minor code improvements diff --git a/apps/animclk/app.js b/apps/animclk/app.js index 4bf63daf6..1158fd0d5 100644 --- a/apps/animclk/app.js +++ b/apps/animclk/app.js @@ -16,7 +16,6 @@ var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; var IX = 80, IY = 10, IBPP = 1; var IW = 174, IH = 45, OY = 24; var inf = {align:0}; -var bgoptions; require("Font7x11Numeric7Seg").add(Graphics); var cg = Graphics.createArrayBuffer(IW,IH,IBPP,{msb:true}); @@ -87,7 +86,6 @@ if (g.drawImages) { draw(); var secondInterval = setInterval(draw,100); // load widgets - Bangle.loadWidgets(); Bangle.drawWidgets(); // Stop when LCD goes off Bangle.on('lcdPower',on=>{ @@ -104,3 +102,5 @@ if (g.drawImages) { } // Show launcher when button pressed Bangle.setUI("clock"); + +Bangle.loadWidgets(); diff --git a/apps/animclk/metadata.json b/apps/animclk/metadata.json index 31dfe453f..157090315 100644 --- a/apps/animclk/metadata.json +++ b/apps/animclk/metadata.json @@ -2,7 +2,7 @@ "id": "animclk", "name": "Animated Clock", "shortName": "Anim Clock", - "version": "0.03", + "version": "0.05", "description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art", "icon": "app.png", "type": "clock", diff --git a/apps/antonclk/ChangeLog b/apps/antonclk/ChangeLog index 73a63f7c7..4ef0cee75 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -9,4 +9,8 @@ when weekday name and calendar weeknumber are on then display is # week is buffered until date or timezone changes 0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users) -0.08: fixed calendar weeknumber not shortened to two digits \ No newline at end of file +0.08: fixed calendar weeknumber not shortened to two digits +0.09: Use default Bangle formatter for booleans +0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 +0.11: Moved enhanced Anton clock to 'Anton Clock Plus' and stripped this clock back down to make it faster for new users (270ms -> 170ms) + Modified to avoid leaving functions defined when using setUI({remove:...}) diff --git a/apps/antonclk/app.js b/apps/antonclk/app.js index 4b1e71bda..528866588 100644 --- a/apps/antonclk/app.js +++ b/apps/antonclk/app.js @@ -1,230 +1,45 @@ // Clock with large digits using the "Anton" bold font - -const SETTINGSFILE = "antonclk.json"; - 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)); }; -Graphics.prototype.setFontAntonSmall = function(scale) { - // Actual height 53 (52 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); -}; +{ // 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 +let drawTimeout; -// variables defined from settings -var secondsMode; -var secondsColoured; -var secondsWithColon; -var dateOnMain; -var dateOnSecs; -var weekDay; -var calWeek; -var upperCase; -var vectorFont; +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); + // Show date and day of week + var dateStr = require("locale").date(date, 0).toUpperCase()+"\n"+ + require("locale").dow(date, 0).toUpperCase(); + g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+48); -// dynamic variables -var drawTimeout; -var queueMillis = 1000; -var secondsScreen = true; - -var isBangle1 = (process.env.HWVERSION == 1); - -//For development purposes -/* -require('Storage').writeJSON(SETTINGSFILE, { - secondsMode: "Unlocked", // "Never", "Unlocked", "Always" - secondsColoured: true, - secondsWithColon: true, - dateOnMain: "Long", // "Short", "Long", "ISO8601" - dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false - weekDay: true, - calWeek: true, - upperCase: true, - vectorFont: true, -}); -*/ - -// OR (also for development purposes) -/* -require('Storage').erase(SETTINGSFILE); -*/ - -// Load settings -function loadSettings() { - // Helper function default setting - function def (value, def) {return value !== undefined ? value : def;} - - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - secondsMode = def(settings.secondsMode, "Never"); - secondsColoured = def(settings.secondsColoured, true); - secondsWithColon = def(settings.secondsWithColon, true); - dateOnMain = def(settings.dateOnMain, "Long"); - dateOnSecs = def(settings.dateOnSecs, "Year"); - weekDay = def(settings.weekDay, true); - calWeek = def(settings.calWeek, false); - upperCase = def(settings.upperCase, true); - vectorFont = def(settings.vectorFont, false); - - // Legacy - if (dateOnSecs === true) - dateOnSecs = "Year"; - if (dateOnSecs === false) - dateOnSecs = "No"; -} - -// schedule a draw for the next second or minute -function queueDraw() { + // queue next draw if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); - }, queueMillis - (Date.now() % queueMillis)); -} + }, 60000 - (Date.now() % 60000)); +}; -function updateState() { - if (Bangle.isLCDOn()) { - if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { - secondsScreen = true; - queueMillis = 1000; - } else { - secondsScreen = false; - queueMillis = 60000; - } - draw(); // draw immediately, queue redraw - } else { // stop draw timer +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; - } -} - -function isoStr(date) { - return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); -} - -var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) -function ISO8601calWeek(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 - dateNoTime = date; dateNoTime.setHours(0,0,0,0); - if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2]; - calWeekBuffer[0] = date.getTimezoneOffset(); - calWeekBuffer[1] = dateNoTime; - 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); - } - calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000); - return calWeekBuffer[2]; -} - -function doColor() { - return !isBangle1 && !Bangle.isLocked() && secondsColoured; -} - -// Actually draw the watch face -function draw() { - var x = g.getWidth() / 2; - var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); - g.reset(); - /* This is to mark the widget areas during development. - g.setColor("#888") - .fillRect(0, 0, g.getWidth(), 23) - .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); - /* */ - g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) - var date = new Date(); // Actually the current date, this one is shown - var timeStr = require("locale").time(date, 1); // Hour and minute - g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time - if (secondsScreen) { - y += 65; - var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2); - if (doColor()) - g.setColor(0, 0, 1); - g.setFont("AntonSmall"); - if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left - g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds - y -= (vectorFont ? 15 : 13); - x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); - var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); - var year; - var md; - var yearfirst; - if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year - year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); - md = dateStr2.slice(0, -4); - if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) - md = md.slice(0, -1); - yearfirst = false; - } else { // formatted date begins with year - if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... - dateStr2 = isoStr(date); // ...use ISO date format instead - year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); - md = dateStr2.slice(5); // never keep separator directly after year - yearfirst = true; - } - if (dateOnSecs === "Weekday" && upperCase) - year = year.toUpperCase(); - g.setFontAlign(0, 0); - if (vectorFont) - g.setFont("Vector", 24); - else - g.setFont("6x8", 2); - if (doColor()) - g.setColor(1, 0, 0); - g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); - g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); - } else { - g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered - } - } else { // No seconds screen: Show date and optionally day of week - y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); - var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); - if (upperCase) - dateStr = dateStr.toUpperCase(); - g.setFontAlign(0, 0); - if (vectorFont) - g.setFont("Vector", 24); - else - g.setFont("6x8", 2); - g.drawString(dateStr, x, y); - if (calWeek || weekDay) { - var dowcwStr = ""; - if (calWeek) - dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2); - if (weekDay) - dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01 - else //week #01 - dowcwStr = /*LANG*/"week" + dowcwStr; - if (upperCase) - dowcwStr = dowcwStr.toUpperCase(); - g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16)); - } - } - - // queue next draw - queueDraw(); -} - -// Init the settings of the app -loadSettings(); -// Clear the screen once, at startup -g.clear(); -// Set dynamic state and perform initial drawing -updateState(); -// Register hooks for LCD on/off event and screen lock on/off event -Bangle.on('lcdPower', on => { - updateState(); -}); -Bangle.on('lock', on => { - updateState(); -}); -// Show launcher when middle button pressed -Bangle.setUI("clock"); + delete Graphics.prototype.setFontAnton; + }}); // Load widgets Bangle.loadWidgets(); -Bangle.drawWidgets(); - -// end of file \ No newline at end of file +draw(); +setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/antonclk/app.png b/apps/antonclk/app.png index a38093c5f..bb764d2a1 100644 Binary files a/apps/antonclk/app.png and b/apps/antonclk/app.png differ diff --git a/apps/antonclk/metadata.json b/apps/antonclk/metadata.json index c58ee2a1b..b8242f11a 100644 --- a/apps/antonclk/metadata.json +++ b/apps/antonclk/metadata.json @@ -1,9 +1,8 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.08", - "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", - "readme":"README.md", + "version": "0.11", + "description": "A simple clock using the bold Anton font. See `Anton Clock Plus` for an enhanced version", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -12,8 +11,6 @@ "allow_emulator": true, "storage": [ {"name":"antonclk.app.js","url":"app.js"}, - {"name":"antonclk.settings.js","url":"settings.js"}, {"name":"antonclk.img","url":"app-icon.js","evaluate":true} - ], - "data": [{"name":"antonclk.json"}] + ] } diff --git a/apps/antonclk/screenshot.png b/apps/antonclk/screenshot.png index e949b8a24..9b38e90d5 100644 Binary files a/apps/antonclk/screenshot.png and b/apps/antonclk/screenshot.png differ diff --git a/apps/antonclk/test.json b/apps/antonclk/test.json new file mode 100644 index 000000000..a719e0a14 --- /dev/null +++ b/apps/antonclk/test.json @@ -0,0 +1,15 @@ +{ + "app" : "antonclk", + "tests" : [{ + "description": "Check memory usage after setUI", + "steps" : [ + {"t":"cmd", "js": "Bangle.loadWidgets()"}, + {"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {"t":"cmd", "js": "Bangle.setUI()"}, + {"t":"saveMemoryUsage"}, + {"t":"cmd", "js": "eval(require('Storage').read('antonclk.app.js'))"}, + {"t":"cmd", "js":"Bangle.setUI()"}, + {"t":"checkMemoryUsage"} + ] + }] +} diff --git a/apps/antonclkplus/ChangeLog b/apps/antonclkplus/ChangeLog new file mode 100644 index 000000000..fc099a165 --- /dev/null +++ b/apps/antonclkplus/ChangeLog @@ -0,0 +1,16 @@ +0.01: New App! +0.02: Load widgets after setUI so widclk knows when to hide +0.03: Clock now shows day of week under date. +0.04: Clock can optionally show seconds, date optionally in ISO-8601 format, weekdays and uppercase configurable, too. +0.05: Clock can optionally show ISO-8601 calendar weeknumber (default: Off) + when weekday name "Off": week #: + when weekday name "On": weekday name is cut at 6th position and .# is added +0.06: fixes #1271 - wrong settings name + when weekday name and calendar weeknumber are on then display is # + week is buffered until date or timezone changes +0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users) +0.08: fixed calendar weeknumber not shortened to two digits +0.09: Use default Bangle formatter for booleans +0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 + Modified to avoid leaving functions defined when using setUI({remove:...}) +0.11: Minor code improvements diff --git a/apps/antonclk/README.md b/apps/antonclkplus/README.md similarity index 87% rename from apps/antonclk/README.md rename to apps/antonclkplus/README.md index 28a38f5fd..25b478dd9 100644 --- a/apps/antonclk/README.md +++ b/apps/antonclkplus/README.md @@ -1,6 +1,6 @@ -# Anton Clock - Large font digital watch with seconds and date +# Anton Clock Plus - Large font digital watch with seconds and date -Anton clock uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. +Anton Clock Plus uses the "Anton" bold font to show the time in a clear, easily readable manner. On the Bangle.js 2, the time can be read easily even if the screen is locked and unlit. ## Features @@ -16,16 +16,16 @@ The basic time representation only shows hours and minutes of the current time. ## Usage -Install Anton clock through the Bangle.js app loader. -Configure it through the default Bangle.js configuration mechanism +* Install Anton Clock Plus through the Bangle.js app loader. +* Configure it through the default Bangle.js configuration mechanism (Settings app, "Apps" menu, "Anton clock" submenu). -If you like it, make it your default watch face +* If you like it, make it your default watch face (Settings app, "System" menu, "Clock" submenu, select "Anton clock"). ## Configuration -Anton clock is configured by the standard settings mechanism of Bangle.js's operating system: -Open the "Settings" app, then the "Apps" submenu and below it the "Anton clock" menu. +Anton Clock is configured by the standard settings mechanism of Bangle.js's operating system: +Open the `Settings` app, then the `Apps` submenu and below it the `Anton Clock+` menu. You configure Anton clock through several "on/off" switches in two menus. ### The main menu diff --git a/apps/antonclkplus/app-icon.js b/apps/antonclkplus/app-icon.js new file mode 100644 index 000000000..0c3aeb210 --- /dev/null +++ b/apps/antonclkplus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgf/AH4At/l/Aofgh4DB+EAj4REQoM/AgP4AoeACIoLCg4FB4AFDCIwLCgAROgYIB8EBAoUH/gVBCIxQBCKYHBCJp9DI4ICBLJYRCn4RQEYMOR5ARDIgIRMYQZZBgARGZwZBDCKQrCgEDR5AdBUIQRJDoLXFCJD7J/xrICIQFCn4RH/4LDAoTaCCI4Ar/LLDCBfypMkCgMkyV/CJOSCIOf5IRGFwOfCJNP//JnmT588z/+pM/BYIRCk4RC/88+f/n4RCngRCz1JCIf5/nzGoQRIHwXPCIPJI4f8CJHJGQJKCCI59LCI5ZCCJ/+v/kBoM/+V/HIJrHBYJWB/JKB5x9JEYP8AQKdBpwRL841Dp41KZoTxBHYTXBWY77PCKKhJ/4/CcgMkXoQAiA=")) diff --git a/apps/antonclkplus/app.js b/apps/antonclkplus/app.js new file mode 100644 index 000000000..4e5128a4e --- /dev/null +++ b/apps/antonclkplus/app.js @@ -0,0 +1,238 @@ +// Clock with large digits using the "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)); +}; + +Graphics.prototype.setFontAntonSmall = function(scale) { + // Actual height 53 (52 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAMAAAAAAAAD8AAAAAAAA/8AAAAAAAf/8AAAAAAH//8AAAAAB///8AAAAA////8AAAAP////8AAAD/////8AAB//////8AAf//////8AH///////4A///////+AA///////AAA//////wAAA/////8AAAA////+AAAAA////gAAAAA///4AAAAAA//8AAAAAAA//AAAAAAAA/wAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/////wAAA//////8AAB//////+AAH///////gAH///////gAP///////wAf///////4Af///////4A////////8A////////8A////////8A//AAAAD/8A/8AAAAA/8A/8AAAAA/8A/8AAAAA/8A/+AAAAB/8A////////8A////////8A////////8Af///////4Af///////4AP///////wAP///////wAH///////gAD///////AAA//////8AAAP/////wAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAA/4AAAAAAAA/4AAAAAAAB/wAAAAAAAB/wAAAAAAAD/wAAAAAAAD/gAAAAAAAH///////8AP///////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAP8AA//4AAA/8AB//4AAH/8AH//4AAP/8AP//4AA//8AP//4AB//8Af//4AD//8Af//4AP//8A///4Af//8A///4A///8A///4D///8A//AAH///8A/8AAP///8A/8AA//+/8A/8AD//8/8A/+Af//w/8A//////g/8A/////+A/8A/////8A/8Af////4A/8Af////wA/8AP////AA/8AP///+AA/8AH///8AA/8AD///wAA/8AA///AAA/8AAP/4AAA/8AAAAAAAAAAAAAAAAAAAAAAH4AAf/gAAA/4AAf/8AAD/4AAf//AAH/4AAf//gAP/4AAf//wAP/4AAf//wAf/4AAf//4Af/4AAf//4A//4AAf//8A//4AAf//8A//4AAP//8A//A/8AB/8A/8A/8AA/8A/8B/8AA/8A/8B/8AA/8A/+D//AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////gAH//9////gAD//4///+AAB//wf//4AAAP/AH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAAAB//wAAAAAAP//wAAAAAD///wAAAAA////wAAAAH////wAAAB/////wAAAf/////wAAD//////wAA///////wAA/////h/wAA////wB/wAA///8AB/wAA///AAB/wAA//gAAB/wAA////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8A////////8AAAAAAB/wAAAAAAAB/wAAAAAAAB/wAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AA////4P/+AA////4P//AA////4P//gA////4P//wA////4P//wA////4P//4A////4P//4A////4P//8A////4P//8A////4P//8A/8H/AAB/8A/8H+AAA/8A/8P+AAA/8A/8P+AAA/8A/8P/gAD/8A/8P/////8A/8P/////8A/8P/////8A/8P/////4A/8H/////4A/8H/////wA/8D/////wA/8B/////gA/8A////+AA/8AP///4AAAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////wAAAf/////8AAB///////AAH///////gAP///////wAP///////wAf///////4Af///////4A////////8A////////8A////////8A/+AH/AB/8A/8AP+AA/8A/4Af+AA/8A/8Af+AA/8A/8Af/gH/8A//4f////8A//4f////8A//4f////8Af/4f////4Af/4f////4AP/4P////wAP/4P////gAH/4H////AAD/4D///+AAB/4B///4AAAP4AP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8AAAAAAAA/8AAAAAAAA/8AAAAAB8A/8AAAAB/8A/8AAAAf/8A/8AAAH//8A/8AAA///8A/8AAH///8A/8AA////8A/8AD////8A/8Af////8A/8B/////8A/8P/////8A/8//////8A////////AA///////AAA//////gAAA/////4AAAA/////AAAAA////4AAAAA////AAAAAA///8AAAAAA///gAAAAAA//+AAAAAAA//wAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gD//gAAA//4P//8AAD//8f///AAH//+////gAH///////wAP///////4AP///////8Af///////8Af///////+Af///////+A////////+A//B//AB/+A/+A/+AA/+A/8Af+AA/+A/+Af+AA/+A//A//AB/+A////////+Af///////+Af///////+Af///////8Af///////8AP///////4AH///////4AH//+////wAD//+////AAA//4P//+AAAP/gH//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAfgAAA///8A/8AAB///+A//AAH////A//gAH////g//wAP////g//wAf////w//4Af////w//4A/////w//8A/////w//8A/////w//8A//gP/wA/8A/8AD/wA/8A/8AD/wAf8A/8AD/gA/8A/+AH/AB/8A////////8A////////8A////////8Af///////4Af///////4Af///////wAP///////wAH///////gAD//////+AAA//////4AAAP/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAP+AA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DhgeFB4eHh4eHh4eDw=="), 60 + (scale << 8) + (1 << 16)); +}; + +{ // 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 + +const SETTINGSFILE = "antonclk.json"; +const isBangle1 = (process.env.HWVERSION == 1); + +// variables defined from settings +let secondsMode; +let secondsColoured; +let secondsWithColon; +let dateOnMain; +let dateOnSecs; +let weekDay; +let calWeek; +let upperCase; +let vectorFont; + +// dynamic variables +let drawTimeout; +let queueMillis = 1000; +let secondsScreen = true; + + + +//For development purposes +/* +require('Storage').writeJSON(SETTINGSFILE, { + secondsMode: "Unlocked", // "Never", "Unlocked", "Always" + secondsColoured: true, + secondsWithColon: true, + dateOnMain: "Long", // "Short", "Long", "ISO8601" + dateOnSecs: "Year", // "No", "Year", "Weekday", LEGACY: true/false + weekDay: true, + calWeek: true, + upperCase: true, + vectorFont: true, +}); +*/ + +// OR (also for development purposes) +/* +require('Storage').erase(SETTINGSFILE); +*/ + +// Load settings +let loadSettings = function() { + // Helper function default setting + function def (value, def) {return value !== undefined ? value : def;} + + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + secondsMode = def(settings.secondsMode, "Never"); + secondsColoured = def(settings.secondsColoured, true); + secondsWithColon = def(settings.secondsWithColon, true); + dateOnMain = def(settings.dateOnMain, "Long"); + dateOnSecs = def(settings.dateOnSecs, "Year"); + weekDay = def(settings.weekDay, true); + calWeek = def(settings.calWeek, false); + upperCase = def(settings.upperCase, true); + vectorFont = def(settings.vectorFont, false); + + // Legacy + if (dateOnSecs === true) + dateOnSecs = "Year"; + if (dateOnSecs === false) + dateOnSecs = "No"; +} + +// schedule a draw for the next second or minute +let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, queueMillis - (Date.now() % queueMillis)); +} + +let updateState = function() { + if (Bangle.isLCDOn()) { + if ((secondsMode === "Unlocked" && !Bangle.isLocked()) || secondsMode === "Always") { + secondsScreen = true; + queueMillis = 1000; + } else { + secondsScreen = false; + queueMillis = 60000; + } + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +} + +let isoStr = function(date) { + return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2); +} + +let calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) +let ISO8601calWeek = function(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 + const dateNoTime = date; dateNoTime.setHours(0,0,0,0); + if (calWeekBuffer[0] === date.getTimezoneOffset() && calWeekBuffer[1] === dateNoTime) return calWeekBuffer[2]; + calWeekBuffer[0] = date.getTimezoneOffset(); + calWeekBuffer[1] = dateNoTime; + 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); + } + calWeekBuffer[2] = 1 + Math.ceil((firstThursday - tdt) / 604800000); + return calWeekBuffer[2]; +} + +let doColor = function() { + return !isBangle1 && !Bangle.isLocked() && secondsColoured; +} + +// Actually draw the watch face +let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2 - (secondsMode !== "Never" ? 24 : (vectorFont ? 12 : 0)); + g.reset(); + /* This is to mark the widget areas during development. + g.setColor("#888") + .fillRect(0, 0, g.getWidth(), 23) + .fillRect(0, g.getHeight() - 23, g.getWidth(), g.getHeight()).reset(); + /* */ + g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); // clear whole background (w/o widgets) + var date = new Date(); // Actually the current date, this one is shown + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time + if (secondsScreen) { + y += 65; + var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2); + if (doColor()) + g.setColor(0, 0, 1); + g.setFont("AntonSmall"); + if (dateOnSecs !== "No") { // A bit of a complex drawing with seconds on the right and date on the left + g.setFontAlign(1, 0).drawString(secStr, g.getWidth() - (isBangle1 ? 32 : 2), y); // seconds + y -= (vectorFont ? 15 : 13); + x = g.getWidth() / 4 + (isBangle1 ? 12 : 4) + (secondsWithColon ? 0 : g.stringWidth(":") / 2); + var dateStr2 = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, 1)); + var year; + var md; + var yearfirst; + if (dateStr2.match(/\d\d\d\d$/)) { // formatted date ends with year + year = (dateOnSecs === "Year" ? dateStr2.slice(-4) : require("locale").dow(date, 1)); + md = dateStr2.slice(0, -4); + if (!md.endsWith(".")) // keep separator before the year only if it is a dot (31.12. but 31/12) + md = md.slice(0, -1); + yearfirst = false; + } else { // formatted date begins with year + if (!dateStr2.match(/^\d\d\d\d/)) // if year position cannot be detected... + dateStr2 = isoStr(date); // ...use ISO date format instead + year = (dateOnSecs === "Year" ? dateStr2.slice(0, 4) : require("locale").dow(date, 1)); + md = dateStr2.slice(5); // never keep separator directly after year + yearfirst = true; + } + if (dateOnSecs === "Weekday" && upperCase) + year = year.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + if (doColor()) + g.setColor(1, 0, 0); + g.drawString(md, x, (yearfirst ? y + (vectorFont ? 26 : 16) : y)); + g.drawString(year, x, (yearfirst ? y : y + (vectorFont ? 26 : 16))); + } else { + g.setFontAlign(0, 0).drawString(secStr, x, y); // Just the seconds centered + } + } else { // No seconds screen: Show date and optionally day of week + y += (vectorFont ? 50 : (secondsMode !== "Never") ? 52 : 40); + var dateStr = (dateOnMain === "ISO8601" ? isoStr(date) : require("locale").date(date, (dateOnMain === "Long" ? 0 : 1))); + if (upperCase) + dateStr = dateStr.toUpperCase(); + g.setFontAlign(0, 0); + if (vectorFont) + g.setFont("Vector", 24); + else + g.setFont("6x8", 2); + g.drawString(dateStr, x, y); + if (calWeek || weekDay) { + var dowcwStr = ""; + if (calWeek) + dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2); + if (weekDay) + dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort # e.g. Mon #01 + else //week #01 + dowcwStr = /*LANG*/"week" + dowcwStr; + if (upperCase) + dowcwStr = dowcwStr.toUpperCase(); + g.drawString(dowcwStr, x, y + (vectorFont ? 26 : 16)); + } + } + + // queue next draw + queueDraw(); +} + +// Init the settings of the app +loadSettings(); +// Clear the screen once, at startup +g.clear(); +// Set dynamic state and perform initial drawing +updateState(); +// Register hooks for LCD on/off event and screen lock on/off event +Bangle.on('lcdPower', updateState); +Bangle.on('lock', updateState); +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', updateState); + Bangle.removeListener('lock', updateState); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + delete Graphics.prototype.setFontAntonSmall; + }}); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +} diff --git a/apps/antonclkplus/app.png b/apps/antonclkplus/app.png new file mode 100644 index 000000000..a38093c5f Binary files /dev/null and b/apps/antonclkplus/app.png differ diff --git a/apps/antonclkplus/metadata.json b/apps/antonclkplus/metadata.json new file mode 100644 index 000000000..75f0b7cd6 --- /dev/null +++ b/apps/antonclkplus/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "antonclkplus", + "name": "Anton Clock Plus", + "shortName": "Anton Clock+", + "version": "0.11", + "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"antonclkplus.app.js","url":"app.js"}, + {"name":"antonclkplus.settings.js","url":"settings.js"}, + {"name":"antonclkplus.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"antonclkplus.json"}] +} diff --git a/apps/antonclkplus/screenshot.png b/apps/antonclkplus/screenshot.png new file mode 100644 index 000000000..e949b8a24 Binary files /dev/null and b/apps/antonclkplus/screenshot.png differ diff --git a/apps/antonclk/settings.js b/apps/antonclkplus/settings.js similarity index 89% rename from apps/antonclk/settings.js rename to apps/antonclkplus/settings.js index 6882cbd0f..70851e983 100644 --- a/apps/antonclk/settings.js +++ b/apps/antonclkplus/settings.js @@ -2,7 +2,6 @@ (function(back) { var FILE = "antonclk.json"; - // Load settings var settings = Object.assign({ secondsOnUnlock: false, }, require('Storage').readJSON(FILE, true) || {}); @@ -41,7 +40,6 @@ "Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]), "Show Weekday": { value: (settings.weekDay !== undefined ? settings.weekDay : true), - format: v => v ? "On" : "Off", onchange: v => { settings.weekDay = v; writeSettings(); @@ -49,7 +47,6 @@ }, "Show CalWeek": { value: (settings.calWeek !== undefined ? settings.calWeek : false), - format: v => v ? "On" : "Off", onchange: v => { settings.calWeek = v; writeSettings(); @@ -57,7 +54,6 @@ }, "Uppercase": { value: (settings.upperCase !== undefined ? settings.upperCase : true), - format: v => v ? "On" : "Off", onchange: v => { settings.upperCase = v; writeSettings(); @@ -65,7 +61,6 @@ }, "Vector font": { value: (settings.vectorFont !== undefined ? settings.vectorFont : false), - format: v => v ? "On" : "Off", onchange: v => { settings.vectorFont = v; writeSettings(); @@ -82,7 +77,6 @@ "Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]), "With \":\"": { value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true), - format: v => v ? "On" : "Off", onchange: v => { settings.secondsWithColon = v; writeSettings(); @@ -90,7 +84,6 @@ }, "Color": { value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true), - format: v => v ? "On" : "Off", onchange: v => { settings.secondsColoured = v; writeSettings(); @@ -99,9 +92,6 @@ "Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"]) }; - // Actually display the menu E.showMenu(mainmenu); -}); - -// end of file +}) diff --git a/apps/approxclock/ChangeLog b/apps/approxclock/ChangeLog new file mode 100644 index 000000000..b6a863560 --- /dev/null +++ b/apps/approxclock/ChangeLog @@ -0,0 +1,5 @@ +0.1: Initial release +0.2: Added more descriptive approximations +0.2f: Bug fixes: Incorrect hour drawn after 50 mins, incorrect quarter minute drawn after 50 mins +0.3: Added touch interaction to display exact time and date. +0.04: Minor code improvements diff --git a/apps/approxclock/app-icon.js b/apps/approxclock/app-icon.js new file mode 100644 index 000000000..d63ad4b1f --- /dev/null +++ b/apps/approxclock/app-icon.js @@ -0,0 +1 @@ +atob("MDAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArgVYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABW19cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrNcrAACBVoGsVgAAgVaBrFYAAKzXK4GsKwAAgayBAAAAgVYAVoEAAAAAAAAAAADXVqyBAACs14Gs1ysArNeBrNcrAIHX14HXgQCB14HXrAAAVtdW11YAAAAAAAAAAFbXK4GsAACs1wAr11YArNcAK9dWAADXrABWVgDXgQCB1wAAAKzXgQAAAAAAAAAAAKzXrNfXKwCs1wAA14EArKwAK9dWAADXVgAAAADXgQBW1ysAAIHXVgAAAAAAAAAAANfXgYHXVgCs11aB11YArNcrgddWACvXgSsAAACsrCus1wAAK9es1ysAAAAAAAAAK9dWAACsrACsrKzXrAAArKzX16wAVtfX16wAAAAr19fXKwAArKwArKwAAAAAAAAAAAAAAAAAAACsrAArAAAArIEAKwAAAAAAAAAAAAAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsrAAAAAAArIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVgAAAAAAVlYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArVoErAAAAAAAAAAAAAAAAAAAAAAArVgAAAAAAAAAAAAAAAAAAAAAAAAArrNfXgQCBrNdWAAAAAAAAAAAAAAAAAAAAAABW1wAAAAAAAAAAAAAAAAAAAAAAACvXrCtWgQAAANdWAAAAAAArKwAAAAAAACsAAABW1wAAAAAAAAAAAAAAAAAAAAAAAFbXKwAAAAAAANdWAAAAAIHX16wAAACB19fXVgBW1wAA14EAAAAAAAAAAAAAAAAAAIGsAAAAAAAAANdWAAAAK9eBVteBACvXgSuBVgBW1wBW1ysAAAAAAAAAAAAAAAAAAIHXAAAAAAAAANdWAAAAVtcrAKyBAFbXKwAAAABW16zX1wAAAAAAAAAAAAAAAAAAAFbXVgAAAAAAANeBAAAAVtcrANeBAFbXKwAAAABW14HXrAAAAAAAAAAAAAAAAAAAAACs14GBrAAAAKzXrIEAK9esrNdWACvX14GBVgBW1wAr11YAAAAAAAAAAAAAAAAAAAAAVoGBgQAAACusrIEAACusrFYAAAArgaysVgBWgQAAgYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") \ No newline at end of file diff --git a/apps/approxclock/app.js b/apps/approxclock/app.js new file mode 100644 index 000000000..c4dff67d3 --- /dev/null +++ b/apps/approxclock/app.js @@ -0,0 +1,156 @@ +//load fonts +require("FontSinclair").add(Graphics); +require("FontTeletext5x9Ascii").add(Graphics); + +//const + +const numbers = { + "0": "Twelve", + "1": "One", + "2": "Two", + "3": "Three", + "4": "Four", + "5": "Five", + "6": "Six", + "7": "Seven", + "8": "Eight", + "9": "Nine", + "10": "Ten", + "11": "Eleven", + "12": "Twelve", + "13": "One", + "14": "Two", + "15": "Three", + "16": "Four", + "17": "Five", + "18": "Six", + "19": "Seven", + "20": "Eight", + "21": "Nine", + "22": "Ten", + "23": "Eleven", + "24": "Twelve", +}; + +const minutesByQuarterString = { + 0: "O'Clock", + 15: "Fifteen", + 30: "Thirty", + 45: "Fourty-Five" +}; + +const width = g.getWidth(); +const height = g.getHeight(); +let drawTimeout; + +const getNearestHour = (hours, minutes) => { + if (minutes > 54) { + return hours + 1; + } + return hours; +}; + +const getApproximatePrefix = (minutes, minutesByQuarter) => { + if (minutes === minutesByQuarter) { + return " exactly"; + } else if (minutesByQuarter - minutes < -54) { + return " nearly"; + } else if (minutesByQuarter - minutes < -5) { + return " after"; + } else if (minutesByQuarter - minutes < 0) { + return " just after"; + } else if (minutesByQuarter - minutes > 5) { + return " before"; + } else { + return " nearly"; + } +}; + +const getMinutesByQuarter = minutes => { + if (minutes < 10) { + return 0; + } else if (minutes < 20) { + return 15; + } else if (minutes < 40) { + return 30; + } else if (minutes < 55) { + return 45; + } else { + return 0; + } +}; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + drawTime(); + }, 60000 - (Date.now() % 60000)); +} + +const drawTimeExact = () => { + var dateTime = Date(); + var hours = dateTime.getHours(); + var minutes = dateTime.getMinutes().toString().padStart(2,0); + //var day = dateTime.getDay(); + var date = dateTime.getDate(); + var month = dateTime.getMonth(); + var year = dateTime.getFullYear(); + g.clear(); + g.setBgColor(0,0,0); + g.clearRect(0,0,width, height); + g.setColor(1,1,1); + g.setFont("Vector", 30); + g.drawString(hours + ":" + minutes, (width - g.stringWidth(hours + ":" + minutes))/2, height * 0.3, false); + g.setFont("Vector", 26); + g.drawString(month + 1 + "/" + date + "/" + year, (width - g.stringWidth(month + 1 + "/" + date + "/" + year))/2, height * 0.6, false); +}; + +const drawTime = () => { + //Grab time vars + var date = Date(); + var hour = date.getHours(); + var minutes = date.getMinutes(); + var minutesByQuarter = getMinutesByQuarter(minutes); + + //reset graphics + g.clear(); + g.reset(); + + //Build watch face + g.setBgColor(0, 0, 0); + g.clearRect(0, 0, width, height); + g.setFont("Vector", 22); + g.setColor(1, 1, 1); + g.drawString("It's" + getApproximatePrefix(minutes, minutesByQuarter), (width - g.stringWidth("It's" + getApproximatePrefix(minutes, minutesByQuarter))) / 2, height * 0.25, false); + g.setFont("Vector", 30); + g.drawString(numbers[getNearestHour(hour, minutes)], (width - g.stringWidth(numbers[getNearestHour(hour, minutes)])) / 2, height * 0.45, false); + g.setFont("Vector", 22); + g.drawString(minutesByQuarterString[minutesByQuarter], (width - g.stringWidth(minutesByQuarterString[minutesByQuarter])) / 2, height * 0.7, false); + + queueDraw(); +}; + +g.clear(); +drawTime(); + +Bangle.on('lcdPower', function (on) { + if (on) { + drawTime(); + } else { + if (idTimeout) { + clearTimeout(idTimeout); + } + } +}); + +Bangle.on('touch', function(button, xy){ + drawTimeExact(); + setTimeout(drawTime, 7000); +}); + +// Show launcher when button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/approxclock/app.png b/apps/approxclock/app.png new file mode 100644 index 000000000..a5fd8db83 Binary files /dev/null and b/apps/approxclock/app.png differ diff --git a/apps/approxclock/metadata.json b/apps/approxclock/metadata.json new file mode 100644 index 000000000..c5861523b --- /dev/null +++ b/apps/approxclock/metadata.json @@ -0,0 +1,18 @@ +{ "id": "approxclock", + "name": "Approximate Clock", + "shortName" : "Approx Clock", + "version": "0.04", + "icon": "app.png", + "description": "A really basic spelled out time display for people looking for the vague time at a glance.", + "readme": "readme.md", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"approxclock.app.js","url":"app.js"}, + {"name":"approxclock.img","url":"app-icon.js","evaluate":true} + ], + "screenshots": [ + {"url": "screenshot.png"} + ] +} diff --git a/apps/approxclock/readme.md b/apps/approxclock/readme.md new file mode 100644 index 000000000..aa1b74267 --- /dev/null +++ b/apps/approxclock/readme.md @@ -0,0 +1,7 @@ +## Approximate Clock + +### Description + +Get a rough idea of the time at a quick glance, mostly made for myself based on a similar watchface on pebble. I find this keeps me from checking my watch too often and also saves me from moments of severe brainfart staring at these mysterious symbols we call numbers. + +Exact time and date can be viewed temporarily by touching the screen. \ No newline at end of file diff --git a/apps/approxclock/screenshot.png b/apps/approxclock/screenshot.png new file mode 100644 index 000000000..fe13bbe33 Binary files /dev/null and b/apps/approxclock/screenshot.png differ diff --git a/apps/aptsciclk/ChangeLog b/apps/aptsciclk/ChangeLog index ed32a45a2..5483b3fb4 100644 --- a/apps/aptsciclk/ChangeLog +++ b/apps/aptsciclk/ChangeLog @@ -6,3 +6,5 @@ 0.06: Formatting 0.07: Added potato GLaDOS and quote functionality when you tap her 0.08: Fixed drawing issues with the quotes and added more +0.09: Minor code improvements +0.10: Minor code improvements diff --git a/apps/aptsciclk/app.js b/apps/aptsciclk/app.js index c2903cf37..311a8cd77 100644 --- a/apps/aptsciclk/app.js +++ b/apps/aptsciclk/app.js @@ -136,6 +136,7 @@ else if (img == "apetureLaboratoriesLight"){ function drawStart(){ g.clear(); g.reset(); + let apSciLab; if (g.theme.dark){apSciLab = getImg("apetureLaboratories");} else {apSciLab = getImg("apetureLaboratoriesLight");} g.drawImage(apSciLab, xyCenter-apSciLab.width/2, xyCenter-apSciLab.height/2); @@ -241,7 +242,7 @@ function buttonPressed(){ if (curWarning < maxWarning) curWarning += 1; else curWarning = 0; g.reset(); - buttonImg = getImg("butPress"); + const buttonImg = getImg("butPress"); g.drawImage(buttonImg, 0, 0); warningImg = getImg("w"+String(curWarning)); @@ -251,7 +252,7 @@ function buttonPressed(){ } function buttonUnpressed(){ if (!pause){ - buttonImg = getImg("butUnpress"); + const buttonImg = getImg("butUnpress"); g.drawImage(buttonImg, 0, 0); } else{ @@ -270,19 +271,19 @@ function queueDraw() { function draw() { - if (pause){} - else{ + if (!pause){ // get date var d = new Date(); var da = d.toString().split(" "); g.reset(); // default draw styles //draw watchface + let apSciWatch; if (g.theme.dark){apSciWatch = getImg("apetureWatch");} else {apSciWatch = getImg("apetureWatchLight");} g.drawImage(apSciWatch, xyCenter-apSciWatch.width/2, xyCenter-apSciWatch.height/2); - potato = getImg("potato"); + const potato = getImg("potato"); g.drawImage(potato, 118, 118); g.drawImage(warningImg, 1, g.getWidth()-61);//update warning diff --git a/apps/aptsciclk/metadata.json b/apps/aptsciclk/metadata.json index c450d926e..d4f1c2c49 100644 --- a/apps/aptsciclk/metadata.json +++ b/apps/aptsciclk/metadata.json @@ -2,9 +2,10 @@ "id": "aptsciclk", "name": "Apeture Science Clock", "shortName":"AptSci Clock", - "version": "0.08", + "version": "0.10", "description": "A clock based on the portal series", "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/aptsciclk/screenshot.png b/apps/aptsciclk/screenshot.png new file mode 100644 index 000000000..4803e4b13 Binary files /dev/null and b/apps/aptsciclk/screenshot.png differ diff --git a/apps/arrow/ChangeLog b/apps/arrow/ChangeLog index edd5ccb3d..4d3a6f1a8 100644 --- a/apps/arrow/ChangeLog +++ b/apps/arrow/ChangeLog @@ -4,3 +4,4 @@ 0.04: removed LED1.write() as it was keeping LCD on 0.05: Turn compass off when screen off Calibrate at start if no info +0.06: Minor code improvements diff --git a/apps/arrow/app.js b/apps/arrow/app.js index f1f85e880..c4cf3d32c 100644 --- a/apps/arrow/app.js +++ b/apps/arrow/app.js @@ -20,9 +20,11 @@ function flip2(x,y) { buf2.clear(); } +/* function radians(d) { return (d*Math.PI) / 180; } +*/ // takes 32ms function drawCompass(hd) { @@ -60,7 +62,7 @@ function newHeading(m,h){ // takes approx 7ms function tiltfixread(O,S){ - var start = Date.now(); + //var start = Date.now(); var m = Bangle.getCompass(); var g = Bangle.getAccel(); m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z; diff --git a/apps/arrow/metadata.json b/apps/arrow/metadata.json index bf462e33b..58b6f84c1 100644 --- a/apps/arrow/metadata.json +++ b/apps/arrow/metadata.json @@ -1,7 +1,7 @@ { "id": "arrow", "name": "Arrow Compass", - "version": "0.05", + "version": "0.06", "description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass", "icon": "arrow.png", "type": "app", diff --git a/apps/ashadyclock/0.bin b/apps/ashadyclock/0.bin new file mode 100644 index 000000000..0be5abd15 Binary files /dev/null and b/apps/ashadyclock/0.bin differ diff --git a/apps/ashadyclock/1.bin b/apps/ashadyclock/1.bin new file mode 100644 index 000000000..54fe59905 Binary files /dev/null and b/apps/ashadyclock/1.bin differ diff --git a/apps/ashadyclock/2.bin b/apps/ashadyclock/2.bin new file mode 100644 index 000000000..a95c74b20 Binary files /dev/null and b/apps/ashadyclock/2.bin differ diff --git a/apps/ashadyclock/3.bin b/apps/ashadyclock/3.bin new file mode 100644 index 000000000..db3821284 Binary files /dev/null and b/apps/ashadyclock/3.bin differ diff --git a/apps/ashadyclock/4.bin b/apps/ashadyclock/4.bin new file mode 100644 index 000000000..b59e41a0d Binary files /dev/null and b/apps/ashadyclock/4.bin differ diff --git a/apps/ashadyclock/5.bin b/apps/ashadyclock/5.bin new file mode 100644 index 000000000..10985534a Binary files /dev/null and b/apps/ashadyclock/5.bin differ diff --git a/apps/ashadyclock/6.bin b/apps/ashadyclock/6.bin new file mode 100644 index 000000000..d45f451b6 Binary files /dev/null and b/apps/ashadyclock/6.bin differ diff --git a/apps/ashadyclock/7.bin b/apps/ashadyclock/7.bin new file mode 100644 index 000000000..f847816ab Binary files /dev/null and b/apps/ashadyclock/7.bin differ diff --git a/apps/ashadyclock/8.bin b/apps/ashadyclock/8.bin new file mode 100644 index 000000000..deb47c3c0 Binary files /dev/null and b/apps/ashadyclock/8.bin differ diff --git a/apps/ashadyclock/9.bin b/apps/ashadyclock/9.bin new file mode 100644 index 000000000..c806c2012 Binary files /dev/null and b/apps/ashadyclock/9.bin differ diff --git a/apps/ashadyclock/app-icon.js b/apps/ashadyclock/app-icon.js new file mode 100644 index 000000000..3e45d1754 --- /dev/null +++ b/apps/ashadyclock/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDADAAAAHA4HHAAAHA4HA4HA4HAAAAA4//////AAH//////6///AAAA///6//9AAV/////////XAAAA//////6AAv//X//////4AAAA//////4AA////X/////4AAAA//////4AA/////6////4AAAA//////4AA///6vX///v4AAAAAA////4AA///4A//6//AAAAAAF///64AAHA4CFX////AAAAAAH//X/oAAAAAAH6///4AAAAAAH////AAAAAAAH////QAAAAAAH////AAAAAAA/////AAAAAAAH////AAAAAAC////4AAAAAAAH////AAAAAAH////4AAAAAAA6v///AAAAAA//X/9QAAAAAAA////VAAAAAA////4AAAAAAAA////4AAAAAH////4AAAAAAAA////4AAAAA/6///AAAAAAAAA////4AAAAA////6AAAAAAAAF////4AAAAH/X/94AAAAAAAAH/X//oAAAAX////AAAAAAAAAH////AAAAA/////AAAAAAAAAH////AEQAB+///4AwEAAAAAAH////AigAH////4EUGEAAAAAX////A0GA///3/AmEUigAAAAjGMYxEw0AxxGOIGg0w0igAAGikUwmGmmgwEUwmmGmk0wwAAmmmmmmmGmmgimmmmmmmGmgAGmmmGgGmmmmAmmmmiE0w00wAE0000wE000wwmmmmiA0000wAE00U0AGmmmmA0000wE0U00wAAAAAAAmmmmmAAAAAAGmmmmAAAAAAAE0000wAAAAAE00U00AAAAAACk0000QAAAAAmmmmmgAAAAAAGmmmmigAAAAA00000AAAAAAA00000UAAAAAmmmmmEAAAAAA000w00AAAAAGmmmmmAAAAAAE00000QAAAAE0U000wAAAAAA00000wAAAAA000mmiAAAAAAmmmmmiAAAAAmimmmkQAAAAAGmmmmmAAAAAE00000QAAAAAA00000wmEwmAmmmmmmEwmEwAE0000w00000wmmmmmmimmmgAGmmmmmmmmmGEw00mmmmmmmgAGmmmmmmmmmGE0w00000000AA0000U000mmmA000000000wwAGmmmmmmmmmEU0GmmmmmmmmAAAAiAEACgAECAAgCEAAiAEAAA")) diff --git a/apps/ashadyclock/app.js b/apps/ashadyclock/app.js new file mode 100644 index 000000000..8c86ca8d2 --- /dev/null +++ b/apps/ashadyclock/app.js @@ -0,0 +1,107 @@ +var settings = Object.assign({ + // default values + showWidgets: false, + alternativeColor: false, +}, require('Storage').readJSON("ashadyclock.json", true) || {}); + +let drawTimeout; +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +let palBottom; +if (settings.alternativeColor) { + palBottom = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([ + g.toColor("#000"), + g.toColor("#000"), + g.toColor("#0FF"), + g.toColor("#0FF"), + g.toColor("#00F"), + g.toColor("#000"), + g.toColor("#00F"), + g.toColor("#000") + ]).buffer))); +} else { + palBottom = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([ + g.toColor("#000"), + g.toColor("#000"), + g.toColor("#F00"), + g.toColor("#FF0"), + g.toColor("#00F"), + g.toColor("#000"), + g.toColor("#FF0"), + g.toColor("#000") + ]).buffer))); +} + +let palTop = new Uint16Array(E.toArrayBuffer(E.toFlatString(new Uint16Array([ + g.toColor("#FFF"), + g.toColor("#000"), + g.toColor("#FFF"), + g.toColor("#FFF"), + g.toColor("#00F"), + g.toColor("#000"), + g.toColor("#FFF"), + g.toColor("#000"), + ]).buffer))); + +let xOffset = (g.getWidth() - 176) / 2; +let yOffset = (g.getHeight() - 176) / 2; + +function drawTop(d0, d1) { + if (settings.showWidgets && g.getHeight()<=176) { + drawNumber(d1, 82 + xOffset, 24 + yOffset, palTop, {scale: 0.825}); + drawNumber(d0, 13 + xOffset, 24 + yOffset, palTop, {scale: 0.825}); + } else { + drawNumber(d1, 80, 0, palTop); + drawNumber(d0, -1, 0, palTop); + } +} + +function drawBottom(d0, d1) { + if (settings.showWidgets && g.getHeight()<=176) { + drawNumber(d1, 82 + xOffset, 92 + yOffset, palBottom, {scale: 0.825}); + drawNumber(d0, 13 + xOffset, 92 + yOffset, palBottom, {scale: 0.825}); + } else { + drawNumber(d1, 80, 75, palBottom); + drawNumber(d0, -1, 75, palBottom); + } +} + +function drawNumber(number, x, y, palette, options) { + let image = + { + width : 98, height : 100, bpp : 3, + transparent: 4, + buffer : require("Storage").read("ashadyclock." + number +".bin") + }; + image.palette = palette; + g.drawImage(image, x, y, options); +} + +function draw() { + let d = new Date(); + g.clearRect(0, settings.showWidgets ? 24 : 0, g.getWidth(),g.getHeight()); + + drawBottom(Math.floor(d.getMinutes()/10), d.getMinutes() % 10); + drawTop(Math.floor(d.getHours()/10), d.getHours() % 10); + + queueDraw(); +} + +g.clear(); +// draw immediately at first +draw(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); + +if(settings.showWidgets) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/ashadyclock/app.png b/apps/ashadyclock/app.png new file mode 100644 index 000000000..e01fd0746 Binary files /dev/null and b/apps/ashadyclock/app.png differ diff --git a/apps/ashadyclock/metadata.json b/apps/ashadyclock/metadata.json new file mode 100644 index 000000000..1b92ecabc --- /dev/null +++ b/apps/ashadyclock/metadata.json @@ -0,0 +1,27 @@ +{ "id": "ashadyclock", + "name": "A Shady Clock", + "shortName":"Shady Clk", + "icon": "app.png", + "version":"0.01", + "description": "A nice clock with drop shadow. Hours and minutes. Configure color and widgets in settings. Create any color combination with the existing images by changing only the app color values.", + "type": "clock", + "tags": "clock", + "screenshots": [{"url":"screenshot-1.png"},{"url":"screenshot.png"}], + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"ashadyclock.app.js","url":"app.js"}, + {"name":"ashadyclock.img","url":"app-icon.js","evaluate":true}, + {"name":"ashadyclock.0.bin","url":"0.bin"}, + {"name":"ashadyclock.1.bin","url":"1.bin"}, + {"name":"ashadyclock.2.bin","url":"2.bin"}, + {"name":"ashadyclock.3.bin","url":"3.bin"}, + {"name":"ashadyclock.4.bin","url":"4.bin"}, + {"name":"ashadyclock.5.bin","url":"5.bin"}, + {"name":"ashadyclock.6.bin","url":"6.bin"}, + {"name":"ashadyclock.7.bin","url":"7.bin"}, + {"name":"ashadyclock.8.bin","url":"8.bin"}, + {"name":"ashadyclock.9.bin","url":"9.bin"}, + {"name":"ashadyclock.settings.js","url":"settings.js"} + ], + "data": [{"name":"ashadyclock.json"}] +} diff --git a/apps/ashadyclock/screenshot-1.png b/apps/ashadyclock/screenshot-1.png new file mode 100644 index 000000000..a0c7ea3be Binary files /dev/null and b/apps/ashadyclock/screenshot-1.png differ diff --git a/apps/ashadyclock/screenshot.png b/apps/ashadyclock/screenshot.png new file mode 100644 index 000000000..731055915 Binary files /dev/null and b/apps/ashadyclock/screenshot.png differ diff --git a/apps/ashadyclock/settings.js b/apps/ashadyclock/settings.js new file mode 100644 index 000000000..eef57e519 --- /dev/null +++ b/apps/ashadyclock/settings.js @@ -0,0 +1,32 @@ +(function(back) { + var FILE = "ashadyclock.json"; + // Load settings + var settings = Object.assign({ + showWidgets: false, + alternativeColor: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Shady Clck" }, + "< Back" : () => back(), + 'Show Widgets': { + value: !!settings.showWidgets, // !! converts undefined to false + onchange: v => { + settings.showWidgets = v; + writeSettings(); + } + }, + 'Blue Color': { + value: !!settings.alternativeColor, // !! converts undefined to false + onchange: v => { + settings.alternativeColor = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog index 739ccf915..89b1c80f8 100644 --- a/apps/assistedgps/ChangeLog +++ b/apps/assistedgps/ChangeLog @@ -1,3 +1,7 @@ 0.01: New App! 0.02: Update to work with Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2 +0.04: Now turns GPS off after upload +0.05: Fix regression in 0.04 that caused AGPS data not to get loaded +0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default +0.07: Bangle.js 2 now gets estimated time + lat/lon from the browser (~3x faster fix) \ No newline at end of file diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 80d68a71f..39290c2e6 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -31,7 +31,7 @@
@@ -60,6 +60,7 @@ diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json index 1dbc42c87..73f775a72 100644 --- a/apps/assistedgps/metadata.json +++ b/apps/assistedgps/metadata.json @@ -1,13 +1,16 @@ { "id": "assistedgps", - "name": "Assisted GPS Update (AGPS)", - "version": "0.03", - "description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "name": "Assisted GPS Updater (AGPS)", + "shortName": "AGPS", + "version": "0.07", + "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", + "sortorder": -1, "icon": "app.png", "type": "RAM", - "tags": "tool,outdoors,agps", + "tags": "tool,outdoors,agps,gps,a-gps,agps", "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "customConnect": true, - "storage": [] + "storage": [], + "sortorder": -1 } diff --git a/apps/astral/ChangeLog b/apps/astral/ChangeLog index a51c96760..e77aa2ca2 100644 --- a/apps/astral/ChangeLog +++ b/apps/astral/ChangeLog @@ -1,3 +1,8 @@ 0.01: Create astral clock app 0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget 0.03: Update to use Bangle.setUI instead of setWatch +0.04: Tell clock widgets to hide. +0.05: Added adjustment for Bangle.js magnetometer heading fix +0.06: optimized to update much faster +0.07: added support for bangle.js 2 +0.08: call setUI before loading widgets to indicate we're a clock diff --git a/apps/astral/README.md b/apps/astral/README.md index f9d897e2f..894d0cb8a 100644 --- a/apps/astral/README.md +++ b/apps/astral/README.md @@ -1,29 +1,29 @@ Astral Clock ============ +NOTE FOR THE BANGLE 2 THIS APP ONLY SUPPORTS USING THE BLACK BACKGROUND CURRENTLY + Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. ![screenshot](./Example.PNG) (The clock does have Pluto now - felt bad for leaving it out) Functions ---------- -**BTN1**: Refreshes Alt/Az readings. The coordinates are NOT continually updated, this is to save resources and battery usage plus it avoids you having to wait for calculations to finish before you can do anything else on the watch - it doesn't take long but it could still be annoying. -**BTN2**: Load side-menu as standard for clocks. -**BTN3**: Changes between planet mode and extra/other targets - discussed below (will still need to press button 1 after switching to update calcs). -**BTN4**: This is the left touchscreen, and when the LCD is on you can use this to change the font between red/white. This will only work after the GPS location has been set initially. +--------- +**BTN2**: Load side-menu as standard for clocks. +Swiping left or right will alternate between planets and other astronomy targets, see below for how to change these addtional ones. -The text will turn blue during calculation and then back again once complete. +The data is refreshed automatically every 2 minutes. You can force a refresh as well by swiping up or, on Bangle 1, pressing Button 3. -When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision, the calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally. +Swiping down will disable/enable the compass and GPS. + +When you first install it, all positions will be estimated from UK as the default location and all the text will be white; from the moment you get your first GPS lock with the clock, it will save your location, recalculate accordingly and change the text to red, ideal for maintaining night vision. One the Bangle.JS 2, the colour will be a light blue rather than red because the colours are not as vibrant. The calculations will also now be relevant to your location and time. If you have not used the GPS yet, I suggest using it outside briefly to get your first fix as the initial one can take a bit longer, although it should still just be a minute or 2 max normally. Lat and Lon are saved in a file called **astral.config**. You can review this file if you want to confirm current coordinates or even hard set different values \- although be careful doing the latter as there is no error handling to manage bad values here so you would have to delete the file and have the app generate a new one if that happens, also the GPS functionality will overwrite anything you put in here once it picks up your location. -There can currently be a slight error mainly to the Az at times due to a firmware issue for acos (arccosine) that affect spherical calculations but I have used an estimator function that gives a good enough accuracy for general observation so shouldn't noticeably be too far off. I\'ll be implementing acos for better accuracy when the fix is in a standard release and the update will still include the current estimate function to support a level of backward compatibility. - The moon phases are split into the 8 phases with an image for each - new moon would show no image. -The compass is displayed above the minute digits, if you get strange values or dashes the compass needs calibration but you just need to move the watch around a bit for this each time - ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or doing Wing Chun Kuen. +The compass is displayed on the left. -Also the compass isn\’t tilt compensated so try and keep the face parallel when taking a reading. +Also the compass isn\’t tilt compensated so try and keep the face parallel when taking a reading. It's more of an indicator, for a more accurate compass reading, you can use one of the many great apps in the apploader that compensated for movement and angles of the watch etc. Additional Astronomy Targets ---------------------------- @@ -37,7 +37,7 @@ The type property is not utilised as yet but relates to whether the object is (i Updates & Feedback ------------------ -Put together, initially at least, by \"Ben Jabituya\", https://jabituyaben.wixsite.com/majorinput, jabituyaben@gmail.com. Feel free to get in touch for any feature request. Also I\'m not precious at all - if you know of efficiencies or improvements you could make, just put the changes in. One thing that would probably be ideal is to change some of the functions to inline C to make it faster. +Put together, initially at least, by \"Ben Jabituya\", https://majorinput.co.uk, jabituyaben@gmail.com. Credit to various sources from which I have literally taken source code and shoehorned to fit on the Bangle: diff --git a/apps/astral/app.js b/apps/astral/app.js index c445463f2..7c8f5512a 100644 --- a/apps/astral/app.js +++ b/apps/astral/app.js @@ -1,7 +1,11 @@ -setupcomplete_colour = "#ff3329"; -default_colour = "#ffffff"; -calc_display_colour = "#00FFFF"; -display_colour = default_colour; + +require("Font8x12").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); + +var setupcomplete_colour = "#ff3329"; +var default_colour = "#ffffff"; +var calc_display_colour = "#00FFFF"; +var display_colour = default_colour; var processing = false; var all_extras_array = []; @@ -10,11 +14,19 @@ var mode = "planetary"; var modeswitch = false; var colours_switched = false; +var sensorsOn = false; // Load fonts -require("Font7x11Numeric7Seg").add(Graphics); + // position on screen -const Xaxis = 150, Yaxis = 55; +var screenSize = g.getHeight(); +console.log(screenSize); +var Xaxis = 150, Yaxis = 55; +if (screenSize <= 176) { + setupcomplete_colour = "#00FFFF"; + Xaxis = 110; + Yaxis = 40; +} //lat lon settings loading var astral_settings; @@ -22,36 +34,36 @@ var config_file = require("Storage").open("astral.config.txt", "r"); var test_file = config_file.read(config_file.getLength()); if (test_file !== undefined) { - astral_settings = JSON.parse(test_file); - if (astral_settings.astral_default) - display_colour = default_colour; - else - display_colour = setupcomplete_colour; + astral_settings = JSON.parse(test_file); + if (astral_settings.astral_default) + display_colour = default_colour; + else + display_colour = setupcomplete_colour; } if (astral_settings === undefined) { - astral_settings = { - version: 1, - lat: 51.5074, - lon: 0.1278, - astral_default: true, - extras: [ - { name: "Andromeda", ra: "004244", de: "411609", type: 3 }, - { name: "Cigar", ra: "095552", de: "694047", type: 3 }, - { name: "Pinwheel", ra: "140313", de: "542057", type: 3 }, - { name: "Whirlpool", ra: "132953", de: "471143", type: 3 }, - { name: "Orion", ra: "053517", de: "-052328", type: 2 }, - { name: "Hercules", ra: "160515", de: "174455", type: 1 }, - { name: "Beehive", ra: "084024", de: "195900", type: 1 }, - { name: "SilverCoin", ra: "004733", de: "-251718", type: 3 }, - { name: "Lagoon", ra: "180337", de: "-242312", type: 2 }, - { name: "Trifid", ra: "180223", de: " -230148", type: 2 }, - { name: "Dumbbell", ra: "195935", de: "224316", type: 2 }, - { name: "Pleiades", ra: "034724", de: "240700", type: 1 } - ] - }; - config_file = require("Storage").open("astral.config.txt", "w"); - config_file.write(JSON.stringify(astral_settings)); + astral_settings = { + version: 1, + lat: 51.5074, + lon: 0.1278, + astral_default: true, + extras: [ + { name: "Andromeda", ra: "004244", de: "411609", type: 3 }, + { name: "Cigar", ra: "095552", de: "694047", type: 3 }, + { name: "Pinwheel", ra: "140313", de: "542057", type: 3 }, + { name: "Whirlpool", ra: "132953", de: "471143", type: 3 }, + { name: "Orion", ra: "053517", de: "-052328", type: 2 }, + { name: "Hercules", ra: "160515", de: "174455", type: 1 }, + { name: "Beehive", ra: "084024", de: "195900", type: 1 }, + { name: "SilverCoin", ra: "004733", de: "-251718", type: 3 }, + { name: "Lagoon", ra: "180337", de: "-242312", type: 2 }, + { name: "Trifid", ra: "180223", de: " -230148", type: 2 }, + { name: "Dumbbell", ra: "195935", de: "224316", type: 2 }, + { name: "Pleiades", ra: "034724", de: "240700", type: 1 } + ] + }; + config_file = require("Storage").open("astral.config.txt", "w"); + config_file.write(JSON.stringify(astral_settings)); } var compass_heading = "--"; @@ -64,66 +76,66 @@ var mins; var secs; var calc = { - lat_degrees: "", - lat_minutes: "", - lon_degrees: "", - lon_minutes: "", - month: "", - day: "", - hour: "", - minute: "", - second: "", - thisday: "", - south: "", - north: "", - west: "", - east: "" + lat_degrees: "", + lat_minutes: "", + lon_degrees: "", + lon_minutes: "", + month: "", + day: "", + hour: "", + minute: "", + second: "", + thisday: "", + south: "", + north: "", + west: "", + east: "" }; var pstrings = []; var pname = new Array("Mercury", "Venus", "Sun", - "Mars", "Jupiter", "Saturn ", - "Uranus ", "Neptune", "Pluto"); + "Mars", "Jupiter", "Saturn ", + "Uranus ", "Neptune", "Pluto"); function acos_estimate(x) { - return (-0.69813170079773212 * x * x - 0.87266462599716477) * x + 1.5707963267948966; + return (-0.69813170079773212 * x * x - 0.87266462599716477) * x + 1.5707963267948966; } function ConvertDEGToDMS(deg, lat) { - var absolute = Math.abs(deg); - var degrees = Math.floor(absolute); - var minutesNotTruncated = (absolute - degrees) * 60; - var minutes = Math.floor(minutesNotTruncated); - return minutes; + var absolute = Math.abs(deg); + var degrees = Math.floor(absolute); + var minutesNotTruncated = (absolute - degrees) * 60; + var minutes = Math.floor(minutesNotTruncated); + return minutes; } function test() { - // coords = [42.407211,-71.082439]; - coords = [astral_settings.lat, astral_settings.lon]; - //coords = [-33.8688, 133.775]; - calc.lat_degrees = Math.abs(coords[0]).toFixed(0); - calc.lon_degrees = Math.abs(coords[1]).toFixed(0); + // coords = [42.407211,-71.082439]; + coords = [astral_settings.lat, astral_settings.lon]; + //coords = [-33.8688, 133.775]; + calc.lat_degrees = Math.abs(coords[0]).toFixed(0); + calc.lon_degrees = Math.abs(coords[1]).toFixed(0); - calc.lat_minutes = ConvertDEGToDMS(coords[0], true).toString(); - calc.lon_minutes = ConvertDEGToDMS(coords[1]).toString(); + calc.lat_minutes = ConvertDEGToDMS(coords[0], true).toString(); + calc.lon_minutes = ConvertDEGToDMS(coords[1]).toString(); - if (coords[1] < 0) { - calc.west = false; - calc.east = true; - } - else { - calc.west = true; - calc.east = false; - } - if (coords[0] < 0) { - calc.south = true; - calc.north = false; - } - else { - calc.south = false; - calc.north = true; - } + if (coords[1] < 0) { + calc.west = false; + calc.east = true; + } + else { + calc.west = true; + calc.east = false; + } + if (coords[0] < 0) { + calc.south = true; + calc.north = false; + } + else { + calc.south = false; + calc.north = true; + } } var DEGS = 180 / Math.PI; // convert radians to degrees @@ -132,334 +144,335 @@ var EPS = 1.0e-12; // machine error constant // right ascension, declination coordinate structure function coord() { - ra = parseFloat("0"); // right ascension [deg] - dec = parseFloat("0"); // declination [deg] - rvec = parseFloat("0"); // distance [AU] + ra = parseFloat("0"); // right ascension [deg] + dec = parseFloat("0"); // declination [deg] + rvec = parseFloat("0"); // distance [AU] } // altitude, azimuth coordinate structure function horizon() { - alt = parseFloat("0"); // altitude [deg] - az = parseFloat("0"); // azimuth [deg] + alt = parseFloat("0"); // altitude [deg] + az = parseFloat("0"); // azimuth [deg] } // orbital element structure function elem() { - a = parseFloat("0"); // semi-major axis [AU] - e = parseFloat("0"); // eccentricity of orbit - i = parseFloat("0"); // inclination of orbit [deg] - O = parseFloat("0"); // longitude of the ascending node [deg] - w = parseFloat("0"); // longitude of perihelion [deg] - L = parseFloat("0"); // mean longitude [deg] + a = parseFloat("0"); // semi-major axis [AU] + e = parseFloat("0"); // eccentricity of orbit + i = parseFloat("0"); // inclination of orbit [deg] + O = parseFloat("0"); // longitude of the ascending node [deg] + w = parseFloat("0"); // longitude of perihelion [deg] + L = parseFloat("0"); // mean longitude [deg] } function process_extras_coord(coord_string) { - var extras_second = parseInt(coord_string.slice(-2)); - var extras_minute; - var extras_hour; - var extras_calc; + var extras_second = parseInt(coord_string.slice(-2)); + var extras_minute; + var extras_hour; + var extras_calc; - var extras_signcheck = coord_string.charAt(0); + var extras_signcheck = coord_string.charAt(0); - if (extras_signcheck == "-") { - extras_minute = parseInt(coord_string.slice(3, -2)); - extras_hour = parseInt(coord_string.slice(1, 3)); - extras_calc = (extras_hour + extras_minute / 60 + extras_second / 3600) * -1; - } - else { - extras_minute = parseInt(coord_string.slice(2, -2)); - extras_hour = parseInt(coord_string.slice(0, 2)); - extras_calc = extras_hour + extras_minute / 60 + extras_second / 3600; - } - return extras_calc; + if (extras_signcheck == "-") { + extras_minute = parseInt(coord_string.slice(3, -2)); + extras_hour = parseInt(coord_string.slice(1, 3)); + extras_calc = (extras_hour + extras_minute / 60 + extras_second / 3600) * -1; + } + else { + extras_minute = parseInt(coord_string.slice(2, -2)); + extras_hour = parseInt(coord_string.slice(0, 2)); + extras_calc = extras_hour + extras_minute / 60 + extras_second / 3600; + } + return extras_calc; } // compute ... function compute() { - var lat_degrees = parseInt(calc.lat_degrees, 10); - var lat_minutes = parseInt(calc.lat_minutes, 10); - var lon_degrees = parseInt(calc.lon_degrees, 10); - var lon_minutes = parseInt(calc.lon_minutes, 10); + var lat_degrees = parseInt(calc.lat_degrees, 10); + var lat_minutes = parseInt(calc.lat_minutes, 10); + var lon_degrees = parseInt(calc.lon_degrees, 10); + var lon_minutes = parseInt(calc.lon_minutes, 10); - var now = new Date(); - year = now.getFullYear(); - month = now.getMonth() + 1; - day = now.getDay(); - hour = now.getHours(); - mins = now.getMinutes(); - secs = now.getSeconds(); + var now = new Date(); + year = now.getFullYear(); + month = now.getMonth() + 1; + day = now.getDay(); + hour = now.getHours(); + mins = now.getMinutes(); + secs = now.getSeconds(); - if (isNaN(lat_degrees) || (lat_degrees < 0) || (lat_degrees >= 90) || - isNaN(lat_minutes) || (lat_minutes < 0) || (lat_minutes >= 60) || - isNaN(lon_degrees) || (lon_degrees < 0) || (lon_degrees >= 180) || - isNaN(lon_minutes) || (lon_minutes < 0) || (lon_minutes >= 60)) { - print("Invalid input!"); - return; + if (isNaN(lat_degrees) || (lat_degrees < 0) || (lat_degrees >= 90) || + isNaN(lat_minutes) || (lat_minutes < 0) || (lat_minutes >= 60) || + isNaN(lon_degrees) || (lon_degrees < 0) || (lon_degrees >= 180) || + isNaN(lon_minutes) || (lon_minutes < 0) || (lon_minutes >= 60)) { + print("Invalid input!"); + return; + } + + var lat = dms2real(lat_degrees, lat_minutes, 0); + var lon = dms2real(lon_degrees, lon_minutes, 0); + if (calc.south == true) lat = -lat; + if (calc.west == true) lon = -lon; + + // compute day number for date/time + var dn = day_number(year, month, day, hour, mins); + + var p; + var obj = new coord(); + var h = new horizon(); + + pstrings = []; + + if (mode == "planetary") { + for (p = 0; p < 9; p++) { + get_coord(obj, p, dn); + coord_to_horizon(now, obj.ra, obj.dec, lat, lon, h); + display_string = (pname[p] + " " + dec2str(h.alt) + " " + degr2str(h.az)); + + pstrings.push(display_string); + } + } + else { + all_extras_arrray = []; + for (p = 0; p < astral_settings.extras.length; p++) { + var extras_ra = process_extras_coord(astral_settings.extras[p].ra); + extras_ra *= 15; + + var extras_dec = process_extras_coord(astral_settings.extras[p].de); + + coord_to_horizon(now, extras_ra, extras_dec, lat, lon, h); + display_string = (astral_settings.extras[p].name + " " + dec2str(h.alt) + " " + degr2str(h.az)); + + all_extras_array.push([h.alt, display_string]); } - var lat = dms2real(lat_degrees, lat_minutes, 0); - var lon = dms2real(lon_degrees, lon_minutes, 0); - if (calc.south == true) lat = -lat; - if (calc.west == true) lon = -lon; + all_extras_array.sort(function (a, b) { + return b[0] - a[0]; + }); - // compute day number for date/time - var dn = day_number(year, month, day, hour, mins); - - var p; - var obj = new coord(); - var h = new horizon(); - - pstrings = []; - - if (mode == "planetary") { - for (p = 0; p < 9; p++) { - get_coord(obj, p, dn); - coord_to_horizon(now, obj.ra, obj.dec, lat, lon, h); - display_string = (pname[p] + " " + dec2str(h.alt) + " " + degr2str(h.az)); - - pstrings.push(display_string); - } - } - else { - all_extras_arrray = []; - for (p = 0; p < astral_settings.extras.length; p++) { - var extras_ra = process_extras_coord(astral_settings.extras[p].ra); - extras_ra *= 15; - - var extras_dec = process_extras_coord(astral_settings.extras[p].de); - - coord_to_horizon(now, extras_ra, extras_dec, lat, lon, h); - display_string = (astral_settings.extras[p].name + " " + dec2str(h.alt) + " " + degr2str(h.az)); - - all_extras_array.push([h.alt, display_string]); - } - - all_extras_array.sort(function (a, b) { - return b[0] - a[0]; - }); - - for (p = 0; p < 9; p++) { - pstrings.push(all_extras_array[p][1]); - } + for (p = 0; p < 9; p++) { + pstrings.push(all_extras_array[p][1]); } + } } // day number to/from J2000 (Jan 1.5, 2000) function day_number(y, m, d, hour, mins) { - var h = hour + mins / 60; - var rv = 367 * y - - Math.floor(7 * (y + Math.floor((m + 9) / 12)) / 4) - + Math.floor(275 * m / 9) + d - 730531.5 + h / 24; - return rv; + var h = hour + mins / 60; + var rv = 367 * y + - Math.floor(7 * (y + Math.floor((m + 9) / 12)) / 4) + + Math.floor(275 * m / 9) + d - 730531.5 + h / 24; + return rv; } // compute RA, DEC, and distance of planet-p for day number-d // result returned in structure obj in degrees and astronomical units function get_coord(obj, p, d) { - var planet = new elem(); - mean_elements(planet, p, d); - var ap = planet.a; - var ep = planet.e; - var ip = planet.i; - var op = planet.O; - var pp = planet.w; - var lp = planet.L; + var planet = new elem(); + mean_elements(planet, p, d); + var ap = planet.a; + var ep = planet.e; + var ip = planet.i; + var op = planet.O; + var pp = planet.w; + var lp = planet.L; - var earth = new elem(); - mean_elements(earth, 2, d); - var ae = earth.a; - var ee = earth.e; - var ie = earth.i; - var oe = earth.O; - var pe = earth.w; - var le = earth.L; + var earth = new elem(); + mean_elements(earth, 2, d); + var ae = earth.a; + var ee = earth.e; + var ie = earth.i; + var oe = earth.O; + var pe = earth.w; + var le = earth.L; - // position of Earth in its orbit - var me = mod2pi(le - pe); - var ve = true_anomaly(me, ee); - var re = ae * (1 - ee * ee) / (1 + ee * Math.cos(ve)); + // position of Earth in its orbit + var me = mod2pi(le - pe); + var ve = true_anomaly(me, ee); + var re = ae * (1 - ee * ee) / (1 + ee * Math.cos(ve)); - // heliocentric rectangular coordinates of Earth - var xe = re * Math.cos(ve + pe); - var ye = re * Math.sin(ve + pe); - var ze = 0.0; + // heliocentric rectangular coordinates of Earth + var xe = re * Math.cos(ve + pe); + var ye = re * Math.sin(ve + pe); + var ze = 0.0; - // position of planet in its orbit - var mp = mod2pi(lp - pp); - var vp = true_anomaly(mp, planet.e); - var rp = ap * (1 - ep * ep) / (1 + ep * Math.cos(vp)); + // position of planet in its orbit + var mp = mod2pi(lp - pp); + var vp = true_anomaly(mp, planet.e); + var rp = ap * (1 - ep * ep) / (1 + ep * Math.cos(vp)); - // heliocentric rectangular coordinates of planet - var xh = rp * (Math.cos(op) * Math.cos(vp + pp - op) - Math.sin(op) * Math.sin(vp + pp - op) * Math.cos(ip)); - var yh = rp * (Math.sin(op) * Math.cos(vp + pp - op) + Math.cos(op) * Math.sin(vp + pp - op) * Math.cos(ip)); - var zh = rp * (Math.sin(vp + pp - op) * Math.sin(ip)); + // heliocentric rectangular coordinates of planet + var xh = rp * (Math.cos(op) * Math.cos(vp + pp - op) - Math.sin(op) * Math.sin(vp + pp - op) * Math.cos(ip)); + var yh = rp * (Math.sin(op) * Math.cos(vp + pp - op) + Math.cos(op) * Math.sin(vp + pp - op) * Math.cos(ip)); + var zh = rp * (Math.sin(vp + pp - op) * Math.sin(ip)); - if (p == 2) // earth --> compute sun - { - xh = 0; - yh = 0; - zh = 0; - } + if (p == 2) // earth --> compute sun + { + xh = 0; + yh = 0; + zh = 0; + } - // convert to geocentric rectangular coordinates - var xg = xh - xe; - var yg = yh - ye; - var zg = zh - ze; + // convert to geocentric rectangular coordinates + var xg = xh - xe; + var yg = yh - ye; + var zg = zh - ze; - // rotate around x axis from ecliptic to equatorial coords - var ecl = 23.439281 * RADS; //value for J2000.0 frame - var xeq = xg; - var yeq = yg * Math.cos(ecl) - zg * Math.sin(ecl); - var zeq = yg * Math.sin(ecl) + zg * Math.cos(ecl); + // rotate around x axis from ecliptic to equatorial coords + var ecl = 23.439281 * RADS; //value for J2000.0 frame + var xeq = xg; + var yeq = yg * Math.cos(ecl) - zg * Math.sin(ecl); + var zeq = yg * Math.sin(ecl) + zg * Math.cos(ecl); - // find the RA and DEC from the rectangular equatorial coords - obj.ra = mod2pi(Math.atan2(yeq, xeq)) * DEGS; - obj.dec = Math.atan(zeq / Math.sqrt(xeq * xeq + yeq * yeq)) * DEGS; - obj.rvec = Math.sqrt(xeq * xeq + yeq * yeq + zeq * zeq); + // find the RA and DEC from the rectangular equatorial coords + obj.ra = mod2pi(Math.atan2(yeq, xeq)) * DEGS; + obj.dec = Math.atan(zeq / Math.sqrt(xeq * xeq + yeq * yeq)) * DEGS; + obj.rvec = Math.sqrt(xeq * xeq + yeq * yeq + zeq * zeq); } // Compute the elements of the orbit for planet-i at day number-d // result is returned in structure p function mean_elements(p, i, d) { - var cy = d / 36525; // centuries since J2000 + var cy = d / 36525; // centuries since J2000 - switch (i) { - case 0: // Mercury - p.a = 0.38709893 + 0.00000066 * cy; - p.e = 0.20563069 + 0.00002527 * cy; - p.i = (7.00487 - 23.51 * cy / 3600) * RADS; - p.O = (48.33167 - 446.30 * cy / 3600) * RADS; - p.w = (77.45645 + 573.57 * cy / 3600) * RADS; - p.L = mod2pi((252.25084 + 538101628.29 * cy / 3600) * RADS); - break; - case 1: // Venus - p.a = 0.72333199 + 0.00000092 * cy; - p.e = 0.00677323 - 0.00004938 * cy; - p.i = (3.39471 - 2.86 * cy / 3600) * RADS; - p.O = (76.68069 - 996.89 * cy / 3600) * RADS; - p.w = (131.53298 - 108.80 * cy / 3600) * RADS; - p.L = mod2pi((181.97973 + 210664136.06 * cy / 3600) * RADS); - break; - case 2: // Earth/Sun - p.a = 1.00000011 - 0.00000005 * cy; - p.e = 0.01671022 - 0.00003804 * cy; - p.i = (0.00005 - 46.94 * cy / 3600) * RADS; - p.O = (-11.26064 - 18228.25 * cy / 3600) * RADS; - p.w = (102.94719 + 1198.28 * cy / 3600) * RADS; - p.L = mod2pi((100.46435 + 129597740.63 * cy / 3600) * RADS); - break; - case 3: // Mars - p.a = 1.52366231 - 0.00007221 * cy; - p.e = 0.09341233 + 0.00011902 * cy; - p.i = (1.85061 - 25.47 * cy / 3600) * RADS; - p.O = (49.57854 - 1020.19 * cy / 3600) * RADS; - p.w = (336.04084 + 1560.78 * cy / 3600) * RADS; - p.L = mod2pi((355.45332 + 68905103.78 * cy / 3600) * RADS); - break; - case 4: // Jupiter - p.a = 5.20336301 + 0.00060737 * cy; - p.e = 0.04839266 - 0.00012880 * cy; - p.i = (1.30530 - 4.15 * cy / 3600) * RADS; - p.O = (100.55615 + 1217.17 * cy / 3600) * RADS; - p.w = (14.75385 + 839.93 * cy / 3600) * RADS; - p.L = mod2pi((34.40438 + 10925078.35 * cy / 3600) * RADS); - break; - case 5: // Saturn - p.a = 9.53707032 - 0.00301530 * cy; - p.e = 0.05415060 - 0.00036762 * cy; - p.i = (2.48446 + 6.11 * cy / 3600) * RADS; - p.O = (113.71504 - 1591.05 * cy / 3600) * RADS; - p.w = (92.43194 - 1948.89 * cy / 3600) * RADS; - p.L = mod2pi((49.94432 + 4401052.95 * cy / 3600) * RADS); - break; - case 6: // Uranus - p.a = 19.19126393 + 0.00152025 * cy; - p.e = 0.04716771 - 0.00019150 * cy; - p.i = (0.76986 - 2.09 * cy / 3600) * RADS; - p.O = (74.22988 - 1681.40 * cy / 3600) * RADS; - p.w = (170.96424 + 1312.56 * cy / 3600) * RADS; - p.L = mod2pi((313.23218 + 1542547.79 * cy / 3600) * RADS); - break; - case 7: // Neptune - p.a = 30.06896348 - 0.00125196 * cy; - p.e = 0.00858587 + 0.00002510 * cy; - p.i = (1.76917 - 3.64 * cy / 3600) * RADS; - p.O = (131.72169 - 151.25 * cy / 3600) * RADS; - p.w = (44.97135 - 844.43 * cy / 3600) * RADS; - p.L = mod2pi((304.88003 + 786449.21 * cy / 3600) * RADS); - break; - case 8: // Pluto - p.a = 39.48168677 - 0.00076912 * cy; - p.e = 0.24880766 + 0.00006465 * cy; - p.i = (17.14175 + 11.07 * cy / 3600) * RADS; - p.O = (110.30347 - 37.33 * cy / 3600) * RADS; - p.w = (224.06676 - 132.25 * cy / 3600) * RADS; - p.L = mod2pi((238.92881 + 522747.90 * cy / 3600) * RADS); - break; - default: - print("function mean_elements() failed!"); - } + switch (i) { + case 0: // Mercury + p.a = 0.38709893 + 0.00000066 * cy; + p.e = 0.20563069 + 0.00002527 * cy; + p.i = (7.00487 - 23.51 * cy / 3600) * RADS; + p.O = (48.33167 - 446.30 * cy / 3600) * RADS; + p.w = (77.45645 + 573.57 * cy / 3600) * RADS; + p.L = mod2pi((252.25084 + 538101628.29 * cy / 3600) * RADS); + break; + case 1: // Venus + p.a = 0.72333199 + 0.00000092 * cy; + p.e = 0.00677323 - 0.00004938 * cy; + p.i = (3.39471 - 2.86 * cy / 3600) * RADS; + p.O = (76.68069 - 996.89 * cy / 3600) * RADS; + p.w = (131.53298 - 108.80 * cy / 3600) * RADS; + p.L = mod2pi((181.97973 + 210664136.06 * cy / 3600) * RADS); + break; + case 2: // Earth/Sun + p.a = 1.00000011 - 0.00000005 * cy; + p.e = 0.01671022 - 0.00003804 * cy; + p.i = (0.00005 - 46.94 * cy / 3600) * RADS; + p.O = (-11.26064 - 18228.25 * cy / 3600) * RADS; + p.w = (102.94719 + 1198.28 * cy / 3600) * RADS; + p.L = mod2pi((100.46435 + 129597740.63 * cy / 3600) * RADS); + break; + case 3: // Mars + p.a = 1.52366231 - 0.00007221 * cy; + p.e = 0.09341233 + 0.00011902 * cy; + p.i = (1.85061 - 25.47 * cy / 3600) * RADS; + p.O = (49.57854 - 1020.19 * cy / 3600) * RADS; + p.w = (336.04084 + 1560.78 * cy / 3600) * RADS; + p.L = mod2pi((355.45332 + 68905103.78 * cy / 3600) * RADS); + break; + case 4: // Jupiter + p.a = 5.20336301 + 0.00060737 * cy; + p.e = 0.04839266 - 0.00012880 * cy; + p.i = (1.30530 - 4.15 * cy / 3600) * RADS; + p.O = (100.55615 + 1217.17 * cy / 3600) * RADS; + p.w = (14.75385 + 839.93 * cy / 3600) * RADS; + p.L = mod2pi((34.40438 + 10925078.35 * cy / 3600) * RADS); + break; + case 5: // Saturn + p.a = 9.53707032 - 0.00301530 * cy; + p.e = 0.05415060 - 0.00036762 * cy; + p.i = (2.48446 + 6.11 * cy / 3600) * RADS; + p.O = (113.71504 - 1591.05 * cy / 3600) * RADS; + p.w = (92.43194 - 1948.89 * cy / 3600) * RADS; + p.L = mod2pi((49.94432 + 4401052.95 * cy / 3600) * RADS); + break; + case 6: // Uranus + p.a = 19.19126393 + 0.00152025 * cy; + p.e = 0.04716771 - 0.00019150 * cy; + p.i = (0.76986 - 2.09 * cy / 3600) * RADS; + p.O = (74.22988 - 1681.40 * cy / 3600) * RADS; + p.w = (170.96424 + 1312.56 * cy / 3600) * RADS; + p.L = mod2pi((313.23218 + 1542547.79 * cy / 3600) * RADS); + break; + case 7: // Neptune + p.a = 30.06896348 - 0.00125196 * cy; + p.e = 0.00858587 + 0.00002510 * cy; + p.i = (1.76917 - 3.64 * cy / 3600) * RADS; + p.O = (131.72169 - 151.25 * cy / 3600) * RADS; + p.w = (44.97135 - 844.43 * cy / 3600) * RADS; + p.L = mod2pi((304.88003 + 786449.21 * cy / 3600) * RADS); + break; + case 8: // Pluto + p.a = 39.48168677 - 0.00076912 * cy; + p.e = 0.24880766 + 0.00006465 * cy; + p.i = (17.14175 + 11.07 * cy / 3600) * RADS; + p.O = (110.30347 - 37.33 * cy / 3600) * RADS; + p.w = (224.06676 - 132.25 * cy / 3600) * RADS; + p.L = mod2pi((238.92881 + 522747.90 * cy / 3600) * RADS); + break; + default: + print("function mean_elements() failed!"); + } } // compute the true anomaly from mean anomaly using iteration // M - mean anomaly in radians // e - orbit eccentricity function true_anomaly(M, e) { - var V, E1; + var V, E1; - // initial approximation of eccentric anomaly - var E = M + e * Math.sin(M) * (1.0 + e * Math.cos(M)); + // initial approximation of eccentric anomaly + var E = M + e * Math.sin(M) * (1.0 + e * Math.cos(M)); - do // iterate to improve accuracy - { - E1 = E; - E = E1 - (E1 - e * Math.sin(E1) - M) / (1 - e * Math.cos(E1)); - } - while (Math.abs(E - E1) > EPS); + do // iterate to improve accuracy + { + E1 = E; + E = E1 - (E1 - e * Math.sin(E1) - M) / (1 - e * Math.cos(E1)); + } + while (Math.abs(E - E1) > EPS); - // convert eccentric anomaly to true anomaly - V = 2 * Math.atan(Math.sqrt((1 + e) / (1 - e)) * Math.tan(0.5 * E)); + // convert eccentric anomaly to true anomaly + V = 2 * Math.atan(Math.sqrt((1 + e) / (1 - e)) * Math.tan(0.5 * E)); - if (V < 0) V = V + (2 * Math.PI); // modulo 2pi + if (V < 0) V = V + (2 * Math.PI); // modulo 2pi - return V; + return V; } // converts hour angle in degrees into hour angle string function ha2str(x) { - if ((x < 0) || (360 < x)) print("function ha2str() range error!"); + if ((x < 0) || (360 < x)) print("function ha2str() range error!"); - var ra = x / 15; // degrees to hours - var h = Math.floor(ra); - var m = 60 * (ra - h); - return cintstr(h, 3) + "h " + frealstr(m, 4, 1) + "m"; + var ra = x / 15; // degrees to hours + var h = Math.floor(ra); + var m = 60 * (ra - h); + return cintstr(h, 3) + "h " + frealstr(m, 4, 1) + "m"; } // converts declination angle in degrees into string function dec2str(x) { - if ((x < -90) || (+90 < x)) print("function dec2str() range error!"); + if ((x < -90) || (+90 < x)) print("function dec2str() range error!"); - var dec = Math.abs(x); - var sgn = (x < 0) ? "-" : " "; - var d = Math.floor(dec); - var m = 60 * (dec - d); - return sgn + cintstr(d, 2) + "° " + frealstr(m, 4, 1) + "'"; + var dec = Math.abs(x); + var sgn = (x < 0) ? "-" : " "; + var d = Math.floor(dec); + var m = 60 * (dec - d); + //return sgn + cintstr(d, 2) + "° " + frealstr(m, 4, 1) + "'"; + return sgn + cintstr(d, 2) + "° "; } // return the integer part of a number function abs_floor(x) { - var r; - if (x >= 0.0) r = Math.floor(x); - else r = Math.ceil(x); - return r; + var r; + if (x >= 0.0) r = Math.floor(x); + else r = Math.ceil(x); + return r; } // return an angle in the range 0 to 2pi radians function mod2pi(x) { - var b = x / (2 * Math.PI); - var a = (2 * Math.PI) * (b - abs_floor(b)); - if (a < 0) a = (2 * Math.PI) + a; - return a; + var b = x / (2 * Math.PI); + var a = (2 * Math.PI) * (b - abs_floor(b)); + if (a < 0) a = (2 * Math.PI) + a; + return a; } // @@ -470,36 +483,36 @@ function mod2pi(x) { // results returned in h : horizon record structure // function coord_to_horizon(utc, ra, dec, lat, lon, h) { - var lmst, ha, sin_alt, cos_az, alt, az; + var lmst, ha, sin_alt, cos_az, alt, az; - // compute hour angle in degrees - ha = mean_sidereal_time(0) - ra; - //ha = mean_sidereal_time(lon) - ra; - if (ha < 0) ha = ha + 360; + // compute hour angle in degrees + ha = mean_sidereal_time(0) - ra; + //ha = mean_sidereal_time(lon) - ra; + if (ha < 0) ha = ha + 360; - // convert degrees to radians - ha = ha * RADS; - dec = dec * RADS; - lat = lat * RADS; + // convert degrees to radians + ha = ha * RADS; + dec = dec * RADS; + lat = lat * RADS; - // compute altitude in radians - sin_alt = Math.sin(dec) * Math.sin(lat) + Math.cos(dec) * Math.cos(lat) * Math.cos(ha); - alt = Math.asin(sin_alt); + // compute altitude in radians + sin_alt = Math.sin(dec) * Math.sin(lat) + Math.cos(dec) * Math.cos(lat) * Math.cos(ha); + alt = Math.asin(sin_alt); - // compute azimuth in radians - // divide by zero error at poles or if alt = 90 deg - cos_az = (Math.sin(dec) - Math.sin(alt) * Math.sin(lat)) / (Math.cos(alt) * Math.cos(lat)); - //az = Math.acos(cos_az); + // compute azimuth in radians + // divide by zero error at poles or if alt = 90 deg + cos_az = (Math.sin(dec) - Math.sin(alt) * Math.sin(lat)) / (Math.cos(alt) * Math.cos(lat)); + //az = Math.acos(cos_az); - az = acos_estimate(cos_az); + az = acos_estimate(cos_az); - // convert radians to degrees - h.alt = alt * DEGS; - h.az = az * DEGS; + // convert radians to degrees + h.alt = alt * DEGS; + h.az = az * DEGS; - // choose hemisphere - if (Math.sin(ha) > 0) h.az = 360 - h.az; + // choose hemisphere + if (Math.sin(ha) > 0) h.az = 360 - h.az; } // @@ -511,262 +524,334 @@ function coord_to_horizon(utc, ra, dec, lat, lon, h) { // function mean_sidereal_time(lon) { - if ((month == 1) || (month == 2)) { - year = year - 1; - month = month + 12; - } + if ((month == 1) || (month == 2)) { + year = year - 1; + month = month + 12; + } - var a = Math.floor(year / 100); - // var a = Math.floor(2019 / 100); + var a = Math.floor(year / 100); + // var a = Math.floor(2019 / 100); - var b = 2 - a + Math.floor(a / 4); - var c = Math.floor(365.25 * year); - var da = Math.floor(30.6001 * (month + 1)); + var b = 2 - a + Math.floor(a / 4); + var c = Math.floor(365.25 * year); + var da = Math.floor(30.6001 * (month + 1)); - // days since J2000.0 - var jd = b + c + da - 730550.5 + day - + (hour + mins / 60.0 + secs / 3600.0) / 24.0; + // days since J2000.0 + var jd = b + c + da - 730550.5 + day + + (hour + mins / 60.0 + secs / 3600.0) / 24.0; - // julian centuries since J2000.0 - var jt = jd / 36525.0; + // julian centuries since J2000.0 + var jt = jd / 36525.0; - // mean sidereal time - var mst = 280.46061837 + 360.98564736629 * jd - + 0.000387933 * jt * jt - jt * jt * jt / 38710000 + lon; + // mean sidereal time + var mst = 280.46061837 + 360.98564736629 * jd + + 0.000387933 * jt * jt - jt * jt * jt / 38710000 + lon; - if (mst > 0.0) { - while (mst > 360.0) - mst = mst - 360.0; - } - else { - while (mst < 0.0) - mst = mst + 360.0; - } - return mst; + mst = mst % 360; + return mst; } // convert angle (deg, min, sec) to degrees as real function dms2real(deg, min, sec) { - var rv; - if (deg < 0) rv = deg - min / 60 - sec / 3600; - else rv = deg + min / 60 + sec / 3600; - return rv; + var rv; + if (deg < 0) rv = deg - min / 60 - sec / 3600; + else rv = deg + min / 60 + sec / 3600; + return rv; } // converts angle in degrees into string function degr2str(x) { - var dec = Math.abs(x); - var sgn = (x < 0) ? "-" : " "; - var d = Math.floor(dec); - var m = 60 * (dec - d); - return sgn + cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'"; + var dec = Math.abs(x); + var sgn = (x < 0) ? "-" : " "; + var d = Math.floor(dec); + var m = 60 * (dec - d); + //return sgn + cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'"; + return sgn + cintstr(d, 3) + "° "; } // converts latitude in signed degrees into string function lat2str(x) { - var dec = Math.abs(x); - var sgn = (x < 0) ? " S" : " N"; - var d = Math.floor(dec); - var m = 60 * (dec - d); - return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn; + var dec = Math.abs(x); + var sgn = (x < 0) ? " S" : " N"; + var d = Math.floor(dec); + var m = 60 * (dec - d); + //return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn; + return cintstr(d, 3) + "° "; } // converts longitude in signed degrees into string function lon2str(x) { - var dec = Math.abs(x); - var sgn = (x < 0) ? " W" : " E"; - var d = Math.floor(dec); - var m = 60 * (dec - d); - return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn; + var dec = Math.abs(x); + var sgn = (x < 0) ? " W" : " E"; + var d = Math.floor(dec); + var m = 60 * (dec - d); + //return cintstr(d, 3) + "° " + frealstr(m, 4, 1) + "'" + sgn; + return cintstr(d, 3) + "° "; } // format two digits with leading zero if needed function d2(n) { - if ((n < 0) || (99 < n)) return "xx"; - return (n < 10) ? ("0" + n) : n; + if ((n < 0) || (99 < n)) return "xx"; + return (n < 10) ? ("0" + n) : n; } // UTILITY FUNCTIONS // format an integer function cintstr(num, width) { - var str = num.toString(10); - var len = str.length; - var intgr = ""; - var i; + var str = num.toString(10); + var len = str.length; + var intgr = ""; + var i; - for (i = 0; i < width - len; i++) // append leading spaces - intgr += ' '; + for (i = 0; i < width - len; i++) // append leading spaces + intgr += ' '; - for (i = 0; i < len; i++) // append digits - intgr += str.charAt(i); + for (i = 0; i < len; i++) // append digits + intgr += str.charAt(i); - return intgr; + return intgr; } function frealstr(num, width, fract) { - var str = num.toFixed(fract); - var len = str.length; - var real = ""; - var i; + var str = num.toFixed(fract); + var len = str.length; + var real = ""; + var i; - for (i = 0; i < width - len; i++) // append leading spaces - real += ' '; + for (i = 0; i < width - len; i++) // append leading spaces + real += ' '; - for (i = 0; i < len; i++) // append digits - real += str.charAt(i); + for (i = 0; i < len; i++) // append digits + real += str.charAt(i); - return real; + return real; } function getMoonPhase() { - var now = new Date(); - year = now.getFullYear(); - month = now.getMonth() + 1; - day = now.getDate(); + var now = new Date(); + year = now.getFullYear(); + month = now.getMonth() + 1; + day = now.getDate(); - if (month < 3) { - year = year - 1; - month += 12; - } - month = month + 1; - c = 365.25 * year; - e = 30.6 * month; - jd = c + e + day - 694039.09; //jd is total days elapsed - jd /= 29.5305882; //divide by the moon cycle - b = parseInt(jd); //int(jd) -> b, take integer part of jd - jd -= b; //subtract integer part to leave fractional part of original jd - b = Math.round(jd * 8); //scale fraction from 0-8 and round - if (b >= 8) { - b = 0; //0 and 8 are the same so turn 8 into 0 - } - return b; + if (month < 3) { + year = year - 1; + month += 12; + } + month = month + 1; + c = 365.25 * year; + e = 30.6 * month; + jd = c + e + day - 694039.09; //jd is total days elapsed + jd /= 29.5305882; //divide by the moon cycle + b = parseInt(jd); //int(jd) -> b, take integer part of jd + jd -= b; //subtract integer part to leave fractional part of original jd + b = Math.round(jd * 8); //scale fraction from 0-8 and round + if (b >= 8) { + b = 0; //0 and 8 are the same so turn 8 into 0 + } + return b; } function write_refresh_note(colour) { - g.setColor(colour); - cursor = Yaxis + 50; - if (!ready_to_compute) { - g.drawString("mode change:", Xaxis + 50, cursor, false); - cursor += 15; - g.drawString("BTN1 to refresh", Xaxis + 50, cursor, true /*clear background*/); - cursor += 15; - g.drawString("BTN3 to cancel", Xaxis + 50, cursor, true /*clear background*/); - } - else - g.drawString("updating, please wait", Xaxis + 50, cursor, false); + g.setColor(colour); + cursor = Yaxis + 50; + if (!ready_to_compute) { + g.drawString("mode change:", Xaxis + 50, cursor, false); + cursor += 15; + g.drawString("swipe up to refresh", Xaxis + 50, cursor, true /*clear background*/); + cursor += 15; + g.drawString("swipe left/right to cancel", Xaxis + 50, cursor, true /*clear background*/); + } + else + g.drawString("updating, please wait", Xaxis + 50, cursor, false); } function draw_moon(phase) { - g.setColor(display_colour); + g.setColor(display_colour); + var moonOffset = 12; + var mooonRadius = 25; + if (screenSize > 176) { if (phase == 5) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillRect(220, 25, 240, 90); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillRect(220, 25, 240, 90); } else if (phase == 6) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillRect(200, 25, 240, 90); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillRect(200, 25, 240, 90); } else if (phase == 1) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillCircle(180, Yaxis, 30); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillCircle(180, Yaxis + moonOffset, mooonRadius); } else if (phase == 4) - g.fillCircle(200, Yaxis, 30); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); else if (phase == 3) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillRect(160, 25, 180, 90); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillRect(160, 25, 180, 90); } else if (phase == 2) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillRect(160, 25, 200, 90); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillRect(160, 25, 200, 90); } else if (phase == 7) { - g.fillCircle(200, Yaxis, 30); - g.setColor("#000000"); - g.fillCircle(220, Yaxis, 30); + g.fillCircle(200, Yaxis + moonOffset, mooonRadius); + g.setColor("#000000"); + g.fillCircle(220, Yaxis, 25); } + } + else { + moonOffset = 12; + //var moonOffsetX = 150; + mooonRadius = 17; g.setColor(display_colour); + if (phase != 0) + g.fillCircle(150, Yaxis + moonOffset, mooonRadius); + if (phase == 5) { + g.setColor("#000000"); + g.fillRect(165, 25, 180, 90); + } + else if (phase == 6) { + g.setColor("#000000"); + g.fillRect(150, 25, 240, 90); + } + else if (phase == 1) { + g.setColor("#000000"); + g.fillCircle(140, Yaxis + moonOffset, mooonRadius); + } + else if (phase == 4) + g.fillCircle(150, Yaxis + moonOffset, mooonRadius); + else if (phase == 3) { + g.setColor("#000000"); + g.fillRect(125, 25, 135, 90); + } + else if (phase == 2) { + g.setColor("#000000"); + g.fillRect(125, 25, 150, 90); + } + else if (phase == 7) { + g.setColor("#000000"); + g.fillCircle(160, Yaxis + moonOffset, mooonRadius); + } + } + g.setColor(display_colour); +} + +function autoUpdate() { + ready_to_compute = true; + g.setColor(display_colour); + g.fillCircle(15, 160, 5); + setTimeout(function () { + print("ready"); + draw(); + secondInterval = setInterval(draw, 1000); + }, 100); } function draw() { - if (astral_settings.astral_default) - display_colour = default_colour; - else if (!colours_switched) - display_colour = setupcomplete_colour; + //print("drawing"); + if (astral_settings.astral_default) + display_colour = default_colour; + else if (!colours_switched) + display_colour = setupcomplete_colour; - // work out how to display the current time - var d = new Date(); - var h = d.getHours(), m = d.getMinutes(); - var time = (" " + h).substr(-2) + ":" + ("0" + m).substr(-2); - // Reset the state of the graphics library - g.reset(); + // work out how to display the current time + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + var time = (" " + h).substr(-2) + ":" + ("0" + m).substr(-2); + // Reset the state of the graphics library + g.reset(); + g.setColor(display_colour); + // draw the current time (4x size 7 segment) + g.setFont("7x11Numeric7Seg", 4); + g.setFontAlign(1, 1); // align right bottom + g.drawString(time, Xaxis + 23, Yaxis + 34, true /*clear background*/); + + g.setFont("6x8"); + g.setFontAlign(1, 1); // align center bottom + // pad the date - this clears the background if the date were to change length + var dateStr = " " + require("locale").date(d) + " "; + + if (screenSize < 177) { + g.drawString(dateStr, 100, 20, true /*clear background*/); + } + else { + //bangle 1 + g.drawString(dateStr, 150, 40, true /*clear background*/); + } + + //compute location of objects + g.setFontAlign(1, 1); + g.setFont("6x8", 1); + + if (ready_to_compute) + g.setColor(calc_display_colour); + + if (modeswitch) + g.setColor("#000000"); + + cursor = Yaxis + 50; + if (pstrings.length != 0) { + + for (let i = 0; i < pstrings.length; i++) { + g.drawString(pstrings[i], Xaxis + 40, cursor, true /*clear background*/); + cursor += 10; + } + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + } + if (ready_to_compute) { + processing = true; + ready_to_compute = false; + test(); + compute(); + g.setColor("#000000"); + g.fillRect(Xaxis - 150, Yaxis + 40, Xaxis + 200, Yaxis + 200); + modeswitch = false; + processing = false; + //Bangle.buzz(); + } + + current_moonphase = getMoonPhase(); + all_extras_array = []; + if (sensorsOn) { g.setColor(display_colour); - // draw the current time (4x size 7 segment) - g.setFont("7x11Numeric7Seg", 5); - g.setFontAlign(1, 1); // align right bottom - g.drawString(time, Xaxis + 20, Yaxis + 30, true /*clear background*/); + g.fillCircle(160, 160, 5); + } +} - g.setFont("6x8"); - g.setFontAlign(1, 1); // align center bottom - // pad the date - this clears the background if the date were to change length - var dateStr = " " + require("locale").date(d) + " "; - g.drawString(dateStr, Xaxis - 40, Yaxis - 40, true /*clear background*/); - - //compute location of objects - g.setFontAlign(1, 1); - g.setFont("6x8"); - - if (ready_to_compute) - g.setColor(calc_display_colour); - - if (modeswitch) - g.setColor("#000000"); - - cursor = Yaxis + 50; - if (pstrings.length == 0) { - if (ready_to_compute) - g.drawString("updating, please wait", Xaxis + 50, cursor, true); - else - g.drawString("press BTN1 to update", Xaxis + 50, cursor, true /*clear background*/); - } - else { - for (let i = 0; i < pstrings.length; i++) { - g.drawString(pstrings[i], Xaxis + 50, cursor, true /*clear background*/); - cursor += 15; - } - } - - if (modeswitch) - if (ready_to_compute) - write_refresh_note(calc_display_colour); - else - write_refresh_note(display_colour); - - if (ready_to_compute) { - processing = true; - ready_to_compute = false; - test(); - compute(); - g.setColor("#000000"); - g.fillRect(Xaxis - 150, Yaxis + 40, Xaxis + 200, Yaxis + 200); - modeswitch = false; - processing = false; - Bangle.buzz(); - } - - current_moonphase = getMoonPhase(); - all_extras_array = []; +function SwitchSensorState() { + if (sensorsOn) { + print("turning sensors off"); + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); + sensorsOn = 0; + g.setColor("#000000"); + g.fillCircle(screenSize - 15, screenSize - 15, 7); + //g.drawString(compass_heading, 30, 115, true /*clear background*/); + g.fillRect(0, 100, 30, 120); + } + else { + Bangle.setCompassPower(1); + Bangle.setGPSPower(1); + sensorsOn = 1; + g.setColor(display_colour); + g.fillCircle(screenSize - 15, screenSize - 15, 5); + } } g.clear(); +g.setColor("#000000"); +g.setBgColor(0, 0, 0); +g.fillRect(0, 0, 175, 175); current_moonphase = getMoonPhase(); +Bangle.setUI("clock"); + // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -774,80 +859,74 @@ Bangle.drawWidgets(); draw_moon(current_moonphase); draw(); -var secondInterval = setInterval(draw, 1000); -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower', on => { - if (secondInterval) clearInterval(secondInterval); - secondInterval = undefined; - Bangle.setCompassPower(0); - if (!astral_settings.astral_default) - Bangle.setGPSPower(0); - if (on) { - Bangle.setCompassPower(1); - Bangle.setGPSPower(1); - if (current_moonphase !== undefined) { - draw_moon(current_moonphase); - } - secondInterval = setInterval(draw, 1000); - draw(); // draw immediately - } -}); +var updateInterval = setInterval(autoUpdate, 120000); +//var magInterval = setInterval(updateMag, 50); Bangle.setCompassPower(1); Bangle.setGPSPower(1); -// Show launcher when button pressed -Bangle.setClockMode(); +var secondInterval; -Bangle.setUI("clockupdown", btn => { - if (btn==0) { - if (!processing) { - if (!modeswitch) { - modeswitch = true; - if (mode == "planetary") mode = "extras"; - else mode = "planetary"; - } - else - modeswitch = false; +autoUpdate(); + +setWatch(SwitchSensorState, BTN1, { repeat: true }); +if(process.env.HWVERSION != 2) + setWatch(autoUpdate, BTN3, { repeat: true }); + +// Show launcher when button pressed +//Bangle.setClockMode(); + +Bangle.on("swipe", function (directionLR, directionUD) { + if (!processing) { + if (-1 == directionUD) { + g.setColor(display_colour); + g.fillCircle(15, 160, 5); + ready_to_compute = true; } - } else { - if (!processing) - ready_to_compute = true; + else if (-1 == directionLR || directionLR == 1) { + print("attempting mode switch"); + if (mode == "planetary") mode = "extras"; + else mode = "planetary"; + g.setColor(display_colour); + g.fillCircle(15, 160, 5); + ready_to_compute = true; + } + else if (directionUD == 1) { + SwitchSensorState(); + } + + setTimeout(function () { + print("ready"); + draw(); + secondInterval = setInterval(draw, 1000); + }, 100); } }); - -setWatch(function () { - if (!astral_settings.astral_default) { - colours_switched = true; - if (display_colour == setupcomplete_colour) - display_colour = default_colour; - else - display_colour = setupcomplete_colour; - draw_moon(current_moonphase); - } -}, BTN4, { repeat: true }); - //events Bangle.on('mag', function (m) { - g.setFont("6x8",2); - if (isNaN(m.heading)) - compass_heading = "---"; - else - compass_heading = 360 - Math.round(m.heading); - // g.setColor("#000000"); - // g.fillRect(160, 10, 160, 20); + if (!isNaN(m.heading)) { + compass_heading = Math.round(m.heading); g.setColor(display_colour); - if(compass_heading<100) + if (compass_heading < 100) compass_heading = " " + compass_heading; - g.drawString(compass_heading, 150, 20, true /*clear background*/); + if (sensorsOn) { + g.setFont("8x12"); + g.setFontAlign(1, 1); + g.drawString(compass_heading, 25, 112, true /*clear background*/); + } + } + //var n = magArray.length; + //var mean = Math.round( magArray.reduce((a, b) => a + b) / n); + //compass_heading = mean; }); Bangle.on('GPS', function (g) { - if (g.fix) { - astral_settings.lat = g.lat; - astral_settings.lon = g.lon; - astral_settings.astral_default = false; - config_file = require("Storage").open("astral.config.txt", "w"); - config_file.write(JSON.stringify(astral_settings)); - } + if (g.fix) { + display_colour = setupcomplete_colour; + astral_settings.lat = g.lat; + astral_settings.lon = g.lon; + astral_settings.astral_default = false; + config_file = require("Storage").open("astral.config.txt", "w"); + config_file.write(JSON.stringify(astral_settings)); + } }); diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json index 3317092db..16696056f 100644 --- a/apps/astral/metadata.json +++ b/apps/astral/metadata.json @@ -1,13 +1,13 @@ { "id": "astral", "name": "Astral Clock", - "version": "0.03", + "version": "0.08", "description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.", "icon": "app-icon.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], "readme": "README.md", + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"astral.app.js","url":"app.js"}, {"name":"astral.img","url":"app-icon.js","evaluate":true} diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 60ef5da0a..df039ba0e 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -1,2 +1,9 @@ 0.01: Create astrocalc app 0.02: Store last GPS lock, can be used instead of waiting for new GPS on start +0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps +0.04: Compatibility with Bangle.js 2, get location from My Location +0.05: Enable widgets +0.06: Fix azimuth (bug #2651), only show degrees +0.07: Minor code improvements +0.08: Minor code improvements +0.09: Fix: Handle when the moon rise/set do not occur on the current day diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 4e7aa0b40..283c5aab7 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -9,10 +9,7 @@ * Calculate the Sun and Moon positions based on watch GPS and display graphically */ -const SunCalc = require("suncalc.js"); -const storage = require("Storage"); -const LAST_GPS_FILE = "astrocalc.gps.json"; -let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null); +const SunCalc = require("suncalc"); // from modules folder function drawMoon(phase, x, y) { const moonImgFiles = [ @@ -26,7 +23,7 @@ function drawMoon(phase, x, y) { "waning-crescent", ]; - img = require("Storage").read(`${moonImgFiles[phase]}.img`); + const img = require("Storage").read(`${moonImgFiles[phase]}.img`); // image width & height = 92px g.drawImage(img, x - parseInt(92 / 2), y); } @@ -54,7 +51,7 @@ function drawTitle(key) { const x = 0; const x2 = g.getWidth() - 1; const y = fontHeight + 26; - const y2 = g.getHeight() - 1; + //const y2 = g.getHeight() - 1; const title = titlizeKey(key); g.setFont("6x8", 2); @@ -73,7 +70,7 @@ function drawTitle(key) { */ function drawPoint(angle, radius, color) { const pRad = Math.PI / 180; - const faceWidth = 80; // watch face radius + const faceWidth = g.getWidth()/3; // watch face radius const centerPx = g.getWidth() / 2; const a = angle * pRad; @@ -111,7 +108,7 @@ function drawPoints() { } function drawData(title, obj, startX, startY) { - g.clear(); + g.clearRect(Bangle.appRect); drawTitle(title); let xPos, yPos; @@ -141,68 +138,68 @@ function drawData(title, obj, startX, startY) { function drawMoonPositionPage(gps, title) { const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); + const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north const pageData = { - Azimuth: pos.azimuth.toFixed(2), - Altitude: pos.altitude.toFixed(2), + Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°', + Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°', Distance: `${pos.distance.toFixed(0)} km`, - "Parallactic Ang": pos.parallacticAngle.toFixed(2), + "Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°', }; - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5); - drawData(title, pageData, null, 80); + drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20); drawPoints(); - drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1}); + drawPoint(azimuthDegrees, 8, moonColor); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BTN3, {repeat: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } function drawMoonIlluminationPage(gps, title) { const phaseNames = [ - "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", - "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent", + /*LANG*/"New Moon", /*LANG*/"Waxing Crescent", /*LANG*/"First Quarter", /*LANG*/"Waxing Gibbous", + /*LANG*/"Full Moon", /*LANG*/"Waning Gibbous", /*LANG*/"Last Quater", /*LANG*/"Waning Crescent", ]; const phase = SunCalc.getMoonIllumination(new Date()); + const phaseIdx = Math.round(phase.phase*8); const pageData = { - Phase: phaseNames[phase.phase], + Phase: phaseNames[phaseIdx], }; drawData(title, pageData, null, 35); - drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2); + drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BTN3, {repease: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } function drawMoonTimesPage(gps, title) { const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon); + const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; const pageData = { - Rise: dateToTimeString(times.rise), - Set: dateToTimeString(times.set), + Rise: times.rise ? dateToTimeString(times.rise) : "Not today", + Set: times.set ? dateToTimeString(times.set) : "Not today", }; - drawData(title, pageData, null, 105); + drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); drawPoints(); // Draw the moon rise position const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); - const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); - drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI); + drawPoint(riseAzimuthDegrees, 8, moonColor); // Draw the moon set position const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); - const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); - drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north + const setAzimuthDegrees = parseInt(setAzimuth * 180 / Math.PI); + drawPoint(setAzimuthDegrees, 8, moonColor); - let m = setWatch(() => { - let m = moonIndexPageMenu(gps); - }, BTN3, {repease: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)}); } function drawSunShowPage(gps, key, date) { @@ -212,28 +209,25 @@ function drawSunShowPage(gps, key, date) { const mins = ("0" + date.getMinutes()).substr(-2); const secs = ("0" + date.getMinutes()).substr(-2); const time = `${hrs}:${mins}:${secs}`; + const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north - const azimuth = Number(pos.azimuth.toFixed(2)); - const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); - const altitude = Number(pos.altitude.toFixed(2)); + const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°'; + const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°'; const pageData = { Time: time, Altitude: altitude, - Azimumth: azimuth, - Degrees: azimuthDegrees + Azimuth: azimuthDegrees, }; - drawData(key, pageData, null, 85); + drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); drawPoints(); // Draw the suns position drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0}); - m = setWatch(() => { - m = sunIndexPageMenu(gps); - }, BTN3, {repeat: false, edge: "falling"}); + Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)}); return null; } @@ -246,7 +240,7 @@ function sunIndexPageMenu(gps) { "title": "-- Sun --", }, "Current Pos": () => { - m = E.showMenu(); + E.showMenu(); drawSunShowPage(gps, "Current Pos", new Date()); }, }; @@ -254,13 +248,13 @@ function sunIndexPageMenu(gps) { Object.keys(sunTimes).sort().reduce((menu, key) => { const title = titlizeKey(key); menu[title] = () => { - m = E.showMenu(); + E.showMenu(); drawSunShowPage(gps, key, sunTimes[key]); }; return menu; }, sunMenu); - sunMenu["< Back"] = () => m = indexPageMenu(gps); + sunMenu["< Back"] = () => indexPageMenu(gps); return E.showMenu(sunMenu); } @@ -272,18 +266,18 @@ function moonIndexPageMenu(gps) { "title": "-- Moon --", }, "Times": () => { - m = E.showMenu(); - drawMoonTimesPage(gps, "Times"); + E.showMenu(); + drawMoonTimesPage(gps, /*LANG*/"Times"); }, "Position": () => { - m = E.showMenu(); - drawMoonPositionPage(gps, "Position"); + E.showMenu(); + drawMoonPositionPage(gps, /*LANG*/"Position"); }, "Illumination": () => { - m = E.showMenu(); - drawMoonIlluminationPage(gps, "Illumination"); + E.showMenu(); + drawMoonIlluminationPage(gps, /*LANG*/"Illumination"); }, - "< Back": () => m = indexPageMenu(gps), + "< Back": () => indexPageMenu(gps), }; return E.showMenu(moonMenu); @@ -292,97 +286,29 @@ function moonIndexPageMenu(gps) { function indexPageMenu(gps) { const menu = { "": { - "title": "Select", + "title": /*LANG*/"Select", }, - "Sun": () => { - m = sunIndexPageMenu(gps); + /*LANG*/"Sun": () => { + sunIndexPageMenu(gps); }, - "Moon": () => { - m = moonIndexPageMenu(gps); + /*LANG*/"Moon": () => { + moonIndexPageMenu(gps); }, - "< Exit": () => { load(); } + "< Back": () => { load(); } }; return E.showMenu(menu); } -function getCenterStringX(str) { - return (g.getWidth() - g.stringWidth(str)) / 2; -} - -/** - * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page - */ -function drawGPSWaitPage() { - const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")); - const str1 = "Astrocalc v0.02"; - const str2 = "Locating GPS"; - const str3 = "Please wait..."; - - g.clear(); - g.drawImage(img, 100, 50); - g.setFont("6x8", 1); - g.drawString(str1, getCenterStringX(str1), 105); - g.drawString(str2, getCenterStringX(str2), 140); - g.drawString(str3, getCenterStringX(str3), 155); - - if (lastGPS) { - lastGPS = JSON.parse(lastGPS); - lastGPS.time = new Date(); - - const str4 = "Press Button 3 to use last GPS"; - g.setColor("#d32e29"); - g.fillRect(0, 190, g.getWidth(), 215); - g.setColor("#ffffff"); - g.drawString(str4, getCenterStringX(str4), 200); - - setWatch(() => { - clearWatch(); - Bangle.setGPSPower(0); - m = indexPageMenu(lastGPS); - }, BTN3, {repeat: false}); - } - - g.flip(); - - const DEBUG = false; - if (DEBUG) { - clearWatch(); - - const gps = { - "lat": 56.45783133333, - "lon": -3.02188583333, - "alt": 75.3, - "speed": 0.070376, - "course": NaN, - "time":new Date(), - "satellites": 4, - "fix": 1 - }; - - m = indexPageMenu(gps); - - return; - } - - Bangle.on('GPS', (gps) => { - if (gps.fix === 0) return; - clearWatch(); - - if (isNaN(gps.course)) gps.course = 0; - require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps)); - Bangle.setGPSPower(0); - Bangle.buzz(); - Bangle.setLCDPower(true); - - m = indexPageMenu(gps); - }); -} +//function getCenterStringX(str) { +// return (g.getWidth() - g.stringWidth(str)) / 2; +//} function init() { - Bangle.setGPSPower(1); - drawGPSWaitPage(); + let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + Bangle.loadWidgets(); + indexPageMenu(location); + Bangle.drawWidgets(); } -let m; -init(); \ No newline at end of file +init(); diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json index 384c7fa1e..d88037d09 100644 --- a/apps/astrocalc/metadata.json +++ b/apps/astrocalc/metadata.json @@ -1,15 +1,15 @@ { "id": "astrocalc", "name": "Astrocalc", - "version": "0.02", - "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", + "version": "0.09", + "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "icon": "astrocalc.png", "tags": "app,sun,moon,cycles,tool,outdoors", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, + "dependencies": {"mylocation":"app"}, "storage": [ {"name":"astrocalc.app.js","url":"astrocalc-app.js"}, - {"name":"suncalc.js","url":"suncalc.js"}, {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, {"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true}, {"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true}, diff --git a/apps/astrocalc/suncalc.js b/apps/astrocalc/suncalc.js deleted file mode 100644 index e2beaedca..000000000 --- a/apps/astrocalc/suncalc.js +++ /dev/null @@ -1,328 +0,0 @@ -/* - (c) 2011-2015, Vladimir Agafonkin - SunCalc is a JavaScript library for calculating sun/moon position and light phases. - https://github.com/mourner/suncalc -*/ - -(function () { 'use strict'; - - // shortcuts for easier to read formulas - - var PI = Math.PI, - sin = Math.sin, - cos = Math.cos, - tan = Math.tan, - asin = Math.asin, - atan = Math.atan2, - acos = Math.acos, - rad = PI / 180; - - // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas - - - // date/time constants and conversions - - var dayMs = 1000 * 60 * 60 * 24, - J1970 = 2440588, - J2000 = 2451545; - - function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } - function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; } - function toDays(date) { return toJulian(date) - J2000; } - - - // general calculations for position - - var e = rad * 23.4397; // obliquity of the Earth - - function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } - function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } - - function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } - function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } - - function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } - - function astroRefraction(h) { - if (h < 0) // the following formula works for positive altitudes only. - h = 0; // if h = -0.08901179 a div/0 would occur. - - // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: - return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); - } - - // general sun calculations - - function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } - - function eclipticLongitude(M) { - - var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center - P = rad * 102.9372; // perihelion of the Earth - - return M + C + P + PI; - } - - function sunCoords(d) { - - var M = solarMeanAnomaly(d), - L = eclipticLongitude(M); - - return { - dec: declination(L, 0), - ra: rightAscension(L, 0) - }; - } - - - var SunCalc = {}; - - - // calculates sun position for a given date and latitude/longitude - - SunCalc.getPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = sunCoords(d), - H = siderealTime(d, lw) - c.ra; - - return { - azimuth: azimuth(H, phi, c.dec), - altitude: altitude(H, phi, c.dec) - }; - }; - - - // sun times configuration (angle, morning name, evening name) - - var times = SunCalc.times = [ - [-0.833, 'sunrise', 'sunset' ], - [ -0.3, 'sunriseEnd', 'sunsetStart' ], - [ -6, 'dawn', 'dusk' ], - [ -12, 'nauticalDawn', 'nauticalDusk'], - [ -18, 'nightEnd', 'night' ], - [ 6, 'goldenHourEnd', 'goldenHour' ] - ]; - - // adds a custom time to the times config - - SunCalc.addTime = function (angle, riseName, setName) { - times.push([angle, riseName, setName]); - }; - - - // calculations for sun times - - var J0 = 0.0009; - - function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } - - function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } - function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } - - function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } - function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } - - // returns set time for the given sun altitude - function getSetJ(h, lw, phi, dec, n, M, L) { - - var w = hourAngle(h, phi, dec), - a = approxTransit(w, lw, n); - return solarTransitJ(a, M, L); - } - - - // calculates sun times for a given date, latitude/longitude, and, optionally, - // the observer height (in meters) relative to the horizon - - SunCalc.getTimes = function (date, lat, lng, height) { - - height = height || 0; - - var lw = rad * -lng, - phi = rad * lat, - - dh = observerAngle(height), - - d = toDays(date), - n = julianCycle(d, lw), - ds = approxTransit(0, lw, n), - - M = solarMeanAnomaly(ds), - L = eclipticLongitude(M), - dec = declination(L, 0), - - Jnoon = solarTransitJ(ds, M, L), - - i, len, time, h0, Jset, Jrise; - - - var result = { - solarNoon: new Date(fromJulian(Jnoon)), - nadir: new Date(fromJulian(Jnoon - 0.5)) - }; - - for (i = 0, len = times.length; i < len; i += 1) { - time = times[i]; - h0 = (time[0] + dh) * rad; - - Jset = getSetJ(h0, lw, phi, dec, n, M, L); - Jrise = Jnoon - (Jset - Jnoon); - - result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2)); - result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2)); - } - - return result; - }; - - - // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas - - function moonCoords(d) { // geocentric ecliptic coordinates of the moon - - var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude - M = rad * (134.963 + 13.064993 * d), // mean anomaly - F = rad * (93.272 + 13.229350 * d), // mean distance - - l = L + rad * 6.289 * sin(M), // longitude - b = rad * 5.128 * sin(F), // latitude - dt = 385001 - 20905 * cos(M); // distance to the moon in km - - return { - ra: rightAscension(l, b), - dec: declination(l, b), - dist: dt - }; - } - - SunCalc.getMoonPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = moonCoords(d), - H = siderealTime(d, lw) - c.ra, - h = altitude(H, phi, c.dec), - // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); - - h = h + astroRefraction(h); // altitude correction for refraction - - return { - azimuth: azimuth(H, phi, c.dec), - altitude: h, - distance: c.dist, - parallacticAngle: pa - }; - }; - - - // calculations for illumination parameters of the moon, - // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and - // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - - // Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c - - SunCalc.getMoonIllumination = function (date) { - let month = date.getMonth(); - let year = date.getFullYear(); - let day = date.getDate(); - - let c = 0; - let e = 0; - let jd = 0; - let b = 0; - - if (month < 3) { - year--; - month += 12; - } - - ++month; - c = 365.25 * year; - e = 30.6 * month; - jd = c + e + day - 694039.09; // jd is total days elapsed - jd /= 29.5305882; // divide by the moon cycle - b = parseInt(jd); // int(jd) -> b, take integer part of jd - jd -= b; // subtract integer part to leave fractional part of original jd - b = Math.round(jd * 8); // scale fraction from 0-8 and round - - if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0 - - return {phase: b}; - }; - - - function hoursLater(date, h) { - return new Date(date.valueOf() + h * dayMs / 24); - } - - // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article - - SunCalc.getMoonTimes = function (date, lat, lng, inUTC) { - var t = date; - if (inUTC) t.setUTCHours(0, 0, 0, 0); - else t.setHours(0, 0, 0, 0); - - var hc = 0.133 * rad, - h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc, - h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; - - // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) - for (var i = 1; i <= 24; i += 2) { - h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc; - h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc; - - a = (h0 + h2) / 2 - h1; - b = (h2 - h0) / 2; - xe = -b / (2 * a); - ye = (a * xe + b) * xe + h1; - d = b * b - 4 * a * h1; - roots = 0; - - if (d >= 0) { - dx = Math.sqrt(d) / (Math.abs(a) * 2); - x1 = xe - dx; - x2 = xe + dx; - if (Math.abs(x1) <= 1) roots++; - if (Math.abs(x2) <= 1) roots++; - if (x1 < -1) x1 = x2; - } - - if (roots === 1) { - if (h0 < 0) rise = i + x1; - else set = i + x1; - - } else if (roots === 2) { - rise = i + (ye < 0 ? x2 : x1); - set = i + (ye < 0 ? x1 : x2); - } - - if (rise && set) break; - - h0 = h2; - } - - var result = {}; - - if (rise) result.rise = hoursLater(t, rise); - if (set) result.set = hoursLater(t, set); - - if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; - - return result; - }; - - - // export as Node module / AMD module / browser variable - if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; - else if (typeof define === 'function' && define.amd) define(SunCalc); - else global.SunCalc = SunCalc; - -}()); diff --git a/apps/astroid/ChangeLog b/apps/astroid/ChangeLog index faa0ca5f8..0e2f13745 100644 --- a/apps/astroid/ChangeLog +++ b/apps/astroid/ChangeLog @@ -1,2 +1,5 @@ 0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.03: Bangle 2 support +0.04: Increase size of ship, asteroids and fonts for better readability +0.05: improve collision detect for larger ship v astroid +0.06: added, 7 point asteroid ploygon, made ship solid, rather than outline diff --git a/apps/astroid/asteroids.js b/apps/astroid/asteroids.js index 6cfa70b47..1b7b983aa 100644 --- a/apps/astroid/asteroids.js +++ b/apps/astroid/asteroids.js @@ -18,6 +18,22 @@ if (process.env.HWVERSION==2) { } var W = g.getWidth(); var H = g.getHeight(); +var SS = W/11; // ship back length +var SL = W/15; // ship side length +var AS = W/18; // asteroid radius +// radius of ship, assumed a circle inside equilateral traingle of side SS +// r = a / root 3 where a is length of equilateral triangle +var SR = SS / Math.sqrt(3); +var AST = [ // asteroid polygon as X/Y pairs + 0 ,-1.5, + 1 , 0, + 0.5, 0, + 0.5, 0.5, + 0 , 1, + -1 , 0, + -1 , -1 +]; + g.clear().setFontAlign(0,-1); function newAst(x,y) { @@ -25,7 +41,7 @@ function newAst(x,y) { x:x,y:y, vx:Math.random()-0.5, vy:Math.random()-0.5, - rad:3+Math.random()*5 + rad:3+Math.random()*AS }; return a; } @@ -41,8 +57,9 @@ var lastFrame; function gameStop() { running = false; - g.clear(); - g.drawString("Game Over!",120,(H-6)/2); + g.setFont('Vector', W/7); + g.setFontAlign(0,0); + g.drawString("Game Over", W/2, H/2); g.flip(); } @@ -104,12 +121,13 @@ function onFrame() { } g.clear(); - g.drawString(score,W-20,0); + g.setFont('Vector', 16); + g.drawString(score,W-20,16); var rs = Math.PI*0.8; - g.drawPoly([ - ship.x+Math.cos(ship.r)*4, ship.y+Math.sin(ship.r)*4, - ship.x+Math.cos(ship.r+rs)*3, ship.y+Math.sin(ship.r+rs)*3, - ship.x+Math.cos(ship.r-rs)*3, ship.y+Math.sin(ship.r-rs)*3, + g.fillPoly([ + ship.x+Math.cos(ship.r)*SS, ship.y+Math.sin(ship.r)*SS, + ship.x+Math.cos(ship.r+rs)*SL, ship.y+Math.sin(ship.r+rs)*SL, + ship.x+Math.cos(ship.r-rs)*SL, ship.y+Math.sin(ship.r-rs)*SL, ],true); var na = []; ammo.forEach(function(a) { @@ -137,7 +155,10 @@ function onFrame() { ast.forEach(function(a) { a.x += a.vx*d; a.y += a.vy*d; - g.drawCircle(a.x, a.y, a.rad); + //g.drawCircle(a.x, a.y, a.rad); + // a 7 point asteroid with rough circle radius of scale 2 + g.drawPoly(g.transformVertices(AST,{x:a.x,y:a.y,scale:a.rad,rotate:t}),true); + if (a.x<0) a.x+=W; if (a.y<0) a.y+=H; if (a.x>=W) a.x-=W; @@ -165,7 +186,7 @@ function onFrame() { var dx = a.x-ship.x; var dy = a.y-ship.y; var d = Math.sqrt(dx*dx+dy*dy); - if (d < a.rad) crashed = true; + if (d < a.rad + SR) crashed = true; }); ast=na; if (!ast.length) { diff --git a/apps/astroid/metadata.json b/apps/astroid/metadata.json index abb3681ff..f73feec43 100644 --- a/apps/astroid/metadata.json +++ b/apps/astroid/metadata.json @@ -1,10 +1,10 @@ { "id": "astroid", "name": "Asteroids!", - "version": "0.03", + "version": "0.06", "description": "Retro asteroids game", "icon": "asteroids.png", - "screenshots": [{"url":"screenshot_asteroids.png"}], + "screenshots": [{"url":"screenshot.png"}], "tags": "game", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, diff --git a/apps/astroid/screenshot.png b/apps/astroid/screenshot.png new file mode 100644 index 000000000..81120267d Binary files /dev/null and b/apps/astroid/screenshot.png differ diff --git a/apps/astroid/screenshot_asteroids.png b/apps/astroid/screenshot_asteroids.png deleted file mode 100644 index 4474c7a66..000000000 Binary files a/apps/astroid/screenshot_asteroids.png and /dev/null differ diff --git a/apps/ateatimer/ChangeLog b/apps/ateatimer/ChangeLog new file mode 100644 index 000000000..81da9fdce --- /dev/null +++ b/apps/ateatimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Fix icon, utilize sched, show running timer on app relaunch \ No newline at end of file diff --git a/apps/ateatimer/app-icon.js b/apps/ateatimer/app-icon.js new file mode 100644 index 000000000..f80208ead --- /dev/null +++ b/apps/ateatimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIKHgwFKo0gAofmsALEGR0H/+f//+gEP/4ACAoXAn4FDAQn8g0DAoX4g0BAoXx4E4AoXhAoN/8EP4AzBn/4h/IC4M//kPzgjBz/+h+MAoMfj0PNYUfh4FDh8HAo0wg/454RBmBDBAoRnBCIIjCAAMPF4IFDHYOIgEBj5HBzkAIIPAIIIFBn4hBLIU+AoPgwEQvwFBOIX8CgP5w0RAoSJC/AsB/0EJwIgB/+Aj/wAoN/VgPgQwQFBwBKCXAQWBAAfgAoocCAoQcCAAPAj7XEcYIABcYLIBAAJBBA==")) \ No newline at end of file diff --git a/apps/ateatimer/app.js b/apps/ateatimer/app.js new file mode 100644 index 000000000..9322d4e46 --- /dev/null +++ b/apps/ateatimer/app.js @@ -0,0 +1,156 @@ +// Tea Timer Application for Bangle.js 2 using sched library + +let timerDuration = (() => { + let file = require("Storage").open("ateatimer.data", "r"); + let data = file.read(4); // Assuming 4 bytes for storage + return data ? parseInt(data, 10) : 4 * 60; // Default to 4 minutes +})(); +let timeRemaining = timerDuration; +let timerRunning = false; + +function saveDefaultDuration() { + let file = require("Storage").open("ateatimer.data", "w"); + file.write(timerDuration.toString()); +} + +function drawTime() { + g.clear(); + g.setFont("Vector", 40); + g.setFontAlign(0, 0); // Center align + + const minutes = Math.floor(Math.abs(timeRemaining) / 60); + const seconds = Math.abs(timeRemaining) % 60; + const sign = timeRemaining < 0 ? "-" : ""; + const timeStr = `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`; + + g.drawString(timeStr, g.getWidth() / 2, g.getHeight() / 2); + + // Draw Increase button (triangle pointing up) + g.fillPoly([ + g.getWidth() / 2, g.getHeight() / 2 - 80, // Top vertex + g.getWidth() / 2 - 20, g.getHeight() / 2 - 60, // Bottom-left vertex + g.getWidth() / 2 + 20, g.getHeight() / 2 - 60 // Bottom-right vertex + ]); + + // Draw Decrease button (triangle pointing down) + g.fillPoly([ + g.getWidth() / 2, g.getHeight() / 2 + 80, // Bottom vertex + g.getWidth() / 2 - 20, g.getHeight() / 2 + 60, // Top-left vertex + g.getWidth() / 2 + 20, g.getHeight() / 2 + 60 // Top-right vertex + ]); + + g.flip(); +} + +function startTimer() { + if (timerRunning) return; + if (timeRemaining == 0) return; + timerRunning = true; + + // Save the default duration on timer start + timerDuration = timeRemaining; + saveDefaultDuration(); + scheduleTimer(); + + // Start the secondary timer to update the display + setInterval(updateDisplay, 1000); +} + +function scheduleTimer() { + // Schedule a new timer using the sched library + require("sched").setAlarm("ateatimer", { + msg: "Tea is ready!", + timer: timeRemaining * 1000, // Convert to milliseconds + vibrate: ".." // Default vibration pattern + }); + + // Ensure the scheduler updates + require("sched").reload(); +} + +function resetTimer() { + // Cancel the existing timer + require("sched").setAlarm("ateatimer", undefined); + require("sched").reload(); + + timerRunning = false; + timeRemaining = timerDuration; + drawTime(); +} + +function adjustTime(amount) { + if (-amount > timeRemaining) { + // Return if result will be negative + return; + } + timeRemaining += amount; + timeRemaining = Math.max(0, timeRemaining); // Ensure time doesn't go negative + if (timerRunning) { + // Update the existing timer with the new remaining time + let alarm = require("sched").getAlarm("ateatimer"); + if (alarm) { + // Cancel the current alarm + require("sched").setAlarm("ateatimer", undefined); + + // Set a new alarm with the updated time + scheduleTimer(); + } + } + + drawTime(); +} + +function handleTouch(x, y) { + const centerY = g.getHeight() / 2; + + if (y < centerY - 40) { + // Increase button area + adjustTime(60); + } else if (y > centerY + 40) { + // Decrease button area + adjustTime(-60); + } else { + // Center area + if (!timerRunning) { + startTimer(); + } + } +} + +// Function to update the display every second +function updateDisplay() { + if (timerRunning) { + let alarm = require("sched").getAlarm("ateatimer"); + timeRemaining = Math.floor(require("sched").getTimeToAlarm(alarm) / 1000); + drawTime(); + if (timeRemaining <= 0) { + timeRemaining = 0; + clearInterval(updateDisplay); + timerRunning = false; + } + } +} + +// Handle physical button press for resetting timer +setWatch(() => { + if (timerRunning) { + resetTimer(); + } else { + startTimer(); + } +}, BTN1, { repeat: true, edge: "falling" }); + +// Handle touch +Bangle.on("touch", (zone, xy) => { + handleTouch(xy.x, xy.y, false); +}); + +let isRunning = require("sched").getAlarm("ateatimer"); +if (isRunning) { + timerRunning = true; + // Start the timer to update the display + setInterval(updateDisplay, 1000); +} else { + // Draw the initial timer display + drawTime(); +} \ No newline at end of file diff --git a/apps/ateatimer/app.json b/apps/ateatimer/app.json new file mode 100644 index 000000000..7304a3d42 --- /dev/null +++ b/apps/ateatimer/app.json @@ -0,0 +1 @@ +{ "duration": 240 } \ No newline at end of file diff --git a/apps/ateatimer/app.png b/apps/ateatimer/app.png new file mode 100644 index 000000000..4c25f7d33 Binary files /dev/null and b/apps/ateatimer/app.png differ diff --git a/apps/ateatimer/metadata.json b/apps/ateatimer/metadata.json new file mode 100644 index 000000000..c4b8a1458 --- /dev/null +++ b/apps/ateatimer/metadata.json @@ -0,0 +1,14 @@ +{ "id": "ateatimer", + "name": "A Tea Timer", + "shortName":"A Tea Timer", + "icon": "app.png", + "version":"0.02", + "description": "Simple app for setting timers for tea. Touch up and down to change time, and time or button to start counting. When timer is running, button will stop timer and reset counter to last used value.", + "tags": "timer", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"ateatimer.app.js","url":"app.js"}, + {"name":"ateatimer.img","url":"app-icon.js","evaluate":true} + ], + "dependencies": {"scheduler":"type"} +} diff --git a/apps/autoreset/ChangeLog b/apps/autoreset/ChangeLog new file mode 100644 index 000000000..cef136817 --- /dev/null +++ b/apps/autoreset/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Add black- and whitelist for apps. Configure the timout time. diff --git a/apps/autoreset/README.md b/apps/autoreset/README.md new file mode 100644 index 000000000..3d086ee0e --- /dev/null +++ b/apps/autoreset/README.md @@ -0,0 +1,21 @@ +# Auto Reset + +Sets a timeout to load the clock face. The timeout is stopped and started again upon user input. + +## Usage + +Install with app loader and Auto Reset will run in background. If you don't interact with the watch it will time out to the clock face after 10 minutes. + +Through the settings apps can be black-/whitelisted and the timeout length can be configured. + +## TODO + +- per app specific timeout lengths? + +## Requests + +Mention @thyttan in an issue on the espruino/BangleApps repo for bug reports and feature requests. + +## Creator + +thyttan diff --git a/apps/autoreset/app.png b/apps/autoreset/app.png new file mode 100644 index 000000000..695c49931 Binary files /dev/null and b/apps/autoreset/app.png differ diff --git a/apps/autoreset/boot.js b/apps/autoreset/boot.js new file mode 100644 index 000000000..fc8d9ec0a --- /dev/null +++ b/apps/autoreset/boot.js @@ -0,0 +1,37 @@ +{ +const DEFAULTS = { + mode: 0, + apps: [], + timeout: 10 +}; +const settings = require("Storage").readJSON("autoreset.json", 1) || DEFAULTS; + +// Check if the back button should be enabled for the current app. +// app is the src file of the app. +// Derivative of the backswipe app's logic. +function enabledForApp(app) { + if (Bangle.CLOCK==1) return false; + if (!settings) return true; + let isListed = settings.apps.filter((a) => a.files.includes(app)).length > 0; + return settings.mode===0?!isListed:isListed; +} + +let timeoutAutoreset; +const resetTimeoutAutoreset = (force)=>{ + if (timeoutAutoreset) clearTimeout(timeoutAutoreset); + setTimeout(()=>{ // Short outer timeout to make sure we have time to leave clock face before checking `Bangle.CLOCK!=1`. + if (enabledForApp(global.__FILE__)) { + timeoutAutoreset = setTimeout(()=>{ + if (Bangle.CLOCK!=1) Bangle.showClock(); + }, settings.timeout*60*1000); + } + },200); +}; + +Bangle.on('touch', resetTimeoutAutoreset); +Bangle.on('swipe', resetTimeoutAutoreset); +Bangle.on('message', resetTimeoutAutoreset); +setWatch(resetTimeoutAutoreset, BTN, {repeat:true, edge:'rising'}); + +if (Bangle.CLOCK!=1) resetTimeoutAutoreset(); +} diff --git a/apps/autoreset/metadata.json b/apps/autoreset/metadata.json new file mode 100644 index 000000000..c8866924b --- /dev/null +++ b/apps/autoreset/metadata.json @@ -0,0 +1,17 @@ +{ "id": "autoreset", + "name": "Auto Reset", + "version":"0.02", + "description": "Sets a timeout to load the clock face. The timeout is stopped and started again upon user input.", + "icon": "app.png", + "type": "bootloader", + "tags": "system", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"autoreset.boot.js","url":"boot.js"}, + {"name":"autoreset.settings.js","url":"settings.js"} + ], + "data":[ + {"name":"autoreset.json"} + ] +} diff --git a/apps/autoreset/settings.js b/apps/autoreset/settings.js new file mode 100644 index 000000000..8cbccd6f0 --- /dev/null +++ b/apps/autoreset/settings.js @@ -0,0 +1,115 @@ +(function(back) { + var FILE = 'autoreset.json'; + // Mode can be 'blacklist' or 'whitelist' + // Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode + var DEFAULTS = { + 'mode': 0, + 'apps': [], + 'timeout': 10 + }; + + var settings = {}; + + var loadSettings = function() { + settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; + }; + + var saveSettings = function(settings) { + require('Storage').write(FILE, settings); + }; + + // Get all app info files + var getApps = function() { + var apps = require('Storage').list(/\.info$/).map(appInfoFileName => { + var appInfo = require('Storage').readJSON(appInfoFileName, 1); + return appInfo && { + 'name': appInfo.name, + 'sortorder': appInfo.sortorder, + 'src': appInfo.src, + 'files': appInfo.files + }; + }).filter(app => app && !!app.src); + apps.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + return apps; + }; + + var showMenu = function() { + var menu = { + '': { 'title': 'Auto Reset' }, + /*LANG*/'< Back': () => { + back(); + }, + /*LANG*/'Mode': { + value: settings.mode, + min: 0, + max: 1, + format: v => ["Blacklist", "Whitelist"][v], + onchange: v => { + settings.mode = v; + saveSettings(settings); + }, + }, + /*LANG*/'App List': () => { + showAppSubMenu(); + }, + /*LANG*/'Timeout [min]': { + value: settings.timeout, + min: 0.25, max: 30, step : 0.25, + format: v => v, + onchange: v => { + settings.timeout = v; + saveSettings(settings); + }, + }, + }; + + E.showMenu(menu); + }; + + var showAppSubMenu = function() { + var menu = { + '': { 'title': 'Auto Reset' }, + '< Back': () => { + showMenu(); + }, + 'Add App': () => { + showAppList(); + } + }; + settings.apps.forEach(app => { + menu[app.name] = () => { + settings.apps.splice(settings.apps.indexOf(app), 1); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + var showAppList = function() { + var apps = getApps(); + var menu = { + '': { 'title': 'Auto Reset' }, + /*LANG*/'< Back': () => { + showMenu(); + } + }; + apps.forEach(app => { + menu[app.name] = () => { + settings.apps.push(app); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) diff --git a/apps/aviatorclk/.gitignore b/apps/aviatorclk/.gitignore new file mode 100644 index 000000000..bdbc0d22e --- /dev/null +++ b/apps/aviatorclk/.gitignore @@ -0,0 +1 @@ +aviatorclk.json diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog new file mode 100644 index 000000000..c086f765a --- /dev/null +++ b/apps/aviatorclk/ChangeLog @@ -0,0 +1,7 @@ +1.00: initial release +1.01: added tap event to scroll METAR and toggle seconds display +1.02: continue showing METAR during AVWX updates (show update status next to time) + re-try GPS fix if it takes too long + bug fix + don't attempt to update METAR if Bluetooth is NOT connected + toggle seconds display on front double-taps (if un-locked) to avoid accidential enabling diff --git a/apps/aviatorclk/README.md b/apps/aviatorclk/README.md new file mode 100644 index 000000000..ac27b80d3 --- /dev/null +++ b/apps/aviatorclk/README.md @@ -0,0 +1,41 @@ +# Aviator Clock + +A clock for aviators, with local time and UTC - and the latest METAR +(Meteorological Aerodrome Report) for the nearest airport + +![](screenshot.png) +![](screenshot2.png) + +This app depends on the [AVWX module](?id=avwx). Make sure to configure that +module after installing this app. + + +## Features + +- Local time (with optional seconds) +- UTC / Zulu time +- Weekday and day of the month +- Latest METAR for the nearest airport (scrollable) + +Tap the screen in the top or bottom half to scroll the METAR text (in case not +the whole report fits on the screen). You can also tap the watch from the top +or bottom to scroll, which works even with the screen locked. + +The colour of the METAR text will change to orange if the report is more than +1h old, and red if it's older than 1.5h. + +To toggle the seconds display, double tap the watch from either the left or +right. This only changes the display "temporarily" (ie. it doesn't change the +default configured through the settings). + + +## Settings + +- **Show Seconds**: to conserve battery power, you can turn the seconds display off (as the default) +- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/aviatorclk/aviatorclk-icon.js b/apps/aviatorclk/aviatorclk-icon.js new file mode 100644 index 000000000..508769a66 --- /dev/null +++ b/apps/aviatorclk/aviatorclk-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp")) diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js new file mode 100644 index 000000000..cee83813f --- /dev/null +++ b/apps/aviatorclk/aviatorclk.app.js @@ -0,0 +1,359 @@ +/* + * Aviator Clock - Bangle.js + * + */ + +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_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) + +const APP_NAME = 'aviatorclk'; + +const horizontalCenter = g.getWidth()/2; +const mainTimeHeight = 38; +const secondaryFontHeight = 22; +require("Font8x16").add(Graphics); // tertiary font +const dateColour = ( g.theme.dark ? COLOUR_YELLOW : COLOUR_BLUE ); +const UTCColour = ( g.theme.dark ? COLOUR_LIGHT_CYAN : COLOUR_DARK_CYAN ); +const separatorColour = ( g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_DARK_GREY ); + +const avwx = require('avwx'); + + +// read in the settings +var settings = Object.assign({ + showSeconds: true, + invertScrolling: false, +}, require('Storage').readJSON(APP_NAME+'.json', true) || {}); + + +// globals +var drawTimeout; +var secondsInterval; +var avwxTimeout; +var gpsTimeout; + +var AVWXrequest; +var METAR = ''; +var METARlinesCount = 0; +var METARscollLines = 0; +var METARts; + + + +// date object to time string in format HH:MM[:SS] +// (with a leading 0 for hours if required, unlike the "locale" time() function) +function timeStr(date, seconds) { + let timeStr = date.getHours().toString(); + if (timeStr.length == 1) timeStr = '0' + timeStr; + let minutes = date.getMinutes().toString(); + if (minutes.length == 1) minutes = '0' + minutes; + timeStr += ':' + minutes; + if (seconds) { + let seconds = date.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + timeStr += ':' + seconds; + } + return timeStr; +} + + +// draw the METAR info +function drawAVWX() { + let now = new Date(); + let METARage = 0; // in minutes + if (METARts) { + METARage = Math.floor((now - METARts) / 60000); + } + + g.setBgColor(g.theme.bg); + + let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight + 4; + g.clearRect(0, y, g.getWidth(), y + (secondaryFontHeight * 4)); + + g.setFontAlign(0, -1).setFont("Vector", secondaryFontHeight); + if (METARage > 90) { // older than 1.5h + g.setColor(COLOUR_RED); + } else if (METARage > 60) { // older than 1h + g.setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW ); + } else { + g.setColor(g.theme.fg); + } + let METARlines = g.wrapString(METAR, g.getWidth()); + METARlinesCount = METARlines.length; + METARlines.splice(0, METARscollLines); + g.drawString(METARlines.join("\n"), horizontalCenter, y, true); + + if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); } +} + +// show AVWX update status +function showUpdateAVWXstatus(status) { + let y = Bangle.appRect.y + 10; + g.setBgColor(g.theme.bg); + g.clearRect(0, y, horizontalCenter - 54, y + 16); + if (status) { + g.setFontAlign(0, -1).setFont("8x16").setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW ); + g.drawString(status, horizontalCenter - 71, y, true); + } +} + +// re-try if the GPS doesn't return a fix in time +function GPStookTooLong() { + Bangle.setGPSPower(false, APP_NAME); + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + + showUpdateAVWXstatus('X'); + + if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); } +} + +// update the METAR info +function updateAVWX() { + if (avwxTimeout) clearTimeout(avwxTimeout); + avwxTimeout = undefined; + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + + if (! NRF.getSecurityStatus().connected) { + // if Bluetooth is NOT connected, try again in 5min + showUpdateAVWXstatus('X'); + avwxTimeout = setTimeout(updateAVWX, 5 * 60000); + return; + } + + showUpdateAVWXstatus('GPS'); + if (! METAR) { + METAR = '\nUpdating METAR'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + } + drawAVWX(); + + gpsTimeout = setTimeout(GPStookTooLong, 30 * 60000); + Bangle.setGPSPower(true, APP_NAME); + Bangle.on('GPS', fix => { + // prevent multiple, simultaneous requests + if (AVWXrequest) { return; } + + if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) { + Bangle.setGPSPower(false, APP_NAME); + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + + let lat = fix.lat; + let lon = fix.lon; + + showUpdateAVWXstatus('AVWX'); + if (! METAR) { + METAR = '\nUpdating METAR'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + } + drawAVWX(); + + // get latest METAR from nearest airport (via AVWX API) + AVWXrequest = avwx.request('metar/'+lat+','+lon, 'onfail=nearest', data => { + if (avwxTimeout) clearTimeout(avwxTimeout); + avwxTimeout = undefined; + + let METARjson = JSON.parse(data.resp); + + if ('sanitized' in METARjson) { + METAR = METARjson.sanitized; + } else { + METAR = 'No "sanitized" METAR data found!'; + } + METARlinesCount = 0; METARscollLines = 0; + + if ('time' in METARjson) { + METARts = new Date(METARjson.time.dt); + let now = new Date(); + let METARage = Math.floor((now - METARts) / 60000); // in minutes + if (METARage <= 30) { + // some METARs update every 30 min -> attempt to update after METAR is 35min old + avwxTimeout = setTimeout(updateAVWX, (35 - METARage) * 60000); + } else if (METARage <= 60) { + // otherwise, attempt METAR update after it's 65min old + avwxTimeout = setTimeout(updateAVWX, (65 - METARage) * 60000); + } + } else { + METARts = undefined; + } + + showUpdateAVWXstatus(''); + drawAVWX(); + AVWXrequest = undefined; + + }, error => { + // AVWX API request failed + console.log(error); + METAR = 'ERR: ' + error; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + showUpdateAVWXstatus(''); + drawAVWX(); + AVWXrequest = undefined; + }); + } + }); +} + + +// draw only the seconds part of the main clock +function drawSeconds() { + let now = new Date(); + let seconds = now.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + let y = Bangle.appRect.y + mainTimeHeight - 3; + g.setBgColor(g.theme.bg); + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY); + g.drawString(seconds, horizontalCenter + 54, y, true); +} + +// sync seconds update +function syncSecondsUpdate() { + drawSeconds(); + setTimeout(function() { + drawSeconds(); + secondsInterval = setInterval(drawSeconds, 1000); + }, 1000 - (Date.now() % 1000)); +} + +// set timeout for per-minute updates +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + if (METARts) { + let now = new Date(); + let METARage = Math.floor((now - METARts) / 60000); + if (METARage > 60) { + // the METAR colour might have to be updated: + drawAVWX(); + } + } + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// draw top part of clock (main time, date and UTC) +function draw() { + let now = new Date(); + let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60)); + + // prepare main clock area + let y = Bangle.appRect.y; + + g.setBgColor(g.theme.bg); + + // main time display + g.setFontAlign(0, -1).setFont("Vector", mainTimeHeight).setColor(g.theme.fg); + g.drawString(timeStr(now, false), horizontalCenter, y, true); + + // prepare second line (UTC and date) + y += mainTimeHeight; + g.clearRect(0, y, g.getWidth(), y + secondaryFontHeight - 1); + + // weekday and day of the month + g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(dateColour); + g.drawString(require("locale").dow(now, 1).toUpperCase() + ' ' + now.getDate(), 0, y, false); + + // UTC + g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(UTCColour); + g.drawString(timeStr(nowUTC, false) + "Z", g.getWidth(), y, false); + + queueDraw(); +} + + +// initialise +g.clear(true); + +// scroll METAR lines (either by touch or tap) +function scrollAVWX(action) { + switch (action) { + case -1: // top touch/tap + if (settings.invertScrolling) { + if (METARscollLines > 0) + METARscollLines--; + } else { + if (METARscollLines < METARlinesCount - 4) + METARscollLines++; + } + break; + case 1: // bottom touch/tap + if (settings.invertScrolling) { + if (METARscollLines < METARlinesCount - 4) + METARscollLines++; + } else { + if (METARscollLines > 0) + METARscollLines--; + } + break; + default: + // ignore other actions + } + drawAVWX(); +} + +Bangle.on('tap', data => { + switch (data.dir) { + case 'top': + scrollAVWX(-1); + break; + case 'bottom': + scrollAVWX(1); + break; + case 'front': + // toggle seconds display on double tap on front/watch-face + // (if watch is un-locked) + if (data.double && ! Bangle.isLocked()) { + if (settings.showSeconds) { + clearInterval(secondsInterval); + let y = Bangle.appRect.y + mainTimeHeight - 3; + g.clearRect(horizontalCenter + 54, y - secondaryFontHeight, g.getWidth(), y); + settings.showSeconds = false; + } else { + settings.showSeconds = true; + syncSecondsUpdate(); + } + } + break; + default: + // ignore other taps + } +}); + +Bangle.setUI("clockupdown", scrollAVWX); + +// load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// draw static separator line +let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight; +g.setColor(separatorColour); +g.drawLine(0, y, g.getWidth(), y); + +// draw times and request METAR +draw(); +if (settings.showSeconds) + syncSecondsUpdate(); +updateAVWX(); + + +// TMP for debugging: +//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; drawAVWX(); +//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX(); +//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX(); +//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert + diff --git a/apps/aviatorclk/aviatorclk.png b/apps/aviatorclk/aviatorclk.png new file mode 100644 index 000000000..af88cfbc4 Binary files /dev/null and b/apps/aviatorclk/aviatorclk.png differ diff --git a/apps/aviatorclk/aviatorclk.settings.js b/apps/aviatorclk/aviatorclk.settings.js new file mode 100644 index 000000000..d3ffbaad2 --- /dev/null +++ b/apps/aviatorclk/aviatorclk.settings.js @@ -0,0 +1,33 @@ +(function(back) { + var FILE = "aviatorclk.json"; + + // Load settings + var settings = Object.assign({ + showSeconds: true, + invertScrolling: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "AV8R Clock" }, + "< Back" : () => back(), + 'Show Seconds': { + value: !!settings.showSeconds, // !! converts undefined to false + onchange: v => { + settings.showSeconds = v; + writeSettings(); + } + }, + 'Invert Scrolling': { + value: !!settings.invertScrolling, // !! converts undefined to false + onchange: v => { + settings.invertScrolling = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json new file mode 100644 index 000000000..54f539c1e --- /dev/null +++ b/apps/aviatorclk/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "aviatorclk", + "name": "Aviator Clock", + "shortName":"AV8R Clock", + "version":"1.02", + "description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport", + "icon": "aviatorclk.png", + "screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "dependencies" : { "avwx": "module" }, + "readme": "README.md", + "storage": [ + { "name":"aviatorclk.app.js", "url":"aviatorclk.app.js" }, + { "name":"aviatorclk.settings.js", "url":"aviatorclk.settings.js" }, + { "name":"aviatorclk.img", "url":"aviatorclk-icon.js", "evaluate":true } + ], + "data": [{ "name":"aviatorclk.json" }] +} diff --git a/apps/aviatorclk/screenshot.png b/apps/aviatorclk/screenshot.png new file mode 100644 index 000000000..127946f42 Binary files /dev/null and b/apps/aviatorclk/screenshot.png differ diff --git a/apps/aviatorclk/screenshot2.png b/apps/aviatorclk/screenshot2.png new file mode 100644 index 000000000..e00e2238b Binary files /dev/null and b/apps/aviatorclk/screenshot2.png differ diff --git a/apps/avwx/ChangeLog b/apps/avwx/ChangeLog new file mode 100644 index 000000000..971e5b97e --- /dev/null +++ b/apps/avwx/ChangeLog @@ -0,0 +1 @@ +1.00: initial release diff --git a/apps/avwx/README.md b/apps/avwx/README.md new file mode 100644 index 000000000..a954d118f --- /dev/null +++ b/apps/avwx/README.md @@ -0,0 +1,41 @@ +# AVWX Module + +This is a module/library to use the [AVWX](https://account.avwx.rest/) Aviation +Weather API. It doesn't include an app. + + +## Configuration + +You will need an AVWX account (see above for link) and generate an API token. +The free "Hobby" plan is normally sufficient, but please consider supporting +the AVWX project. + +After installing the module on your Bangle, use the "interface" page (floppy +disk icon) in the App Loader to set the API token. + + +## Usage + +Include the module in your app with: + + const avwx = require('avwx'); + +Then use the exported function, for example to get the "sanitized" METAR from +the nearest station to a lat/lon coordinate pair: + + reqID = avwx.request('metar/'+lat+','+lon, + 'filter=sanitized&onfail=nearest', + data => { console.log(data); }, + error => { console.log(error); }); + +The returned reqID can be useful to track whether a request has already been +made (ie. the app is still waiting on a response). + +Please consult the [AVWX documentation](https://avwx.docs.apiary.io/) for +information about the available end-points and request parameters. + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/avwx/avwx.js b/apps/avwx/avwx.js new file mode 100644 index 000000000..1a9193b26 --- /dev/null +++ b/apps/avwx/avwx.js @@ -0,0 +1,47 @@ +/* + * AVWX Bangle Module + * + * AVWX doco: https://avwx.docs.apiary.io/ + * test AVWX API request with eg.: curl -X GET 'https://avwx.rest/api/metar/43.9844,-88.5570?token=...' + * + */ + + +const AVWX_BASE_URL = 'https://avwx.rest/api/'; // must end with a slash +const AVWX_CONFIG_FILE = 'avwx.json'; + + +// read in the settings +var AVWXsettings = Object.assign({ + AVWXtoken: '', +}, require('Storage').readJSON(AVWX_CONFIG_FILE, true) || {}); + + +/** + * Make an AVWX API request + * + * @param {string} requestPath API path (after /api/), eg. 'meta/KOSH' + * @param {string} params optional request parameters, eg. 'onfail=nearest' (use '&' in the string to combine multiple params) + * @param {function} successCB callback if the API request was successful - will supply the returned data: successCB(data) + * @param {function} failCB callback in case the API request failed - will supply the error: failCB(error) + * + * @returns {number} the HTTP request ID + * + * Example: + * reqID = avwx.request('metar/'+lat+','+lon, + * 'filter=sanitized&onfail=nearest', + * data => { console.log(data); }, + * error => { console.log(error); }); + * + */ +exports.request = function(requestPath, optParams, successCB, failCB) { + if (! AVWXsettings.AVWXtoken) { + failCB('No AVWX API Token defined!'); + return undefined; + } + let params = 'token='+AVWXsettings.AVWXtoken; + if (optParams) + params += '&'+optParams; + return Bangle.http(AVWX_BASE_URL+requestPath+'?'+params).then(successCB).catch(failCB); +}; + diff --git a/apps/avwx/avwx.png b/apps/avwx/avwx.png new file mode 100644 index 000000000..129c9f9f4 Binary files /dev/null and b/apps/avwx/avwx.png differ diff --git a/apps/avwx/interface.html b/apps/avwx/interface.html new file mode 100644 index 000000000..cdd77cb74 --- /dev/null +++ b/apps/avwx/interface.html @@ -0,0 +1,47 @@ + + + + + + +

To use the AVWX API, you need an account and generate an API token. The free "Hobby" plan is sufficient, but please consider supporting the AVWX project.

+

+ + +

+

+ +

+ +

+ + + + + + diff --git a/apps/avwx/metadata.json b/apps/avwx/metadata.json new file mode 100644 index 000000000..0b07f32d4 --- /dev/null +++ b/apps/avwx/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "avwx", + "name": "AVWX Module", + "shortName":"AVWX", + "version":"1.00", + "description": "Module/library for the AVWX API", + "icon": "avwx.png", + "type": "module", + "tags": "outdoors", + "supports": ["BANGLEJS2"], + "provides_modules": ["avwx"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + { "name":"avwx", "url":"avwx.js" } + ], + "data": [{ "name":"avwx.json" }] +} diff --git a/apps/awairmonitor/ChangeLog b/apps/awairmonitor/ChangeLog index 71d6399c4..17417e79b 100644 --- a/apps/awairmonitor/ChangeLog +++ b/apps/awairmonitor/ChangeLog @@ -1,3 +1,4 @@ 0.01: Beta version for Bangle 2 paired with Chrome (2021/12/11) 0.02: The app is now a clock, the data is greyed after the connection is lost (2021/12/22) 0.03: Set the Awair's IP directly on the webpage (2021/12/27) +0.04: Minor code improvements diff --git a/apps/awairmonitor/app.js b/apps/awairmonitor/app.js index 9123a9c2c..ef2f33a9e 100644 --- a/apps/awairmonitor/app.js +++ b/apps/awairmonitor/app.js @@ -80,7 +80,7 @@ function draw() { g.drawString(""+bt_current_humi, 123, 110); g.drawString(""+bt_current_temp, 158, 110); - for (i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { if (display_frozen) { g.setColor("#888"); } // max height = 32 diff --git a/apps/awairmonitor/metadata.json b/apps/awairmonitor/metadata.json index a58175b1b..dd59b4965 100644 --- a/apps/awairmonitor/metadata.json +++ b/apps/awairmonitor/metadata.json @@ -4,7 +4,7 @@ "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "allow_emulator": true, - "version":"0.03", + "version": "0.04", "description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.", "type": "clock", "tags": "clock,tool,health", diff --git a/apps/backswipe/ChangeLog b/apps/backswipe/ChangeLog new file mode 100644 index 000000000..c67453a09 --- /dev/null +++ b/apps/backswipe/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Don't fire if the app uses swipes already. +0.03: Only count defined handlers in the handler array. +0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs a refresh by deselecting and reselecting the "Messages" app throught Back Swipe settings. +0.05: React on swipes before the active app (for the most part) by using `prependListener`. diff --git a/apps/backswipe/README.md b/apps/backswipe/README.md new file mode 100644 index 000000000..21aa357b3 --- /dev/null +++ b/apps/backswipe/README.md @@ -0,0 +1,23 @@ +Service that allows you to use an app's back button using left to right swipe gesture. + +## Settings + +Mode: Blacklist/Whitelist/Always On/Disabled +App List: Black-/whitelisted apps +Standard # of swipe handlers: 0-10 (Default: 0, must be changed for backswipe to work at all) +Standard # of drag handlers: 0-10 (Default: 0, must be changed for backswipe to work at all) + + +Standard # of handlers settings are used to fine tune when backswipe should trigger the back function. E.g. when using a keyboard that works on drags, we don't want the backswipe to trigger when we just wanted to select a letter. This might not be able to cover all cases however. + +To get an indication for standard # of handlers `Bangle["#onswipe"]` and `Bangle["#ondrag"]` can be entered in the [Espruino Web IDE](https://www.espruino.com/ide) console field. They return `undefined` if no handler is active, a function if one is active, or a list of functions if multiple are active. Calling this on the clock app is a good start. + +## TODO + +- Possibly add option to tweak standard # of handlers on per app basis. + +## Creator +Kedlub + +## Contributors +thyttan diff --git a/apps/backswipe/app.png b/apps/backswipe/app.png new file mode 100644 index 000000000..93019e1e2 Binary files /dev/null and b/apps/backswipe/app.png differ diff --git a/apps/backswipe/boot.js b/apps/backswipe/boot.js new file mode 100644 index 000000000..cbc0f2563 --- /dev/null +++ b/apps/backswipe/boot.js @@ -0,0 +1,61 @@ +(function () { + var DEFAULTS = { + mode: 0, + apps: [], + }; + var settings = require("Storage").readJSON("backswipe.json", 1) || DEFAULTS; + + // Overrride the default setUI method, so we can save the back button callback + var setUI = Bangle.setUI; + Bangle.setUI = function (mode, cb) { + var options = {}; + if ("object"==typeof mode) { + options = mode; + } + + var currentFile = global.__FILE__ || ""; + + if (global.BACK) delete global.BACK; + if (options && options.back && enabledForApp(currentFile)) { + global.BACK = options.back; + } + setUI(mode, cb); + }; + + function countHandlers(eventType) { + if (Bangle["#on"+eventType] === undefined) { + return 0; + } else if (Bangle["#on"+eventType] instanceof Array) { + return Bangle["#on"+eventType].filter(x=>x).length; + } else if (Bangle["#on"+eventType] !== undefined) { + return 1; + } + } + + function goBack(lr, _) { + // if it is a left to right swipe + if (lr === 1) { + // if we're in an app that has a back button, run the callback for it + if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) { + global.BACK(); + E.stopEventPropagation(); + } + } + } + + // Check if the back button should be enabled for the current app + // app is the src file of the app + function enabledForApp(app) { + if (!settings) return true; + if (settings.mode === 0) { + return !(settings.apps.filter((a) => (a.src===app)||(a.files&&a.files.includes(app))).length > 0); // The `a.src===app` and `a.files&&...` checks are for backwards compatibility. Otherwise only `a.files.includes(app)` is needed. + } else if (settings.mode === 1) { + return settings.apps.filter((a) => (a.src===app)||(a.files&&a.files.includes(app))).length > 0; + } else { + return settings.mode === 2 ? true : false; + } + } + + // Listen to left to right swipe + Bangle.prependListener("swipe", goBack); +})(); diff --git a/apps/backswipe/metadata.json b/apps/backswipe/metadata.json new file mode 100644 index 000000000..78cd4dbe5 --- /dev/null +++ b/apps/backswipe/metadata.json @@ -0,0 +1,18 @@ +{ "id": "backswipe", + "name": "Back Swipe", + "shortName":"BackSwipe", + "version":"0.05", + "description": "Service that allows you to use an app's back button using left to right swipe gesture", + "icon": "app.png", + "tags": "back,gesture,swipe", + "supports" : ["BANGLEJS2"], + "readme":"README.md", + "type": "bootloader", + "storage": [ + {"name":"backswipe.boot.js","url":"boot.js"}, + {"name":"backswipe.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"backswipe.json"} + ] +} diff --git a/apps/backswipe/settings.js b/apps/backswipe/settings.js new file mode 100644 index 000000000..c98f706eb --- /dev/null +++ b/apps/backswipe/settings.js @@ -0,0 +1,127 @@ +(function(back) { + var FILE = 'backswipe.json'; + // Mode can be 'blacklist', 'whitelist', 'on' or 'disabled' + // Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode + var DEFAULTS = { + 'mode': 0, + 'apps': [], + 'standardNumSwipeHandlers': 0, + 'standardNumDragHandlers': 0 + }; + + var settings = {}; + + var loadSettings = function() { + settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; + }; + + var saveSettings = function(settings) { + require('Storage').write(FILE, settings); + }; + + // Get all app info files + var getApps = function() { + var apps = require('Storage').list(/\.info$/).map(appInfoFileName => { + var appInfo = require('Storage').readJSON(appInfoFileName, 1); + return appInfo && { + 'name': appInfo.name, + 'sortorder': appInfo.sortorder, + 'src': appInfo.src, + 'files': appInfo.files + }; + }).filter(app => app && !!app.src); + apps.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + return apps; + }; + + var showMenu = function() { + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + back(); + }, + 'Mode': { + value: settings.mode, + min: 0, + max: 3, + format: v => ["Blacklist", "Whitelist", "Always On", "Disabled"][v], + onchange: v => { + settings.mode = v; + saveSettings(settings); + }, + }, + 'App List': () => { + showAppSubMenu(); + }, + 'Standard # of swipe handlers' : { // If more than this many handlers are present backswipe will not go back + value: 0|settings.standardNumSwipeHandlers, + min: 0, + max: 10, + format: v=>v, + onchange: v => { + settings.standardNumSwipeHandlers = v; + saveSettings(settings); + }, + }, + 'Standard # of drag handlers' : { // If more than this many handlers are present backswipe will not go back + value: 0|settings.standardNumDragHandlers, + min: 0, + max: 10, + format: v=>v, + onchange: v => { + settings.standardNumDragHandlers = v; + saveSettings(settings); + }, + } + }; + + E.showMenu(menu); + }; + + var showAppSubMenu = function() { + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + showMenu(); + }, + 'Add App': () => { + showAppList(); + } + }; + settings.apps.forEach(app => { + menu[app.name] = () => { + settings.apps.splice(settings.apps.indexOf(app), 1); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + var showAppList = function() { + var apps = getApps(); + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + showMenu(); + } + }; + apps.forEach(app => { + menu[app.name] = () => { + settings.apps.push(app); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) diff --git a/apps/bad/ChangeLog b/apps/bad/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/bad/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/bad/README.md b/apps/bad/README.md new file mode 100644 index 000000000..156b34cbf --- /dev/null +++ b/apps/bad/README.md @@ -0,0 +1,8 @@ +# Bad Apple demo ![](app.png) + +"Bad Apple" is like "Hello World" for graphics system. So this is it +for Bangle.js2. Watch have no speaker, so vibration motor is used, +instead, to produce (pretty quiet) sound. + +Tools for preparing bad.araw and bad.vraw are in prep/ directory. Full +3 minute demo should actually fit to the watch. \ No newline at end of file diff --git a/apps/bad/app-icon.js b/apps/bad/app-icon.js new file mode 100644 index 000000000..e153d6397 --- /dev/null +++ b/apps/bad/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIifiAFWj//Aod///gAgMH///+AFBn4FB/AQDAoYEB//8gEBAokDAoX+ApguCAAIFqGoYFNLIZHBApZxFAoyDCAoqJCSoqsBUIcPAoKtF4AFBJAS/DHIQAeA=")) diff --git a/apps/bad/app.png b/apps/bad/app.png new file mode 100644 index 000000000..e3e3dad82 Binary files /dev/null and b/apps/bad/app.png differ diff --git a/apps/bad/bad.app.js b/apps/bad/bad.app.js new file mode 100644 index 000000000..453f0003c --- /dev/null +++ b/apps/bad/bad.app.js @@ -0,0 +1,65 @@ +/* sox Rear_Right.wav -r 4k -b 8 -c 1 -e unsigned-integer 0.raw vol 2 + aplay -r 4000 /tmp/0.raw +*/ + +/* https://forum.espruino.com/conversations/348912/ */ + +let pin = D19; + +function play(name, callback) { + function playChar(offs) { + var l = 10240; + var s = require("Storage").read(name, offs, l); + //print("Waveform " + name + " " + s.length); + if (!s.length) { + digitalWrite(pin,0); + if (callback) callback(); + return; + } + var w = new Waveform(s.length); + var b = w.buffer; + b.set(s); + //print("Buffer", s.length); + //for (var i=s.length-1;i>=0;i--)b[i]/=4; + w.startOutput(pin, 4000); + w.on("finish", function(buf) { + playChar(offs+l); + }); + } + analogWrite(pin, 0.1, {freq:40000}); + playChar(0); +} + +function video(name, callback) { + function frame() { + var s = require("Storage").read(name, offs, l); + if (!s) + return; + g.drawImage(s, 0, 0, { scale: 2 }); + g.flip(); + offs += l; + } + g.clear(); + var offs = 0; + //var l = 3875; for 176x176 + //var l = 515; for 64x64 + var l = 971; + setInterval(frame, 200); +} + +function run() { + clearInterval(i); + print("Running"); + play('bad.araw'); + t1 = getTime(); + video('bad.vraw'); + print("100 frames in ", getTime()-t1); + // 1.7s, unscaled + // 2.68s, scale 1.01 + // 5.73s, scale 2.00 + // 9.93s, scale 2, full screen + // 14.4s scaled. 176/64 +} + +print("Loaded"); +i = setInterval(run, 100); \ No newline at end of file diff --git a/apps/bad/bad.araw b/apps/bad/bad.araw new file mode 100644 index 000000000..cfe5f4fdd Binary files /dev/null and b/apps/bad/bad.araw differ diff --git a/apps/bad/bad.vraw b/apps/bad/bad.vraw new file mode 100644 index 000000000..ff8fec1b0 Binary files /dev/null and b/apps/bad/bad.vraw differ diff --git a/apps/bad/metadata.json b/apps/bad/metadata.json new file mode 100644 index 000000000..882b7f066 --- /dev/null +++ b/apps/bad/metadata.json @@ -0,0 +1,16 @@ +{ "id": "bad", + "name": "Bad Apple", + "version":"0.01", + "description": "Bad Apple demo", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "allow_emulator": false, + "tags": "game", + "storage": [ + {"name":"bad.app.js","url":"bad.app.js"}, + {"name":"bad.vraw","url":"bad.vraw"}, + {"name":"bad.araw","url":"bad.araw"}, + {"name":"bad.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bad/prep/img_convert.py b/apps/bad/prep/img_convert.py new file mode 100755 index 000000000..2edbbdef5 --- /dev/null +++ b/apps/bad/prep/img_convert.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +from PIL import Image +import os + +def convert_image(input_path, output_width, output_height): + img = Image.open(input_path) + img_resized = img.resize((output_width, output_height), Image.ANTIALIAS) + img_gray = img_resized.convert('L') + img_1bpp = img_gray.point(lambda x: 0 if x < 128 else 255, '1') + return img_1bpp + +def convert_and_append_header(input_directory, size): + input_files = [f for f in os.listdir(input_directory) if f.startswith("image_") and f.endswith(".png")] + input_files.sort() + header_bytes = size.to_bytes(1, byteorder='big') + size.to_bytes(1, byteorder='big') + b'\x01' + + for i, input_file in enumerate(input_files): + input_path = os.path.join(input_directory, input_file) + img_1bpp = convert_image(input_path, size, size) + output_file = input_path + ".raw" + + with open(output_file, 'wb') as raw_file: + raw_file.write(header_bytes) + raw_file.write(img_1bpp.tobytes()) + +if __name__ == "__main__": + input_directory = "." # Replace with the path to your image directory + output_width = 88 + output_file_path = "output_with_header.raw" # Replace with the desired output file path + + convert_and_append_header(input_directory, output_width) diff --git a/apps/bad/prep/run b/apps/bad/prep/run new file mode 100755 index 000000000..907e711f3 --- /dev/null +++ b/apps/bad/prep/run @@ -0,0 +1,20 @@ +#!/bin/bash + +# aplay -r 4000 /tmp/0.raw +#bug: Terminal exists on b.js, it is dumb terminal, not vt100. + +rm image_*.png image_*.png.raw output.wav ../bad.araw ../bad.vraw + +I=bad.mp4 +S=1:18 +E=1:50 + +ffmpeg -i $I -ss $S -to $E -vn -acodec pcm_u8 -ar 4000 -ac 1 -y output.wav +./wav_divider.py +mv output.raw ../bad.araw + +ffmpeg -i $I -ss $S -to $E -r 5 -vf fps=5 image_%04d.png +./img_convert.py +cat *.png.raw > ../bad.vraw + +ls -al ../bad.* diff --git a/apps/bad/prep/wav_divider.py b/apps/bad/prep/wav_divider.py new file mode 100755 index 000000000..9ce08e28b --- /dev/null +++ b/apps/bad/prep/wav_divider.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 + +def divide_bytes(input_file, output_file): + with open(input_file, 'rb') as infile: + with open(output_file, 'wb') as outfile: + byte = infile.read(1) + while byte: + # Convert byte to integer, divide by 4, and write back as byte + new_byte = bytes([int.from_bytes(byte, byteorder='big') // 3]) + outfile.write(new_byte) + byte = infile.read(1) + +divide_bytes("output.wav", "output.raw") diff --git a/apps/ballmaze/ChangeLog b/apps/ballmaze/ChangeLog index de6240f46..fedae2fc5 100644 --- a/apps/ballmaze/ChangeLog +++ b/apps/ballmaze/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Set LCD timeout for Espruino 2v10 compatibility \ No newline at end of file +0.02: Set LCD timeout for Espruino 2v10 compatibility +0.03: Minor code improvements diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js index 2d55887f0..d54cf4372 100644 --- a/apps/ballmaze/app.js +++ b/apps/ballmaze/app.js @@ -25,6 +25,7 @@ 10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial", }; // even size 1 actually works, but larger mazes take forever to generate + // TODO: should `sizes`, `minSize` and `defaultSize` have been declared outside the if block? if (!BANGLEJS2) { const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), minSize = 4, defaultSize = 10; } else { diff --git a/apps/ballmaze/metadata.json b/apps/ballmaze/metadata.json index 3223789d4..fc011623a 100644 --- a/apps/ballmaze/metadata.json +++ b/apps/ballmaze/metadata.json @@ -1,7 +1,7 @@ { "id": "ballmaze", "name": "Ball Maze", - "version": "0.02", + "version": "0.03", "description": "Navigate a ball through a maze by tilting your watch.", "icon": "icon.png", "type": "app", diff --git a/apps/balltastic/ChangeLog b/apps/balltastic/ChangeLog index 6ed48b5df..c3411c031 100644 --- a/apps/balltastic/ChangeLog +++ b/apps/balltastic/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version of Balltastic released! Happy! 0.02: Set LCD timeout for Espruino 2v10 compatibility 0.03: Now also works on Bangle.js 2 +0.04: Minor code improvements diff --git a/apps/balltastic/app.js b/apps/balltastic/app.js index d0262c3cb..d9241591e 100644 --- a/apps/balltastic/app.js +++ b/apps/balltastic/app.js @@ -1,4 +1,4 @@ -BANGLEJS2 = process.env.HWVERSION==2; +const BANGLEJS2 = process.env.HWVERSION==2; Bangle.setLCDBrightness(1); if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered"); Bangle.setLCDTimeout(0); diff --git a/apps/balltastic/metadata.json b/apps/balltastic/metadata.json index 09e265829..19ec3f901 100644 --- a/apps/balltastic/metadata.json +++ b/apps/balltastic/metadata.json @@ -1,7 +1,7 @@ { "id": "balltastic", "name": "Balltastic", - "version": "0.03", + "version": "0.04", "description": "Simple but fun ball eats dots game.", "icon": "app.png", "screenshots": [{"url":"bangle2-balltastic-screenshot.png"}], diff --git a/apps/banglebridge/ChangeLog b/apps/banglebridge/ChangeLog new file mode 100644 index 000000000..a4b22db22 --- /dev/null +++ b/apps/banglebridge/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Minor code improvements +0.03: Remove clearing of the screen (will break running apps) and fix lint errors \ No newline at end of file diff --git a/apps/banglebridge/metadata.json b/apps/banglebridge/metadata.json index 8a9eaa6e4..1dbb2e1de 100644 --- a/apps/banglebridge/metadata.json +++ b/apps/banglebridge/metadata.json @@ -2,8 +2,8 @@ "id": "banglebridge", "name": "BangleBridge", "shortName": "BangleBridge", - "version": "0.01", - "description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App", + "version": "0.03", + "description": "Widget that allows Bangle.js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App (**Note:** this has nothing to do with Gadgetbridge)", "icon": "widget.png", "type": "widget", "tags": "widget", diff --git a/apps/banglebridge/widget.js b/apps/banglebridge/widget.js index 48078de30..c805b0f39 100644 --- a/apps/banglebridge/widget.js +++ b/apps/banglebridge/widget.js @@ -1,30 +1,30 @@ (() => { /** * Widget measurements - * Description: + * Description: * name: connection.wid.js *icon: conectionIcon.icon - * + * */ //Font g.setFont("Vector", 100); //variabangle.Sensorss - let acclS, bttS, compssS, gpsS, hrmS, stepS; //Strings + //let acclS, bttS, compssS, gpsS, hrmS, stepS; //Strings let accelN, compssN, gpsN, hrmN, stepN; //Num - let prueba = 1; + //let prueba = 1; let data = [0, 0, 0, 0, 0, 0]; //Constants for redabangle.Sensors code let storage = require('Storage'); - let deCom = require('heatshrink'); + //let deCom = require('heatshrink'); //Sensors code /** - * + * * @author Jorge */ function accel() { @@ -35,8 +35,7 @@ }); setInterval(function () { - - acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag; + //acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag; data[3] = accelN; }, 2 * 1000); @@ -45,8 +44,7 @@ function btt() { setInterval(function () { - - bttS = E.getBattery(); //return String + //bttS = E.getBattery(); //return String data[2] = E.getBattery(); }, 15 * 1000); @@ -65,9 +63,9 @@ setInterval(function () { - compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" + + /*compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" + "B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" + - "C: " + compssN.heading; //return String + "C: " + compssN.heading; *///return String data[4] = compssN; }, 2 * 1000); @@ -86,8 +84,8 @@ setInterval(function () { - gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" + - "C: " + gpsN.satellites + " ## " + gpsN.fix; //return String + /*gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" + + "C: " + gpsN.satellites + " ## " + gpsN.fix; *///return String // work out how to display the current time var d = new Date(); var year = d.getFullYear(); @@ -129,10 +127,10 @@ finalS = s; } var z = d.getMilliseconds(); - var zFinal = new String(z); - zFinal = zFinal.replace('.', ''); + //var zFinal = new String(z); + //zFinal = zFinal.replace('.', ''); var completeTime = year + "-" + finalMonth + "-" + finalDay + "T" + finalh + ":" + finalM + ":" + finalS + "." + z + "Z"; - var time = h + ":" + ("0" + m).substr(-2); + //var time = h + ":" + ("0" + m).substr(-2); gpsN.time = completeTime; data[5] = gpsN; }, 2 * 1000); @@ -150,7 +148,7 @@ //console.log("Index ==> "+ index); msr[indexFinal] = nueva; - item = nueva; + //item = nueva; lastInsert = indexFinal; } @@ -180,7 +178,7 @@ hrmN = normalize(hrmN); var roundedRate = parseFloat(hrmN).toFixed(2); - hrmS = String.valueOf(roundedRate); //return String + //hrmS = String.valueOf(roundedRate); //return String //console.log("array----->" + msr); data[0] = roundedRate; @@ -205,7 +203,7 @@ setInterval(function () { - stepS = String.valueOf(stepN); //return String + //stepS = String.valueOf(stepN); //return String data[1] = stepN; }, 2 * 1000); @@ -240,12 +238,11 @@ g.setFont("Vector", 45); g.drawString(prueba,100,200);*/ if (flip == 1) { //when off - + flip = 0; //Bangle.buzz(1000); - g.clear(); } else { //when on - + flip = 1; g.setFont("Vector", 30); g.drawString(data[0], 65, 180); @@ -283,7 +280,7 @@ com: data[4], gps: data[5] }; - /* g.clear(); + /* g.drawString(compssS,100,200); */ @@ -293,7 +290,7 @@ //draw(); }, 5 * 1000); - + WIDGETS["banglebridge"]={ area: "tl", width: 10, diff --git a/apps/banglexercise/ChangeLog b/apps/banglexercise/ChangeLog index 5f1d3bd7d..06c4adf1b 100644 --- a/apps/banglexercise/ChangeLog +++ b/apps/banglexercise/ChangeLog @@ -2,3 +2,6 @@ 0.02: Add sit ups Add more feedback to the user about the exercises Clean up code +0.03: Add software back button on main menu +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/banglexercise/app.js b/apps/banglexercise/app.js index bc6e35f07..f4addc05a 100644 --- a/apps/banglexercise/app.js +++ b/apps/banglexercise/app.js @@ -11,7 +11,7 @@ let historySlopeY = []; let historySlopeZ = []; let lastZeroPassCameFromPositive; -let lastZeroPassTime = 0; +//let lastZeroPassTime = 0; let lastExerciseCompletionTime = 0; let lastExerciseHalfCompletionTime = 0; @@ -71,7 +71,8 @@ function showMainMenu() { let menu; menu = { "": { - title: "BanglExercise" + title: "BanglExercise", + back: load } }; @@ -147,7 +148,7 @@ function accelHandler(accel) { // slope for Z if (exerciseType.useZaxis) { - l = historyAvgZ.length; + let l = historyAvgZ.length; if (l > 1) { const p1 = historyAvgZ[l - 2]; const p2 = historyAvgZ[l - 1]; @@ -197,7 +198,7 @@ function isValidExercise(slope, t) { } lastZeroPassCameFromPositive = true; - lastZeroPassTime = t; + //lastZeroPassTime = t; } if (p2 > 0 && p1 < 0) { if (lastZeroPassCameFromPositive == true) { @@ -255,7 +256,7 @@ function isValidExercise(slope, t) { } lastZeroPassCameFromPositive = false; - lastZeroPassTime = t; + //lastZeroPassTime = t; } } } @@ -271,7 +272,7 @@ function reset() { historySlopeZ = []; lastZeroPassCameFromPositive = undefined; - lastZeroPassTime = 0; + //lastZeroPassTime = 0; lastExerciseHalfCompletionTime = 0; lastExerciseCompletionTime = 0; exerciseCounter = 0; @@ -381,4 +382,5 @@ Bangle.on('HRM', function(hrm) { }); g.clear(1); +Bangle.loadWidgets(); showMainMenu(); diff --git a/apps/banglexercise/metadata.json b/apps/banglexercise/metadata.json index 9bb93f112..b2f8e39ea 100644 --- a/apps/banglexercise/metadata.json +++ b/apps/banglexercise/metadata.json @@ -1,7 +1,7 @@ { "id": "banglexercise", "name": "BanglExercise", "shortName":"BanglExercise", - "version":"0.02", + "version": "0.05", "description": "Can automatically track exercises while wearing the Bangle.js watch.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/banglexercise/settings.js b/apps/banglexercise/settings.js index 3208c6eca..a52634faf 100644 --- a/apps/banglexercise/settings.js +++ b/apps/banglexercise/settings.js @@ -11,11 +11,10 @@ '< Back': back, 'Buzz': { value: "buzz" in settings ? settings.buzz : false, - format: () => (settings.buzz ? 'Yes' : 'No'), onchange: () => { settings.buzz = !settings.buzz; save('buzz', settings.buzz); } } }); -}); +}) diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 5df032c4d..96ee0141e 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -9,3 +9,9 @@ 0.09: Fix time/date disappearing after fullscreen notification 0.10: Use ClockFace library 0.11: Use ClockFace.is12Hour +0.12: Add settings to hide date,widgets +0.13: Add font setting +0.14: Use ClockFace_menu.addItems +0.15: Add Power saving option +0.16: Support Fast Loading +0.17: Hide widgets instead of not loading them at all diff --git a/apps/barclock/README.md b/apps/barclock/README.md index 4b92313c5..28572e37c 100644 --- a/apps/barclock/README.md +++ b/apps/barclock/README.md @@ -4,3 +4,8 @@ A simple digital clock showing seconds as a horizontal bar. | 24hr style | 12hr style | | --- | --- | | ![24-hour bar clock](screenshot.png) | ![12-hour bar clock with meridian](screenshot_pm.png) | + +## Settings +* `Show date`: display date at the bottom of screen +* `Font`: choose between bitmap or vector fonts +* `Power saving`: (Bangle.js 2 only) don't draw the seconds bar while the watch is locked \ No newline at end of file diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index 987d41cc6..f2499189b 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -1,94 +1,128 @@ /* jshint esversion: 6 */ -/** - * A simple digital clock showing seconds as a bar - **/ +{ + /** + * A simple digital clock showing seconds as a bar + **/ // Check settings for what type our clock should be -let locale = require("locale"); -{ // add some more info to locale - let date = new Date(); - date.setFullYear(1111); - date.setMonth(1, 3); // februari: months are zero-indexed - const localized = locale.date(date, true); - locale.dayFirst = /3.*2/.test(localized); - locale.hasMeridian = (locale.meridian(date)!==""); -} - -function renderBar(l) { - if (!this.fraction) { - // zero-size fillRect stills draws one line of pixels, we don't want that - return; + let locale = require("locale"); + { // add some more info to locale + let date = new Date(); + date.setFullYear(1111); + date.setMonth(1, 3); // februari: months are zero-indexed + const localized = locale.date(date, true); + locale.dayFirst = /3.*2/.test(localized); + locale.hasMeridian = (locale.meridian(date)!==""); } - const width = this.fraction*l.w; - g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); -} - -function timeText(date) { - if (!clock.is12Hour) { - return locale.time(date, true); + let barW = 0, prevX = 0; + const renderBar = function (l) { + "ram"; + if (l) prevX = 0; // called from Layout: drawing area was cleared + else l = clock.layout.bar; + let x2 = l.x+barW; + if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar + if (x2===prevX) return; // nothing to do + if (x2===0) x2--; // don't leave 1px line + if (x212) { - date12.setHours(hours-12); + + const timeText = function(date) { + if (!clock.is12Hour) { + return locale.time(date, true); + } + const date12 = new Date(date.getTime()); + const hours = date12.getHours(); + if (hours===0) { + date12.setHours(12); + } else if (hours>12) { + date12.setHours(hours-12); + } + return locale.time(date12, true); } - return locale.time(date12, true); -} -function ampmText(date) { - return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; -} -function dateText(date) { - const dayName = locale.dow(date, true), - month = locale.month(date, true), - day = date.getDate(); - const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; - return `${dayName} ${dayMonth}`; -} + const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; + const dateText = date => { + const dayName = locale.dow(date, true), + month = locale.month(date, true), + day = date.getDate(); + const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; + return `${dayName} ${dayMonth}`; + }; + const ClockFace = require("ClockFace"), + clock = new ClockFace({ + precision: 1, + settingsFile: "barclock.settings.json", + init: function() { + const Layout = require("Layout"); + this.layout = new Layout({ + type: "v", c: [ + { + type: "h", c: [ + {id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below + {id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg}, + ], + }, + {id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, + this.showDate ? {height: 40} : {}, + this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {}, + ], + }, {lazy: true}); + // adjustments based on screen size and whether we display am/pm + let thickness; // bar thickness, same as time font "pixel block" size + if (this.is12Hour && locale.hasMeridian) { + // Maximum font size = ( - ) / (5chars * 6px) + thickness = Math.floor((Bangle.appRect.w-24)/(5*6)); + } else { + this.layout.ampm.label = ""; + thickness = Math.floor(Bangle.appRect.w/(5*6)); + } + let bar = this.layout.bar; + bar.height = thickness+1; + if (this.font===1) { // vector + const B2 = process.env.HWVERSION>1; + if (this.is12Hour && locale.hasMeridian) { + this.layout.time.font = "Vector:"+(B2 ? 50 : 60); + this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40); + } else { + this.layout.time.font = "Vector:"+(B2 ? 60 : 80); + } + } else { + this.layout.time.font = "6x8:"+thickness; + } + this.layout.update(); + bar.y2 = bar.y+bar.height-1; + }, + update: function(date, c) { + "ram"; + if (c.m) this.layout.time.label = timeText(date); + if (c.h) this.layout.ampm.label = ampmText(date); + if (c.d && this.showDate) this.layout.date.label = dateText(date); + if (c.m) this.layout.render(); + if (c.s) { + barW = Math.round(date.getSeconds()/60*this.layout.bar.w); + renderBar(); + } + }, + resume: function() { + prevX = 0; // force redraw of bar + this.layout.forgetLazyState(); + }, + remove: function() { + if (this.onLock) Bangle.removeListener("lock", this.onLock); + }, + }); -const ClockFace = require("ClockFace"), - clock = new ClockFace({ - precision:1, - init: function() { - const Layout = require("Layout"); - this.layout = new Layout({ - type: "v", c: [ - { - type: "h", c: [ - {id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // size updated below - {id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg}, - ], - }, - {id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, - {height: 40}, - {id: "date", type: "txt", font: "10%", valign: 1}, - ], - }, {lazy: true}); - // adjustments based on screen size and whether we display am/pm - let thickness; // bar thickness, same as time font "pixel block" size - if (this.is12Hour) { - // Maximum font size = ( - ) / (5chars * 6px) - thickness = Math.floor((Bangle.appRect.w-24)/(5*6)); - } else { - this.layout.ampm.label = ""; - thickness = Math.floor(Bangle.appRect.w/(5*6)); - } - this.layout.bar.height = thickness+1; - this.layout.time.font = "6x8:"+thickness; - this.layout.update(); - }, - update: function(date, c) { - if (c.m) this.layout.time.label = timeText(date); - if (c.h) this.layout.ampm.label = ampmText(date); - if (c.d) this.layout.date.label = dateText(date); - const SECONDS_PER_MINUTE = 60; - if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; - this.layout.render(); - }, - resume: function() { - this.layout.forgetLazyState(); - }, - }); -clock.start(); + // power saving: only update once a minute while locked, hide bar + if (clock.powerSave) { + clock.onLock = lock => { + clock.precision = lock ? 60 : 1; + clock.tick(); + renderBar(); // hide/redraw bar right away + } + Bangle.on("lock", clock.onLock); + } + + clock.start(); +} \ No newline at end of file diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json index 7bc61096d..010852083 100644 --- a/apps/barclock/metadata.json +++ b/apps/barclock/metadata.json @@ -1,7 +1,7 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.11", + "version": "0.17", "description": "A simple digital clock showing seconds as a bar", "icon": "clock-bar.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], @@ -12,6 +12,10 @@ "allow_emulator": true, "storage": [ {"name":"barclock.app.js","url":"clock-bar.js"}, + {"name":"barclock.settings.js","url":"settings.js"}, {"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true} + ], + "data": [ + {"name":"barclock.settings.json"} ] } diff --git a/apps/barclock/settings.js b/apps/barclock/settings.js new file mode 100644 index 000000000..bc292ef6f --- /dev/null +++ b/apps/barclock/settings.js @@ -0,0 +1,35 @@ +(function(back) { + let s = require("Storage").readJSON("barclock.settings.json", true) || {}; + // migrate "don't load widgets" to "hide widgets" + if (!("hideWidgets" in s) && ("loadWidgets" in s) && !s.loadWidgets) { + s.hideWidgets = 1; + } + delete s.loadWidgets; + + function save(key, value) { + s[key] = value; + require("Storage").writeJSON("barclock.settings.json", s); + } + + const fonts = [/*LANG*/"Bitmap",/*LANG*/"Vector"]; + let menu = { + "": {"title": /*LANG*/"Bar Clock"}, + /*LANG*/"< Back": back, + /*LANG*/"Font": { + value: s.font|0, + min: 0, max: 1, wrap: true, + format: v => fonts[v], + onchange: v => save("font", v), + }, + }; + let items = { + showDate: s.showDate, + hideWidgets: s.hideWidgets, + }; + // Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway) + if (process.env.HWVERSION>1) { + items.powerSave = s.powerSave; + } + require("ClockFace_menu").addItems(menu, save, items); + E.showMenu(menu); +}) diff --git a/apps/barcode/ChangeLog b/apps/barcode/ChangeLog new file mode 100644 index 000000000..4974b7029 --- /dev/null +++ b/apps/barcode/ChangeLog @@ -0,0 +1,11 @@ +0.01: Please forgive me +0.02: Now tells time! +0.03: Interaction +0.04: Shows day of week +0.05: Shows day of month +0.06: Updates every 5 minutes when locked, or when unlock occurs. Also shows nr of steps. +0.07: Step count resets at midnight +0.08: Step count stored in memory to survive reloads. Now shows step count daily and since last reboot. +0.09: NOW it really should reset daily (instead of every other day...) +0.10: Tell clock widgets to hide. +0.11: Minor code improvements diff --git a/apps/barcode/README.md b/apps/barcode/README.md new file mode 100644 index 000000000..17b365d45 --- /dev/null +++ b/apps/barcode/README.md @@ -0,0 +1,23 @@ +# Barcode clockwatchface + +A scannable EAN-8 compatible clockwatchface for your Bangle 2 + +The format of the bars are + +`||HHmm||MMwc||` + +* Left section: HHmm + * H: Hours + * m: Minutes +* Right section: MM9c + * M: Day of month + * w: Day of week + * c: Calculated EAN-8 digit checksum + +Apart from that + +* The upper left section displays total number of steps per day +* The upper right section displays total number of steps from last boot ("stepuptime") +* The face updates every 5 minutes or on demant by pressing the hardware button + +This clockwathface is aware of theme choice, so it will adapt to Light/Dark themes. diff --git a/apps/barcode/barcode.app.js b/apps/barcode/barcode.app.js new file mode 100644 index 000000000..3242164ae --- /dev/null +++ b/apps/barcode/barcode.app.js @@ -0,0 +1,428 @@ +/* Sizes */ +let checkBarWidth = 10; +let checkBarHeight = 140; + +let digitBarWidth = 14; +let digitBarHeight = 100; + +let textBarWidth = 56; +let textBarHeight = 20; + +//let textWidth = 14; +//let textHeight = 20; + +/* Offsets */ +var startOffsetX = 17; +var startOffsetY = 30; + +let startBarOffsetX = startOffsetX; +let startBarOffsetY = startOffsetY; + +let upperTextBarLeftOffsetX = startBarOffsetX + checkBarWidth; +let upperTextBarLeftOffsetY = startOffsetY; + +let midBarOffsetX = upperTextBarLeftOffsetX + textBarWidth; +let midBarOffsetY = startOffsetY; + +let upperTextBarRightOffsetX = midBarOffsetX + checkBarWidth; +let upperTextBarRightOffsetY = startOffsetY; + +let endBarOffsetX = upperTextBarRightOffsetX + textBarWidth; +let endBarOffsetY = startOffsetY; + +let leftBarsStartX = startBarOffsetX + checkBarWidth; +let leftBarsStartY = upperTextBarLeftOffsetY + textBarHeight; + +let rightBarsStartX = midBarOffsetX + checkBarWidth; +let rightBarsStartY = upperTextBarRightOffsetY + textBarHeight; + +/* Utilities */ +let stepCount = require("Storage").readJSON("stepCount",1); +if(stepCount === undefined) stepCount = 0; +//let intCaster = num => Number(num); + +var drawTimeout; + +function renderWatch(l) { + g.setFont("4x6",2); + + // work out how to display the current time + + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + //var time = h + ":" + ("0"+m).substr(-2); + //var month = ("0" + (d.getMonth()+1)).slice(-2); + var dayOfMonth = ('0' + d.getDate()).slice(-2); + var dayOfWeek = d.getDay() || 7; + var concatTime = ("0"+h).substr(-2) + ("0"+m).substr(-2) + dayOfMonth + dayOfWeek; + + const chars = String(concatTime).split("").map((concatTime) => { + return Number(concatTime); + }); + const checkSum = calculateChecksum(chars); + concatTime += checkSum; + + drawCheckBar(startBarOffsetX, startBarOffsetY); + + drawLDigit(chars[0], 0, leftBarsStartY); + drawLDigit(chars[1], 1, leftBarsStartY); + drawLDigit(chars[2], 2, leftBarsStartY); + drawLDigit(chars[3], 3, leftBarsStartY); + + g.drawString(getStepCount(), startOffsetX + checkBarWidth + 3, startOffsetY + 4); + g.drawString(concatTime.substring(0,4), startOffsetX + checkBarWidth + 3, startOffsetY + textBarHeight + digitBarHeight + 6); + + drawCheckBar(midBarOffsetX, midBarOffsetY); + + drawRDigit(chars[4], 0, rightBarsStartY); + drawRDigit(chars[5], 1, rightBarsStartY); + drawRDigit(chars[6], 2, rightBarsStartY); + drawRDigit(checkSum, 3, rightBarsStartY); + + g.drawString(Bangle.getStepCount(), midBarOffsetX + checkBarWidth + 3, startOffsetY + 4); + g.drawString(concatTime.substring(4), midBarOffsetX + checkBarWidth + 3, startOffsetY + textBarHeight + digitBarHeight + 6); + + drawCheckBar(endBarOffsetX, endBarOffsetY); + + // schedule a draw for the next minute + if (drawTimeout) { + clearTimeout(drawTimeout); + } + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + layout.render(layout.watch); + }, (1000 * 60 * 5) - (Date.now() % (1000 * 60 * 5))); +} + +function drawLDigit(digit, index, offsetY) { + switch(digit) { + case 0: + drawLZeroWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 1: + drawLOneWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 2: + drawLTwoWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 3: + drawLThreeWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 4: + drawLFourWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 5: + drawLFiveWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 6: + drawLSixWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 7: + drawLSevenWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 8: + drawLEightWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + case 9: + drawLNineWithOffset(leftBarsStartX+(digitBarWidth*index), offsetY); + break; + } +} + +function drawRDigit(digit, index, offsetY) { + switch(digit) { + case 0: + drawRZeroWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 1: + drawROneWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 2: + drawRTwoWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 3: + drawRThreeWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 4: + drawRFourWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 5: + drawRFiveWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 6: + drawRSixWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 7: + drawRSevenWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 8: + drawREightWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + case 9: + drawRNineWithOffset(rightBarsStartX+(digitBarWidth*index), offsetY); + break; + } +} + +/* +LEAN + +01234567890123 + xxxx xx + xx xxxx + xxxxxxxx xx + xx xxxx + xxxx xx + xx xxxxxxxx + xxxxxx xxxx + xxxx xxxxxx + xx xxxx + xxxx xx +*/ +function drawLOneWithOffset(offset, offsetY) { + let barOneX = 4; + let barTwoX = 12; + g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("1",offset+3,offsetY+digitHeight+5); +} + +function drawLTwoWithOffset(offset, offsetY) { + let barOneX = 4; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight); + //g.drawString("2",offset+3,offsetY+digitHeight+5); +} + +function drawLThreeWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 12; + g.fillRect(barOneX+offset,offsetY+0,barOneX+7+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("3",offset+3,offsetY+digitHeight+5); +} + +function drawLFourWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight); + //g.drawString("4",offset+3,offsetY+digitHeight+5); +} + +function drawLFiveWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 12; + g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("5",offset+3,offsetY+digitHeight+5); +} + +function drawLSixWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 6; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+7+offset,offsetY+digitBarHeight); + //g.drawString("6",offset+3,offsetY+digitHeight+5); +} + +function drawLSevenWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight); + //g.drawString("7",offset+3,offsetY+digitHeight+5); +} + +function drawLEightWithOffset(offset, offsetY) { + let barOneX = 2; + let barTwoX = 8; + g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight); + //g.drawString("8",offset+3,offsetY+digitHeight+5); +} + +function drawLNineWithOffset(offset, offsetY) { + let barOneX = 6; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+3+offset,offsetY+digitBarHeight); + //g.drawString("9",offset+3,offsetY+digitHeight+5); +} + +function drawLZeroWithOffset(offset, offsetY) { + let barOneX = 6; + let barTwoX = 12; + g.fillRect(barOneX+offset,offsetY+0,barOneX+3+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("0",offset+3,offsetY+digitHeight+5); +} + + + +/* +REAN + +01234567890123 +xxxx xxxx +xxxx xxxx +xx xx +xx xxxxxx +xx xxxxxx +xx xx +xx xx +xx xx +xxxxxx xx +xxxxxx xx + +*/ +function drawROneWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 8; + g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+3,offsetY+digitBarHeight); + g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+3,offsetY+digitBarHeight); + //g.drawString("1",offset+2,offsetY+textHeight+5); +} + +function drawRTwoWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 6; + g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+3,offsetY+digitBarHeight); + g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+3,offsetY+digitBarHeight); + //g.drawString("2",offset+2,offsetY+textHeight+5); +} + +function drawRThreeWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("3",offset+2,offsetY+textHeight+5); +} + +function drawRFourWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 4; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight); + //g.drawString("4",offset+2,offsetY+textHeight+5); +} + +function drawRFiveWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 6; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+5+offset,offsetY+digitBarHeight); + //g.drawString("5",offset+2,offsetY+textHeight+5); +} + +function drawRSixWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 4; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("6",offset+2,offsetY+textHeight+5); +} + +function drawRSevenWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 8; + g.fillRect(barOneX+offset,offsetY+0,barOneX+1+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("7",offset+2,offsetY+textHeight+5); +} + +function drawREightWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 6; + g.fillRect(offset+barOneX,offsetY+0,offset+barOneX+1,offsetY+digitBarHeight); + g.fillRect(offset+barTwoX,offsetY+0,offset+barTwoX+1,offsetY+digitBarHeight); + //g.drawString("8",offset+2,offsetY+textHeight+5); +} + +function drawRNineWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 8; + g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("9",offset+2,offsetY+textHeight+5); +} + +function drawRZeroWithOffset(offset, offsetY) { + let barOneX = 0; + let barTwoX = 10; + g.fillRect(barOneX+offset,offsetY+0,barOneX+5+offset,offsetY+digitBarHeight); + g.fillRect(barTwoX+offset,offsetY+0,barTwoX+1+offset,offsetY+digitBarHeight); + //g.drawString("0",offset+2,offsetY+textHeight+5); +} + +function drawCheckBar(offsetX, offsetY) { + const barOneX = offsetX+2; + const barOneWidth = 1; + const barTwoX = offsetX+6; + const barTwoWidth = 1; + g.fillRect(barOneX,offsetY,barOneX+barOneWidth,offsetY+checkBarHeight); + g.fillRect(barTwoX,offsetY,barTwoX+barTwoWidth,offsetY+checkBarHeight); +} + +function calculateChecksum(digits) { + let oddSum = digits[6] + digits[4] + digits[2] + digits[0]; + let evenSum = digits[5] + digits[3] + digits[1]; + + let checkSum = (10 - ((3 * oddSum + evenSum) % 10)) % 10; + + return checkSum; +} + +function storeStepCount() { + stepCount = Bangle.getStepCount(); + require("Storage").writeJSON("stepCount",stepCount); +} + +function getStepCount() { + let accumulatedSteps = Bangle.getStepCount(); + if(accumulatedSteps <= stepCount) { + return 0; + } + return accumulatedSteps - stepCount; +} + +function resetAtMidnight() { + let now = new Date(); + let night = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), // the next day, ... + 23, 58, 0 // ...at 00:00:00 hours +); + let msToMidnight = night.getTime() - now.getTime(); + + setTimeout(function() { + storeStepCount(); // <-- This is the function being called at midnight. + resetAtMidnight(); // Then, reset again next midnight. + }, msToMidnight); +} + +resetAtMidnight(); + +// The layout, referencing the custom renderer +var Layout = require("Layout"); +var layout = new Layout( { + type:"v", c: [ + {type:"custom", render:renderWatch, id:"watch", bgCol:g.theme.bg, fillx:1, filly:1 } + ] +}); + +// Clear the screen once, at startup +g.clear(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +layout.render(); + +Bangle.on('lock', function(locked) { + if(!locked) { + layout.render(); + } +}); diff --git a/apps/barcode/barcode.clock.png b/apps/barcode/barcode.clock.png new file mode 100644 index 000000000..7d249cdeb Binary files /dev/null and b/apps/barcode/barcode.clock.png differ diff --git a/apps/barcode/barcode.icon.js b/apps/barcode/barcode.icon.js new file mode 100644 index 000000000..969943e0e --- /dev/null +++ b/apps/barcode/barcode.icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDAE///+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifnD6khY+bTIiDTIn8gWq8n+if/////+ifrV6mlp+rXYiFXZr8k4q8r+if/////+if7+///u//79ie7/7//u///+if/////+ifnP6t+378r+ienvyf/K/7v+if/////+ifrfx6/I78j+ibe/+e/W75n+if/////+iee/+t/Z77f+iervyv/r/7v+if//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////")) diff --git a/apps/barcode/barcode.icon.png b/apps/barcode/barcode.icon.png new file mode 100644 index 000000000..43fa77a6f Binary files /dev/null and b/apps/barcode/barcode.icon.png differ diff --git a/apps/barcode/metadata.json b/apps/barcode/metadata.json new file mode 100644 index 000000000..007be2778 --- /dev/null +++ b/apps/barcode/metadata.json @@ -0,0 +1,16 @@ +{ "id": "barcode", + "name": "Barcode clock", + "shortName":"Barcode clock", + "icon": "barcode.icon.png", + "version": "0.11", + "description": "EAN-8 compatible barcode clock.", + "tags": "barcode,ean,ean-8,watchface,clock,clockface", + "type": "clock", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"barcode.app.js","url":"barcode.app.js"}, + {"name":"barcode.img","url":"barcode.icon.js","evaluate":true} + ], + "readme":"README.md", + "screenshots": [{"url":"barcode.clock.png"}] +} diff --git a/apps/barometer/ChangeLog b/apps/barometer/ChangeLog index de3a5cb96..b429dda17 100644 --- a/apps/barometer/ChangeLog +++ b/apps/barometer/ChangeLog @@ -1,3 +1,4 @@ 0.01: Display pressure as number and hand 0.02: Use theme color 0.03: workaround for some firmwares that return 'undefined' for first call to barometer +0.04: Update every second, go back with short button press diff --git a/apps/barometer/app.js b/apps/barometer/app.js index 77d4c974f..7e793af4f 100644 --- a/apps/barometer/app.js +++ b/apps/barometer/app.js @@ -59,6 +59,7 @@ function drawTicks(){ function drawScaleLabels(){ g.setColor(g.theme.fg); g.setFont("Vector",12); + g.setFontAlign(-1,-1); let label = MIN; for (let i=0;i <= NUMBER_OF_LABELS; i++){ @@ -103,22 +104,29 @@ function drawIcons() { } g.setBgColor(g.theme.bg); -g.clear(); - -drawTicks(); -drawScaleLabels(); -drawIcons(); try { function baroHandler(data) { - if (data===undefined) // workaround for https://github.com/espruino/BangleApps/issues/1429 - setTimeout(() => Bangle.getPressure().then(baroHandler), 500); - else + g.clear(); + + drawTicks(); + drawScaleLabels(); + drawIcons(); + if (data!==undefined) { drawHand(Math.round(data.pressure)); + } } Bangle.getPressure().then(baroHandler); + setInterval(() => Bangle.getPressure().then(baroHandler), 1000); } catch(e) { - print(e.message); - print("barometer not supporter, show a demo value"); + if (e !== undefined) { + print(e.message); + } + print("barometer not supported, show a demo value"); drawHand(MIN); } + +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); diff --git a/apps/barometer/metadata.json b/apps/barometer/metadata.json index a385f2be2..767fa630b 100644 --- a/apps/barometer/metadata.json +++ b/apps/barometer/metadata.json @@ -1,7 +1,7 @@ { "id": "barometer", "name": "Barometer", "shortName":"Barometer", - "version":"0.03", + "version":"0.04", "description": "A simple barometer that displays the current air pressure", "icon": "barometer.png", "tags": "tool,outdoors", diff --git a/apps/barwatch/ChangeLog b/apps/barwatch/ChangeLog new file mode 100644 index 000000000..7f837e50e --- /dev/null +++ b/apps/barwatch/ChangeLog @@ -0,0 +1 @@ +0.01: First version diff --git a/apps/barwatch/README.md b/apps/barwatch/README.md new file mode 100644 index 000000000..c37caa6e4 --- /dev/null +++ b/apps/barwatch/README.md @@ -0,0 +1,5 @@ +# BarWatch - an experimental watch + +For too long the watches have shown the time with digits or hands. No more! +With this stylish watch the time is represented by bars. Up to 24 as the day goes by. +Practical? Not really, but a different look! \ No newline at end of file diff --git a/apps/barwatch/app-icon.js b/apps/barwatch/app-icon.js new file mode 100644 index 000000000..82416ee28 --- /dev/null +++ b/apps/barwatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("l0uwkE/4A/AH4A/AB0gicQmUB+EPgEigExh8gj8A+ECAgMQn4WCgcACyotWC34W/C34W/CycACw0wgYWFBYIWCAAc/+YGHCAgNFACkxl8hGYwAMLYUvCykQC34WycoIW/C34W0gAWTmUjkUzkbmSAFY=")) \ No newline at end of file diff --git a/apps/barwatch/app.js b/apps/barwatch/app.js new file mode 100644 index 000000000..e0ed15ce6 --- /dev/null +++ b/apps/barwatch/app.js @@ -0,0 +1,76 @@ +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function draw() { + g.reset(); + + if(g.theme.dark){ + g.setColor(1,1,1); + }else{ + g.setColor(0,0,0); + } + + // work out how to display the current time + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + + // hour bars + var bx_offset = 10, by_offset = 35; + var b_width = 8, b_height = 60; + var b_space = 5; + + for(var i=0; i 11){ + by_offset = 105; + } + var iter = i % 12; + //console.log(iter); + g.fillRect(bx_offset+(b_width*(iter+1))+(b_space*iter), + by_offset, + bx_offset+(b_width*iter)+(b_space*iter), + by_offset+b_height); + } + + // minute bar + if(h > 11){ + by_offset = 105; + } + var m_bar = h % 12; + if(m != 0){ + g.fillRect(bx_offset+(b_width*(m_bar+1))+(b_space*m_bar), + by_offset+b_height-m, + bx_offset+(b_width*m_bar)+(b_space*m_bar), + by_offset+b_height); + } + + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first +draw(); +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/barwatch/app.png b/apps/barwatch/app.png new file mode 100644 index 000000000..134de9424 Binary files /dev/null and b/apps/barwatch/app.png differ diff --git a/apps/barwatch/metadata.json b/apps/barwatch/metadata.json new file mode 100644 index 000000000..adcd44107 --- /dev/null +++ b/apps/barwatch/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "barwatch", + "name": "BarWatch", + "shortName":"BarWatch", + "version":"0.01", + "description": "A watch that displays the time using bars. One bar for each hour.", + "readme": "README.md", + "icon": "screenshot.png", + "tags": "clock", + "type": "clock", + "allow_emulator":true, + "screenshots" : [ { "url": "screenshot.png" } ], + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"barwatch.app.js","url":"app.js"}, + {"name":"barwatch.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/barwatch/screenshot.png b/apps/barwatch/screenshot.png new file mode 100644 index 000000000..305138252 Binary files /dev/null and b/apps/barwatch/screenshot.png differ diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 31c386684..21ee5c3e7 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -7,4 +7,8 @@ 0.07: Improve logging and charting of component states and add widget icon 0.08: Fix for Home button in the app and README added. 0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state -0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381) \ No newline at end of file +0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381) +0.11: Initial port to the BangleJS2 +0.12: Remove debug log +0.13: Minor code improvements +0.14: Minor code improvements diff --git a/apps/batchart/app.js b/apps/batchart/app.js index 472fb3a8a..b93f004d4 100644 --- a/apps/batchart/app.js +++ b/apps/batchart/app.js @@ -1,9 +1,13 @@ + + +const board = process.env.BOARD; +const isBangle2 = board === "BANGLEJS2" || board === "EMSCRIPTEN2"; const GraphXZero = 40; -const GraphYZero = 180; -const GraphY100 = 80; +const GraphYZero = isBangle2? g.getHeight() - (g.getHeight() * 0.1): 180; +const GraphY100 = isBangle2? 50: 80; +const MaxValueCount = g.getWidth() - (GraphXZero * 2); // 144 const GraphMarkerOffset = 5; -const MaxValueCount = 144; const GraphXMax = GraphXZero + MaxValueCount; const GraphLcdY = GraphYZero + 10; @@ -12,9 +16,11 @@ const GraphBluetoothY = GraphYZero + 22; const GraphGpsY = GraphYZero + 28; const GraphHrmY = GraphYZero + 34; + const Storage = require("Storage"); function renderCoordinateSystem() { + g.setBgColor(0,0,0); g.setFont("6x8", 1); // Left Y axis (Battery) @@ -36,7 +42,12 @@ function renderCoordinateSystem() { g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero); // Right Y axis (Temperature) - g.setColor(0.4, 0.4, 1); + if (isBangle2) { + g.setColor(1, 0, 0); + } else { + g.setColor(0.4, 0.4, 1); + } + g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100); g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10); g.setFontAlign(-1, -1, 0); @@ -62,6 +73,10 @@ function loadData() { let logFileName = "bclog" + startingDay; let dataLines = loadLinesFromFile(MaxValueCount, logFileName); + if (!dataLines) { + console.log("Cannot load lines from file"); + dataLines = []; + } // Top up to MaxValueCount from previous days as required let previousDay = decrementDay(startingDay); @@ -86,6 +101,7 @@ function loadLinesFromFile(requestedLineCount, fileName) { var readFile = Storage.open(fileName, "r"); + let nextLine; while ((nextLine = readFile.readLine())) { if(nextLine) { allLines.push(nextLine); @@ -131,7 +147,7 @@ function renderData(dataArray) { const belowMinIndicatorValue = minTemperature - 1; const aboveMaxIndicatorValue = maxTemparature + 1; - var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; + //var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm; for (let i = 0; i < dataArray.length; i++) { const element = dataArray[i]; @@ -235,8 +251,12 @@ Bangle.on('lcdPower', (on) => { } }); -setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); - +if (isBangle2) { + setWatch(switchOffApp, BTN1, {edge:"falling", debounce:50, repeat:true}); + g.setBgColor(0,0,0); +} else { + setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true}); +} g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/batchart/metadata.json b/apps/batchart/metadata.json index f2ed9f0b6..7be1637f0 100644 --- a/apps/batchart/metadata.json +++ b/apps/batchart/metadata.json @@ -2,11 +2,11 @@ "id": "batchart", "name": "Battery Chart", "shortName": "Battery Chart", - "version": "0.10", + "version": "0.14", "description": "A widget and an app for recording and visualizing battery percentage over time.", "icon": "app.png", "tags": "app,widget,battery,time,record,chart,tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"batchart.wid.js","url":"widget.js"}, diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index d6e00b283..40198065b 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,7 +1,7 @@ (() => { let recordingInterval = null; const Storage = require("Storage"); - + const switchableConsumers = { none: 0, lcd: 1, @@ -11,10 +11,10 @@ hrm: 16 }; - var batChartFile; // file for battery percentage recording + //var batChartFile; // file for battery percentage recording const recordingInterval10Min = 60 * 10 * 1000; - const recordingInterval1Min = 60 * 1000; //For testing - const recordingInterval10S = 10 * 1000; //For testing + //const recordingInterval1Min = 60 * 1000; //For testing + //const recordingInterval10S = 10 * 1000; //For testing var compassEventReceived = false; var gpsEventReceived = false; @@ -96,15 +96,14 @@ let logPercent = E.getBattery(); let logTemperature = E.getTemperature(); let logConsumers = getEnabledConsumersValue(); - + let logString = [logTime, logPercent, logTemperature, logConsumers].join(","); - + bcLogFileA.write(logString + "\n"); } } function reload() { - console.log("Reloading BatteryChart widget"); WIDGETS["batchart"].width = 0; if (recordingInterval) { @@ -121,4 +120,4 @@ }; reload(); -})(); \ No newline at end of file +})(); diff --git a/apps/batclock/ChangeLog b/apps/batclock/ChangeLog index e6e21b146..2a2d91b74 100644 --- a/apps/batclock/ChangeLog +++ b/apps/batclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: App Created! 0.02: Update to use Bangle.setUI instead of setWatch +0.03: Tell clock widgets to hide. diff --git a/apps/batclock/bat-clock.app.js b/apps/batclock/bat-clock.app.js index 31b8f5b9b..dc649160f 100644 --- a/apps/batclock/bat-clock.app.js +++ b/apps/batclock/bat-clock.app.js @@ -249,6 +249,9 @@ g.clear(); g.setColor(0, 0.5, 0).drawImage(bg_crack); g.setColor(1, 1, 1).drawImage(batman); +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -256,5 +259,3 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/batclock/metadata.json b/apps/batclock/metadata.json index 8aa115780..e6520cb90 100644 --- a/apps/batclock/metadata.json +++ b/apps/batclock/metadata.json @@ -2,7 +2,7 @@ "id": "batclock", "name": "Bat Clock", "shortName": "Bat Clock", - "version": "0.02", + "version": "0.03", "description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.", "icon": "bat-clock.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/batterybooster/ChangeLog b/apps/batterybooster/ChangeLog new file mode 100644 index 000000000..6eedfa435 --- /dev/null +++ b/apps/batterybooster/ChangeLog @@ -0,0 +1 @@ +0.01: New app introduced to the app loader! diff --git a/apps/batterybooster/README.md b/apps/batterybooster/README.md new file mode 100644 index 000000000..d6da4942d --- /dev/null +++ b/apps/batterybooster/README.md @@ -0,0 +1,30 @@ +# Battery Booster + +A Bangle.js app designed to optimize battery life through smart screen and power management features. + +## Features + +### 1. Auto Soft-Off +- Automatically puts the watch into soft-off mode after 3 hours (10,800,000 ms) of being locked +- This feature is activated when the watch is locked and cancelled when unlocked + +### 2. Dynamic Screen Timeout +- Sets LCD timeout to 2 seconds when the watch is locked +- Extends LCD timeout to 10 seconds when the screen is touched +- Helps preserve battery life while maintaining usability + +### 3. Adaptive Brightness Control +- Automatically adjusts screen brightness based on the time of day +- Uses a sinusoidal pattern that follows natural daylight: + - Peak brightness at noon + - Lowest brightness at midnight + - Gradual transitions in between +- Updates brightness every hour + +## How It Works +The app runs in the background and manages three main aspects of power consumption: +1. Screen timeout duration +2. Automatic soft-off functionality +3. Time-based brightness adjustment + +This combination of features helps extend battery life while maintaining a good user experience. \ No newline at end of file diff --git a/apps/batterybooster/app.png b/apps/batterybooster/app.png new file mode 100644 index 000000000..3580b4895 Binary files /dev/null and b/apps/batterybooster/app.png differ diff --git a/apps/batterybooster/boot.js b/apps/batterybooster/boot.js new file mode 100644 index 000000000..22dbda316 --- /dev/null +++ b/apps/batterybooster/boot.js @@ -0,0 +1,26 @@ +{ + let softOffTimeout; + Bangle.on("lock", (on) => { + if (on) { + softOffTimeout = setTimeout(() => Bangle.softOff(), 10800000); + Bangle.setLCDTimeout(2); + } + else { + if (softOffTimeout) clearTimeout(softOffTimeout); + } + }); + Bangle.on("touch", () => { + Bangle.setLCDTimeout(10); + }); + setInterval(() => { + let getBrightness = (hour) => { + let radians = (Math.PI / 12) * (hour - 6); + let brightness = Math.sin(radians) / 2 + 0.5; + return brightness; + }; + + const d = new Date(); + let hour = d.getHours(); + Bangle.setLCDBrightness(getBrightness(hour)); + }, 3600000); +} \ No newline at end of file diff --git a/apps/batterybooster/metadata.json b/apps/batterybooster/metadata.json new file mode 100644 index 000000000..bd78e22d6 --- /dev/null +++ b/apps/batterybooster/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "batterybooster", + "name": "Battery Booster", + "icon": "app.png", + "version": "0.01", + "description": "A bootloader app which adds scripts to boost battery life of your Bangle.js 2", + "type": "bootloader", + "tags": "tools,system", + "readme": "README.md", + "supports": [ + "BANGLEJS2" + ], + "storage": [ + { + "name": "batterybooster.boot.js", + "url": "boot.js" + } + ] +} diff --git a/apps/battleship/ChangeLog b/apps/battleship/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/battleship/ChangeLog +++ b/apps/battleship/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/battleship/battleship.js b/apps/battleship/battleship.js index 3661ef494..237eca3d2 100644 --- a/apps/battleship/battleship.js +++ b/apps/battleship/battleship.js @@ -1,6 +1,6 @@ const FIELD_WIDTH = [11, 11, 15]; // for each phase const FIELD_HEIGHT = FIELD_WIDTH; -const FIELD_LINE_WIDTH = 2; +//const FIELD_LINE_WIDTH = 2; const FIELD_MARGIN = 2; const FIELD_COUNT_X = 10; const FIELD_COUNT_Y = FIELD_COUNT_X; diff --git a/apps/battleship/metadata.json b/apps/battleship/metadata.json index 12e92c1d7..46399c68f 100644 --- a/apps/battleship/metadata.json +++ b/apps/battleship/metadata.json @@ -1,7 +1,7 @@ { "id": "battleship", "name": "Battleship", - "version": "0.01", + "version": "0.02", "description": "The classic game of battleship", "icon": "battleship-icon.png", "tags": "game", diff --git a/apps/bblobface/ChangeLog b/apps/bblobface/ChangeLog new file mode 100644 index 000000000..6a29fdb72 --- /dev/null +++ b/apps/bblobface/ChangeLog @@ -0,0 +1 @@ +1.00: Initial release of Bangle Blobs Clock! diff --git a/apps/bblobface/README.md b/apps/bblobface/README.md new file mode 100644 index 000000000..54e07e9f8 --- /dev/null +++ b/apps/bblobface/README.md @@ -0,0 +1,35 @@ +# Bangle Blobs Clock +What if every time you checked the time, you could play a turn of a turn-based puzzle game? +You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments! +Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock. + +![](screenshot1.png) +![](screenshot2.png) + +## Clock Features +- Hour and minute +- Seconds (only while the screen is unlocked to save power) +- Month, day, and day of week +- Battery percentage. Blue while charging, red when low, green otherwise. +- Respects your 24-hour/12-hour time setting in Locale +- Press the pause button to access your Widgets +- Supports Fast Loading + +## The Game +This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA. +Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall. +If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs! +- Drag left and right to move the pair +- Tap the left or right half of the screen to rotate the pair +- Swipe down to place the pair + +## More Info +If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing + +## Special Thanks +I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app: +- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started. +- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2. +- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects! +- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games! +- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games. diff --git a/apps/bblobface/app-icon.js b/apps/bblobface/app-icon.js new file mode 100644 index 000000000..e8d9baced --- /dev/null +++ b/apps/bblobface/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA=")) diff --git a/apps/bblobface/app.js b/apps/bblobface/app.js new file mode 100644 index 000000000..579a6bbb4 --- /dev/null +++ b/apps/bblobface/app.js @@ -0,0 +1,768 @@ +{ + // ~~ Variables for clock ~~ + let clockDrawTimeout; + let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour']; + let updateSeconds = !Bangle.isLocked(); + let batteryLevel = E.getBattery(); + + // ~~ Variables for game logic ~~ + const NUM_COLORS = 6; + const NUISANCE_COLOR = 7; + let grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}]; + let currentPair = {pivot: 0, leaf: 0}; + let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10}; + let pairX = 2; + let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + let slotsToCheck = []; + let selectedColors; + let lastChain = 0; + let gameLost = false; + let gamePaused = false; + let midChain = false; + + /* + Sets up a new game. + Must be called once before the first round. + */ + let restartGame = function() { + grid = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]); + currentPair = {pivot: 0, leaf: 0}; + pairX = 2; + pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left + slotsToCheck = []; + gameLost = false; + lastChain = 0; + + //Set up random colors + selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]); + for (let i = NUM_COLORS - 1; i > 0; i--) { + let swap = selectedColors[i]; + let swapIndex = Math.floor(Math.random() * (i + 1)); + selectedColors[i] = selectedColors[swapIndex]; + selectedColors[swapIndex] = swap; + } + + //Create the first two pairs (Always in the first three colors) + nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)]; + }; + + /* + Readies the next pair and generates a new one for the queue. + */ + let newPair = function() { + currentPair.pivot = nextQueue[0].pivot; + currentPair.leaf = nextQueue[0].leaf; + + nextQueue[0].pivot = nextQueue[1].pivot; + nextQueue[0].leaf = nextQueue[1].leaf; + + nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)]; + nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)]; + + pairX = 2; + pairOrientation = 0; + + calcDropCoordinates(); + }; + + /* + Calculates the coordinates at which the current pair will be placed when quick dropped. + */ + let calcDropCoordinates = function() { + dropCoordinates.pivotX = pairX; + + //Find Y coordinate of pivot + dropCoordinates.pivotY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX] == 0) { + dropCoordinates.pivotY = i; + break; + } + } + if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0) + dropCoordinates.pivotY = -1; + + //Find coordinates of leaf + if (pairOrientation == 1) { + dropCoordinates.leafX = pairX + 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX + 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 3) { + dropCoordinates.leafX = pairX - 1; + + dropCoordinates.leafY = -2; + for (let i = 11; i >= 0; i--) { + if (grid[i][pairX - 1] == 0) { + dropCoordinates.leafY = i; + break; + } + } + if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0) + dropCoordinates.leafY = -1; + } else if (pairOrientation == 2) { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY; + dropCoordinates.pivotY--; + } else { + dropCoordinates.leafX = pairX; + dropCoordinates.leafY = dropCoordinates.pivotY - 1; + } + }; + + /* + Moves the current pair a certain number of slots. + */ + let movePair = function(dx) { + pairX += dx; + + if (dx < 0) { + if (pairX < (pairOrientation == 3 ? 1 : 0)) + pairX = (pairOrientation == 3 ? 1 : 0); + } + if (dx > 0) { + if (pairX > (pairOrientation == 1 ? 4 : 5)) + pairX = (pairOrientation == 1 ? 4 : 5); + } + + calcDropCoordinates(); + }; + + /* + Rotates the pair in the given direction around the pivot. + */ + let rotatePair = function(clockwise) { + pairOrientation += (clockwise ? 1 : -1); + if (pairOrientation > 3) + pairOrientation = 0; + if (pairOrientation < 0) + pairOrientation = 3; + + if (pairOrientation == 1 && pairX == 5) + pairX = 4; + if (pairOrientation == 3 && pairX == 0) + pairX = 1; + + calcDropCoordinates(); + }; + + /* + Places the current pair at the drop coordinates. + */ + let quickDrop = function() { + if (dropCoordinates.pivotY == -1) { + hiddenRow[dropCoordinates.pivotX] = currentPair.pivot; + } else if (dropCoordinates.pivotY > -1) { + grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot; + } + + if (dropCoordinates.leafY == -1) { + hiddenRow[dropCoordinates.leafX] = currentPair.leaf; + } else if (dropCoordinates.leafY > -1) { + grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf; + } + + currentPair.pivot = 0; + currentPair.leaf = 0; + }; + + /* + Makes all blobs fall to the lowest available slot. + All blobs that fall will be added to slotsToCheck. + */ + let settleBlobs = function() { + for (let x = 0; x < 6; x++) { + let lowestOpen = 11; + for (let y = 11; y >= 0; y--) { + if (grid[y][x] != 0) { + if (y != lowestOpen) { + grid[lowestOpen][x] = grid[y][x]; + grid[y][x] = 0; + addSlotToCheck(x, lowestOpen); + } + lowestOpen--; + } + } + + if (lowestOpen >= 0 && hiddenRow[x] != 0) { + grid[lowestOpen][x] = hiddenRow[x]; + hiddenRow[x] = 0; + addSlotToCheck(x, lowestOpen); + } + } + }; + + /* + Adds a slot to slotsToCheck. This slot will be checked for a pop + next time popAll is called. + */ + let addSlotToCheck = function(x, y) { + slotsToCheck.push({x: x, y: y}); + }; + + /* + Checks for a pop at every slot in slotsToCheck. + Pops at all locations. + */ + let popAll = function() { + let result = {pops: 0}; + while(slotsToCheck.length > 0) { + let coord = slotsToCheck.pop(); + if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) { + if (checkSlotForPop(coord.x, coord.y)) + result.pops += 1; + } + } + return result; + }; + + /* + Checks a specific slot for a pop. + If there are four or more adjacent blobs of the same color, they are removed. + */ + let checkSlotForPop = function(x, y) { + let toDelete = [ + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]), + new Uint8Array([0, 0, 0, 0, 0, 0]) + ]; + let blobsInClump = 0; + let color = grid[y][x]; + let toCheck = [{x: x, y: y}]; + + //Count every blob in this clump + while (toCheck.length > 0) { + let coord = toCheck.pop(); + if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) { + blobsInClump++; + toDelete[coord.y][coord.x] = 1; + if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y}); + if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y}); + if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1}); + if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1}); + } + if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0) + toDelete[coord.y][coord.x] = 1; //For erasing garbage + } + + //If there are at least four blobs in this clump, remove them from the grid and draw a pop. + if (blobsInClump >= 4) { + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + if (toDelete[y][x] == 1) { + grid[y][x] = 0; + + //Clear the blob out of the slot + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + //Draw the pop + let colorInfo = getColor(color); + g.setColor(colorInfo.r, colorInfo.g, colorInfo.b); + if (color < NUISANCE_COLOR) { + //A fancy pop for popped colors! + g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21); + g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30); + } else if (color == NUISANCE_COLOR) { + //Nuisance Blobs are simply crossed out. + //TODO: Nuisance Blobs are currently unusued, but also untested. Test before use. + g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + } + } + } + return true; + } + return false; + }; + + // Variables for graphics + let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0}; + + /* + Draws the time on the side. + */ + let drawTime = function(scheduleNext) { + //Change this to alter the y-coordinate of the top edge. + let dy = 25; + + g.setBgColor(0, 0, 0); + g.clearRect(2, dy, 30, dy + 121); + + //Draw the time + let d = new Date(); + let h = d.getHours(), m = d.getMinutes(); + if (twelveHourTime) { + let mer = 'A'; + if (h >= 12) mer = 'P'; + if (h >= 13) h -= 12; + if (h == 0) h = 12; + + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(mer, 23, dy + 63); + } + let hs = h.toString().padStart(2, 0); + let ms = m.toString().padStart(2, 0); + g.setFont("Vector", 24); + g.setColor(1, 0.2, 1); + g.drawString(hs, 3, dy + 21); + g.setColor(0.5, 0.5, 1); + g.drawString(ms, 3, dy + 42); + + //Draw seconds + let s = d.getSeconds(); + if (updateSeconds) { + let ss = s.toString().padStart(2, 0); + g.setFont("Vector", 12); + g.setColor(0.2, 1, 0.2); + g.drawString(ss, 3, dy + 63); + } + + //Draw the date + let dayString = d.getDate().toString(); + let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; + let dayName = dayNames[d.getDay()]; + let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"]; + let monthName = monthNames[d.getMonth()]; + g.setColor(1, 1, 1); + g.setFont("Vector", 12); + g.drawString(monthName, 3, dy + 84); + g.drawString(dayString, 3, dy + 97); + g.setColor(0.5, 0.5, 0.5); + g.drawString(dayName, 3, dy + 110); + + //Draw battery + if (s == 0) batteryLevel = E.getBattery(); + if (Bangle.isCharging()) { + g.setColor(0, 0, 1); + } else if (batteryLevel <= 15) { + g.setColor(1, 0, 0); + } else { + g.setColor(0, 1, 0); + } + g.drawString(batteryLevel + "%", 3, dy + 1); + + //Schedule the next draw if requested. + if (!scheduleNext) return; + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + let interval = updateSeconds ? 1000 : 60000; + clockDrawTimeout = setTimeout(function() { + clockDrawTimeout = undefined; + drawTime(true); + }, interval - (Date.now() % interval)); + }; + + /* + Returns a tuple in the format {r, g, b} with the color + of the blob with the given ID. + This saves memory compared to having the colors stored in an array. + */ + let getColor = function(color) { + if (color == 1) + return {r: 1, g: 0, b: 0}; + if (color == 2) + return {r: 0, g: 1, b: 0}; + if (color == 3) + return {r: 0, g: 0, b: 1}; + if (color == 4) + return {r: 1, g: 1, b: 0}; + if (color == 5) + return {r: 1, g: 0, b: 1}; + if (color == 6) + return {r: 0, g: 1, b: 1}; + if (color == 7) + return {r: 0.5, g: 0.5, b: 0.5}; + return {r: 1, g: 1, b: 1}; + }; + + /* + Clears the screen and draws the background. + */ + let drawBackground = function() { + //Background + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + g.setBgColor(0, 0, 0); + g.clearRect(33, 0, 142, 176); + g.setBgColor(0.5, 0.5, 0.5); + g.clearRect(33, 4, 142, 6); + + //Reset button + g.setBgColor(0.5, 0.5, 0.5); + g.setColor(0, 0, 0); + g.clearRect(143, 150, 175, 175); + g.setFont("Vector", 30); + g.drawString("R", 152, 150); + + //Pause button + g.clearRect(0, 150, 32, 175); + g.fillRect(9, 154, 13, 171); + g.fillRect(18, 154, 22, 171); + }; + + /* + Draws a box under the next queue that displays + the current value of lastChain. + */ + let drawChainCount = function() { + g.setBgColor(0, 0, 0); + g.setColor(1, 0.2, 0.2); + g.setFont("Vector", 23); + g.clearRect(145, 42, 173, 64); + + if (lastChain > 0) { + if (lastChain < 10) g.drawString(lastChain, 154, 44); + if (lastChain >= 10) g.drawString(lastChain, 147, 44); + } + }; + + /* + Draws the blob at the given slot. + */ + let drawBlobAtSlot = function(x, y) { + //If this blob is in the hidden row, clear it out and stop. + if (y < 0) { + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, 0, (x*18)+52, 3); + return; + } + + //First, clear what was in that slot. + g.setBgColor(0, 0, 0); + g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + + let color = grid[y][x]; + + if (color != 0) { + let myColor = getColor(color); + g.setColor(myColor.r, myColor.g, myColor.b); + g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + g.setColor(1, 1, 1); + g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21); + } + }; + + /* + Draws the ghost piece. + clearOld: if the previous location of the ghost piece should be cleared. + */ + let drawGhostPiece = function(clearOld) { + if (clearOld) { + g.setColor(0, 0, 0); + g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17); + g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17); + } + + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + let pivotColor = getColor(currentPair.pivot); + let leafColor = getColor(currentPair.leaf); + + g.setColor(pivotColor.r, pivotColor.g, pivotColor.b); + g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15); + g.setColor(1, 1, 1); + g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17); + g.setColor(leafColor.r, leafColor.g, leafColor.b); + g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15); + + oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY}; + }; + + /* + Draws the next queue. + */ + let drawNextQueue = function() { + g.setBgColor(0, 0, 0); + g.clearRect(145, 4, 173, 28); + + let p1 = nextQueue[0].pivot; + let l1 = nextQueue[0].leaf; + let p2 = nextQueue[1].pivot; + let l2 = nextQueue[1].leaf; + let p1C = getColor(p1); + let l1C = getColor(l1); + let p2C = getColor(p2); + let l2C = getColor(l2); + + g.setColor(p1C.r, p1C.g, p1C.b); + g.fillEllipse(146, 17, 157, 28); + g.setColor(l1C.r, l1C.g, l1C.b); + g.fillEllipse(146, 5, 157, 16); + g.setColor(p2C.r, p2C.g, p2C.b); + g.fillEllipse(162, 17, 173, 28); + g.setColor(l2C.r, l2C.g, l2C.b); + g.fillEllipse(162, 5, 173, 16); + + g.setColor(1, 1, 1); + g.drawLine(159, 4, 159, 28); + g.drawEllipse(146, 17, 157, 28); + g.drawEllipse(146, 5, 157, 16); + g.drawEllipse(162, 17, 173, 28); + g.drawEllipse(162, 5, 173, 16); + }; + + /* + Redraws the screen, except for the ghost piece. + */ + let redrawBoard = function() { + drawBackground(); + drawNextQueue(); + drawChainCount(); + drawTime(false); + for (let y = 0; y < 12; y++) { + for (let x = 0; x < 6; x++) { + drawBlobAtSlot(x, y); + } + } + }; + + /* + Toggles the pause screen. + */ + let togglePause = function() { + gamePaused = !gamePaused; + + if (gamePaused) { + g.setBgColor(0.5, 0.2, 0.1); + g.clear(); + drawTime(false); + + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.setFont("Vector", 20); + g.drawString("Tap here\nto unpause", 50, 68); + + require("widget_utils").show(); + Bangle.drawWidgets(); + } else { + require("widget_utils").hide(); + + redrawBoard(); + drawGhostPiece(false); + + //Display the loss text if the game is lost. + if (gameLost) { + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + } + }; + + // ~~ Events ~~ + let dragAmnt = 0; + + let onTouch = (z, e) => { + if (midChain) return; + + if (gamePaused) { + if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) { + g.setBgColor(1, 1, 1); + g.clearRect(48, 66, 157, 110); + g.flip(); + togglePause(); + } + } else { + //Tap reset button + if (e.x >= 143 && e.y >= 150) { + restartGame(); + newPair(); + redrawBoard(); + drawGhostPiece(false); + g.flip(); + return; + } + + //Tap pause button + if (e.x <= 32 && e.y >= 150) { + togglePause(); + return; + } + + //While playing, rotate pieces. + if (!gameLost && !gamePaused) { + if (e.x < 88) { + rotatePair(false); + drawGhostPiece(true); + } else { + rotatePair(true); + drawGhostPiece(true); + } + } + } + }; + + Bangle.on("touch", onTouch); + + let onDrag = (e) => { + if (gameLost || gamePaused || midChain) return; + + //Do nothing if the user is dragging down so that they don't accidentally move while dropping + if (e.dy >= 5) { + return; + } + + dragAmnt += e.dx; + if (e.b == 0) { + dragAmnt = 0; + } + if (dragAmnt >= 20) { + movePair(Math.floor(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + if (dragAmnt <= -20) { + movePair(Math.ceil(dragAmnt / 20)); + drawGhostPiece(true); + dragAmnt = dragAmnt % 20; + } + }; + + Bangle.on("drag", onDrag); + + let onSwipe = (x, y) => { + if (gameLost || gamePaused || midChain) return; + + if (y > 0) { + let pivotX = dropCoordinates.pivotX; + let pivotY = dropCoordinates.pivotY; + let leafX = dropCoordinates.leafX; + let leafY = dropCoordinates.leafY; + + if (pivotY < -1 && leafY < -1) return; + + quickDrop(); + drawBlobAtSlot(pivotX, pivotY); + drawBlobAtSlot(leafX, leafY); + g.flip(); + + //Check for pops + if (pivotY >= 0) addSlotToCheck(pivotX, pivotY); + if (leafY >= 0) addSlotToCheck(leafX, leafY); + midChain = true; + let currentChain = 0; + while (popAll().pops > 0) { + currentChain++; + lastChain = currentChain; + drawChainCount(); + g.flip(); + settleBlobs(); + redrawBoard(); + g.flip(); + } + + newPair(); + drawNextQueue(); + drawGhostPiece(false); + + //If the top slot of the third column is taken, lose the game. + if (grid[0][2] != 0) { + gameLost = true; + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + g.clearRect(33, 73, 142, 103); + g.setFont("Vector", 20); + g.drawString("You Lose", 43, 80); + } + + midChain = false; + } + }; + + Bangle.on("swipe", onSwipe); + + let onLock = on => { + updateSeconds = !on; + drawTime(true); + }; + + Bangle.on('lock', onLock); + + let onCharging = charging => { + drawTime(false); + }; + + Bangle.on('charging', onCharging); + + Bangle.setUI({mode:"clock", remove:function() { + //Remove listeners + Bangle.removeListener("touch", onTouch); + Bangle.removeListener("drag", onDrag); + Bangle.removeListener("swipe", onSwipe); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + + if (clockDrawTimeout) clearTimeout(clockDrawTimeout); + require("widget_utils").show(); + }}); + + g.reset(); + + Bangle.loadWidgets(); + require("widget_utils").hide(); + + drawBackground(); + drawTime(true); + + restartGame(); + + newPair(); + drawGhostPiece(false); + + drawNextQueue(); + drawChainCount(); +} diff --git a/apps/bblobface/app.png b/apps/bblobface/app.png new file mode 100644 index 000000000..2201fa621 Binary files /dev/null and b/apps/bblobface/app.png differ diff --git a/apps/bblobface/metadata.json b/apps/bblobface/metadata.json new file mode 100644 index 000000000..8393755b0 --- /dev/null +++ b/apps/bblobface/metadata.json @@ -0,0 +1,15 @@ +{ "id": "bblobface", + "name": "Bangle Blobs Clock", + "shortName":"BBClock", + "icon": "app.png", + "version": "1.00", + "description": "A fully featured watch face with a playable game on the side.", + "readme":"README.md", + "type": "clock", + "tags": "clock,game", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"bblobface.app.js","url":"app.js"}, + {"name":"bblobface.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bblobface/screenshot1.png b/apps/bblobface/screenshot1.png new file mode 100644 index 000000000..91650c07a Binary files /dev/null and b/apps/bblobface/screenshot1.png differ diff --git a/apps/bblobface/screenshot2.png b/apps/bblobface/screenshot2.png new file mode 100644 index 000000000..64644965f Binary files /dev/null and b/apps/bblobface/screenshot2.png differ diff --git a/apps/bclock/ChangeLog b/apps/bclock/ChangeLog index 5b2cf598c..79c198431 100644 --- a/apps/bclock/ChangeLog +++ b/apps/bclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: Update to use Bangle.setUI instead of setWatch +0.04: Tell clock widgets to hide. diff --git a/apps/bclock/clock-binary.js b/apps/bclock/clock-binary.js index fdf945ee6..c08a7abe6 100644 --- a/apps/bclock/clock-binary.js +++ b/apps/bclock/clock-binary.js @@ -100,10 +100,12 @@ Bangle.on('lcdPower', on => { if (on) drawClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(() => { drawClock(); }, 1000); drawClock(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/bclock/metadata.json b/apps/bclock/metadata.json index 94219a30b..c6a24d89f 100644 --- a/apps/bclock/metadata.json +++ b/apps/bclock/metadata.json @@ -1,7 +1,7 @@ { "id": "bclock", "name": "Binary Clock", - "version": "0.03", + "version": "0.04", "description": "A simple binary clock watch face", "icon": "clock-binary.png", "type": "clock", diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog index 84ec7c1d7..4646b4010 100644 --- a/apps/beebclock/ChangeLog +++ b/apps/beebclock/ChangeLog @@ -4,3 +4,4 @@ 0.04: Update to use Bangle.setUI instead of setWatch 0.05: Avoid 'loadWidgets' at LCD on, which will cause memory leak Avoid clearTimeout() usage, as it may break other widgets +0.06: Minor code improvements diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js index c85f68c55..d220096d2 100644 --- a/apps/beebclock/beebclock.js +++ b/apps/beebclock/beebclock.js @@ -108,10 +108,10 @@ for (let h=1; h<=12; h++) { // so we buffer once and minute, and draw the second hand dynamically // (with a bit of flicker) const drawFace = (G) => { - const fw = R1 * 2; - const fh = R1 * 2; - const fw2 = R1; - const fh2 = R1; + //const fw = R1 * 2; + //const fh = R1 * 2; + //const fw2 = R1; + //const fh2 = R1; let hs = []; // Wipe the image and start with white @@ -182,7 +182,7 @@ const drawAll = (force) => { if (!faceImg) force = true; let face_changed = force; - let date_changed = false; + //let date_changed = false; tmp = hours; hours = now.getHours(); @@ -214,7 +214,7 @@ const drawAll = (force) => { tmp = date; date = now.getDate(); if (tmp !== date) { - date_changed = true; + //date_changed = true; face_changed = true; // Should have changed anyway with hour/minute rollover } } diff --git a/apps/beebclock/metadata.json b/apps/beebclock/metadata.json index 31316a80c..5790cf564 100644 --- a/apps/beebclock/metadata.json +++ b/apps/beebclock/metadata.json @@ -1,7 +1,7 @@ { "id": "beebclock", "name": "Beeb Clock", - "version": "0.05", + "version": "0.06", "description": "Clock face that may be coincidentally familiar to BBC viewers", "icon": "beebclock.png", "type": "clock", diff --git a/apps/nato/changelog.txt b/apps/beeptest/ChangeLog similarity index 100% rename from apps/nato/changelog.txt rename to apps/beeptest/ChangeLog diff --git a/apps/beeptest/README.md b/apps/beeptest/README.md new file mode 100644 index 000000000..dcb1f1574 --- /dev/null +++ b/apps/beeptest/README.md @@ -0,0 +1,27 @@ +# App Name + +Beep Test + +## Usage + +Mark out a 20m space +Click the side button to start the test +Shuttle run between your markers when the watch buzzes +Push the button when you need to stop + +## Features + +Buzzing on each shuttle run +Results page with vO2max and total distance covered. + +## Controls + +Side button starts, stops and resets the app. + +## Requests + +bb0x88 on giuthub + +## Creator + +Blade diff --git a/apps/beeptest/app-icon.js b/apps/beeptest/app-icon.js new file mode 100644 index 000000000..b29e2b459 --- /dev/null +++ b/apps/beeptest/app-icon.js @@ -0,0 +1,2 @@ +require("heatshrink").decompress(atob("mEw4UA///+f8lky6f8HFmqBRMK1WgBAtUBYUABYtVqtAgEoAIQACioLBqALHBQIABBZMFEgIjHgEBqtUHY4aDKZA+CoBrIBYJJBBZJuCAA3VBYkC1QABGoJhDBYxTBBYUFEQoLDoEVSgIADO4ILCUASdGqtRGIYLFKoY7CIwdUEwJtBBYY6CqADBFwoLDDYIuFIwQUBigLITJQLFHYKNEHAgLGXw6NDBZbKHTIYLLKg6lDBY4KDEY5EIIwahFHQoKIBYIrHIwYLLuALJHRTcHAAjcGAEwA==")) + diff --git a/apps/beeptest/beeptest.js b/apps/beeptest/beeptest.js new file mode 100644 index 000000000..5f6438c24 --- /dev/null +++ b/apps/beeptest/beeptest.js @@ -0,0 +1,274 @@ +var Layout = require("Layout"); + +// Beep Test Data +const BEET_TEST_DATA = [ + { shuttles: 7, timePerShuttle: 9.0, totalTime: 63.0, distancePerLevel: 140 }, + { shuttles: 8, timePerShuttle: 8.0, totalTime: 64.0, distancePerLevel: 160 }, + { shuttles: 8, timePerShuttle: 7.58, totalTime: 60.6, distancePerLevel: 160 }, + { shuttles: 9, timePerShuttle: 7.2, totalTime: 64.8, distancePerLevel: 180 }, + { shuttles: 9, timePerShuttle: 6.86, totalTime: 61.7, distancePerLevel: 180 }, + { + shuttles: 10, + timePerShuttle: 6.55, + totalTime: 65.5, + distancePerLevel: 200, + }, + { + shuttles: 10, + timePerShuttle: 6.26, + totalTime: 62.6, + distancePerLevel: 200, + }, + { shuttles: 11, timePerShuttle: 6.0, totalTime: 66.0, distancePerLevel: 220 }, + { + shuttles: 11, + timePerShuttle: 5.76, + totalTime: 63.4, + distancePerLevel: 220, + }, + { + shuttles: 11, + timePerShuttle: 5.54, + totalTime: 60.9, + distancePerLevel: 220, + }, + { + shuttles: 12, + timePerShuttle: 5.33, + totalTime: 64.0, + distancePerLevel: 240, + }, + { + shuttles: 12, + timePerShuttle: 5.14, + totalTime: 61.7, + distancePerLevel: 240, + }, + { + shuttles: 13, + timePerShuttle: 4.97, + totalTime: 64.6, + distancePerLevel: 260, + }, + { shuttles: 13, timePerShuttle: 4.8, totalTime: 62.4, distancePerLevel: 260 }, + { + shuttles: 13, + timePerShuttle: 4.65, + totalTime: 60.4, + distancePerLevel: 260, + }, + { shuttles: 14, timePerShuttle: 4.5, totalTime: 63.0, distancePerLevel: 280 }, + { + shuttles: 14, + timePerShuttle: 4.36, + totalTime: 61.1, + distancePerLevel: 280, + }, + { + shuttles: 15, + timePerShuttle: 4.24, + totalTime: 63.5, + distancePerLevel: 300, + }, + { + shuttles: 15, + timePerShuttle: 4.11, + totalTime: 61.7, + distancePerLevel: 300, + }, + { shuttles: 16, timePerShuttle: 4.0, totalTime: 64.0, distancePerLevel: 320 }, + { + shuttles: 16, + timePerShuttle: 3.89, + totalTime: 62.3, + distancePerLevel: 320, + }, +]; + +// VO2max Data +const VO2MAX_DATA = [ + { level: 1, vo2max: 16.7 }, + { level: 2, vo2max: 23.0 }, + { level: 3, vo2max: 26.2 }, + { level: 4, vo2max: 29.3 }, + { level: 5, vo2max: 32.5 }, + { level: 6, vo2max: 35.7 }, + { level: 7, vo2max: 38.8 }, + { level: 8, vo2max: 42.0 }, + { level: 9, vo2max: 45.1 }, + { level: 10, vo2max: 48.3 }, + { level: 11, vo2max: 51.5 }, + { level: 12, vo2max: 54.6 }, + { level: 13, vo2max: 57.8 }, + { level: 14, vo2max: 60.9 }, + { level: 15, vo2max: 64.1 }, + { level: 16, vo2max: 67.3 }, + { level: 17, vo2max: 70.4 }, + { level: 18, vo2max: 73.6 }, + { level: 19, vo2max: 76.7 }, + { level: 20, vo2max: 79.9 }, + { level: 21, vo2max: 83.0 }, +]; + +let currentLevel = 0; +let currentShuttle = 0; +let timeRemaining = 0; +let intervalId; +let beepTestLayout; +let testState = "start"; // 'start' | 'running' | 'result' + +function initBeepTestLayout() { + beepTestLayout = new Layout( + { + type: "v", + c: [ + { type: "txt", font: "30%", pad: 0, label: "Start Test", id: "status" }, + { type: "txt", font: "15%", pad: 0, label: "", id: "level" }, + { type: "txt", font: "10%", pad: 0, label: "", id: "vo2max" }, // Smaller font for VO2max + { type: "txt", font: "10%", pad: 0, label: "", id: "distance" }, // Smaller font for Distance + ], + }, + { + btns: [ + { + label: "Start/Stop", + cb: (l) => { + if (testState === "start") { + startTest(); + } else if (testState === "running") { + stopTest(); + } else { + showStartScreen(); + } + }, + }, + ], + }, + ); +} + +function showStartScreen() { + testState = "start"; + g.clear(); + beepTestLayout.clear(beepTestLayout.status); + beepTestLayout.status.label = "Start\nTest"; + beepTestLayout.clear(beepTestLayout.level); + beepTestLayout.level.label = ""; + beepTestLayout.clear(beepTestLayout.vo2max); // Clear VO2max text + beepTestLayout.vo2max.label = ""; + beepTestLayout.clear(beepTestLayout.distance); // Clear Distance text + beepTestLayout.distance.label = ""; + beepTestLayout.render(); +} + +function startTest() { + testState = "running"; + currentLevel = 0; + currentShuttle = 0; + Bangle.buzz(2000); // Buzz for 2 seconds at the start of the test + runLevel(); +} + +function runLevel() { + if (currentLevel >= BEET_TEST_DATA.length) { + stopTest(); + return; + } + + const levelData = BEET_TEST_DATA[currentLevel]; + timeRemaining = levelData.timePerShuttle * 1000; // Convert to milliseconds + updateDisplay(); + + if (intervalId) clearInterval(intervalId); + intervalId = setInterval(() => { + if (timeRemaining <= 0) { + currentShuttle++; + Bangle.buzz(100); // Short buzz after each shuttle + + if (currentShuttle >= levelData.shuttles) { + // Buzz longer or twice at the end of each level + Bangle.buzz(1000); // Buzz for 1 second at level end + setTimeout(() => Bangle.buzz(1000), 500); // Buzz again after 0.5 seconds + currentLevel++; + currentShuttle = 0; + runLevel(); + return; + } + + timeRemaining = levelData.timePerShuttle * 1000; // Reset to original time for the next shuttle + } + + updateDisplay(); + timeRemaining -= 100; // Decrement time by 100 milliseconds + }, 100); // Update every 100 milliseconds +} + +function updateDisplay() { + g.clear(); // Clear the entire screen + beepTestLayout.status.label = formatTime(timeRemaining); + beepTestLayout.level.label = `Level: ${currentLevel + 1}.${currentShuttle + 1}`; + beepTestLayout.render(); +} + +function stopTest() { + g.clear(); // Clear the entire screen + testState = "result"; + clearInterval(intervalId); + + // Determine previous level and shuttle + let prevLevel = currentLevel; + let prevShuttle = currentShuttle; + + if (prevShuttle === 0) { + if (prevLevel > 0) { + prevLevel--; + prevShuttle = BEET_TEST_DATA[prevLevel].shuttles - 1; + } else { + prevShuttle = 0; + } + } else { + prevShuttle--; + } + + // Determine VO2max and total distance + const vo2max = getVO2max(prevLevel + 1); + const totalDistance = calculateTotalDistance(prevLevel + 1); + + beepTestLayout.clear(beepTestLayout.status); + beepTestLayout.status.label = "Result"; + beepTestLayout.clear(beepTestLayout.level); + beepTestLayout.level.label = `Level: ${prevLevel + 1}.${prevShuttle + 1}`; + beepTestLayout.clear(beepTestLayout.vo2max); + beepTestLayout.vo2max.label = `VO2max: ${vo2max}`; + beepTestLayout.clear(beepTestLayout.distance); + beepTestLayout.distance.label = `Distance: ${totalDistance} m`; + beepTestLayout.render(); +} + +function getVO2max(level) { + const result = VO2MAX_DATA.find((item) => item.level === level); + return result ? result.vo2max : "N/A"; +} + +function calculateTotalDistance(level) { + // Calculate the total number of shuttles completed + let totalShuttles = 0; + for (let i = 0; i < level - 1; i++) { + totalShuttles += BEET_TEST_DATA[i].shuttles; + } + const levelData = BEET_TEST_DATA[level - 1]; + totalShuttles += levelData.shuttles; // Add the shuttles completed in the current level + const distancePerShuttle = 20; // Distance per shuttle in meters + return totalShuttles * distancePerShuttle; // Total distance +} + +function formatTime(milliseconds) { + let seconds = Math.floor(milliseconds / 1000); + let tenths = Math.floor((milliseconds % 1000) / 100); // Get tenths of a second + return (seconds < 10 ? "" : "") + seconds + "." + tenths; // Display only the tenths digit +} + +// Initialize the app +Bangle.setLCDPower(1); // Keep the watch LCD lit up +initBeepTestLayout(); +showStartScreen(); diff --git a/apps/beeptest/beeptest.png b/apps/beeptest/beeptest.png new file mode 100644 index 000000000..ee892bbe2 Binary files /dev/null and b/apps/beeptest/beeptest.png differ diff --git a/apps/beeptest/metadata.json b/apps/beeptest/metadata.json new file mode 100644 index 000000000..7c25a3848 --- /dev/null +++ b/apps/beeptest/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "beeptest", + "name": "Beep Test", + "shortName": "Beep Test", + "version": "0.01", + "description": "Aerobic fitness test created by Léger & Lambert", + "icon": "beeptest.png", + "tags": "health", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "beeptest.app.js", "url": "beeptest.js" }, + { "name": "beeptest.img", "url": "app-icon.js", "evaluate": true } + ] +} diff --git a/apps/beer/ChangeLog b/apps/beer/ChangeLog new file mode 100644 index 000000000..21ec45242 --- /dev/null +++ b/apps/beer/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Added adjustment for Bangle.js magnetometer heading fix + Bangle.js 2 compatibility diff --git a/apps/beer/app-icon.js b/apps/beer/app-icon.js index c700b3bd2..734985cb5 100644 --- a/apps/beer/app-icon.js +++ b/apps/beer/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4")) +require("heatshrink").decompress(atob("mEw4cA///wH9/++1P+u3//3/qv/gv+KHkJkmABxcBBwNJkmQCJYOByQCCCBUCCItJkARQkgQHggLBku25IRDJQ4LCtu27Mt2RKJCInbAQIRLpYROglt24OB6wSC7dwLQ4LB9u2EgfbsARJ8u2mwRO+u3CNJtHCJFpCINALJoRCpCiGBoMSdQcpegIRGyaPB+QRDkARIyQRBc4YRKyet23iCJxHB6QRBzOJCJ+dCJY1CpfMGphrCp2YNZlL54CBEZgLBAQoRBiTFFCNMvmQRPndiEcJHEyQQECJMpAYIRQyARQwAROI4IAGB4wCBNAoRmhIRHCA4A/AAo")) diff --git a/apps/beer/custom.html b/apps/beer/custom.html index a357ab378..f0895f93f 100644 --- a/apps/beer/custom.html +++ b/apps/beer/custom.html @@ -127,6 +127,8 @@ var img_nofix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF8u02gziGBoyhQ5gwDGRozRGCQydGCgybGCwyZC5gAaGPQwnGRAwpGQ4xwGFYyFDKsrlYxYDCsBmUyg4yXLyUsFwMyq1WAgUsNCRjUmVXroAEq8yMbcllkskwCEkplDmQwDq0sC54xEHQ9RqQAGqIwCFgOBAASYBSgMBltRAA0sgJsOGJeBxAAGwMrgIXIloxOJYNSvl8CwIDCqMBlYxNC4wxQDIOCwVYDIIDBGJ9YwV8rADBwRJCSqAVCAYaVMC4oxCPYYxQSo4xMSpIxPY4T5HY54XIMbIxKgwXKfKjhEllWGJNWlgXJGLNXruCGI+CrtXGKP+GJB9HMZ6VO/wxJcI8lfJclfKAxKfJEAGJIXLGKSvBWYQZCMZbfEqTHBGJYyFfIo1DGJ4tDGJQwCGJB9IMZyVNGIYyEfJQxPfJgwEMgoZJgAxMltRAA0tGJQyEksslkmAQklGINXxDTBFwIDCq8rC4YACC4gwJMowAJldWAAwwBABowIGJ4AYGJIymGBQylGBgyjGBwyhGCAzeF6YycGCwzYF7IzVF7o1PDqYA==")); var img_fix = require("heatshrink").decompress(atob("mUyxH+ACYhJDygtYGsqLVF94zaDYkq6wAOlQyYJo2A63VAAIoC2m0GI16My5/H5/V64ABGQIwBGQ+rTKwWHkhiBGIYwDGQ3VZioVIqoiBGAJhEGRFPGSYTIYwQxCGA4yFqodJGKeqSgQwJGQmkGKQSJfAYwLGQfPDxQwRgHVfAi/EAA4xLGQwRLYwb5BABoxQCBcA43G5wABAgIAMEBgxQ0QxB54xB5gAG4xgBBYOiGJ4PMGInPGIhcCGIt4EJoxPvHM5oxBGAnO6xrCGoXMqgxdpwxD5qQFL4QADlQxdgAhBGILIDMYoADEBwwPgCHBfQzHDAAb4NACTIIAA74OACLIIMo7GOACQoBZAoHBHQPNA4QwggGiZBA5B54HBY0DIKMYtUGMMqFYLIGY4jGhZAr6FAAYwiZAgxIY0TIFfQgADvAfR/zISGJTGR/wxRkj6CGJBiSGKL6DGP4xOGSKVDGAwxRGAQxU5oxcGR75DGJEkGCYxPlXM5vPGA/MlQxUGR1OGIL4I5lOGCgyOqgxBShHMqgwVGJt4GJd4GKwyMvHG5vGABAxMGBQyM1mtABWsGC4yLGBYABGDAyKGKwwQGZKVUF6b/OABowWGbAvZGaovdGp4dTA")); +var W = g.getWidth(), H = g.getHeight(); + // https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js function project(latlong) { var d = Math.PI / 180, @@ -170,32 +172,30 @@ Bangle.on('GPS', function(f) { Bangle.on('mag', function(m) { if (!Bangle.isLCDOn()) return; - var headingrad = m.heading*Math.PI/180; // in radians + var headingrad = (360-m.heading)*Math.PI/180; // in radians if (!isFinite(headingrad)) headingrad=0; if (nearest) - g.drawImage(img_fix,120,120,{ + g.drawImage(img_fix,W/2,H/2,{ rotate: (Math.PI/2)+headingrad-nearestangle, scale:3, }); else - g.drawImage(img_nofix,120,120,{ + g.drawImage(img_nofix,W/2,H/2,{ rotate: headingrad, scale:2, }); - g.clearRect(60,0,180,24); - g.setFontAlign(0,0); - g.setFont("6x8"); + g.clearRect(0,0,W,24).setFontAlign(0,0).setFont("6x8"); if (fix.fix) { - g.drawString(nearest ? nearest.name : "---",120,4); + g.drawString(nearest ? nearest.name : "---",W/2,4); g.setFont("6x8",2); - g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16); + g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",W/2,16); } else { - g.drawString(fix.satellites+" satellites",120,4); + g.drawString(fix.satellites+" satellites",W/2,4); } }); Bangle.setCompassPower(1); Bangle.setGPSPower(1); -g.clear();`; +g.setColor("#fff").setBgColor("#000").clear();`; sendCustomizedApp({ storage:[ diff --git a/apps/beer/metadata.json b/apps/beer/metadata.json index cf69aee90..3a2421bd1 100644 --- a/apps/beer/metadata.json +++ b/apps/beer/metadata.json @@ -1,11 +1,11 @@ { "id": "beer", "name": "Beer Compass", - "version": "0.01", + "version": "0.02", "description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one", "icon": "app.png", "tags": "", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "storage": [ {"name":"beer.app.js"}, diff --git a/apps/berlinc/ChangeLog b/apps/berlinc/ChangeLog index 9e9c1a6aa..6820ab11d 100644 --- a/apps/berlinc/ChangeLog +++ b/apps/berlinc/ChangeLog @@ -4,3 +4,6 @@ 0.05: Update *on* the minute rather than every 15 secs Now show widgets Make compatible with themes, and Bangle.js 2 +0.06: Enable fastloading +0.07: Adds fullscreen mode setting +0.08: Minor code improvements diff --git a/apps/berlinc/berlin-clock.js b/apps/berlinc/berlin-clock.js index 0dd8ff8ee..94fbe5be1 100644 --- a/apps/berlinc/berlin-clock.js +++ b/apps/berlinc/berlin-clock.js @@ -1,32 +1,40 @@ +{ // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // https://github.com/eska-muc/BangleApps + +var settings = require('Storage').readJSON("berlinc.json", true) || {}; const fields = [4, 4, 11, 4]; -const offset = 24; -const width = g.getWidth() - 2 * offset; -const height = g.getHeight() - 2 * offset; -const rowHeight = height / 4; -var show_date = false; -var show_time = false; -var yy = 0; +let fullscreen = !!settings.fullscreen; -var rowlights = []; -var time_digit = []; +let show_date = false; +let show_time = false; + +let rowlights = []; +let time_digit = []; // timeout used to update every minute -var drawTimeout; +let drawTimeout; // schedule a draw for the next minute -function queueDraw() { +let queueDraw = () => { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); -} +}; -function draw() { - g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); +let draw = () => { + let width = Math.min(Bangle.appRect.w,Bangle.appRect.h); + let height = width; + let offset = g.getHeight() - height; + let x = Math.floor((g.getWidth() - width)/2); + + if (show_date) height -= 8; + let rowHeight = (height - 1) / 4; + g.setBgColor(g.theme.bg); + g.reset().clearRect(Bangle.appRect); var now = new Date(); // show date below the clock @@ -37,7 +45,7 @@ function draw() { var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; var strWidth = g.stringWidth(dateString); g.setColor(g.theme.fg).setFontAlign(-1,-1); - g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4); + g.drawString(dateString, ( Bangle.appRect.x + Bangle.appRect.w - strWidth ) / 2, Bangle.appRect.y2 - 5); } rowlights[0] = Math.floor(now.getHours() / 5); @@ -50,15 +58,16 @@ function draw() { time_digit[2] = Math.floor(now.getMinutes() / 10); time_digit[3] = now.getMinutes() % 10; - g.drawRect(offset, offset, width + offset, height + offset); + g.setColor(g.theme.fg); + g.drawRect(x, offset, x + width - 1, height + offset - 1); for (row = 0; row < 4; row++) { nfields = fields[row]; - boxWidth = width / nfields; + boxWidth = (width - 1) / nfields; for (col = 0; col < nfields; col++) { - x1 = col * boxWidth + offset; + x1 = col * boxWidth + x; y1 = row * rowHeight + offset; - x2 = (col + 1) * boxWidth + offset; + x2 = (col + 1) * boxWidth + x; y2 = (row + 1) * rowHeight + offset; g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2); @@ -84,33 +93,53 @@ function draw() { queueDraw(); } -function toggleDate() { +let toggleDate = () => { show_date = ! show_date; draw(); } -function toggleTime() { +let toggleTime = () => { show_time = ! show_time; draw(); } -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +let clear = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +let onLcdPower = on => { if (on) { draw(); // draw immediately, queue redraw } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; + clear(); } -}); +} + +let cleanup = () => { + clear(); + Bangle.removeListener("lcdPower", onLcdPower); + require("widget_utils").show(); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',onLcdPower); // Show launcher when button pressed, handle up/down -Bangle.setUI("clockupdown", dir=> { +Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> { if (dir<0) toggleTime(); if (dir>0) toggleDate(); }); g.clear(); Bangle.loadWidgets(); + +if (fullscreen){ + if (process.env.HWVERSION == 2) require("widget_utils").swipeOn(); + else require("widget_utils").hide(); +} + Bangle.drawWidgets(); + draw(); +} \ No newline at end of file diff --git a/apps/berlinc/metadata.json b/apps/berlinc/metadata.json index 85c42fc47..983602d90 100644 --- a/apps/berlinc/metadata.json +++ b/apps/berlinc/metadata.json @@ -1,7 +1,7 @@ { "id": "berlinc", "name": "Berlin Clock", - "version": "0.05", + "version": "0.08", "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", "icon": "berlin-clock.png", "type": "clock", @@ -12,6 +12,8 @@ "screenshots": [{"url":"berlin-clock-screenshot.png"}], "storage": [ {"name":"berlinc.app.js","url":"berlin-clock.js"}, + {"name":"berlinc.settings.js","url":"settings.js"}, {"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true} - ] + ], + "data": [{"name":"berlinc.json"}] } diff --git a/apps/berlinc/settings.js b/apps/berlinc/settings.js new file mode 100644 index 000000000..b240a5f46 --- /dev/null +++ b/apps/berlinc/settings.js @@ -0,0 +1,26 @@ +(function(back) { + var FILE = "berlinc.json"; + var settings = Object.assign({ + fullscreem: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var mainmenu = { + "": { + "title": "Berlin clock" + }, + "< Back": () => back(), + "Fullscreen": { + value: !!settings.fullscreen, + onchange: v => { + settings.fullscreen = v; + writeSettings(); + } + } + }; + E.showMenu(mainmenu); + +}) diff --git a/apps/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog new file mode 100644 index 000000000..4089c823d --- /dev/null +++ b/apps/bigdclock/ChangeLog @@ -0,0 +1,9 @@ +0.01: Initial version +0.02: setTimeout bug fix; no leading zero on date; lightmode; 12 hour format; cleanup +0.03: Internationalisation; bug fix - battery icon responds promptly to charging state +0.04: bug fix +0.05: proper fix for the race condition in queueDraw() +0.06: Tell clock widgets to hide. +0.07: Better battery graphic - now has green, yellow and red sections; battery status reflected in the bar across the middle of the screen; current battery state checked only once every 15 minutes, leading to longer-lasting battery charge +0.08: Minor code improvements +0.09: Something was changing the value of the "width" variable, which caused the battery usage feature to malfunction. The "width" variable has been renamed - the cause remains a mystery. diff --git a/apps/bigdclock/README.md b/apps/bigdclock/README.md new file mode 100644 index 000000000..71f4362fa --- /dev/null +++ b/apps/bigdclock/README.md @@ -0,0 +1,14 @@ +# Big Digit Clock + +There are a number of big digit clocks available for the Bangle, but this is +the first which shows all the essential information that a clock needs to show +in a manner that is easy to read by those with poor eyesight. + +The clock shows the time-of-day, the day-of-week and the day-of-month, as well +as an easy-to-see icon showing the current charge on the battery. + +![screenshot](./screenshot.png) + +## Creator + +Created by [Deirdre O'Byrne](https://github.com/deirdreobyrne) diff --git a/apps/bigdclock/bigdclock.app.js b/apps/bigdclock/bigdclock.app.js new file mode 100644 index 000000000..0ebc33bed --- /dev/null +++ b/apps/bigdclock/bigdclock.app.js @@ -0,0 +1,108 @@ +// + +Graphics.prototype.setFontOpenSans = function(scale) { + // Actual height 48 (50 - 3) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAAP8AAAAAAAAB/wAAAAAAAAH/gAAAAAAAAf+AAAAAAAAB/4AAAAAAAAH/gAAAAAAAAf8AAAAAAAAA/wAAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAPAAAAAAAAAH8AAAAAAAAD/wAAAAAAAA//AAAAAAAAf/8AAAAAAAP//wAAAAAAH///AAAAAAB///4AAAAAA///8AAAAAAf///AAAAAAH///gAAAAAD///wAAAAAB///4AAAAAAf//+AAAAAAP///AAAAAAH///gAAAAAA///4AAAAAAD//8AAAAAAAP/+AAAAAAAA//gAAAAAAAD/wAAAAAAAAP4AAAAAAAAA8AAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAP///AAAAAAH////gAAAAD/////gAAAAf/////gAAAH//////AAAA//////+AAAD//////8AAAf//////4AAD//4AH//gAAP/gAAAf/AAA/4AAAAf8AAH/AAAAA/4AAf4AAAAB/gAB/AAAAAD+AAH8AAAAAP4AAfwAAAAA/gAB/AAAAAD+AAH8AAAAAP4AAf4AAAAB/gAB/gAAAAH+AAD/gAAAB/wAAP/gAAAf/AAA//4AAf/8AAB///////gAAD//////8AAAH//////wAAAP/////+AAAAf/////wAAAA/////8AAAAAf////AAAAAAf///gAAAAAAB//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHgAAAAAAAAA/AAAAAAAAAH+AAAAAAAAA/4AAAAAAAAD/AAAAAAAAAf8AAAAAAAAD/gAAAAAAAAf8AAAAAAAAB/gAAAAAAAAP8AAAAAAAAB/wAAAAAAAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAD8AAAOAAAAAfwAAB+AAAAD/AAAH8AAAAf8AAA/wAAAH/wAAH/AAAA//AAAf4AAAH/8AAD/gAAA//wAAP8AAAH//AAA/gAAA//8AAD+AAAH//wAAf4AAA/9/AAB/AAAH/n8AAH8AAA/8fwAAfwAAH/h/AAB/AAA/8H8AAH8AAH/gfwAAfwAA/8B/AAB/gAH/gH8AAH+AB/8AfwAAP8Af/gB/AAA////8AH8AAD////gAfwAAH///8AB/AAAf///gAH8AAA///8AAfwAAB///gAB/AAAD//4AAH8AAAH/+AAAfwAAAH/gAAB/AAAAAAAAAH8AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAYAAAAB/gAAB4AAAAD+AAAPwAAAAP4AAA/gAAAAfwAAH+AAAAB/AAAf4AAAAH8AAD/AB+AAfwAAP8AH4AA/gAA/gAfgAD+AAH+AB+AAP4AAfwAH4AA/gAB/AAfgAD+AAH8AB+AAP4AAfwAH4AA/gAB/AA/wAD+AAH8AD/AAP4AAfwAP8AA/gAB/AA/wAD+AAH+AH/AAf4AAf4Af+AB/AAA/wH/8AP8AAD////4D/wAAP//+////AAAf//7///4AAB///P///gAAD//8f//8AAAP//h///gAAAf/8D//+AAAA//gH//wAAAA/4AP/8AAAAAAAAP/AAAAAAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAA/wAAAAAAAAH/AAAAAAAAB/8AAAAAAAAP/wAAAAAAAD//AAAAAAAAf/8AAAAAAAH//wAAAAAAA/+/AAAAAAAP/z8AAAAAAB/8PwAAAAAAf/g/AAAAAAD/4D8AAAAAAf/APwAAAAAH/4A/AAAAAA/+AD8AAAAAP/wAPwAAAAB/8AA/AAAAAf/gAD8AAAAD/4AAPwAAAA//AAA/AAAAD///////wAAP///////AAA///////8AAD///////wAAP///////AAA///////8AAD///////wAAP///////AAAAAAAA/AAAAAAAAAD8AAAAAAAAAPwAAAAAAAAA/AAAAAAAAAD8AAAAAAAAAPwAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAQAB/gAAAD//gAD+AAA////AAP8AAD///8AAfwAAP///wAB/AAA////AAH8AAD///8AAf4AAP///wAA/gAA////AAD+AAD/+H8AAP4AAP4AfwAA/gAA/gB+AAD+AAD+AH4AAP4AAP4AfwAA/gAA/gB/AAD+AAD+AH8AAP4AAP4AfwAB/gAA/gB/AAH+AAD+AH+AA/wAAP4Af8AD/AAA/gB/4A/8AAD+AD////gAAP4AP///+AAA/gAf///wAAD+AB////AAAP4AD///4AAA/gAH///AAAAAAAP//4AAAAAAAf/+AAAAAAAAf/gAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//AAAAAAAf///gAAAAAP////gAAAAD/////gAAAAf/////AAAAD/////+AAAA//////8AAAD//////4AAAf/8/4//gAAD/8H+Af/AAAP/AfgAf8AAB/wD+AA/wAAH+APwAB/gAA/wB/AAD+AAD+AH4AAP4AAP4AfgAA/gAB/gB+AAD+AAH8AH4AAP4AAfwAfgAA/gAB/AB/AAH+AAH8AH8AAf4AAfwAf4AD/AAB/AB/4A/8AAH8AH////wAAfwAP///+AAB/AA////4AAH8AB////AAAfwAD///4AAA/AAH///AAAAAAAP//4AAAAAAAP/+AAAAAAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAA/gAAAAAAAAD+AAAAAAAAAP4AAAAAAAAA/gAAAAAAAAD+AAAAAAAAAP4AAAAADAAA/gAAAAB8AAD+AAAAAfwAAP4AAAAH/AAA/gAAAB/8AAD+AAAAf/wAAP4AAAP//AAA/gAAD//8AAD+AAA///wAAP4AAP//8AAA/gAD///AAAD+AB///wAAAP4Af//4AAAA/gH//+AAAAD+B///gAAAAP4f//wAAAAA/v//8AAAAAD////AAAAAAP///wAAAAAA///4AAAAAAD//+AAAAAAAP//gAAAAAAA//wAAAAAAAD/8AAAAAAAAP/AAAAAAAAA/wAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAH/wAAAAH8AB//gAAAB/8AP//gAAAf/8B//+AAAB//4P//8AAAP//w///4AAB///n///gAAH//+////AAA/////gf8AAD/h//4AfwAAP4B//AB/gAB/gD/4AD+AAH8AP/gAP4AAfwAf8AA/gAB/AA/wAB+AAH4AD/AAH4AAfwAP8AAfgAB/AB/4AD+AAH8AH/gAP4AAfwA//AA/gAA/gH/+AH+AAD/h//8AfwAAP//+/4D/AAAf//7///8AAB///H///gAAD//4P//+AAAP//g///wAAAf/8B//+AAAA//gD//wAAAA/4AH/+AAAAAAAAH/wAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAAAD//AAAAAAAA///AAAAAAAH//+AAPwAAB///8AA/gAAP///4AD+AAA////wAP4AAH////AA/gAAf///8AD+AAD/4B/4AP4AAP+AB/gA/gAA/gAD+AD+AAH+AAP4AP4AAfwAAfgA/gAB/AAB+AD+AAH8AAH4AP4AAfwAAfgB/AAB/AAB+AH8AAH8AAH4A/wAAf4AA/AH+AAA/gAD8A/4AAD/gAfwH/gAAP/AD+B/8AAAf/g/w//gAAB//////+AAAD//////wAAAH/////+AAAAP/////wAAAAf////8AAAAA/////AAAAAA////wAAAAAAf//4AAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAfgAAAAP8AAD/AAAAA/4AAf8AAAAH/gAB/4AAAAf+AAH/gAAAB/4AAf+AAAAH/gAB/4AAAAf+AAH/AAAAA/wAAP8AAAAB+AAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), + 46, + atob("EhklJSUlJSUlJSUlEg=="), + 64+(scale<<8)+(1<<16) + ); +}; + +var drawTimeout; +var lastBattCheck = 0; +var batteryUsedWidth = 0; + +function queueDraw(millis_now) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw(); + }, 60000 - (millis_now % 60000)); +} + +function draw() { + var date = new Date(); + var h = date.getHours(), + m = date.getMinutes(); + var d = date.getDate(); + var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + var dow = require("date_utils").dows(0,1)[date.getDay()]; + + if ((date.getTime() >= lastBattCheck + 15*60000) || Bangle.isCharging()) { + lastBattCheck = date.getTime(); + batteryUsedWidth = E.getBattery(); + batteryUsedWidth += batteryUsedWidth/2; + } + + g.reset(); + g.clear(); + + g.setFontOpenSans(); + g.setFontAlign(0, -1); + if (is12Hour) { + if (h > 12) h -= 12; + if (h == 0) h = 12; + g.drawString(h + ":" + ("0"+m).substr(-2), g.getWidth() / 2, 30); + } else { + g.drawString(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2), g.getWidth() / 2, 30); + } + g.setFontAlign(1, -1); + g.drawString(d, g.getWidth() -6, 98); + g.setFont('Vector', 52); + g.setFontAlign(-1, -1); + g.drawString(dow.slice(0,2).toUpperCase(), 6, 103); + + g.fillRect(9,159,166,171); + g.fillRect(167,163,170,167); + if (Bangle.isCharging()) { + g.setColor(1,1,0); + g.fillRect(12,162,12+batteryUsedWidth,168); + } else { + g.setColor(1,0,0); + g.fillRect(12,162,57,168); + g.setColor(1,1,0); + g.fillRect(58,162,72,168); + g.setColor(0,1,0); + g.fillRect(73,162,162,168); + } + if (batteryUsedWidth < 150) { + g.setColor(g.theme.bg); + g.fillRect(12+batteryUsedWidth+1,162,162,168); + } + + if (Bangle.isCharging()) { + g.setColor(1,1,0); + } else if (batteryUsedWidth <= 45) { + g.setColor(1,0,0); + } else if (batteryUsedWidth <= 60) { + g.setColor(1,1,0); + } else { + g.setColor(0, 1, 0); + } + g.fillRect(0, 90, g.getWidth(), 94); + + // widget redraw + Bangle.drawWidgets(); + queueDraw(date.getTime()); +} + +Bangle.on('lcdPower', on => { + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('charging', (charging) => { + draw(); +}); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +draw(); + diff --git a/apps/bigdclock/bigdclock.icon.js b/apps/bigdclock/bigdclock.icon.js new file mode 100644 index 000000000..4aaecfa23 --- /dev/null +++ b/apps/bigdclock/bigdclock.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AAMD4F4AgN4g/D/4FB/E/AoUH/F/AoOAh4FCz4FD4EPAoUHAoOHwAFDx/AAoUfAol/g4RD/w1Cg/B/AFD4fwn4XC4fg8/wAoPH//P7AFE9wFE8YFEEwcf4+BwAFBiACBAoUwAQPAAQMgAQNAArIjFF4sYgEBAoUIAoIRChi3B8AFBg8Ah/wAoIVBjH8ZAXguF+AoSDBn7WEh4FEg4")) diff --git a/apps/bigdclock/bigdclock.png b/apps/bigdclock/bigdclock.png new file mode 100644 index 000000000..4da1a9010 Binary files /dev/null and b/apps/bigdclock/bigdclock.png differ diff --git a/apps/bigdclock/metadata.json b/apps/bigdclock/metadata.json new file mode 100644 index 000000000..dc3bcb143 --- /dev/null +++ b/apps/bigdclock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "bigdclock", + "name": "Big digit clock containing just the essentials", + "shortName":"Big digit clk", + "version": "0.09", + "description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.", + "icon": "bigdclock.png", + "type": "clock", + "tags": "clock", + "allow_emulator":true, + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"bigdclock.app.js","url":"bigdclock.app.js"}, + {"name":"bigdclock.img","url":"bigdclock.icon.js","evaluate":true} + ] +} diff --git a/apps/bigdclock/screenshot.png b/apps/bigdclock/screenshot.png new file mode 100644 index 000000000..acac53ea9 Binary files /dev/null and b/apps/bigdclock/screenshot.png differ diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog index 2a3023750..7330eb937 100644 --- a/apps/bikespeedo/ChangeLog +++ b/apps/bikespeedo/ChangeLog @@ -1,2 +1,7 @@ 0.01: New App! 0.02: Barometer altitude adjustment setting +0.03: Use default Bangle formatter for booleans +0.04: Add options for units in locale and recording GPS +0.05: Allow toggling of "max" values (screen tap) and recording (button press) +0.06: Fix local unit setting +0.07: Minor code improvements diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js index a62a429e5..a1f0b53ce 100644 --- a/apps/bikespeedo/app.js +++ b/apps/bikespeedo/app.js @@ -12,7 +12,7 @@ const fontFactorB2 = 2/3; const colfg=g.theme.fg, colbg=g.theme.bg; const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain); -var altiGPS=0, altiBaro=0; +var altiBaro=0; var hdngGPS=0, hdngCompass=0, calibrateCompass=false; /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ @@ -183,7 +183,6 @@ var KalmanFilter = (function () { var lf = {fix:0,satellites:0}; var showMax = 0; // 1 = display the max values. 0 = display the cur fix -var canDraw = 1; var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. var sec; // actual seconds for testing purposes @@ -194,30 +193,9 @@ max.n = 0; // counter. Only start comparing for max after a certain number of var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values; -var wp = {}; // Waypoint to use for distance from cur position. var SATinView = 0; -function radians(a) { - return a*Math.PI/180; -} - -function distance(a,b){ - var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); - var y = radians(b.lat-a.lat); - - // Distance in selected units - var d = Math.sqrt(x*x + y*y) * 6371000; - d = (d/parseFloat(cfg.dist)).toFixed(2); - if ( d >= 100 ) d = parseFloat(d).toFixed(1); - if ( d >= 1000 ) d = parseFloat(d).toFixed(0); - - return d; -} - function drawFix(dat) { - - if (!canDraw) return; - g.clearRect(0,screenYstart,screenW,screenH); var v = ''; @@ -227,7 +205,7 @@ function drawFix(dat) { v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString(); // Primary Units - u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units; + u = (showMax ? 'max ' : '') + (cfg.primSpd?cfg.spd_unit:dat.alt_units); drawPrimary(v,u); @@ -260,14 +238,6 @@ function drawFix(dat) { } -function drawClock() { - if (!canDraw) return; - g.clearRect(0,screenYstart,screenW,screenH); - drawTime(); - g.reset(); -} - - function drawPrimary(n,u) { //if(emulator)console.log("\n1: " + n +" "+ u); var s=40; // Font size @@ -337,16 +307,6 @@ function drawSats(sats) { g.setFont("6x8", 2); g.setFontAlign(1,1); //right, bottom g.drawString(sats,screenW,screenH); - - g.setFontVector(18); - g.setColor(col1); - - if ( cfg.modeA == 1 ) { - if ( showMax ) { - g.setFontAlign(0,1); //centre, bottom - g.drawString('MAX',120,164); - } - } } function onGPS(fix) { @@ -367,7 +327,6 @@ function onGPS(fix) { var sp = '---'; var al = '---'; - var di = '---'; var age = '---'; if (fix.fix) lf = fix; @@ -403,6 +362,8 @@ function onGPS(fix) { if ( sp < 10 ) sp = sp.toFixed(1); else sp = Math.round(sp); + if (isNaN(sp)) sp = '---'; + if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp); // Altitude @@ -410,12 +371,14 @@ function onGPS(fix) { al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al); - // Distance to waypoint - di = distance(lf,wp); - if (isNaN(di)) di = 0; - // Age of last fix (secs) age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); + } else { + // populate spd_unit + if (cfg.spd == 0) { + m = require("locale").speed(0).match(/[0-9,\.]+(.*)/); + cfg.spd_unit = m[1]; + } } if ( cfg.modeA == 1 ) { @@ -440,21 +403,14 @@ function onGPS(fix) { } } -function setButtons(){ - setWatch(_=>load(), BTN1); - -onGPS(lf); -} - - function updateClock() { - if (!canDraw) return; drawTime(); g.reset(); if ( emulator ) { max.spd++; max.alt++; - d=new Date(); sec=d.getSeconds(); + const d=new Date(); + sec=d.getSeconds(); onGPS(lf); } } @@ -465,7 +421,7 @@ function updateClock() { // Read settings. let cfg = require('Storage').readJSON('bikespeedo.json',1)||{}; -cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed +cfg.spd = cfg.localeUnits ? 0 : 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed cfg.spd_unit = 'km/h'; // Displayed speed unit cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048') cfg.alt_unit = 'm'; // Displayed altitude units ('feet') @@ -499,14 +455,6 @@ function onPressure(dat) { altiBaro = Number(dat.altitude.toFixed(0)) + Number(cfg.altDiff); } -Bangle.setBarometerPower(1); // needs some time... -g.clearRect(0,screenYstart,screenW,screenH); -onGPS(lf); -Bangle.setGPSPower(1); -Bangle.on('GPS', onGPS); -Bangle.on('pressure', onPressure); - -Bangle.setCompassPower(1); var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; if (!CALIBDATA) calibrateCompass = true; function Compass_tiltfixread(O,S){ @@ -544,11 +492,58 @@ function Compass_reading() { Compass_heading = Compass_newHeading(d,Compass_heading); hdngCompass = Compass_heading.toFixed(0); } -if (!calibrateCompass) setInterval(Compass_reading,200); -setButtons(); -if (emulator) setInterval(updateClock, 2000); -else setInterval(updateClock, 10000); +function nextMode() { + showMax = 1 - showMax; +} + +function start() { + Bangle.setBarometerPower(1); // needs some time... + g.clearRect(0,screenYstart,screenW,screenH); + onGPS(lf); + Bangle.setGPSPower(1); + Bangle.on('GPS', onGPS); + Bangle.on('pressure', onPressure); + + Bangle.setCompassPower(1); + if (!calibrateCompass) setInterval(Compass_reading,200); + + if (emulator) setInterval(updateClock, 2000); + else setInterval(updateClock, 10000); + + let createdRecording = false; + Bangle.setUI({ + mode: "custom", + touch: nextMode, + btn: () => { + const rec = WIDGETS["recorder"]; + if(rec){ + const active = rec.isRecording(); + if(active){ + createdRecording = true; + rec.setRecording(false); + }else{ + rec.setRecording(true, { force: createdRecording ? "append" : "new" }); + } + }else{ + nextMode(); + } + }, + }); + + // can't delay loadWidgets til here - need to have already done so for recorder + Bangle.drawWidgets(); +} Bangle.loadWidgets(); -Bangle.drawWidgets(); +if (cfg.record && WIDGETS["recorder"]) { + WIDGETS["recorder"] + .setRecording(true) + .then(start); + + if (cfg.recordStopOnExit) + E.on('kill', () => WIDGETS["recorder"].setRecording(false)); + +} else { + start(); +} diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json index c3de0487c..20c7d4f53 100644 --- a/apps/bikespeedo/metadata.json +++ b/apps/bikespeedo/metadata.json @@ -2,7 +2,7 @@ "id": "bikespeedo", "name": "Bike Speedometer (beta)", "shortName": "Bike Speedometer", - "version": "0.02", + "version": "0.07", "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources", "icon": "app.png", "screenshots": [{"url":"Screenshot.png"}], diff --git a/apps/bikespeedo/settings.js b/apps/bikespeedo/settings.js index a3921f4a3..0326d7529 100644 --- a/apps/bikespeedo/settings.js +++ b/apps/bikespeedo/settings.js @@ -11,9 +11,34 @@ '< Back': back, '< Load Bike Speedometer': ()=>{load('bikespeedo.app.js');}, 'Barometer Altitude adjustment' : function() { E.showMenu(altdiffMenu); }, - 'Kalman Filters' : function() { E.showMenu(kalMenu); } + 'Kalman Filters' : function() { E.showMenu(kalMenu); }, + 'Speed units': { + value: !!settings.localeUnits, + format: b => b ? "Locale" : "km/h", + onchange: b => { + settings.localeUnits = b; + writeSettings(); + } + }, }; + if (global.WIDGETS && WIDGETS["recorder"]) { + appMenu[/*LANG*/"Record rides"] = { + value : !!settings.record, + onchange : v => { + settings.record = v; + writeSettings(); + } + }; + appMenu[/*LANG*/"Stop record on exit"] = { + value : !!settings.recordStopOnExit, + onchange : v => { + settings.recordStopOnExit = v; + writeSettings(); + } + }; + } + const altdiffMenu = { '': { 'title': 'Altitude adjustment' }, '< Back': function() { E.showMenu(appMenu); }, @@ -33,16 +58,14 @@ '< Back': function() { E.showMenu(appMenu); }, 'Speed' : { value : settings.spdFilt, - format : v => v?"On":"Off", onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); } }, 'Altitude' : { value : settings.altFilt, - format : v => v?"On":"Off", onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); } } }; E.showMenu(appMenu); -}); +}) diff --git a/apps/binaryclk/ChangeLog b/apps/binaryclk/ChangeLog new file mode 100644 index 000000000..7f3507499 --- /dev/null +++ b/apps/binaryclk/ChangeLog @@ -0,0 +1,12 @@ +0.01: Added app +0.02: Removed unneeded squares +0.03: Added setting for fullscreen option +0.04: Added settings to hide unused squares and show date +0.05: Minor code improvements +0.06: Added setting to show battery and added artwork to date +0.07: Removed percentage from battery and cleaned up logic +0.08: Changed month to day and text color to black on date +0.09: Changed day color back to white +0.10: Add blinking when charging +0.11: Changed battery to buzz instead of blink and fixed battery counter +0.12: Got rid of battery counter \ No newline at end of file diff --git a/apps/binaryclk/app-icon.js b/apps/binaryclk/app-icon.js new file mode 100644 index 000000000..3cb526a4f --- /dev/null +++ b/apps/binaryclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AEUiAAQEEkECBRAX/C/4Xrd+hCDI4kgR/4X/C/4XIAF53/C/4X/A4gSDC4kgC5AAvR/4X/C/4A/ADoA==")) diff --git a/apps/binaryclk/app-icon.png b/apps/binaryclk/app-icon.png new file mode 100644 index 000000000..b28ffdab4 Binary files /dev/null and b/apps/binaryclk/app-icon.png differ diff --git a/apps/binaryclk/app.js b/apps/binaryclk/app.js new file mode 100644 index 000000000..afc72aabe --- /dev/null +++ b/apps/binaryclk/app.js @@ -0,0 +1,114 @@ +var settings = Object.assign({ + fullscreen: true, + hidesq: false, + showdate: true, + showbat: true, +}, require('Storage').readJSON("binaryclk.json", true) || {}); + +var gap = 4; +var mgn = 24; +var sq = 33; + +if (settings.fullscreen) { + gap = 8; + mgn = 0; + sq = 34; +} + +var pos = sq + gap; + +function drawbat() { + var bat = E.getBattery(); + if (bat < 20) { + g.setColor('#FF0000'); + } else if (bat < 40) { + g.setColor('#FFA500'); + } else { + g.setColor('#00FF00'); + } + g.fillRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + Math.floor(bat * sq / 100), mgn + gap + sq); +} + +function drawbatrect() { + if (g.theme.dark) { + g.setColor(-1); + } else { + g.setColor(1); + } + g.drawRect(Math.floor(mgn/2) + gap + 2 * pos, mgn + gap, Math.floor(mgn/2) + gap + 2 * pos + sq, mgn + gap + sq); +} + +function draw() { + let i = 0; + var dt = new Date(); + var h = dt.getHours(); + var m = dt.getMinutes(); + var d = dt.getDate(); + var day = dt.toString().substring(0,3); + const t = []; + + t[0] = Math.floor(h/10); + t[1] = Math.floor(h%10); + t[2] = Math.floor(m/10); + t[3] = Math.floor(m%10); + + g.reset(); + g.clearRect(Bangle.appRect); + + for (let r = 3; r >= 0; r--) { + for (let c = 0; c < 4; c++) { + if (t[c] & Math.pow(2, r)) { + g.fillRect(Math.floor(mgn/2) + gap + c * pos, mgn + gap + i * pos, Math.floor(mgn/2) + gap + c * pos + sq, mgn + gap + i * pos + sq); + } else { + g.drawRect(Math.floor(mgn/2) + gap + c * pos, mgn + gap + i * pos, Math.floor(mgn/2) + gap + c * pos + sq, mgn + gap + i * pos + sq); + } + } + i++; + } + + var c1sqhide = 0; + var c3sqhide = 0; + + if (settings.hidesq) { + c1sqhide = 2; + c3sqhide = 1; + g.clearRect(Math.floor(mgn/2), mgn, Math.floor(mgn/2) + pos, mgn + c1sqhide * pos); + g.clearRect(Math.floor(mgn/2) + 2 * pos + gap, mgn, Math.floor(mgn/2) + 3 * pos, mgn + c3sqhide * pos); + } + + if (settings.showdate) { + g.setColor(-1).fillRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + sq); + g.setColor('#FF0000').fillRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + 12); + g.setFontAlign(0, -1); + g.setFont("Vector",12); + g.setColor(-1).drawString(day, Math.ceil(mgn/2) + gap + Math.ceil(sq/2) + 1, mgn + gap + 1); + g.setFontAlign(0, 1); + g.setFont("Vector",20); + g.setColor(1).drawString(d, Math.ceil(mgn/2) + gap + Math.ceil(sq/2) + 1, mgn + gap + sq + 2); + if (g.theme.dark) { + g.setColor(-1); + } else { + g.setColor(1); + g.drawLine(Math.floor(mgn/2) + gap, mgn + gap + 13, Math.floor(mgn/2) + gap + sq, mgn + gap + 13); + } + g.drawRect(Math.floor(mgn/2) + gap, mgn + gap, Math.floor(mgn/2) + gap + sq, mgn + gap + sq); + } + + if (settings.showbat) { + drawbat(); + drawbatrect(); + } +} + +g.clear(); +draw(); +setInterval(draw, 60000); +Bangle.setUI("clock"); +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +Bangle.on('charging', function(charging) { + if(charging) Bangle.buzz(); +}); \ No newline at end of file diff --git a/apps/binaryclk/metadata.json b/apps/binaryclk/metadata.json new file mode 100644 index 000000000..432bed25e --- /dev/null +++ b/apps/binaryclk/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "binaryclk", + "name": "Bin Clock", + "version": "0.12", + "description": "Binary clock with date and battery", + "icon": "app-icon.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"binaryclk.app.js","url":"app.js"}, + {"name":"binaryclk.settings.js","url":"settings.js"}, + {"name":"binaryclk.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"binaryclk.json"}] +} diff --git a/apps/binaryclk/screenshot.png b/apps/binaryclk/screenshot.png new file mode 100644 index 000000000..2b459c87c Binary files /dev/null and b/apps/binaryclk/screenshot.png differ diff --git a/apps/binaryclk/settings.js b/apps/binaryclk/settings.js new file mode 100644 index 000000000..34b3ae180 --- /dev/null +++ b/apps/binaryclk/settings.js @@ -0,0 +1,46 @@ +(function(back) { + var FILE = "binaryclk.json"; + var settings = Object.assign({ + fullscreen: false, + hidesq: false, + showdate: false, + showbat: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Bin Clock" }, + "< Back" : () => back(), + 'Fullscreen': { + value: settings.fullscreen, + onchange: v => { + settings.fullscreen = v; + writeSettings(); + }, + }, + 'Hide Squares': { + value: settings.hidesq, + onchange: v => { + settings.hidesq = v; + writeSettings(); + }, + }, + 'Show Date': { + value: settings.showdate, + onchange: v => { + settings.showdate = v; + writeSettings(); + }, + }, + 'Show Battery': { + value: settings.showbat, + onchange: v => { + settings.showbat = v; + writeSettings(); + }, + }, + }); +}) diff --git a/apps/binclock/ChangeLog b/apps/binclock/ChangeLog index dc4ed8308..7c31cc0d3 100644 --- a/apps/binclock/ChangeLog +++ b/apps/binclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixed bug where screen didn't clear so incorrect time displayed. 0.03: Update to use Bangle.setUI instead of setWatch +0.04: Tell clock widgets to hide. diff --git a/apps/binclock/app.js b/apps/binclock/app.js index f8cbe8dd5..d9c74e6ce 100644 --- a/apps/binclock/app.js +++ b/apps/binclock/app.js @@ -164,9 +164,6 @@ Bangle.on('lcdPower',on=>{ draw(); // draw immediately } }); -// Load widgets -Bangle.loadWidgets(); -Bangle.drawWidgets(); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ if (btn!=1) return; @@ -176,3 +173,6 @@ Bangle.setUI("clockupdown", btn=>{ displayTime = 0; } }); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/binclock/metadata.json b/apps/binclock/metadata.json index d17045868..2ca2755a6 100644 --- a/apps/binclock/metadata.json +++ b/apps/binclock/metadata.json @@ -2,7 +2,7 @@ "id": "binclock", "name": "Binary Clock", "shortName": "Binary Clock", - "version": "0.03", + "version": "0.04", "description": "A binary clock with hours and minutes. BTN1 toggles a digital clock.", "icon": "app.png", "type": "clock", diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog index 1e54f489c..2d60667d2 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -2,3 +2,7 @@ 0.02: first running version for BangleJs2 0.03: corrected icon, added screen shot, extended description 0.04: corrected format of background image (raw binary) +0.05: move setUI() up before draw() as to not have a false positive 'sanity +check' when building on github. +0.06: Minor code improvements +0.07: Minor code improvements diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js index 28d7a06a5..a25595bf5 100644 --- a/apps/binwatch/app.js +++ b/apps/binwatch/app.js @@ -63,8 +63,8 @@ const V2_BAT_SIZE_Y = 2; const V2_SCREEN_SIZE_X = 176; const V2_SCREEN_SIZE_Y = 176; const V2_BACKGROUND_IMAGE = "binwatch.bg176.img"; -const V2_BG_COLOR = 0; -const V2_FG_COLOR = 1; +//const V2_BG_COLOR = 0; +//const V2_FG_COLOR = 1; /* Bangle 1: 240 x 240 */ @@ -91,15 +91,15 @@ const V1_BAT_SIZE_Y = 5; const V1_SCREEN_SIZE_X = 240; const V1_SCREEN_SIZE_Y = 240; const V1_BACKGROUND_IMAGE = "binwatch.bg240.img"; -const V1_BG_COLOR = 1; -const V1_FG_COLOR = 0; +//const V1_BG_COLOR = 1; +//const V1_FG_COLOR = 0; /* runtime settings */ var x_step = 0; var y_step = 0; -var time_y_offset = 0; +//var time_y_offset = 0; var hx = 0, hy = 0; var mx = 0, my = 0; var sx = 0, sy = 0; @@ -110,10 +110,9 @@ var bat_pos_x, bat_pos_y, bat_size_x, bat_size_y; var backgroundImage = ""; var screen_size_x = 0; var screen_size_y = 0; -var bg_color = 0; -var fg_color = 1; +//var bg_color = 0; +//var fg_color = 1; -/* global variables */ var showDateTime = 2; /* show noting, time or date */ var cg; @@ -137,7 +136,7 @@ var cgimg; */ function drawSquare(gfx, x, y, data, numOfBits) { - for(i = numOfBits; i > 0 ; i--) { + for(let i = numOfBits; i > 0 ; i--) { if( (data & 1) != 0) { gfx.fillRect(x + (i - 1) * x_step, y, x + i * x_step , y + y_step); @@ -246,7 +245,7 @@ function drawBattery(gfx, level) { var pos_y = bat_pos_y - 1; var stepLevel = Math.round((level + 10) / 20); - for(i = 0; i < stepLevel; i++) { + for(let i = 0; i < stepLevel; i++) { pos_y -= bat_size_y + 2; gfx.fillRect(bat_pos_x, pos_y, bat_pos_x + bat_size_x, pos_y + bat_size_y); @@ -271,7 +270,7 @@ function setRuntimeValues(resolution) { x_step = V1_X_STEP; y_step = V1_Y_STEP; - time_y_offset = V1_TIME_Y_OFFSET; + //time_y_offset = V1_TIME_Y_OFFSET; hx = V1_HX; hy = V1_HY; mx = V1_MX; @@ -298,7 +297,7 @@ function setRuntimeValues(resolution) { x_step = V2_X_STEP; y_step = V2_Y_STEP; - time_y_offset = V2_TIME_Y_OFFSET; + //time_y_offset = V2_TIME_Y_OFFSET; hx = V2_HX; hy = V2_HY; @@ -334,6 +333,7 @@ function setRuntimeValues(resolution) { var hour = 0, minute = 1, second = 50; var batVLevel = 20; +Bangle.setUI("clock"); function draw() { var d = new Date(); @@ -371,7 +371,6 @@ function draw() { } // Show launcher when button pressed -Bangle.setUI("clock"); setRuntimeValues(g.getWidth()); g.reset().clear(); Bangle.loadWidgets(); diff --git a/apps/binwatch/metadata.json b/apps/binwatch/metadata.json index 0b5fb2c72..1e71362cb 100644 --- a/apps/binwatch/metadata.json +++ b/apps/binwatch/metadata.json @@ -3,7 +3,7 @@ "shortName":"BinWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.04", + "version": "0.07", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator":true, diff --git a/apps/blc/ChangeLog b/apps/blc/ChangeLog new file mode 100644 index 000000000..6075cacf8 --- /dev/null +++ b/apps/blc/ChangeLog @@ -0,0 +1,4 @@ +0.10: New app introduced to the app loader! +0.20: skipped (internal revision) +0.30: skipped (internal revision) +0.40: added functionality for customizing colors diff --git a/apps/blc/README.md b/apps/blc/README.md new file mode 100644 index 000000000..3d126bcf8 --- /dev/null +++ b/apps/blc/README.md @@ -0,0 +1,17 @@ +# Binary LED Clock + +A binary watch with LEDs, showing time and date. + +From top to bottom the watch face shows four rows of leds: + +* hours (red leds) +* minutes (green leds) +* day (yellow leds, top row) +* month (yellow leds, bottom row) + +The colors are default colors and can be changed at the settings page, also, the outer ring can be disabled. + +The rightmost LED represents 1, the second 2, the third 4, the next 8 and so on, i.e. values are the powers of two. + +As usual, luminous leds represent a logical one, and "dark" leds a logcal '0'. Dark means the color of the background. +Widgets aren't affected and are shown as normal. diff --git a/apps/blc/blc-icon.js b/apps/blc/blc-icon.js new file mode 100644 index 000000000..a8d30baee --- /dev/null +++ b/apps/blc/blc-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/AAIHBqut8FPgH4sspk1T885/feoMI74TB1Fc51Dmfg28gKmMCrNSAgMlyo5BgV7uQIKgEhiMRkECAYMSgErolLBBIXBqIKBqEFAYMVgF0olEuAIIC4ORBQOQhIDBjMA2gOB2AIIF7JfXR67X0lvdHwQII7vSa4/TmYKBBBEtmc9a40NmYKBBBIbBmfQa4oOEBBAXFF65fXR64A/AG8IvN4AgOG62ABAuHy4IGgEHiMXAgNu91gBAtxiNwBAsAhMRjIEB73ucIIIEyMRyAIFF7BfXAH6/IttoKxRoIgEG93mQxSYIgEN93tWxTIIF7BfXAH4AGw93u/A44IDhl8vQRFBogXB0ECuGoBAcKxRxBC53Hhlyk8ggVyuQGBvlwhgNBk98BAN6I4UgC4N4BwWgAwWsC4fAk4IB0AvBAgIQBBwUIkQOBAwQXCJIIEBI4UAkQXE48sAwgXJF40mgAvDvRtCC4pfEC4WCPYJdBDYNyC4wAX")) diff --git a/apps/blc/blc-icon.png b/apps/blc/blc-icon.png new file mode 100644 index 000000000..8bbf6ae71 Binary files /dev/null and b/apps/blc/blc-icon.png differ diff --git a/apps/blc/blc.js b/apps/blc/blc.js new file mode 100644 index 000000000..6015bc548 --- /dev/null +++ b/apps/blc/blc.js @@ -0,0 +1,182 @@ +//Binary LED Clock (BLC) by aeMKai + +{ // 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 + + const SETTINGSFILE = "BinaryClk.settings.json"; + + // variables defined from settings + let HourCol; + let MinCol; + let DayCol; + let MonCol; + let RingOn; + + // color arrays + // !!! don't change order unless change oder in BinaryClk.settings.js !!! + // !!! order must correspond to each other between arrays !!! + let LED_Colors = ["#FFF", "#F00", "#0F0", "#00F", "#FF0", "#F0F", "#0FF", "#000"]; + let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"]; + + // load settings + let loadSettings = function() + { + function def (value, def) {return value !== undefined ? value : def;} + + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + // get name from setting, find index of name and assign corresponding color code by index + HourCol = LED_Colors[LED_ColorNames.indexOf(def(settings.HourCol, "red"))]; + MinCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MinCol, "green"))]; + DayCol = LED_Colors[LED_ColorNames.indexOf(def(settings.DayCol, "yellow"))]; + MonCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MonCol, "yellow"))]; + RingOn = def(settings.RingOn, true); + + delete settings; // settings in local var -> no more required + } + + let drawTimeout; + + // actually draw the watch face + let draw = function() + { + // Bangle.js2 -> 176x176 + var x_rgt = g.getWidth(); + var y_bot = g.getHeight(); + //var x_cntr = x_rgt / 2; + var y_cntr = y_bot / 18*7; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + + var white = "#FFF"; + var black = "#000"; + var bord_col = white; + var col_off = black; + + var col = new Array(HourCol, MinCol, DayCol, MonCol); // each #rgb + if (g.theme.dark) + { + bord_col = white; + col_off = black; + } + else + { + bord_col = black; + col_off = white; + } + + let pwr2 = [1, 2, 4, 8, 16, 32]; // array with powers of 2, because poweroperator '**' doesnt work + // maybe also faster + + + var no_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon) + var no_hour = 5; + var no_min = 6; + var no_day = 5; + var no_mon = 4; + + // arrays: [hr, min, day, mon] + let msbits = [no_hour-1, no_min-1, no_day-1, no_mon-1]; // MSB = No bits - 1 + let rad = [13, 13, 9, 9]; // radiuses for each row + var x_dist = 29; + let y_dist = [0, 35, 75, 100]; // y-position from y_centr for each row from top + // don't calc. automatic as for x, because of different spaces + var x_offs_rgt = 15; // offset from right border (layout) + var y_offs_cntr = 25; // vertical offset from center + + //////////////////////////////////////// + // compute bit-pattern from time/date and draw leds + //////////////////////////////////////// + + // date-time-array: 4x6 bit + //var idx_hour = 0; + //var idx_min = 1; + //var idx_day = 2; + //var idx_mon = 3; + var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]]; + + var date_time = new Date(); + var hour = date_time.getHours(); // 0..23 + var min = date_time.getMinutes(); // 0..59 + var day = date_time.getDate(); // 1..31 + var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11 + + let dt_array = [hour, min, day, mon]; + + var line_cnt = 0; + var cnt = 0; + var bit_cnt = 0; + + while (line_cnt < no_lines) + { + + //////////////////////////////////////// + // compute bit-pattern + bit_cnt = msbits[line_cnt]; + + while (bit_cnt >= 0) + { + if (dt_array[line_cnt] >= pwr2[bit_cnt]) + { + dt_array[line_cnt] -= pwr2[bit_cnt]; + dt_bit_arr[line_cnt][bit_cnt] = 1; + } + else + { + dt_bit_arr[line_cnt][bit_cnt] = 0; + } + bit_cnt--; + } + + //////////////////////////////////////// + // draw leds (and border, if enabled) + cnt = 0; + + while (cnt <= msbits[line_cnt]) + { + if (RingOn) // draw outer ring, if enabled + { + g.setColor(bord_col); + g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]); + } + if (dt_bit_arr[line_cnt][cnt] == 1) + { + g.setColor(col[line_cnt]); + } + else + { + g.setColor(col_off); + } + g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]-1); + cnt++; + } + line_cnt++; + } + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() + { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + } + + + // Init the settings of the app + loadSettings(); + + // Show launcher when middle button pressed + Bangle.setUI( + { + mode : "clock", + remove : function() + { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } + }); + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/blc/blc.settings.js b/apps/blc/blc.settings.js new file mode 100644 index 000000000..9143fc16e --- /dev/null +++ b/apps/blc/blc.settings.js @@ -0,0 +1,72 @@ +// Change settings for BinaryClk + +(function(back){ + + // color array -- don't change order unless change oder in BinaryClk.js + let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"]; + + var FILE = "BinaryClk.settings.json"; + // Load settings + var settings = Object.assign({ + HourCol: "red", + MinCol: "green", + DayCol: "yellow", + MonCol: "yellow", + RingOn: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings(){ + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return{ + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + // Show the menu + var mainmenu = { + "" : { + "title" : "BinaryCLK" + }, + "< Back" : () => back(), + 'Color Hour.:': stringInSettings("HourCol", LED_ColorNames), + 'Color Minute:': stringInSettings("MinCol", LED_ColorNames), + 'Color Day': stringInSettings("DayCol", LED_ColorNames), + 'Color Month:': stringInSettings("MonCol", LED_ColorNames), + 'LED ring on/off': { + value: (settings.RingOn !== undefined ? settings.RingOn : true), + onchange: v => { + settings.RingOn = v; + writeSettings(); + } + }, + }; + + // Show submenues + //var submenu1 = { + //"": { + // "title": "Show sub1..." + //}, + //"< Back": () => E.showMenu(mainmenu), + //"ItemName": stringInSettings("settingsVar", ["Yes", "No", "DontCare"]), + //}; + + E.showMenu(mainmenu); +}) diff --git a/apps/blc/blc.settings.json b/apps/blc/blc.settings.json new file mode 100644 index 000000000..37abfc700 --- /dev/null +++ b/apps/blc/blc.settings.json @@ -0,0 +1,7 @@ +{ + "HourCol": "red", + "MinCol": "green", + "DayCol": "yellow", + "MonCol": "yellow", + "RingOn": true +} diff --git a/apps/blc/metadata.json b/apps/blc/metadata.json new file mode 100644 index 000000000..53dde47e3 --- /dev/null +++ b/apps/blc/metadata.json @@ -0,0 +1,19 @@ +{ + "id":"blc", + "name":"Binary LED Clock", + "version": "0.40", + "description": "a binary LED-Clock with time and date and customizable LED-colors", + "icon":"blc-icon.png", + "screenshots": [{"url":"screenshot_blc_1.bmp"},{"url":"screenshot_blc_2.bmp"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"blc.app.js","url":"blc.js"}, + {"name":"blc.settings.js","url":"blc.settings.js"}, + {"name":"blc.img","url":"blc-icon.js","evaluate":true} + ], + "data": [{"name":"blc.settings.json"}] +} diff --git a/apps/blc/screenshot_blc_1.bmp b/apps/blc/screenshot_blc_1.bmp new file mode 100644 index 000000000..8a8c86ce5 Binary files /dev/null and b/apps/blc/screenshot_blc_1.bmp differ diff --git a/apps/blc/screenshot_blc_2.bmp b/apps/blc/screenshot_blc_2.bmp new file mode 100644 index 000000000..ee07e3bb3 Binary files /dev/null and b/apps/blc/screenshot_blc_2.bmp differ diff --git a/apps/blecsc/ChangeLog b/apps/blecsc/ChangeLog new file mode 100644 index 000000000..1500000b5 --- /dev/null +++ b/apps/blecsc/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version +0.02: Minor code improvements +0.03: Moved from cycling app, fixed connection issues and cadence +0.04: Added support for <1 wheel/crank event/second (using idle counters) (ref #3434) +0.05: Fix <1 event/second issue \ No newline at end of file diff --git a/apps/blecsc/README.md b/apps/blecsc/README.md new file mode 100644 index 000000000..5cde87168 --- /dev/null +++ b/apps/blecsc/README.md @@ -0,0 +1,32 @@ +# BLE Cycling Speed Sencor (CSC) + +Displays data from a BLE Cycling Speed and Cadence sensor. + +Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over. + +## Settings + +Accessible from `Settings -> Apps -> BLE CSC` + +Here you can set the wheel diameter + +## Development + +``` +var csc = require("blecsc").getInstance(); +csc.on("status", txt => { + print("##", txt); + E.showMessage(txt); +}); +csc.on("data", e => print(e)); +csc.start(); +``` + +The `data` event contains: + + * cwr/ccr => wheel/crank cumulative revs + * lwet/lcet => wheel/crank last event time in 1/1024s + * wrps/crps => calculated wheel/crank revs per second + * wdt/cdt => time period in seconds between events + * wr => wheel revs + * kph => kilometers per hour \ No newline at end of file diff --git a/apps/blecsc/blecsc.js b/apps/blecsc/blecsc.js new file mode 100644 index 000000000..9b7d8b751 --- /dev/null +++ b/apps/blecsc/blecsc.js @@ -0,0 +1,230 @@ +/** + * This library communicates with a Bluetooth CSC peripherial using the Espruino NRF library. + * + * ## Usage: + * 1. Register event handlers using the \`on(eventName, handlerFunction)\` method + * You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can + * have raw characteristic values passed through using the \`value\` event. + * 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method + * 3. To tear down the connection, call the \`disconnect()\` method + * + * ## Events + * - \`status\` - string containing connection status + * - \`data\` - the peripheral sends a notification containing wheel/crank event data + * - \`disconnect\` - the peripheral ends the connection or the connection is lost + * + * cwr/ccr => wheel/crank cumulative revs + * lwet/lcet => wheel/crank last event time in 1/1024s + * wrps/crps => calculated wheel/crank revs per second + * wdt/cdt => time period in seconds between events + * wr => wheel revs + * kph => kilometers per hour + */ +class BLECSC { + constructor() { + this.reconnect = false; // set when start called + this.device = undefined; // set when device found + this.gatt = undefined; // set when connected + // .on("status", => string + // .on("data" + // .on("disconnect" + this.resetStats(); + // Set default values and merge with stored values + this.settings = Object.assign({ + circum: 2068 // circumference in mm + }, (require('Storage').readJSON('blecsc.json', true) || {})); + } + + resetStats() { + this.cwr = undefined; + this.ccr = undefined; + this.lwet = undefined; + this.lcet = undefined; + this.lastCwr = undefined; + this.lastCcr = undefined; + this.lastLwet = undefined; + this.lastLcet = undefined; + this.kph = undefined; + this.wrps = 0; // wheel revs per second + this.crps = 0; // crank revs per second + this.widle = 0; // wheel idle counter + this.cidle = 0; // crank idle counter + //this.batteryLevel = undefined; + } + + getDeviceAddress() { + if (!this.device || !this.device.id) + return '00:00:00:00:00:00'; + return this.device.id.split(" ")[0]; + } + + status(txt) { + this.emit("status", txt); + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + this.status("Scanning"); + // Find a device, then get the CSC Service and subscribe to + // notifications on the CSC Measurement characteristic. + // NRF.setLowPowerConnection(true); + var reconnect = this.reconnect; // auto-reconnect + return NRF.requestDevice({ + timeout: 5000, + filters: [{ + services: ["1816"] + }], + }).then(device => { + this.status("Connecting"); + this.device = device; + this.device.on('gattserverdisconnected', event => { + this.device = undefined; + this.gatt = undefined; + this.resetStats(); + this.status("Disconnected"); + this.emit("disconnect", event); + if (reconnect) {// auto-reconnect + reconnect = false; + setTimeout(() => { + if (this.reconnect) this.connect().then(() => {}, () => {}); + }, 500); + } + }); + + return new Promise(resolve => setTimeout(resolve, 150)); // On CooSpo we get a 'Connection Timeout' if we try and connect too soon + }).then(() => { + return this.device.gatt.connect(); + }).then(gatt => { + this.status("Connected"); + this.gatt = gatt; + return gatt.getPrimaryService("1816"); + }).then(service => { + return service.getCharacteristic("2a5b"); // UUID of the CSC measurement characteristic + }).then(characteristic => { + // register for changes on 2a5b + characteristic.on('characteristicvaluechanged', event => { + const flags = event.target.value.getUint8(0); + var offs = 0; + var data = {}; + if (flags & 1) { // FLAGS_WREV_BM + this.lastCwr = this.cwr; + this.lastLwet = this.lwet; + this.cwr = event.target.value.getUint32(1, true); + this.lwet = event.target.value.getUint16(5, true); + if (this.lastCwr === undefined) this.lastCwr = this.cwr; + if (this.lastLwet === undefined) this.lastLwet = this.lwet; + if (this.lwet < this.lastLwet) this.lastLwet -= 65536; + let secs = (this.lwet - this.lastLwet) / 1024; + if (secs) { + this.wrps = (this.cwr - this.lastCwr) / secs; + this.widle = 0; + } else { + if (this.widle<5) this.widle++; + else this.wrps = 0; + } + this.kph = this.wrps * this.settings.circum / 3600; + Object.assign(data, { // Notify the 'wheelEvent' handler + cwr: this.cwr, // cumulative wheel revolutions + lwet: this.lwet, // last wheel event time + wrps: this.wrps, // wheel revs per second + wr: this.cwr - this.lastCwr, // wheel revs + wdt : secs, // time period + kph : this.kph + }); + offs += 6; + } + if (flags & 2) { // FLAGS_CREV_BM + this.lastCcr = this.ccr; + this.lastLcet = this.lcet; + this.ccr = event.target.value.getUint16(offs + 1, true); + this.lcet = event.target.value.getUint16(offs + 3, true); + if (this.lastCcr === undefined) this.lastCcr = this.ccr; + if (this.lastLcet === undefined) this.lastLcet = this.lcet; + if (this.lcet < this.lastLcet) this.lastLcet -= 65536; + let secs = (this.lcet - this.lastLcet) / 1024; + if (secs) { + this.crps = (this.ccr - this.lastCcr) / secs; + this.cidle = 0; + } else { + if (this.cidle<5) this.cidle++; + else this.crps = 0; + } + Object.assign(data, { // Notify the 'crankEvent' handler + ccr: this.ccr, // cumulative crank revolutions + lcet: this.lcet, // last crank event time + crps: this.crps, // crank revs per second + cdt : secs, // time period + }); + } + this.emit("data",data); + }); + return characteristic.startNotifications(); +/* }).then(() => { + return this.gatt.getPrimaryService("180f"); + }).then(service => { + return service.getCharacteristic("2a19"); + }).then(characteristic => { + characteristic.on('characteristicvaluechanged', (event)=>{ + this.batteryLevel = event.target.value.getUint8(0); + }); + return characteristic.startNotifications();*/ + }).then(() => { + this.status("Ready"); + }, err => { + this.status("Error: " + err); + if (reconnect) { // auto-reconnect + reconnect = false; + setTimeout(() => { + if (this.reconnect) this.connect().then(() => {}, () => {}); + }, 500); + } + throw err; + }); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (!this.gatt) return; + this.gatt.disconnect(); + this.gatt = undefined; + } + + /* Start trying to connect - will keep searching and attempting to connect*/ + start() { + this.reconnect = true; + if (!this.device) + this.connect().then(() => {}, () => {}); + } + + /* Stop trying to connect, and disconnect */ + stop() { + this.reconnect = false; + this.disconnect(); + } +} + +// Get an instance of BLECSC or create one if it doesn't exist +BLECSC.getInstance = function() { + if (!BLECSC.instance) { + BLECSC.instance = new BLECSC(); + } + return BLECSC.instance; +}; + +exports = BLECSC; + +/* +var csc = require("blecsc").getInstance(); +csc.on("status", txt => { + print("##", txt); + E.showMessage(txt); +}); +csc.on("data", e => print(e)); +csc.start(); +*/ diff --git a/apps/blecsc/clkinfo.js b/apps/blecsc/clkinfo.js new file mode 100644 index 000000000..69cd022af --- /dev/null +++ b/apps/blecsc/clkinfo.js @@ -0,0 +1,72 @@ +(function() { + var csc = require("blecsc").getInstance(); + //csc.on("status", txt => { print("CSC",txt); }); + csc.on("data", e => { + ci.items.forEach(it => { if (it._visible) it.emit('redraw'); }); + }); + csc.on("disconnect", e => { + // redraw all with no info + ci.items.forEach(it => { if (it._visible) it.emit('redraw'); }); + }); + var uses = 0; + var ci = { + name: "CSC", + items: [ + { name : "Speed", + get : () => { + return { + text : (csc.kph === undefined) ? "--" : require("locale").speed(csc.kph), + img : atob("GBiBAAAAAAAAAAAAAAABwAABwAeBgAMBgAH/gAH/wAPDwA/DcD9m/Ge35sW9o8//M8/7E8CBA2GBhn8A/h4AeAAAAAAAAAAAAAAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + }, + { name : "Distance", + get : () => { + return { + text : (csc.kph === undefined) ? "--" : require("locale").distance(csc.cwr * csc.settings.circum / 1000), + img : atob("GBiBAAAAAB8AADuAAGDAAGTAAGRAAEBAAGBAAGDAADCAADGAIB8B+A/BjAfjBgAyJgAyIgAyAj/jBnADBmABjGAA2HAA8D//4AAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + }, + { name : "Cadence", + get : () => { + return { + text : (csc.crps === undefined) ? "--" : Math.round(csc.crps*60), + img : atob("GBiBAAAAAAAAAAB+EAH/sAeB8A4A8AwB8BgAABgAADAAADAAADAAADAADDAADDAAABgAABgAGAwAEA4AAAeAwAH8gAB8AAAAAAAAAA==") + }; + }, + show : function() { + uses++; + if (uses==1) csc.start(); + this._visible = true; + }, + hide : function() { + this._visible = false; + uses--; + if (uses==0) csc.stop(); + } + } + ] + }; + return ci; +}) diff --git a/apps/blecsc/icons8-cycling-48.png b/apps/blecsc/icons8-cycling-48.png new file mode 100644 index 000000000..0bc83859f Binary files /dev/null and b/apps/blecsc/icons8-cycling-48.png differ diff --git a/apps/blecsc/metadata.json b/apps/blecsc/metadata.json new file mode 100644 index 000000000..0daa01fc8 --- /dev/null +++ b/apps/blecsc/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "blecsc", + "name": "BLE Cycling Speed Sensor Library", + "shortName": "BLE CSC", + "version": "0.05", + "description": "Module to get live values from a BLE Cycle Speed (CSC) sensor. Includes recorder and clockinfo plugins", + "icon": "icons8-cycling-48.png", + "tags": "outdoors,exercise,ble,bluetooth,clkinfo", + "type":"module", + "provides_modules" : ["blecsc"], + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"blecsc","url":"blecsc.js"}, + {"name":"blecsc.settings.js","url":"settings.js"}, + {"name":"blecsc.recorder.js","url":"recorder.js"}, + {"name":"blecsc.clkinfo.js","url":"clkinfo.js"} + ], + "data": [ + {"name":"blecsc.json"} + ] +} diff --git a/apps/blecsc/recorder.js b/apps/blecsc/recorder.js new file mode 100644 index 000000000..510f50c3f --- /dev/null +++ b/apps/blecsc/recorder.js @@ -0,0 +1,28 @@ +(function(recorders) { + recorders.blecsc = function() { + var csc = require("blecsc").getInstance(); + var speed, cadence; + csc.on("data", e => { + speed = e.kph; // speed in KPH + cadence = (e.crps===undefined)?"":Math.round(e.crps*60); // crank rotations per minute + }); + return { + name : "CSC", + fields : ["Speed (kph)","Cadence (rpm)"], + getValues : () => { + var r = [speed,cadence]; + speed = ""; + cadence = ""; + return r; + }, + start : () => { + csc.start(); + }, + stop : () => { + csc.stop(); + }, + draw : (x,y) => g.setColor(csc.device?"#0f0":"#8f8").drawImage(atob("Dw+BAAAAAAABgOIA5gHcBxw9fpfTPqYRC8HgAAAAAAAA"),x,y) + }; + } +}) + diff --git a/apps/blecsc/settings.js b/apps/blecsc/settings.js new file mode 100644 index 000000000..b445b2541 --- /dev/null +++ b/apps/blecsc/settings.js @@ -0,0 +1,85 @@ +(function(back) { + const storage = require('Storage') + const SETTINGS_FILE = 'blecsc.json' + + // Set default values and merge with stored values + let settings = Object.assign({ + circum: 2068 // circumference in mm + }, (storage.readJSON(SETTINGS_FILE, true) || {})); + + function saveSettings() { + storage.writeJSON(SETTINGS_FILE, settings); + } + + function circumMenu() { + var v = 0|settings.circum; + var cm = 0|(v/10); + var mm = v-(cm*10); + E.showMenu({ + '': { title: /*LANG*/"Circumference", back: mainMenu }, + 'cm': { + value: cm, + min: 80, max: 240, step: 1, + onchange: (v) => { + cm = v; + settings.circum = (cm*10)+mm; + saveSettings(); + }, + }, + '+ mm': { + value: mm, + min: 0, max: 9, step: 1, + onchange: (v) => { + mm = v; + settings.circum = (cm*10)+mm; + saveSettings(); + }, + }, + /*LANG*/'Std Wheels': function() { + // https://support.wahoofitness.com/hc/en-us/articles/115000738484-Tire-Size-Wheel-Circumference-Chart + E.showMenu({ + '': { title: /*LANG*/'Std Wheels', back: circumMenu }, + '650x38 wheel' : function() { + settings.circum = 1995; + saveSettings(); + mainMenu(); + }, + '700x32c wheel' : function() { + settings.circum = 2152; + saveSettings(); + mainMenu(); + }, + '24"x1.75 wheel' : function() { + settings.circum = 1890; + saveSettings(); + mainMenu(); + }, + '26"x1.5 wheel' : function() { + settings.circum = 2010; + saveSettings(); + mainMenu(); + }, + '27.5"x1.5 wheel' : function() { + settings.circum = 2079; + saveSettings(); + mainMenu(); + } + }); + } + + }); + } + + function mainMenu() { + E.showMenu({ + '': { 'title': 'BLE CSC' }, + '< Back': back, + /*LANG*/'Circumference': { + value: settings.circum+"mm", + onchange: circumMenu + }, + }); + } + + mainMenu(); +}) \ No newline at end of file diff --git a/apps/blescanner/ChangeLog b/apps/blescanner/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/blescanner/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/blescanner/app-icon.js b/apps/blescanner/app-icon.js new file mode 100644 index 000000000..a08a17ae4 --- /dev/null +++ b/apps/blescanner/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ARkQAHBwsBiIACiAHBgQXIkAXJiIuKGAwWEC4cjmYABn//AAMyC63yC653FC6HwC5aQBC5ybIC44WChGAWxMgC44rCxGIZxYXFIoYXBGAQNCAAQXILYYXBGAUDBoK0EC5AsBC4QwEC5wAEC853BhAWDI6CPCFwp3OX4ouCC8xHXCAJ3VX94XCwBHVGIiPTU4oNCAAQWBX5gDBgQRCAAoXGGAUIFwQXHkAXHJIgABCw4IBC5sAiIAEiAgHAAQXLHBAYIC+6wJQYIADgIXGGBJ3FC4iOBAH4A/ACAA==")) diff --git a/apps/blescanner/app.js b/apps/blescanner/app.js new file mode 100644 index 000000000..7cbf80d7e --- /dev/null +++ b/apps/blescanner/app.js @@ -0,0 +1,41 @@ +E.showMessage("Scanning..."); +var devices = []; + +setInterval(function() { + NRF.findDevices(function(devs) { + devs.forEach(dev=>{ + var existing = devices.find(d=>d.id==dev.id); + if (existing) { + existing.timeout = 0; + existing.rssi = (existing.rssi*3 + dev.rssi)/4; + } else { + dev.timeout = 0; + dev.new = 0; + devices.push(dev); + } + }); + devices.forEach(d=>{d.timeout++;d.new++}); + devices = devices.filter(dev=>dev.timeout<8); + devices.sort((a,b)=>b.rssi - a.rssi); + g.clear(1).setFont("12x20"); + var wasNew = false; + devices.forEach((d,y)=>{ + y*=20; + var n = d.name; + if (!n) n=d.id.substr(0,22); + if (d.new<4) { + g.fillRect(0,y,g.getWidth(),y+19); + g.setColor(g.theme.bg); + if (d.rssi > -70) wasNew = true; + } else { + g.setColor(g.theme.fg); + } + g.setFontAlign(-1,-1); + g.drawString(n,0,y); + g.setFontAlign(1,-1); + g.drawString(0|d.rssi,g.getWidth()-1,y); + }); + g.flip(); + Bangle.setLCDBrightness(wasNew); + }, 1200); +}, 1500); diff --git a/apps/blescanner/app.png b/apps/blescanner/app.png new file mode 100644 index 000000000..8665f24ad Binary files /dev/null and b/apps/blescanner/app.png differ diff --git a/apps/blescanner/metadata.json b/apps/blescanner/metadata.json new file mode 100644 index 000000000..54cde3ede --- /dev/null +++ b/apps/blescanner/metadata.json @@ -0,0 +1,14 @@ +{ "id": "blescanner", + "name": "BLE Scanner", + "shortName":"BLE Scan", + "version":"0.01", + "description": "Scans for bluetooth devices nearby and shows their names on the screen ordered by signal strength. The most recently discovered items are highlighted.", + "icon": "app.png", + "screenshots" : [ { "url":"screenshot.png" } ], + "tags": "tool,bluetooth", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"blescanner.app.js","url":"app.js"}, + {"name":"blescanner.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/blescanner/screenshot.png b/apps/blescanner/screenshot.png new file mode 100644 index 000000000..55bd44a52 Binary files /dev/null and b/apps/blescanner/screenshot.png differ diff --git a/apps/blobclk/ChangeLog b/apps/blobclk/ChangeLog index 9c4ef5b7b..193eb5024 100644 --- a/apps/blobclk/ChangeLog +++ b/apps/blobclk/ChangeLog @@ -5,3 +5,4 @@ 0.04: Modified to account for changes in the behavior of Graphics.fillPoly 0.05: Slight increase to draw speed after LCD on 0.06: Update to use Bangle.setUI instead of setWatch, allow themes and different size screens +0.07: Tell clock widgets to hide. diff --git a/apps/blobclk/clock-blob.js b/apps/blobclk/clock-blob.js index c84b8a1e6..d23e18ff9 100644 --- a/apps/blobclk/clock-blob.js +++ b/apps/blobclk/clock-blob.js @@ -99,6 +99,10 @@ function startTimers() { Bangle.drawWidgets(); intervalRef = setInterval(redraw,1000); } + +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); startTimers(); Bangle.on('lcdPower',function(on) { @@ -108,5 +112,3 @@ Bangle.on('lcdPower',function(on) { clearTimers(); } }); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/blobclk/metadata.json b/apps/blobclk/metadata.json index 85d7deabe..3ae8de222 100644 --- a/apps/blobclk/metadata.json +++ b/apps/blobclk/metadata.json @@ -2,7 +2,7 @@ "id": "blobclk", "name": "Large Digit Blob Clock", "shortName": "Blob Clock", - "version": "0.06", + "version": "0.07", "description": "A clock with big digits", "icon": "clock-blob.png", "type": "clock", diff --git a/apps/boldclk/ChangeLog b/apps/boldclk/ChangeLog index 30ac31c61..0c6e8cb52 100644 --- a/apps/boldclk/ChangeLog +++ b/apps/boldclk/ChangeLog @@ -3,3 +3,4 @@ 0.04: Work with themes, smaller screens 0.05: Adjust hand lengths to be within 'tick' points 0.06: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.07: Tell clock widgets to hide. diff --git a/apps/boldclk/bold_clock.js b/apps/boldclk/bold_clock.js index 9d3ea0756..763530a32 100644 --- a/apps/boldclk/bold_clock.js +++ b/apps/boldclk/bold_clock.js @@ -130,9 +130,10 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/boldclk/metadata.json b/apps/boldclk/metadata.json index cf961347d..086203142 100644 --- a/apps/boldclk/metadata.json +++ b/apps/boldclk/metadata.json @@ -1,7 +1,7 @@ { "id": "boldclk", "name": "Bold Clock", - "version": "0.06", + "version": "0.07", "description": "Simple, readable and practical clock", "icon": "bold_clock.png", "screenshots": [{"url":"screenshot_bold.png"}], diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index a43ecf86e..5b0fcc583 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -52,3 +52,27 @@ 0.46: Fix no clock found error on Bangle.js 2 0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed) 0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks) +0.49: Store first found clock as a setting to speed up further boots +0.50: Allow setting of screen rotation + Remove support for 2v11 and earlier firmware +0.51: Remove patches for 2v10 firmware (BEEPSET and setUI) + Add patch to ensure that compass heading is corrected on pre-2v15.68 firmware + Ensure clock is only fast-loaded if it doesn't contain widgets +0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass +0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock +0.54: Fix for invalid version comparison in polyfill +0.55: Add toLocalISOString polyfill for pre-2v15 firmwares + Only add boot info comments if settings.bootDebug was set + If settings.bootDebug is set, output timing for each section of .boot0 +0.56: Settings.log = 0,1,2,3 for off,display, log, both +0.57: Handle the whitelist being disabled +0.58: "Make Connectable" temporarily bypasses the whitelist +0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds +0.60: Minor code improvements +0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined') +0.62: Handle setting for configuring BLE privacy +0.63: Only set BLE `display:1` if we have a passkey +0.64: Automatically create .widcache and .clkinfocache to speed up loads + Bangle.loadWidgets overwritten with fast version on success + Refuse to work on firmware <2v16 and remove old polyfills +0.65: Only display interpreter errors if log is nonzero \ No newline at end of file diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js index 45e271f30..6e6466f48 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -1,8 +1,13 @@ // This runs after a 'fresh' boot -var clockApp=(require("Storage").readJSON("setting.json",1)||{}).clock; -if (clockApp) clockApp = require("Storage").read(clockApp); -if (!clockApp) { - clockApp = require("Storage").list(/\.info$/) +var s = require("Storage").readJSON("setting.json",1)||{}; +/* If were being called from JS code in order to load the clock quickly (eg from a launcher) +and the clock in question doesn't have widgets, force a normal 'load' as this will then +reset everything and remove the widgets. */ +if (global.__FILE__ && !s.clockHasWidgets) {load();throw "Clock has no widgets, can't fast load";} +// Otherwise continue to try and load the clock +var _clkApp = require("Storage").read(s.clock); +if (!_clkApp) { + _clkApp = require("Storage").list(/\.info$/) .map(file => { const app = require("Storage").readJSON(file,1); if (app && app.type == "clock") { @@ -11,9 +16,14 @@ if (!clockApp) { }) .filter(x=>x) .sort((a, b) => a.sortorder - b.sortorder)[0]; - if (clockApp) - clockApp = require("Storage").read(clockApp.src); + if (_clkApp){ + s.clock = _clkApp.src; + _clkApp = require("Storage").read(_clkApp.src); + s.clockHasWidgets = _clkApp.includes("Bangle.loadWidgets"); + require("Storage").writeJSON("setting.json", s); + } } -if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`; -eval(clockApp); -delete clockApp; +delete s; +if (!_clkApp) _clkApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`; +eval(_clkApp); +delete _clkApp; diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 4cb3c52e4..07d8d2031 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -1,21 +1,27 @@ /* This rewrites boot0.js based on current settings. If settings changed then it recalculates, but this avoids us doing a whole bunch of reconfiguration most of the time. */ +{ // execute in our own scope so we don't have to free variables... E.showMessage(/*LANG*/"Updating boot0..."); -var s = require('Storage').readJSON('setting.json',1)||{}; -var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 -var boot = "", bootPost = ""; -if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed - var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; -} else { - var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; +let s = require('Storage').readJSON('setting.json',1)||{}; +//const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 +const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1")); +const DEBUG = s.bootDebug; // we can set this to enable debugging output in boot0 +let boot = "", bootPost = ""; +if (DEBUG) { + boot += "var _tm=Date.now()\n"; + bootPost += "delete _tm;"; } -boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; +if (FWVERSION < 216) { + E.showMessage(/*LANG*/"Please update Bangle.js firmware\n\nCurrent = "+process.env.VERSION,{title:"ERROR"}); + throw new Error("Old firmware"); +} +let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.js$/)+E.CRC32(process.env.GIT_COMMIT); +boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.js$/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; +boot += `{eval(require('Storage').read('bootupdate.js'));}else{\n`; boot += `E.setFlags({pretokenise:1});\n`; boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; -bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code +bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code if (s.ble!==false) { if (s.HID) { // Human interface device if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; @@ -25,21 +31,19 @@ if (s.ble!==false) { boot += `bleServiceOptions.hid=Bangle.HID;\n`; } } -if (s.log==2) { // logging to file - boot += `_DBGLOG=require("Storage").open("log.txt","a"); -`; -} if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth - if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); -LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); +// settings.log 0-off, 1-display, 2-log, 3-both +if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth + if (s.log>=2) { boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}}); LoopbackA.setConsole(true);\n`; - else if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal + } else if (s.log==1) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console /* If not programmable add our own handler for Bluetooth data to allow Gadgetbridge commands to be received*/ boot += ` Bluetooth.line=""; Bluetooth.on('data',function(d) { - var l = (Bluetooth.line + d).split(/[\\n\\r]/); + let l = (Bluetooth.line + d).split(/[\\n\\r]/); Bluetooth.line = l.pop(); l.forEach(n=>Bluetooth.emit("line",n)); }); @@ -49,10 +53,10 @@ Bluetooth.on('line',function(l) { try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {} });\n`; } else { - if (s.log==2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); -LoopbackB.on('data',function(d) {_DBGLOG.write(d);Terminal.write(d);}); + if (s.log>=2) boot += `_DBGLOG=require("Storage").open("log.txt","a"); +LoopbackB.on('data',function(d) {_DBGLOG.write(d);${(s.log==3)?"Terminal.write(d);":""}}); if (!NRF.getSecurityStatus().connected) LoopbackA.setConsole();\n`; - else if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) + else if (s.log==1) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection) else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth } // we just reset, so BLE should be on. @@ -62,112 +66,44 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep(); if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`; if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`; boot += `E.setTimeZone(${s.timezone});`; -// Set vibrate, beep, etc IF on older firmwares -if (!Bangle.F_BEEPSET) { - if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n` - if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n` - else if (s.beep=="vib" && !BANGLEJS2) boot += `Bangle.beep = function (time, freq) { - return new Promise(function(resolve) { - if ((0|freq)<=0) freq=4000; - if ((0|time)<=0) time=200; - if (time>5000) time=5000; - analogWrite(D13,0.1,{freq:freq}); - setTimeout(function() { - digitalWrite(D13,0); - resolve(); - }, time); - }); - };\n`; -} -// Draw out of memory errors onto the screen -boot += `E.on('errorFlag', function(errorFlags) { +// Draw out of memory errors onto the screen if logging enabled +if (s.log) boot += `E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); print("Interpreter error:", errorFlags); - E.getErrorFlags(); // clear flags so we get called next time -});\n`; + E.getErrorFlags(); +});\n`;// E.getErrorFlags() -> clear flags so we get called next time // stop users doing bad things! if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; // Apply any settings-specific stuff if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; -if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; -if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; -// Pre-2v10 firmwares without a theme/setUI -delete g.theme; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.theme) { - boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`; -} -try { - Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied -} catch(e) { - boot += `Bangle._setUI = Bangle.setUI; -Bangle.setUI=function(mode, cb) { - if (Bangle.uiRemove) { - Bangle.uiRemove(); - delete Bangle.uiRemove; - } - if ("object"==typeof mode) { - // TODO: handle mode.back? - mode = mode.mode; - } - Bangle._setUI(mode, cb); -};\n`; -} -delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!E.showScroller) { // added in 2v11 - this is a limited functionality polyfill - boot += `E.showScroller = (function(a){function n(){g.reset();b>=l+c&&(c=1+b-l);bm||m>=a.c)break;var f=24+d*a.h;a.draw(m,{x:0,y:f,w:h,h:a.h});d+c==b&&g.setColor(g.theme.fg).drawRect(0,f,h-1,f+a.h-1).drawRect(1,f+1,h-2,f+a.h-2)}g.setColor(c?g.theme.fg:g.theme.bg);g.fillPoly([e,6,e-14,20,e+14,20]);g.setColor(a.c>l+c?g.theme.fg:g.theme.bg);g.fillPoly([e,k-7,e-14,k-21,e+14,k-21])}if(!a)return Bangle.setUI();var b=0,c=0,h=g.getWidth(), -k=g.getHeight(),e=h/2,l=Math.floor((k-48)/a.h);g.reset().clearRect(0,24,h-1,k-1);n();Bangle.setUI("updown",d=>{d?(b+=d,0>b&&(b=a.c-1),b>=a.c&&(b=0),n()):a.select(b)})});\n`; -} -delete g.imageMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.imageMetrics) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.imageMetrics=function(src) { - if (src[0]) return {width:src[0],height:src[1]}; - else if ('object'==typeof src) return { - width:("width" in src) ? src.width : src.getWidth(), - height:("height" in src) ? src.height : src.getHeight()}; - var im = E.toString(src); - return {width:im.charCodeAt(0), height:im.charCodeAt(1)}; -};\n`; -} -delete g.stringMetrics; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.stringMetrics) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.stringMetrics=function(txt) { - txt = txt.toString().split("\\n"); - return {width:Math.max.apply(null,txt.map(x=>g.stringWidth(x))), height:this.getFontHeight()*txt.length}; -};\n`; -} -delete g.wrapString; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!g.wrapString) { // added in 2v11 - this is a limited functionality polyfill - boot += `Graphics.prototype.wrapString=function(str, maxWidth) { - var lines = []; - for (var unwrappedLine of str.split("\\n")) { - var words = unwrappedLine.split(" "); - var line = words.shift(); - for (var word of words) { - if (g.stringWidth(line + " " + word) > maxWidth) { - lines.push(line); - line = word; - } else { - line += " " + word; - } - } - lines.push(line); - } - return lines; -};\n`; -} -delete Bangle.appRect; // deleting stops us getting confused by our own decl. builtins can't be deleted -if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares - boot += `Bangle.appRect = ((y,w,h)=>({x:0,y:0,w:w,h:h,x2:w-1,y2:h-1}))(g.getWidth(),g.getHeight()); - (lw=>{ Bangle.loadWidgets = () => { lw(); Bangle.appRect = ((y,w,h)=>({x:0,y:y,w:w,h:h-y,x2:w-1,y2:h-(1+h)}))(global.WIDGETS?24:0,g.getWidth(),g.getHeight()); }; })(Bangle.loadWidgets);\n`; +if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) { + let passkey = s.passkey ? `passkey:${E.toJS(s.passkey.toString())},display:1,mitm:1,` : ""; + let privacy = s.bleprivacy ? `privacy:${E.toJS(s.bleprivacy)},` : ""; + boot+=`NRF.setSecurity({${passkey}${privacy}});\n`; } +if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`; +if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`; +if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation +boot+=`Bangle.loadWidgets=function(){if(!global.WIDGETS)eval(require("Storage").read(".widcache"))};\n`; +// ================================================== FIXING OLDER FIRMWARES +// deleting stops us getting confused by our own decl. builtins can't be deleted +// this is a polyfill without fastloading capability +delete Bangle.showClock; +if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`; +delete Bangle.load; +if (!Bangle.load) boot += `Bangle.load = load;\n`; -// Append *.boot.js files +// show timings +if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n` +// ================================================== .BOOT0 +// Append *.boot.js files. +// Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered // These could change bleServices/bleServiceOptions if needed -var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ - var getPriority = /.*\.(\d+)\.boot\.js$/; - var aPriority = a.match(getPriority); - var bPriority = b.match(getPriority); +let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ + let getPriority = /.*\.(\d+)\.boot\.js$/; + let aPriority = a.match(getPriority); + let bPriority = b.match(getPriority); if (aPriority && bPriority){ return parseInt(aPriority[1]) - parseInt(bPriority[1]); } else if (aPriority && !bPriority){ @@ -178,15 +114,48 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ return a==b ? 0 : (a>b ? 1 : -1); }); // precalculate file size -var fileSize = boot.length + bootPost.length; -bootFiles.forEach(bootFile=>{ - // match the size of data we're adding below in bootFiles.forEach - fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2; -}); -// write file in chunks (so as not to use up all RAM) -require('Storage').write('.boot0',boot,0,fileSize); -var fileOffset = boot.length; -bootFiles.forEach(bootFile=>{ +bootPost += "}"; +let fileOffset,fileSize; +/* code to output a file, plus preable and postable +when called with dst==undefined it just increments +fileOffset so we can see ho wbig the file has to be */ +let outputFile = (dst,src,pre,post) => {"ram"; + if (DEBUG) { + if (dst) require('Storage').write(dst,`//${src}\n`,fileOffset); + fileOffset+=2+src.length+1; + } + if (pre) { + if (dst) require('Storage').write(dst,pre,fileOffset); + fileOffset+=pre.length; + } + let f = require('Storage').read(src); + if (src.endsWith("clkinfo.js") && f[0]!="(") { + /* we shouldn't have to do this but it seems sometimes (sched 0.28) folks have + used libraries which get added into the clockinfo, and we can't use them directly + to we have to revert back to eval */ + f = `eval(require('Storage').read(${E.toJS(src)}))`; + } + if (dst) { + // we can't just write 'f' in one go because it can be too big + let len = f.length; + let offset = 0; + while (len) { + let chunk = Math.min(len, 2048); + require('Storage').write(dst,f.substr(offset, chunk),fileOffset); + fileOffset+=chunk; + offset+=chunk; + len-=chunk; + } + } else + fileOffset+=f.length; + if (dst) require('Storage').write(dst,post,fileOffset); + fileOffset+=post.length; + if (DEBUG) { + if (dst) require('Storage').write(dst,`print(${E.toJS(src)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset); + fileOffset += 48+E.toJS(src).length; + } +}; +let outputFileComplete = (dst,fn) => { // we add a semicolon so if the file is wrapped in (function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() // which would cause an error! @@ -194,32 +163,50 @@ bootFiles.forEach(bootFile=>{ // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; // but we need to do this without ever loading everything into RAM as some // boot files seem to be getting pretty big now. - require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset); - fileOffset+=2+bootFile.length+1; - var bf = require('Storage').read(bootFile); - // we can't just write 'bf' in one go because at least in 2v13 and earlier - // Espruino wants to read the whole file into RAM first, and on Bangle.js 1 - // it can be too big (especially BTHRM). - var bflen = bf.length; - var bfoffset = 0; - while (bflen) { - var bfchunk = Math.min(bflen, 2048); - require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset); - fileOffset+=bfchunk; - bfoffset+=bfchunk; - bflen-=bfchunk; - } - require('Storage').write('.boot0',";\n",fileOffset); - fileOffset+=2; -}); + outputFile(dst,fn,"",";\n"); +}; +fileOffset = boot.length + bootPost.length; +bootFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes +fileSize = fileOffset; +require('Storage').write('.boot0',boot,0,fileSize); +fileOffset = boot.length; +bootFiles.forEach(fn=>outputFileComplete('.boot0',fn)); require('Storage').write('.boot0',bootPost,fileOffset); - -delete boot; -delete bootPost; -delete bootFiles; -delete fileSize; -delete fileOffset; +delete boot,bootPost,bootFiles; +// ================================================== .WIDCACHE for widgets +let widgetFiles = require("Storage").list(/\.wid\.js$/); +let widget = `// Made by bootupdate.js\nglobal.WIDGETS={};`, widgetPost = `var W=WIDGETS;WIDGETS={}; +Object.keys(W).sort((a,b)=>(0|W[b].sortorder)-(0|W[a].sortorder)).forEach(k=>WIDGETS[k]=W[k]);`; // sort +if (DEBUG) widget+="var _tm=Date.now();"; +outputFileComplete = (dst,fn) => { + outputFile(dst,fn,"try{",`}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`); +}; +fileOffset = widget.length + widgetPost.length; +widgetFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes +fileSize = fileOffset; +require('Storage').write('.widcache',widget,0,fileSize); +fileOffset = widget.length; +widgetFiles.forEach(fn=>outputFileComplete('.widcache',fn)); +require('Storage').write('.widcache',widgetPost,fileOffset); +delete widget,widgetPost,widgetFiles; +// ================================================== .clkinfocache for clockinfos +let ciFiles = require("Storage").list(/\.clkinfo\.js$/); +let ci = `// Made by bootupdate.js\n`; +if (DEBUG) ci+="var _tm=Date.now();"; +outputFileComplete = (dst,fn) => { + outputFile(dst,fn,"try{let fn=",`;let a=fn(),b=menu.find(x=>x.name===a.name);if(b)b.items=b.items.concat(a.items)else menu=menu.concat(a);}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`); +}; +fileOffset = ci.length; +ciFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes +fileSize = fileOffset; +require('Storage').write('.clkinfocache',ci,0,fileSize); +fileOffset = ci.length; +ciFiles.forEach(fn=>outputFileComplete('.clkinfocache',fn)); +delete ci,ciFiles; +// test with require("clock_info").load() +// ================================================== END E.showMessage(/*LANG*/"Reloading..."); -eval(require('Storage').read('.boot0')); +} // .bootcde should be run automatically after if required, since // we normally get called automatically from '.boot0' +eval(require('Storage').read('.boot0')); diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 62adc4db1..afe576e71 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.48", + "version": "0.65", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -11,6 +11,9 @@ {"name":".boot0","url":"boot0.js"}, {"name":".bootcde","url":"bootloader.js"}, {"name":"bootupdate.js","url":"bootupdate.js"} + ],"data": [ + {"name":".widcache"}, + {"name":".clkinfocache"} ], "sortorder": -10 } diff --git a/apps/bootbthomebatt/ChangeLog b/apps/bootbthomebatt/ChangeLog new file mode 100644 index 000000000..2a37193a3 --- /dev/null +++ b/apps/bootbthomebatt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release. diff --git a/apps/bootbthomebatt/README.md b/apps/bootbthomebatt/README.md new file mode 100644 index 000000000..79c379f33 --- /dev/null +++ b/apps/bootbthomebatt/README.md @@ -0,0 +1,11 @@ +# BLE BTHome Battery Service + +Broadcasts battery remaining percentage over BLE using the [BTHome protocol](https://bthome.io/) - which makes for easy integration into [Home Assistant](https://www.home-assistant.io/) + +## Usage + +This boot code runs in the background and has no user interface. + +## Creator + +[Deirdre O'Byrne](https://github.com/deirdreobyrne) diff --git a/apps/bootbthomebatt/bluetooth.png b/apps/bootbthomebatt/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/bootbthomebatt/bluetooth.png differ diff --git a/apps/bootbthomebatt/boot.js b/apps/bootbthomebatt/boot.js new file mode 100644 index 000000000..a9bdcd4d3 --- /dev/null +++ b/apps/bootbthomebatt/boot.js @@ -0,0 +1,14 @@ +var btHomeBatterySequence = 0; + +function advertiseBTHomeBattery() { + var advert = [0x40, 0x00, btHomeBatterySequence, 0x01, E.getBattery()]; + + require("ble_advert").set(0xFCD2, advert); + btHomeBatterySequence = (btHomeBatterySequence + 1) & 255; +} + +setInterval(function() { + advertiseBTHomeBattery(); +}, 300000); // update every 5 min + +advertiseBTHomeBattery(); diff --git a/apps/bootbthomebatt/metadata.json b/apps/bootbthomebatt/metadata.json new file mode 100644 index 000000000..0992edc24 --- /dev/null +++ b/apps/bootbthomebatt/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "bootbthomebatt", + "name": "BLE BTHome Battery Service", + "shortName": "BTHome Battery Service", + "version": "0.01", + "description": "Broadcasts battery remaining over bluetooth using the BTHome protocol - makes for easy integration with Home Assistant.\n", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "battery,ble,bluetooth,bthome", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"bthomebat.boot.js","url":"boot.js"} + ] +} diff --git a/apps/bootgattbat/ChangeLog b/apps/bootgattbat/ChangeLog index 2a37193a3..3df6422e5 100644 --- a/apps/bootgattbat/ChangeLog +++ b/apps/bootgattbat/ChangeLog @@ -1 +1,3 @@ 0.01: Initial release. +0.02: Handle the case where other apps have set bleAdvert to an array +0.03: Use the bleAdvert module diff --git a/apps/bootgattbat/boot.js b/apps/bootgattbat/boot.js index d67b766b5..f9b5969e2 100644 --- a/apps/bootgattbat/boot.js +++ b/apps/bootgattbat/boot.js @@ -1,10 +1,8 @@ (() => { function advertiseBattery() { - Bangle.bleAdvert[0x180F] = [E.getBattery()]; - NRF.setAdvertising(Bangle.bleAdvert); + require("ble_advert").set(0x180F, [E.getBattery()]); } - if (!Bangle.bleAdvert) Bangle.bleAdvert = {}; setInterval(advertiseBattery, 60 * 1000); advertiseBattery(); })(); diff --git a/apps/bootgattbat/metadata.json b/apps/bootgattbat/metadata.json index 95a521f47..b53708c2e 100644 --- a/apps/bootgattbat/metadata.json +++ b/apps/bootgattbat/metadata.json @@ -2,7 +2,7 @@ "id": "bootgattbat", "name": "BLE GATT Battery Service", "shortName": "BLE Battery Service", - "version": "0.01", + "version": "0.03", "description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n", "icon": "bluetooth.png", "type": "bootloader", diff --git a/apps/bootgatthrm/ChangeLog b/apps/bootgatthrm/ChangeLog new file mode 100644 index 000000000..205f036b5 --- /dev/null +++ b/apps/bootgatthrm/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial release. +0.02: Added compatibility to OpenTracks and added HRM Location +0.03: Allow setting to keep BLE connected +0.04: Use the bleAdvert module diff --git a/apps/bootgatthrm/README.md b/apps/bootgatthrm/README.md new file mode 100644 index 000000000..15bb2b670 --- /dev/null +++ b/apps/bootgatthrm/README.md @@ -0,0 +1,16 @@ +# BLE GATT HRM Service + +Adds the GATT HRM Service to advertise the current HRM over Bluetooth. + +## Usage + +This boot code runs in the background and has no user interface. + +## Creator + +[Another Stranger](https://github.com/anotherstranger) + +## Aknowledgements + +Special thanks to [Jonathan Jefferies](https://github.com/jjok) for creating the +bootgattbat app, which was the inspiration for this App! diff --git a/apps/bootgatthrm/bluetooth.png b/apps/bootgatthrm/bluetooth.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/bootgatthrm/bluetooth.png differ diff --git a/apps/bootgatthrm/boot.js b/apps/bootgatthrm/boot.js new file mode 100644 index 000000000..efba1f453 --- /dev/null +++ b/apps/bootgatthrm/boot.js @@ -0,0 +1,68 @@ +(() => { + function setupHRMAdvertising() { + /* + * This function prepares BLE heart rate Advertisement. + */ + + require("ble_advert").set(0x180d, undefined, { + // We need custom Advertisement settings for Apps like OpenTracks + connectable: true, + discoverable: true, + scannable: true, + whenConnected: true, + }); + + NRF.setServices({ + 0x180D: { // heart_rate + 0x2A37: { // heart_rate_measurement + notify: true, + value: [0x06, 0], + }, + 0x2A38: { // Sensor Location: Wrist + value: 0x02, + } + } + }); + } + + const keepConnected = (require("Storage").readJSON("gatthrm.settings.json", 1) || {}).keepConnected; + + function updateBLEHeartRate(hrm) { + /* + * Send updated heart rate measurement via BLE + */ + if (hrm === undefined || hrm.confidence < 50) return; + try { + NRF.updateServices({ + 0x180D: { + 0x2A37: { + value: [0x06, hrm.bpm], + notify: true + }, + 0x2A38: { + value: 0x02, + } + } + }); + } catch (error) { + if (error.message.includes("BLE restart")) { + /* + * BLE has to restart after service setup. + */ + if(!keepConnected) + NRF.disconnect(); + } + else if (error.message.includes("UUID 0x2a37")) { + /* + * Setup service if it wasn't setup correctly for some reason + */ + setupHRMAdvertising(); + } else { + console.log("[bootgatthrm]: Unexpected error occured while updating HRM over BLE! Error: " + error.message); + } + } + } + + setupHRMAdvertising(); + Bangle.on("HRM", updateBLEHeartRate); +})(); diff --git a/apps/bootgatthrm/metadata.json b/apps/bootgatthrm/metadata.json new file mode 100644 index 000000000..d32b51601 --- /dev/null +++ b/apps/bootgatthrm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "bootgatthrm", + "name": "BLE GATT HRM Service", + "shortName": "BLE HRM Service", + "version": "0.04", + "description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n", + "icon": "bluetooth.png", + "type": "bootloader", + "tags": "hrm,health,ble,bluetooth,gatt", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gatthrm.boot.js","url":"boot.js"} + ] +} diff --git a/apps/bordle/metadata.json b/apps/bordle/metadata.json index f6011f798..0dbcf09f9 100644 --- a/apps/bordle/metadata.json +++ b/apps/bordle/metadata.json @@ -6,7 +6,7 @@ "description": "Bangle version of a popular word search game", "supports" : ["BANGLEJS2"], "readme": "README.md", - "tags": "game, text", + "tags": "game,text", "storage": [ {"name":"bordle.app.js","url":"bordle.app.js"}, {"name":"wordlencr.txt","url":"wordlencr.txt"}, diff --git a/apps/bowserWF/ChangeLog b/apps/bowserWF/ChangeLog new file mode 100644 index 000000000..4417339fa --- /dev/null +++ b/apps/bowserWF/ChangeLog @@ -0,0 +1,4 @@ +... +0.02: First update with ChangeLog Added +0.03: updated watch face to use the ClockFace library +0.04: Minor code improvements diff --git a/apps/bowserWF/app.js b/apps/bowserWF/app.js index e53d945cc..e93617b18 100644 --- a/apps/bowserWF/app.js +++ b/apps/bowserWF/app.js @@ -1,102 +1,233 @@ var sprite = { - width : 47, height : 47, bpp : 3, - transparent : 1, - buffer : require("heatshrink").decompress(atob("kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA")) + width: 47, + height: 47, + bpp: 3, + transparent: 1, + buffer: require("heatshrink").decompress( + atob( + "kmSpICFn/+BAwCImV//VICJuT//SogRMpmT/2SCJtSyQDB/4RMymRkmX/gRLygDC3/piVhCJElAYf/pNIkgRIlIDCl/6pVBkIRIGwWJEYPypMJCI9KGwQRBLANIPRI2CGoPkyVCBwmeyVLTYNJom8yImBz4gEqV/6Vf+g2BPwf/IIq8C/+kyVRkgDBp/5CIX/+mkz/+y/9BIOf0v6///5LdCz+kCIOk34RBYQMSp5XBGQVk/pNBAQP/9IyBxGSv4yCk/1OIK8EC4QgEpM/JgJ+EGoIRBTApQCEYvplLOFXIIdBO4SqBeQJABGoeTDQMlk5WCAAPSYQLgEz4aBlM/9IgB/7CCcAvP/QsBiVfUwOJBgUiCIcmpAVCy/+pMAKwMkRgIRCp6VBAwW6qVOgmSgPkwgRDv53E6WSuEkyEPRgmf2VJv5HBl2SgAKBwEJRgnJiVKp/Sr/0y/yBQOQv56DKwVSv2STwO/DgWD/BADmaDByRoBYoQRCgFCCIf/+jgDNwOUAwMg/kSPQbODX4IJBAwUH8B6DsmRl5oBl7OBklMyV+gBoDycSxMpiVLZwS8EAQeYyjaByR6BBIJBDAQnEIgbFCogOFRgQDBr//I4L0EAQsxAYP//5WCGQ6MCAAKbCpKYEAQiMB//kIQOUyf+CJF/CIIEBTYOfcgQRHBQv/CJKnBpP8GRTCDJIPkGRQCB5I3C/n/EZUgA" + ) + ), }; const boxes = { - width : 122, height : 56, bpp : 3, - transparent : 1, - buffer : require("heatshrink").decompress(atob("kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA=")) + width: 122, + height: 56, + bpp: 3, + transparent: 1, + buffer: require("heatshrink").decompress( + atob( + "kmZkmSpICPwgDBmQUQAQMJAYNkFiOSiQDB5JESAYQsSpADByYsSyBZBydt23bAR+wgFJkwUQAQNggGSposR23AgMkzZESwECpM2IiUAgmSFiW2gDlBFiVsgDlBFiXYgDNBL4MDWZy2FgEGWZy2FgENWZy2EL4MbWZpTBWwZfBXJpTCWwZiCWZpTBWwZiCWZsbWwhiCWZpWCWwTORWwgXRWwgXRWwZESWwZESWwZESWwYXRWwgXRW362/W362/W362/W362/W362/W362/W362/W362/W362/W362/WwuAgazOWwsAgyzOWwsAhqzOWwhfBjazNKYK2DL4K5NKYS2DMQSzNKYK2DMQSzNja2EMQSzNKwS2CZyK2EC6K2EC6K2DIiS2DIiS2DIiUAFoMAAFTkBFtckyAtrLgWSpICnLIIsqyVAgAsqpIA=" + ) + ), }; const background = { - width : 176, height : 176, bpp : 3, - transparent : 5, - buffer : require("heatshrink").decompress(atob("kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA=")) + width: 176, + height: 176, + bpp: 3, + transparent: 5, + buffer: require("heatshrink").decompress( + atob( + "kmSpIC/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ATWAgEAIP1///8iRB8gf/AAOCIPdIIARBBoJB/+E4IP4ABghB9v4CB8BB5g/92//9pB7wP/97FEIO9IgDACAAn8iVBIOlHH4xBDnA+wyY9IAAmB/BB//5B/IOQ/OAARBup5B/yV/IP5B/IP5BRt5B7/wDC7aD8/w+B+3bBgP7IP5B7HYNt23/AQPfIPX/9oCC24IDINwCBIRAAHIOACBHI3+g4EC/l/4BByAQkA//wpED//4gGAhJB3pMAgQFBgEBH3AC/AX4C/AX4C/AX4C/AX4C/AUOAgBB/v//ghB9gf///gH3UgiVIIAJBBwRB5j+CIIf8uBB5//wIIXb//+hJB6o/92/7v5B7/0/97GCIPYAG4MgIP/BjkSIP34/hB//5B/AAQ+0IP5B/IP5BN7ZB97///wCBIPX93yAB2wCB+5B5tv//dt24CB35B5v/+n/t+P/I4PH8ESIO38gFA/+CgH/+EIgiD3gACCPoMAgQ+2AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/AX4C/ASVIgAACgRB/IPY8GkAHBiRB/IPBLKgJB/IP5B/AQUAkmQghB/IP2AgEAyVAiRB/IP5BBpMAIP5B/IIUkgBB/IP5BpoAsBgJBOgEEIIoIBIP5BlyE27dt2EEIJ4CBBAlIgRBgpEAhu2IIO24ESQwxB/IJQhGkEJIL8GHwQCDgOweQpB/IKMkwAKJILVgAofYeQhBzsEAIKICLoESILmBQARBBtuwgZB3kA4B4ENIgJBcpMAIMYCDIOcAgEbHYgCGsEJkhEBE6cBIP5BZfYQ+JIIkDsEBIP5BVyEAIKtAHxgCDwBEBINk2IKCGCIKmSpECIP5BUkEBHyACD2BBUFoMJIP5BSpEbHyQCDIP5BXkmAIP5B/AQcAbKJB/ILH/AAP8hM/AgWSv4KCAAP+gmfAoXJk4ME//gpIEC8mTBgvwkgEC+QRDAAX4gVPAgP5kgsCLwWQh/kMIUf5LuFg4jBAoMBKAJ5EwF/AoUA/yFFoE/CI6RDgY+BCIQsDIP5B/IP5B/IP5B/IJ/AIJfghJBKv0EIJcAIJfwIP5BMhMAAAMEz5BGgmABoVJII9IBgUkII8kBgUSII8CoAMBhJB/IIsQoMAYoP/AAP4YpAMC/+BII9/BgXAYpAMC8DFIBgXwIIcCIP6DCgkQh/kCIRBIbQcBIJAFCgBBICI5BE/IRDFgQA=" + ) + ), }; -numbersDims = { - width: 20, - height: 44 -}; -const numbers = [ - require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA=")), - require("heatshrink").decompress(atob("ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA=")), +/*const numbersDims = { + width: 20, + height: 44, +};*/ +const numbers = [ + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/ARGQKYQIDAwUEBxMAAQNAgECpMgAQMkB4IOIAQQLCgEQBwQaBgEBB1oCBBwYCCiRWDCIRWEO5wOHAX4CnA=" + ) + ), + require("heatshrink").decompress( + atob("ikswcBkmSpIC/ARNIKYIIEwEAggOKNIQODyAHCBxQsWB3TUFgMgA4sSBwzU/AVA=") + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBwwyDDwUSCgVAAwIOBEwI7EpI7FBw4FDghZGHwgOEF4Y+CEYQ+DBxQADNAIAFNAIOFa/4CoA=" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ8gKosSAwsBBw4aCoEAgQjEBoIpEBwtIBoIUEwEAggUDBwwyDDoWQA4ZWHhIIEJQoOCgI+EBwMQEAYOJO4oLBO4oRDJQrX/AU4" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/ARNIKgQIDwAGBgQOJNQYOCyAHDBxEggB6BBwYDBiVABxIjBCIIODF4YOEAAkBV40QBwxiDNAosEB0IC/AUg" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ5UFkmQAwkCBxIdGCIIIDBxAsTgAaEkEASooOBiQOVJQgOBiBKDBxMSJQwRBLIgRCBwjX/AVA=" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/ARGQKgYICAwcCBxADBiQdDkEANYoOGEAYyEHYoOIHYqfFBxIdDBAMQFgZHCBysSFgwRBO46GFa/4CnA" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ5VGiAGFgIOIDQUgBwUCEYQOJGQYNBHAlADQgOHwEAggUDpANBCgYpBBwmQAwJiGhIjDB1gC/AU4A=" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ8gKYYICAwcEBxGQgAaDgVJgACBDQQOJgB6CBwcAiQODHa4AEhIRBpAHDiARBwAGCgIgCFIYOCFIYOHiQrEJQxlCBwzX/AVAA=" + ) + ), + require("heatshrink").decompress( + atob( + "ikswcBkmSpIC/AQ8gKggIBAwkCBw+QCIQLCgIRCDQcQBzkSTAsBHYoOIL4gOCMooOENAYOCoA4EBwoqDgiGGF4gOEa/4CoA=" + ) + ), ]; -digitPositions = [ // relative to the box - {x:13, y:6}, {x:32, y:6}, - {x:74, y:6}, {x:93, y:6}, +const digitPositions = [ + // relative to the box + { x: 13, y: 6 }, + { x: 32, y: 6 }, + { x: 74, y: 6 }, + { x: 93, y: 6 }, ]; -var drawTimeout; const animation_duration = 1; // seconds -const animation_steps = 20; +const animation_steps = 20; const jump_height = 45; // top coordinate of the jump const seconds_per_minute = 60; -function draw() { - const now = new Date(); - g.drawImage(background, 0, 0); - var boxTL_x = 27; var boxTL_y = 29; - var sprite_TL_x = 72; var sprite_TL_y = 161 - sprite.height; - const seconds = now.getSeconds()%seconds_per_minute + now.getMilliseconds()/1000; - const hours = now.getHours(); - const minutes = now.getMinutes(); - - var time_advance = seconds / animation_duration; - - if (time_advance < 0.5) { - sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2; - } else if (time_advance < 1) { - sprite_TL_y = jump_height + (sprite_TL_y-jump_height) * (time_advance-0.5) * 2; - } - const box_penetration = boxTL_y + boxes.height - sprite_TL_y; - if (box_penetration > 0) { - boxTL_y -= box_penetration; - } - g.drawImage(boxes, boxTL_x, boxTL_y); - g.drawImage(numbers[(hours / 10) >> 0], boxTL_x+digitPositions[0].x, boxTL_y+digitPositions[0].y); - g.drawImage(numbers[(hours % 10) >> 0], boxTL_x+digitPositions[1].x, boxTL_y+digitPositions[1].y); - g.drawImage(numbers[(minutes / 10) >> 0], boxTL_x+digitPositions[2].x, boxTL_y+digitPositions[2].y); - g.drawImage(numbers[(minutes % 10) >> 0], boxTL_x+digitPositions[3].x, boxTL_y+digitPositions[3].y); - g.drawImage(sprite, sprite_TL_x, sprite_TL_y); - Bangle.drawWidgets(); - - const timeout = time_advance <= 1? - animation_duration / animation_steps - : (seconds_per_minute - seconds); - setTimeout( _=>{ - drawTimeout = undefined; - draw(); - }, timeout * 1000); -} +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + precision: 60, // just once a minute -// Clear the screen once, at startup -g.setTheme({bg:"#00f",fg:"#fff",dark:true}).clear(); + init: function() { + // Clear the screen once, at startup + g.setTheme({ bg: "#00f", fg: "#fff", dark: true }).clear(); -Bangle.on('lcdPower',on=>{ - if (on) { - draw(); // draw immediately, queue redraw - } else { // stop draw timer - if (drawTimeout) { - clearTimeout(drawTimeout); - } - drawTimeout = undefined; - } + this.drawing = true; + + this.simpleDraw = function(now) { + var boxTL_x = 27; + var boxTL_y = 29; + var sprite_TL_x = 72; + var sprite_TL_y = 161 - sprite.height; + const seconds = + (now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000; + const hours = + this.is12Hour && now.getHours() > 12 + ? now.getHours() - 12 + : now.getHours(); + + const minutes = now.getMinutes(); + + g.drawImage(boxes, boxTL_x, boxTL_y); + g.drawImage( + numbers[(hours / 10) >> 0], + boxTL_x + digitPositions[0].x, + boxTL_y + digitPositions[0].y + ); + g.drawImage( + numbers[hours % 10 >> 0], + boxTL_x + digitPositions[1].x, + boxTL_y + digitPositions[1].y + ); + g.drawImage( + numbers[(minutes / 10) >> 0], + boxTL_x + digitPositions[2].x, + boxTL_y + digitPositions[2].y + ); + g.drawImage( + numbers[minutes % 10 >> 0], + boxTL_x + digitPositions[3].x, + boxTL_y + digitPositions[3].y + ); + }; + }, + + pause: function() { + this.drawing = false; + }, + + resume: function() { + this.drawing = true; + }, + + draw: function(now) { + if (!this.drawing) { + this.simpleDraw(now); + return; + } + g.drawImage(background, 0, 0); + var boxTL_x = 27; + var boxTL_y = 29; + var sprite_TL_x = 72; + var sprite_TL_y = 161 - sprite.height; + const seconds = + (now.getSeconds() % seconds_per_minute) + now.getMilliseconds() / 1000; + const hours = + this.is12Hour && now.getHours() > 12 + ? now.getHours() - 12 + : now.getHours(); + + const minutes = now.getMinutes(); + + var time_advance = seconds / animation_duration; + + if (time_advance < 0.5) { + sprite_TL_y += (jump_height - sprite_TL_y) * time_advance * 2; + } else if (time_advance < 1) { + sprite_TL_y = + jump_height + (sprite_TL_y - jump_height) * (time_advance - 0.5) * 2; + } + const box_penetration = boxTL_y + boxes.height - sprite_TL_y; + if (box_penetration > 0) { + boxTL_y -= box_penetration; + } + g.drawImage(boxes, boxTL_x, boxTL_y); + g.drawImage( + numbers[(hours / 10) >> 0], + boxTL_x + digitPositions[0].x, + boxTL_y + digitPositions[0].y + ); + g.drawImage( + numbers[hours % 10 >> 0], + boxTL_x + digitPositions[1].x, + boxTL_y + digitPositions[1].y + ); + g.drawImage( + numbers[(minutes / 10) >> 0], + boxTL_x + digitPositions[2].x, + boxTL_y + digitPositions[2].y + ); + g.drawImage( + numbers[minutes % 10 >> 0], + boxTL_x + digitPositions[3].x, + boxTL_y + digitPositions[3].y + ); + g.drawImage(sprite, sprite_TL_x, sprite_TL_y); + // Bangle.drawWidgets(); + + if (this.drawing) { + const timeout = + time_advance <= 1 ? animation_duration / animation_steps : -999; + if (timeout > 0) { + setTimeout((_) => { + this.draw(new Date()); + }, timeout * 1000); + } + } + }, + + update: function(date, changed) { + if (this.drawing && changed.m) { + this.draw(date); + } + }, }); -// Show launcher when middle button pressed -Bangle.setUI("clock"); -// Load widgets -Bangle.loadWidgets(); - -draw(); +clock.start(); diff --git a/apps/bowserWF/metadata.json b/apps/bowserWF/metadata.json index a0bdfb8e9..462592902 100644 --- a/apps/bowserWF/metadata.json +++ b/apps/bowserWF/metadata.json @@ -1,18 +1,18 @@ -{ +{ "id": "bowserWF", "name": "Bowser Watchface", - "shortName":"Bowser Watchface", - "version":"0.02", + "shortName": "Bowser Watchface", + "version": "0.04", "description": "Let bowser show you the time", "icon": "app.png", "type": "clock", "tags": "clock", - "supports" : ["BANGLEJS2"], + "supports": ["BANGLEJS2"], "allow_emulator": true, "readme": "README.md", "storage": [ - {"name":"bowserWF.app.js","url":"app.js"}, - {"name":"bowserWF.img","url":"app-icon.js","evaluate":true} + { "name": "bowserWF.app.js", "url": "app.js" }, + { "name": "bowserWF.img", "url": "app-icon.js", "evaluate": true } ], - "data": [{"name":"bowserWF.json"}] + "data": [{ "name": "bowserWF.json" }] } diff --git a/apps/boxclk/ChangeLog b/apps/boxclk/ChangeLog new file mode 100644 index 000000000..65a6c979f --- /dev/null +++ b/apps/boxclk/ChangeLog @@ -0,0 +1,16 @@ +0.01: New App! +0.02: New config options such as step, meridian, short/long formats, custom prefix/suffix +0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false +0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher +0.05: Fixes step count not resetting after a new day starts +0.06: Added clockbackground app functionality +0.07: Allow custom backgrounds per boxclk config and from the clockbg module +0.08: Improves performance, responsiveness, and bug fixes +- [+] Added box size caching to reduce calculations +- [+] Improved step count with real-time updates +- [+] Improved battery level update logic to reduce unnecessary refreshes +- [+] Fixed optional seconds not displaying in time +- [+] Fixed drag handler by adding E.stopEventPropagation() +- [+] General code optimization and cleanup +0.09: Revised event handler code +0.10: Revised suffix formatting in getDate function \ No newline at end of file diff --git a/apps/boxclk/README.md b/apps/boxclk/README.md new file mode 100644 index 000000000..c72d932a4 --- /dev/null +++ b/apps/boxclk/README.md @@ -0,0 +1,118 @@ +# Box Clock + +Box Clock is a customizable clock app for Bangle.js 2 that features an interactive drag and drop interface and easy JSON configuration. + +## Unique Features + +__Drag & Drop:__ + +This intuitive feature allows you to reposition any element (box) on the clock face with ease. Tap on the box(s) you want to move and the border will show, drag into position and tap outside of the boxes to finish placing. **Note:** Roll the tip of your finger slowly on the screen for fine adjustments. + +__Double Tap to Save:__ + +After you've found the perfect position for your boxes, you can save their positions with a quick double tap on the background. This makes it easy to ensure your custom layout is stored for future use. A save icon will appear momentarily to confirm that your configuration has been saved. + +__JSON Configuration:__ + +Each box can be customized extensively via a simple JSON configuration. You can add a custom text string to your configuration with the "string" parameter and you can match system theme colors by using "fg", "bg", "fg2", "bg2", "fgH", or "bgH" for any of the color parameters. + +## Config File Structure + +Here's an example of what a configuration might contain: + +``` +{ + "customBox": { + "string": "Your text here", + "font": "CustomFont", // Custom fonts must be removed in setUI + "fontSize": 1, + "outline": 2, + "color": "#FF9900", // Use 6 or 3 digit hex color codes + "outlineColor": "bgH", // Or match system theme colors like this + "border": 65535, // Or use 16-bit RGB565 like this + "xPadding": 1, + "yPadding": -4, + "xOffset": 0, + "yOffset": 3, + "boxPos": { "x": 0.5, "y": 0.5 }, + "prefix": "", // Adds a string to the beginning of the main string + "suffix": "", // Adds a string to the end of the main string + "disableSuffix": true, // Use to remove DayOfMonth suffix only + "short": false, // Use long format of time, meridian, date, or DoW + "shortMonth": false // Use long format of month within date + + }, + "bg": { // Can also be removed for no background + "img": "YourImageName.img" + } +} +``` + +__Breakdown of Parameters:__ + +* **Box Name:** The name of your text box. Box Clock includes functional support for "time", "date", "meridian" (AM/PM), "dow" (Day of Week), "batt" (Battery), and "step" (Step count). You can add additional custom boxes with unique titles. + +* **string:** The text string to be displayed inside the box. This is only required for custom Box Names. + +* **font:** The font name given to g.setFont(). + +* **fontSize:** The size of the font. + +* **outline:** The thickness of the outline around the text. + +* **color:** The color of the text. + +* **outlineColor:** The color of the text outline. + +* **border:** The color of the box border when moving. + +* **xPadding, yPadding:** Additional padding around the text inside the box. + +* **xOffset, yOffset:** Offsets the text position within the box. + +* **boxPos:** Initial position of the box on the screen. Values are fractions of the screen width (x) and height (y), so { "x": 0.5, "y": 0.5 } would be in the middle of the screen. + +* **prefix:** Adds a string to the beginning of the main string. For example, you can set "prefix": "Steps: " to display "Steps: 100" for the step count. + +* **suffix:** Adds a string to the end of the main string. For example, you can set "suffix": "%" to display "80%" for the battery percentage. + +* **disableSuffix:** Applies only to the "date" box. Set to true to disable the DayOfMonth suffix. This is used to remove the "st","nd","rd", or "th" from the DayOfMonth number. + +* **short:** Set to false to get the long format value of time, meridian, date, or DayOfWeek. Short formats are used by default if not specified. + +* **shortMonth:** Applies only to the "date" box. Set to false to get the long format value of the month. Short format is used by default if not specified. + +* **bg:** This specifies a custom background image, with the img property defining the name of the image file on the Bangle.js storage. + +## Multiple Configurations + +__Settings Menu:__ + +The app includes a settings menu that allows you to switch between different configurations. The selected configuration is stored as a number in the default `boxclk.json` file using the selectedConfig property. + +If the selectedConfig property is not present or is set to 0, the app will use the default configuration. To create additional configurations, create separate JSON files with the naming convention `boxclk-N.json`, where `N` is the configuration number. The settings menu will list all available configurations. + +## Example Configs: + +To easily try out other configs, download and place the JSON configs and/or background images from below onto your Bangle.js storage. Then go to the Box Clock settings menu to select the new config number. You can also modify them to suit your personal preferences. + +__Space Theme:__ + +- **Config:** [boxclk-1.json](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk-1.json) +- **Background:** [boxclk.space.img](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk.space.img) ([Original Source](https://www.pixilart.com/art/fallin-from-outer-space-sr2e0c1a705749a)) + +__System Color Theme:__ + +- **Config:** [boxclk-2.json](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk-2.json) + +## Compatibility + +This app was built and tested with Bangle.js 2. + +## Feedback + +If you have any issues or suggestions, please open an issue on this GitHub repository. Contributions to improve the application are also welcomed. + +## Creator + +[stweedo](https://github.com/stweedo) diff --git a/apps/boxclk/app.js b/apps/boxclk/app.js new file mode 100644 index 000000000..71dbda94f --- /dev/null +++ b/apps/boxclk/app.js @@ -0,0 +1,468 @@ +{ + // 1. Module dependencies and initial configurations + let background = require("clockbg"); + let storage = require("Storage"); + let locale = require("locale"); + let widgets = require("widget_utils"); + let bgImage; + let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; + let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; + if (!storage.read(fileName)) { + fileName = 'boxclk.json'; + } + let boxesConfig = storage.readJSON(fileName, 1) || {}; + let boxes = {}; + let isDragging = false; + let doubleTapTimer = null; + let g_setColor; + + let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA")); + + // 2. Graphical and visual configurations + let w = g.getWidth(); + let h = g.getHeight(); + let drawTimeout; + + // 3. Event handlers + let touchHandler = function(zone, e) { + let boxTouched = false; + let touchedBox = null; + + for (let boxKey in boxes) { + if (touchInText(e, boxes[boxKey])) { + touchedBox = boxKey; + boxTouched = true; + break; + } + } + + if (boxTouched) { + // Toggle the selected state of the touched box + boxes[touchedBox].selected = !boxes[touchedBox].selected; + + // Update isDragging based on whether any box is selected + isDragging = Object.values(boxes).some(box => box.selected); + + if (isDragging) { + widgets.hide(); + } else { + deselectAllBoxes(); + } + } else { + // If tapped outside any box, deselect all boxes + deselectAllBoxes(); + } + + // Always redraw after a touch event + draw(); + + // Handle double tap for saving + if (!boxTouched && !isDragging) { + if (doubleTapTimer) { + clearTimeout(doubleTapTimer); + doubleTapTimer = null; + for (let boxKey in boxes) { + boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3); + boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3); + } + storage.write(fileName, JSON.stringify(boxesConfig)); + displaySaveIcon(); + return; + } + + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 500); + } + }; + + let dragHandler = function(e) { + if (!isDragging) return; + + // Stop propagation of the drag event to prevent other handlers + E.stopEventPropagation(); + + for (let key in boxes) { + if (boxes[key].selected) { + let boxItem = boxes[key]; + calcBoxSize(boxItem); + let newX = boxItem.pos.x + e.dx; + let newY = boxItem.pos.y + e.dy; + + if (newX - boxItem.cachedSize.width / 2 >= 0 && + newX + boxItem.cachedSize.width / 2 <= w && + newY - boxItem.cachedSize.height / 2 >= 0 && + newY + boxItem.cachedSize.height / 2 <= h) { + boxItem.pos.x = newX; + boxItem.pos.y = newY; + } + } + } + + draw(); + }; + + let stepHandler = function(up) { + if (boxes.step && !isDragging) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + boxes.step.cachedSize = null; + draw(); + } + }; + + let lockHandler = function(isLocked) { + if (isLocked) { + deselectAllBoxes(); + draw(); + } + }; + + // 4. Font loading function + let loadCustomFont = function() { + Graphics.prototype.setFontBrunoAce = function() { + // Actual height 23 (24 - 2) + return this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))), + 46, + atob("CBEdChgYGhgaGBsaCQ=="), + 32 | 65536 + ); + }; + }; + + // 5. Initial settings of boxes and their positions + let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal; + + for (let key in boxesConfig) { + if (key === 'bg' && boxesConfig[key].img) { + bgImage = storage.read(boxesConfig[key].img); + } else if (key !== 'selectedConfig') { + boxes[key] = Object.assign({}, boxesConfig[key]); + // Set default values for short, shortMonth, and disableSuffix + boxes[key].short = isBool(boxes[key].short, true); + boxes[key].shortMonth = isBool(boxes[key].shortMonth, true); + boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false); + + // Set box position + boxes[key].pos = { + x: w * boxes[key].boxPos.x, + y: h * boxes[key].boxPos.y + }; + // Cache box size + boxes[key].cachedSize = null; + } + } + + // 6. Text and drawing functions + + /* + Overwrite the setColor function to allow the + use of (x) in g.theme.x as a string + in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") + */ + let modSetColor = function() { + g_setColor = g.setColor; + g.setColor = function(color) { + if (typeof color === "string" && color in g.theme) { + g_setColor.call(g, g.theme[color]); + } else { + g_setColor.call(g, color); + } + }; + }; + + let restoreSetColor = function() { + if (g_setColor) { + g.setColor = g_setColor; + } + }; + + // Overwrite the drawString function + let g_drawString = g.drawString; + g.drawString = function(box, str, x, y) { + outlineText(box, str, x, y); + g.setColor(box.color); + g_drawString.call(g, str, x, y); + }; + + let outlineText = function(box, str, x, y) { + let px = box.outline; + let dx = [-px, 0, px, -px, px, -px, 0, px]; + let dy = [-px, -px, -px, 0, 0, px, px, px]; + g.setColor(box.outlineColor); + for (let i = 0; i < dx.length; i++) { + g_drawString.call(g, str, x + dx[i], y + dy[i]); + } + }; + + let displaySaveIcon = function() { + draw(boxes); + g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24); + // Display save icon for 2 seconds + setTimeout(() => { + g.clearRect(w / 2 - 24, h / 2 - 24, w / 2 + 24, h / 2 + 24); + draw(boxes); + }, 2000); + }; + + // 7. String forming helper functions + let getDate = function(short, shortMonth, disableSuffix) { + const date = new Date(); + const day = date.getDate(); + const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); + const year = date.getFullYear(); + + const getSuffix = (day) => { + if (day >= 11 && day <= 13) return 'th'; + const lastDigit = day % 10; + switch (lastDigit) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + default: return 'th'; + } + }; + + const dayStr = disableSuffix ? day : `${day}${getSuffix(day)}`; + return `${month} ${dayStr}${short ? '' : `, ${year}`}`; // not including year for short version + }; + + let getDayOfWeek = function(date, short) { + return locale.dow(date, short ? 1 : 0); + }; + + locale.meridian = function(date, short) { + let hours = date.getHours(); + let meridian = hours >= 12 ? 'PM' : 'AM'; + return short ? meridian[0] : meridian; + }; + + let formatStr = function(boxItem, data) { + return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`; + }; + + // 8. Main draw function and update logic + let lastDay = -1; + const BATTERY_UPDATE_INTERVAL = 300000; + + let updateBoxData = function() { + let date = new Date(); + let currentDay = date.getDate(); + let now = Date.now(); + + if (boxes.time || boxes.meridian || boxes.date || boxes.dow) { + if (boxes.time) { + let showSeconds = !boxes.time.short; + let timeString = locale.time(date, 1).trim(); + if (showSeconds) { + let seconds = date.getSeconds().toString().padStart(2, '0'); + timeString += ':' + seconds; + } + let newTimeString = formatStr(boxes.time, timeString); + if (newTimeString !== boxes.time.string) { + boxes.time.string = newTimeString; + boxes.time.cachedSize = null; + } + } + + if (boxes.meridian) { + let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short)); + if (newMeridianString !== boxes.meridian.string) { + boxes.meridian.string = newMeridianString; + boxes.meridian.cachedSize = null; + } + } + + if (boxes.date && currentDay !== lastDay) { + let newDateString = formatStr(boxes.date, + getDate(boxes.date.short, + boxes.date.shortMonth, + boxes.date.noSuffix) + ); + if (newDateString !== boxes.date.string) { + boxes.date.string = newDateString; + boxes.date.cachedSize = null; + } + } + + if (boxes.dow) { + let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short)); + if (newDowString !== boxes.dow.string) { + boxes.dow.string = newDowString; + boxes.dow.cachedSize = null; + } + } + + lastDay = currentDay; + } + + if (boxes.step) { + let newStepCount = Bangle.getHealthStatus("day").steps; + let newStepString = formatStr(boxes.step, newStepCount); + if (newStepString !== boxes.step.string) { + boxes.step.string = newStepString; + boxes.step.cachedSize = null; + } + } + + if (boxes.batt) { + if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) { + let currentLevel = E.getBattery(); + if (currentLevel !== boxes.batt.lastLevel) { + let newBattString = formatStr(boxes.batt, currentLevel); + if (newBattString !== boxes.batt.string) { + boxes.batt.string = newBattString; + boxes.batt.cachedSize = null; + boxes.batt.lastLevel = currentLevel; + } + } + boxes.batt.lastUpdate = now; + } + } + }; + + let draw = function() { + g.clear(); + + // Always draw backgrounds full screen + if (bgImage) { // Check for bg in boxclk config + g.drawImage(bgImage, 0, 0); + } else { // Otherwise use clockbg module + background.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + if (!isDragging) { + updateBoxData(); + } + + for (let boxKey in boxes) { + let boxItem = boxes[boxKey]; + + // Set font and alignment for each box individually + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + calcBoxSize(boxItem); + + const pos = calcBoxPos(boxItem); + + if (boxItem.selected) { + g.setColor(boxItem.border); + g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + } + + g.drawString( + boxItem, + boxItem.string, + boxItem.pos.x + boxItem.xOffset, + boxItem.pos.y + boxItem.yOffset + ); + } + + if (!isDragging) { + if (drawTimeout) clearTimeout(drawTimeout); + let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000); + drawTimeout = setTimeout(draw, updateInterval); + } + }; + + // 9. Helper function for touch event + let calcBoxPos = function(boxItem) { + calcBoxSize(boxItem); + return { + x1: boxItem.pos.x - boxItem.cachedSize.width / 2, + y1: boxItem.pos.y - boxItem.cachedSize.height / 2, + x2: boxItem.pos.x + boxItem.cachedSize.width / 2, + y2: boxItem.pos.y + boxItem.cachedSize.height / 2 + }; + }; + + // Use cached size if available, otherwise calculate and cache + let calcBoxSize = function(boxItem) { + if (boxItem.cachedSize) { + return boxItem.cachedSize; + } + + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; + let fontHeight = g.getFontHeight() + 2 * boxItem.outline; + let totalWidth = strWidth + 2 * boxItem.xPadding; + let totalHeight = fontHeight + 2 * boxItem.yPadding; + + boxItem.cachedSize = { + width: totalWidth, + height: totalHeight + }; + + return boxItem.cachedSize; + }; + + let touchInText = function(e, boxItem) { + calcBoxSize(boxItem); + const pos = calcBoxPos(boxItem); + return e.x >= pos.x1 && + e.x <= pos.x2 && + e.y >= pos.y1 && + e.y <= pos.y2; + }; + + let deselectAllBoxes = function() { + isDragging = false; + for (let boxKey in boxes) { + boxes[boxKey].selected = false; + } + restoreSetColor(); + widgets.show(); + widgets.swipeOn(); + modSetColor(); + }; + + // 10. Setup function to configure event handlers + let setup = function() { + Bangle.on('lock', lockHandler); + Bangle.on('touch', touchHandler); + Bangle.on('drag', dragHandler); + + if (boxes.step) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + Bangle.on('step', stepHandler); + } + + if (boxes.batt) { + boxes.batt.lastLevel = E.getBattery(); + boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel); + boxes.batt.lastUpdate = Date.now(); + } + + Bangle.setUI({ + mode: "clock", + remove: function() { + // Remove event handlers, stop draw timer, remove custom font + Bangle.removeListener('touch', touchHandler); + Bangle.removeListener('drag', dragHandler); + Bangle.removeListener('lock', lockHandler); + if (boxes.step) { + Bangle.removeListener('step', stepHandler); + } + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontBrunoAce; + // Restore original drawString function (no outlines) + g.drawString = g_drawString; + restoreSetColor(); + widgets.show(); + } + }); + + loadCustomFont(); + draw(); + }; + + // 11. Main execution + Bangle.loadWidgets(); + widgets.swipeOn(); + modSetColor(); + setup(); +} diff --git a/apps/boxclk/app.png b/apps/boxclk/app.png new file mode 100644 index 000000000..89da3b695 Binary files /dev/null and b/apps/boxclk/app.png differ diff --git a/apps/boxclk/beachhouse.js b/apps/boxclk/beachhouse.js new file mode 100644 index 000000000..ef0648658 --- /dev/null +++ b/apps/boxclk/beachhouse.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("2GwxH+GFkGhgDBhkGAAIJCAYUIgMBiEdAH4AKoOm0AAD02m0+JAAOP2YAE1ut2ePBoQPCAQllizhtWASoBg08g0SjAABiUOgKyBi6k/ABEXr6uF0HE4mm1qiEACVlnSvtVYKuBgFarUMm08oUZjMUV4MBV/4AEndfweCwWgVxGg06uXV6qTBhiVCBYweRrU2gEMAYM2jMah8IhEQVX4ADwapGAA6uZV60Ms1CiU8swZSAAk8YYOo19hjEai0XVH4AEr6tO0yuaAAKvQRgIDCnlCoVmV6UGAAIFChgABmFhtEfjio/AAkXwatNV4SubV6SSCV4M8jEYiQOEisWUQbLKAoUSW4UMiSv/AAk7wSoKAgfE0CucV6IAEhitBiUMhEPh8AhBSBiwOBikbaBitCXIMGVX4ADwatJWAem1umwWCV96NBVwKtBAgMOi0Wi4ABKgUVjkcBoIABg8HZQsbhoNBiwUBAQIA/js7wSuN1utVbqvVhgBBnlCAAMSiqtCV4SxCjkHh0QXAa1DgEcA4LHBVP4ADi+DVpyshV5yPEhivBiU8iUSjEbVwiZEAoYIDh8NDwMVTepKHCJNeVpmnVkivDg8BV5cGAAKyCV4UZMaQABiwQPAEVBndfvgAB0F8r5NIK4VeCAIALwSunV4MRiEQWJMMiSuCWocSiquEMIS1HXiIAki+J1umSo9fiJPDAIM7VhoAB02CWFE6QwMRhCvJVwcMAQU8VAqv+GQKaB0+mV5GgviwBAANeVp6wEV9UXV4MBhMKiEQhK0Jg09VogAGV2wxBr+DVZKwFneDViKuCV9oABiL4DiMBWAwAEh6wORZ6tiQwIABTyasPVoOCryvtAAsIV5cAiAZKTxjBVVx87wasjVwSpnV6MXiECgKtHg8OiyvMUQi6QUh4bBD4wFBVwKtjV1+JryVOiEIVgUBiEQCxy4OV68doNfvtfWgdYVwKvQ02n1unCoIAD04AJV1yvQAAMRVjivJBx4CBneCR4KWCweDwWCVpwTBTOKvnAF6uGUoSiMABmDU36v/V5wEBr6sZAAWCU34AHsqv/WIoDBwSrY0wCC02DVH6v/VxYEDvivYVoQACVH6rDVwavBiMRWH6v/AEOI1ms02l0vJ5HI4WeiNSuywCiMKhUQTlM7ndfAQINJV7asEV/+B4IAD4AAD4MJz2XlsRiMCgIABWE+DwSBEwaxGr+mwQJCV7CyFwau70+eVQgAFlm73V0hUKgUAAAMJV0s7TQiDCwVYmoOCrwKDmkQV6waBFIiu81nCVxXAqyvB3csgQABhMQV0qvGWIijVVhWt1uDwSq81nJ02l4KuL4Ge3W64N0hMKiMRV00Xr6mSXKitBVXYAE5SrNAAedV4WXhMQV9E1vqkLVCgfHUWms6HJ0o5F0zwB1qwRgFIznB3UChUKV81fvihaV5xuBV+STD4PJ02s0vC4QFB02fV6MGv3H4FJgKxBiKxfnc7r+DwSsoAAOCwWDwapr1jeE0qVE4KnDAgIGEABsGV4PG4FygAABgMJWDdfvt8QgICCVsyosVw3J5OmWIWmUSIAMhkGqgEBztQWAUAhSvawSrc0wADBxStxV4XIQ4PB4XCVz3AgyvDy17V4cJiMXAIU1vtZmquQnaub02DN4enWBOnV+nJVT4AEUwMGlVOuV6VwUDiIADvt2vl3vqwQvqubOI+DWQKzFX4gAw03J4XBWQICB4QGBV7cGhgACmCuBgkFhauCrF8AAN2vt3WBs7waECC4KKDDwSuXAA+nV2qyF4elJ4WCz6xZVwkShivBgdMu9xu6tFAwN9iKuKrN9UYYaCVR6uUAHVldgQIEwWlWDEMWAsGhsouFKt1uuqWDuy2CrKuJvoABVgiuU0BgFAH6vGWAQAE03CVy6vBAAkSAAMHksAAAV0VoKbFWBE1vivBCQqvUwak/ACOI5PBVzHAg0GWIsMgyuDgGgTQykBV49ZVwwZDVyOmTn6fD1gAB0vJAAnI5OsxOs5CtZAAMAV48MVwd7V6NY0KwFVaIAD06s/AAOtUoPH4XBAAPH4KPD4+m5KubV4KuHV4dQTYKvNiEQh8PBQKwa02D0+mAYKs5xGs1igBVAavBSA3IBA6vfg0AletVxAABnavDVoMPh0zV4QABq9dwSwUWgiv51ml0vQV4gAD46qdV4sGAASwGg8ygVSV5E1V4cJVwMOh6tCBwM4ildV/WIVqwAB03KV5KuiV4MApFyvC0BV4cShgMBgFRV5cRhEOg8NhF80KwCicaV/Wq52q1YGC02s1oWM1fB5Gs5XJ5HHU0SvJpG62lskkksFaWQMSiUGgywBq6vKiEIhEHAANRV4WCV4OBV/Gr43G4/I5CYBNoIGCAAfH4/BBIOl0oQBWAIVB4KliIAIJH2gAByFIpGQ4FypEGV4SuBg1eVgdYAIM0V4atCAAMIvl3vteiUZrqv6RLK4BV0XNAAJCHyCvBtlItm6WYVIlSvDsKvCrAADmqvHg0HqN9vtdiUTrl8WDGDV7vO4ywaAEXGV5UkpGQV4OQV4IEBpFyvFOgF+wkxryuErERiCvHhsIV4k4vavYAAOCAAKvZ1ZtIAF3HHAgFBAwPM5vH44SEgEAyCrB2gABAgNsWgOWuQAB0VcwKvEiEPiMXh6uDg8Nhstvs5V7wAC0yyBwavV1mq5xuBV2fOG4StBAgPGAYPPBYPICYdIpCnB3W0VgK1BAwOQ4G6BANspFykquCnUQg8QV4KsBgwABWAMIvk4V4M3wKvdAAaxQxGO2Oy2WOAAOx1fP5CyvVwPVUoQAE5/V1ZHB1a7BCgO0VAKpBUYMkWoWQtmW3S+BAQXAp8zh8Ng8AWAMIVoQAChsNq6vCiddV8KwBVxuO2AAC2IABWYIABWQJvBOAPBWtKuBAAfPVYJAC2ONeYON1eq5vHV4KfBtivBAYIHCAga3BXwIABqsHU4YEEV4cOq8UV4U4vivh02mxGIVxWxVogAI1Z8BAAKwo46wCGwmyVYIAExuNw+qyCrCU4WQ2inBWwIDBAAKsBBYPAuSrBVg6wEq8TV4UTrqvhWISwDWgmIMIKvOPQWy63O5CxmV4QxCxqsGWAuqUgKuDVAW0WQSqCAYIIC4FIgClCAYavGrqvDikVrufAASUDAoQKDABqvG1gAFWQKtB2SjFWBfV54ABV5/HABPGCBPGV4IvCVpT+D6itDAAS2BUoKoBAAQFCy3AyEAAAquGhkNnMSV4YABzvB4QAN4IQK4YABWYemAAavBUA+rVpauB53O5/OV5yYCYgQABDQIACWAgQD53NV4g3HAAIED6ivDUIKtBVwW6WAgDCF4IQBlSwLhsHhqvDWIWdNJoAMXQK8D4fEWwXD5XK1anGAwWwAAJ3IVYPGAAI5PV4yyEZYvGCIpECUQQABZIjQFpEkpCuFVggECWwIGC3XAVxQABgCvFiivcAAqwB4fJ5ICB0quGAASuCV5KwJAwS5IWA6QDWAXG4/H5qeGABy5BpAABVwwFEA5GQVY6tCW4KvBVwSvkWAKuB5QAB1avX1Z1C5qPBSAQIEWIS3DWAIAlV4auCUIIABWowIDWwZHBlSsGAAUHhivp4HBWAXPV4IADVgquCWZQVB55/JXIa7CXoqvkkivBWQKzHAAasDAAO64G6yyqEV5CxCV4OcV8XA4SwB5PPSgOqAAKkBAgQFCA4YKEQCHNAASrmV4sAWAMkAQKyDVIKwIWotIWBMMVgKvCWAOjV8fA4IAB4/HQ1avrAAcktizCUgNsWYIBBWYu6WgckV5USV4MUWAOiV8gAF4yzBABQNCYP6vFSgatCtlIAAStDWgIFBVwS0D2iwEgwAChqvBVgIACV9bASWP6vDAAavBAANypC0BWAPAV4KpCVYStCWgO0gytChoACVwKv/WIvGAQKz8V4qoBkgDDAAVIuVytiwFWQQACtivBVYUMVgKuFV/KpBAA3HV3YACklOAAtyAYVPA4dIpGXWAwACtkkgysBAAaqBAYcTvyPYJYfHTyHHAAXNAAgeCAooA+p3HzgACzoEDAoIAB0ejBImc4xzB3WWVwNspCtEVwivc4/O6wAB53BVgqiDLwyh/V6Wi0ei0XGAAPH44CCAAhtCAA/AV4y0FAIKva6qvH4ytJAH6vUABqmEDpHHpEMhixKiSvYWAPIFoQJFIJgA/V7Gk0gMB5vH4xsNV4axLrSvYABPGWAaW/V8GkgEAq3VWAJsDCoWkqtVXwSvDm0MgyxKV8awCAAKy/V76uBkkAlUAV4RoEqq8BgEyqyvGgCwCV9ixHTX6vWLgedT4Or1eHmNVV4YTBv8ArnW1crlWqV4a5CgywBV5Gi0WcznGWMqv/V63GV4ejgFV63W5+Agy8BV4JnBgErBgPVqsA0ivIWIsMAgICBhk2rWK1CjXHYJLCAAhGCV7PP6oABV/SwCp0A1avC1cAV4nNgGAV4QMB0avGUgSwEA4QAB1PU188nmiVy6mJ6gABOK3VLgPXAAJgBV/d5UQWy1eAV4vOgFWJoOxV5KrBV4YAEm2E2GwsANBV6auHAA/O5pWBAAKuTVoQADWOqvCK4dWmXW1mrmMH5p1D5y9B6y9BrivIAAawBWIcSoU8iS7Cg3GV6SwCHYRLDAAIfCKwIAB4G64CwP6itHWIiv452khquBw8GN4KvDWAMAwOrwMqgAHBV5KxFjCtDBIPMVya8OKoYACIYQAL6quLWAXUV+yiCAAMrv6vC4yvD0cAmMrgF5V6CyDAAMw1HGVDSlFAAhsR56uNWASv1LgXH0d40YGCV4vO5uj0bDCV4cGVAKuEhgICVoMSAIMSsGozmcVq5LBVQpDENiPVVxwAB6qvyMQ4ADBowYCAoivBVIKvFAAiwBV4M2nk2CIMqWKxLJH4ZHEABfWV6HWVtpQBV6pzGV4UkVpKvCWAUMsFmWgMSmCwW45MLWB/VVyAAB6qvuvHGMRivcgyxDg02oU8nk2g1UV6vAH4nHAAKv/VyoABV74AB0XMVgqtCAAkMjE8jEMAoOc0V+lUAHoKpJ3QAF4CrBBgSwIWxfPV/hFBKYXOvHMLBivD43MAoPMAYSvFtnM6SXBAA60DhkSAIUAlVasE2BgMwV5KuGWIYCC4yvNWA3WV9qgB44MJBYIACV4RPFWoqiB45oIBAYkBklsyC7BpCvIWQimBg0wwmv1+ErS8C0SwDvFUzivLVwQABLgavj56oVGAStCUIiuKAASvBVQivJAYIpBEgZlEV4VsRYPGzlIpyuJAgdarWEsCvElSwB0V4lUwWASvOWKJUCV6PWVyZ7CTw4ACVpYMBV4KuEV4yuCAYKqEAAvHpHAtlIyyvBAAKwHV4lasFam02V4kA0Wiv0wg0wrWcVxaxDWgZfC4K0JOwXPWCHVT5CrJPAgAWV43GV4itDAAQOCAgYREkmcpAACtiyCV4wAEVgKjBhgDCWAV4v1UmFUql+V56zEAAawCAAawG6qwO6yhED4quJVi6vE41IznN5ivE5gOBBAKvIHIsky1sV4ecCAKvLU4QECg0wrQICAwMGrV+V4PAWCSzDV4ySEWAauN56uJDwaseAAXIvAgBFgiuCcIgMGBIhKCgCuDV4PAV5q2HrVaAwuEV4O62gADV6KwBAAa2FK4fP6yuSAFPOV4LUG0ei4/P6vVHwJBB0aDClUrqujJgSvBkgABWAWc43AV6SpBhkGAwcwrVOyGQV64AFV4hwEWIJlBAAqtxV49OOgUkgGjIYSvD1QMBnM/hwOBV5VsWANIWCcAhkMAwlIVwitQWAi0EV5CwCABR6BSB7AeV4t4WAUqkivH5yvCoEHhukV46wCAYNyzipOgwFFhkSAoUwzmW3SwDABq0LAAKwPO4acOCQgAFW66vM6ivIg8Ug8HV48ApFsWAVIywjCABkqDYIECqmGwFVql+zgAB4/AWKK7OWwYAE4yMVAEavEzivE0SuBV4SwCV4UNV5CqBtivEtnGvOk5ABB5/IAgOe5PPAAXW6wEC6AFBAAfQBAPX6GdyCxbAH7zGV5fUV5ENhwDB0avEuWWVIKuBV4K1B4CrB5+r0ulVoN5vN60gABBIILC0vPAYOk6AAD63XWAPO0fH5HOZ4PI4/H4IAE4AAFM4gAJVGQLKpyvHSIOi5yvHhsUnEGV5KsCAQdsy2d0l6p4ACp0kAYKzBAYNVWwKyCBIOeAwV6AAWAZ4IAFHAQANC4xeBDY4oRQwYACHBwPB5IAM5HCV5pBBV4kGg2BxCvGVoIaBDYSvDtlIlUqBIYABlS2DWAWkYAN6vK1BV4LIEq2lVqqfEUYwOG63XAAnV6inHAB7EHV4vKAAoHC6CvIlUAV5IABw6vHTgINCWQa4BXRCxDlUrAAKwBAAKmBvWl0uAWwIFDwKwEGoPIAAIDBWyYACDYQAB56uF6/WVy62JV4iwHV4+iSgSvMq2HhqvGABCjBV4gJEWwyyCAAayBAANWWAN7WgfP5Ok5/W5+kAA96vOkXYSlGABKvH66vnWBKvEzivEznO6gfBV4uHxADBV4ejV5K5KWYi1CgIHDV4IFEWga1CWAPQQwXPAAKqCqoABqwABvScC56vX56weV4alDWZKvCzgABQQVOgF4UASvGhsNV5skpwABUwiTBAwYICksllYJBAgMlBAIQCV4MqWwNODYNOpFzRYnWAAPQ0ivBVoV7AQOs6+s0ul1fJV5fO6yvG6yveRgKpEVQKtGV4XM5vH42iQgl4DwOpvHHEQOqBQMGgwDBwAKC5Gd0ZkI0meudyuWdztsWAoAJkquCAAiwBud5udzzvP1ek0igBwF6vSuCq8zlssmVdrus2et1iwM6CvHVzyvHWQavGCgSvI53O0fHVwOqqpoBNgV6UgoABz/DAgWYzGXAIOXyFsy1suVIWBwAIpCtBudPp8qBAMBVAIABwQAB0+DrGC02s6Gs1nXxOI0ywL5/WVwvOVbivMWA1O44ABV4150gACT4efAgQAEvV60wMCz6rBAgOZAgOZz2jzudYIOkqyuWkkqVgNV0qcBxACBwWs1um0yvBwICBVoKXCWINYrF8V5awBCwaudWYquO5Uq4+jAAOduaZBAAKpHAoIGCAYYVBwOlvV0VAOYugVG0fH0oQBlksmdWAAssqCgBp8rAAQGCqoPCIQOBQwYAE6GlvZSCFwN7veB0y/Bq8tmd7V4PJ6AABWo3OAAPPAYSQCAIIGDV9FJvKqDVYQANuYFEulJp9zzGeGRHJ53HqEshEIhMImdXq+BwIDBqwCBAYIAJIwSbBVous6Gsvd0qF0AQIvBhIuBltXgUCq6vB54YC6PQVw4AFVoavlWA10R4IAR0qvGz3DFQphC4QCBz2c4HAVwMPmaxBhEyToKqDAwQADmQABAwdQWIOlAAPQVoWlDoN7qKrBkymBVoIABgYBBAANY03RYwfX6HPJAPPAAamCAgYJDVzKvLWQquNz+fAodzpFIV4QkHVQIACAgLEBvdyV4MIhMzh6vCg4ABlcrAYUsVIIHBAAgMCgMBqF6vV0WoNQp8skwACqNKV4KqBABFYwQAD1jLB0vK6DTCAYIAFYASuaV5arBV4arJ4YUDAwOdBYarM4QHB6HR6Gm1stV4UAgUISwMAAAq0CBYIMGBAQACgUsUYMmkssVwUCgSvBAocDgYDCWwYDDlszmktltX6IABVwIACKQJVCAAfP53OV8SrCAAarKAAOludzBYPC5CpEAAKpC5HBzt6q1WliIBq8tgSOBpCvCABilFABKbBAAamCAAILBVwIQHAAy7CWYK0Bvmm0/R0+C04ABvmCvl7AAOBwIDBwGkV9AOHToOey4ABVwKpEVgueudOznApyUDTgyvNVh6vIAA42BDaKzCBIUmk8nagUmlrRCkoADAoKvqTIQABVQPB4Od4SmBVwnCBQNzudISYavBkiYCleAUIqvCBwSwRQQKvTcwYUFBJILDfoagBqFPq2r6Gl0ejzoABAgWcV8oECVAOXVoKrBWQSoCAAeeHgOWSopYCV4kA0mjpyvF3SvMWJCFEWZcDAQNQ0ulEY0Dvel596Fw4EBk3PMYN4zvP6oAB63X63V6aoXV6CxF5SoCAAaqGp6PCSoOWLYhhD4HANAlyV5FIV5jTDUwy+JWAcCk2kNYPHzud0gAD0Z6G0l6p8lgEkvILEVgIAC66wB5yvqWIisG4WeuaKHkivEAAvGV4kk42cWAkky26vFOAIKyOYBywEXwPGAAfMPp4BCBIyxBVoIACV+DzBkgACNAqvGyyvK4wECvGcAAlyZIW6A4isbAA3H5iwEWKAAI5nOVwfWV9KrCAAaqDNZCvF3SvJ4HGuWcPAoADuWWDQOWticRG4oHCgQADBwclR4IyFRDCvE63VVzyvFVQwAETRBvCA4tIV5WcWAIAKuW63StMTwgACgYABWwgPHWQMA46vEVrIAB53WVwPU6fTVrquBVZavNAA6vQ0WcvFyp0kBwavBAwivFTpCvDUYQPJBgVy4ywCTQyuURYKvB6vS6nVSQKuaVofEVpfL5avd4HAzlIDJMk4CvIVwizIWAgAMknHdASwB461BV6vN6quBV4KuB63W6qvZ5+g0OgAQemAAunAAavT3VyvGc0XGvCvEDRgZBV4ypGAoisCVpwOCk3CWAgADWCXM53VVoIABVgIDCV7fKVQSlEABKvTy3AMYJnBzivXVgKuGACq+El18z/I4/I0oAB5HI53ORCHOVAQAFVwIdRE5XJVwKwOV6nG5nNWAN4V6EkV4iuDWTyzBl9Zvt8vmDAAem02s56HP56uJVbICC5/PVppNBV62c0WiVwavCUASvLVxKwUgYACAwcRiM7ndfr6vEAASwBPgQAK5nO6ytG6gYNVxel5PQdQIABUYKoFAAQGEV6ikHkivBDRivBkiuJV6awEgcKiMXi6vBWAoEE02lS5fMAAPN6qxE6itXAAPIvilGAByIFuWc43G4/GV5CfHkm64CgBV5lyV7SrEVoULVwSwDVQKsCAgYFBwWlRZawC53WWAava52fwSvYkmc4CuBAAVOV6auKV4VIgCwSUYKtIhQABiKuEWA4AFV4PORx3O6qvC6vU54XOABPI0qvYp3AV9AOCVp6qHVoarCVw6wDWRGCS4IACVxXN5nOV4QAC56vXEYOlV69IV66gBV5i+B3SuGVZKvKVoSrIAAarGAAWD1ivEUoKyI5nN6qvF6qdHZ4jUIA4PJ0umV68kziwCzmcuSVFV5tIV5VsV4SuEWJqtGhatNi87weJV5GC02m0urAA3PAAKUB5nM6vWVoXW6qfF0l7wSQHFIIAEB5AACwQABV5iiBkiVKV4PABZGW3VODJe02kkV46wJVwsEVwSvMnasHWQwALwXOV4PM6iuCAoXPV4emDxgAbRxQAGtivB0ivJpAZKkm03VzVp6uChQAEhawDWJqyOABWr5yjB6yvC5vU63WXYILB06qjHIivTzkkV6tI3W6kiuPWIcEVgQACV4ayIBIc1V7Gs6CnBVwKwCAoKvBVwPN1iqgfgJUCAYSvTywLIV5+0gCuLgYAGVwqvMLwIADVy+DxPRVoYAE6vNWAPM5ymWV5awDV6aVBV5OQV5kk2m0pCuKWAUKhStCVwywCi6yGiKpEV7NfwXQVo3W6vMV4fS1ilKvtZU4QwOVgKuDjqvV3SvWpCvEUwKyJVoSsHV4YAILYQDBV4KxYwWr63U6fU6yuB6nMAAPNV4PM5yvKVZ6vFKIQADV6fAV5UkDJVsV4MkVQSuCWIisBVxqvKWIiwDV62D56nC6SvCVQQAE5umVIqvUCQZXHV6ikItivMpGQyCvCVgoACVoQABV7CsCV7OJ6yjD6HR6CtGWwOCSoKdHbqKvCnb/CAAavS2ivY2m0pCqCVoisBAAMEV4SwLV6SwW1vPUQPO5/Q6HOVgnO53T51YCwdZvodErM1HYoGGCYhOGi/ISBaVFyFsBZCvNtivCVwSvDVyavNAAsRWCur6XMUQPP5/U6QAB6fUAAXPwaoFAAKcLABpUEiPA42cpyvPUZKvBpAZKkmQ2kkVoKuDWAivQWJoAECIs1mp4LBgVX1CpD6YECAwfU6utZpitWAAmcznA4F4V5tsBZSvMBoKvCVgYEEgatQWCZ7TAAWCVYSuD6fV6nT5vU1iuMV6cXJ4+i43GzlOV5qiKki7KV4e6p6pDAAkDV4SteQR6KKr+C1Wq5+q1PV63X6+Ir01mrVWABU1KAYoBiPOAAOkvN4kiVKtivKABqvCkkBVo6uBVp6uPV4RpDCJZvBTQ4JBiAOBWwOJxNlZJkXAAivSewtli6vC5HH0fHAAOjWY9IV7EktlsAgKwIV8IAMEY5+GRySxLACpGCV4SxDWAdypyvFXA6wSuQbBV4ywBhSwQV7gAGUBYAFBooQDCgyjQABasB5vNVogABzt4zudvN4vFOWwoASpGW4FIVwMDV40EWAIAOWis1ABSsQACOD1s7VhZTEKpStBV4qxC0SzCAAd4V68kuWWgCvBAAMBAAKvDWCBbGAA60LPYSrGABijGBZVerwNBV5KxDAAavNWAS0H0S0DkivWpFyuQEBVgQADV4SuPV54ASYRYAGms1XhICDABCvFBgyvNAAyyF0eizl4pCvVyGWDAKvHgSvzWKgACTYoNLUok1Bgq8DV6a1H42i4wCBvFOki0UV4ywBgawDQIQDCV9MRwder1fACqUKADSvRWQizBAAQGC0ejWqKvGWAIABgizDABCvihWDTQazBAANZAQNYAAIJDYIYGEDYODxOJnadLmoABYwIRLhavUWxKvIUASkEAoIHCWxAPCWoS2CAAQFBVTptDAoZ/BWQaiGAB2DwSzBncXAAinCAAsXYBcL53OV7Ok0krmUHVwsAwGAAYNVUYSqFCgyyDWwwADXIcKAAKnRNxKyDCRM1AASXIB4Q8NHBavKAASyZAAOjAggFC0Wc44AD0YCBGAMxV5oAIW4x1cZxwhCCKCqUAAkKV4iyeAAnMBZV5VwqvCToSlHVYwAEhQAGRKIASYQgqKVjCuChXN0ej5qzHWsYACAoN4pwABkiwEU5KrIAAMJV0EKndfnYABQo4LBiEKhI0BiE0mlYAAdZmg4CV694AASwCWY6ue4yvEAIS0C42idQOjv8rgKzJWA0EVDDHHhUEV4IABweDwQAJ0wAC1gAB6AAD6ODIAKvb0asGWIujAIOiRQPHVyamB4wABVYYCDAAroBp6xBWYa3GVwKtYAAMRiCxIiAADmoADmk0BAKJBB4gABBgIOCIISuXV4iwDRwfOzmiAIQREAASwTWoymCVwwACvMAgCxDWoqxEAASXIggABAoihCiMXi6YCAAkLSIIVCAYTDBbx4PEVzEJp1OT44AP0SpSAgqlD4wAH0auBAASuGWIq0HABsJiCvBAA6wBWIazEBIYAVVqKuBgavBuSeFuS5DuQMEAoujVBapFAAizFzlIpEkAQOcvCtEWA6zEWS8JAAJyDAAcQiK2GA4SHCTgYaHYQq0ODopACIoKlBAAVyqgGEvAGEVoKuCAQOiVxSpGABXGbwQnCzlyV46xJWQ6vPWYy0DhMQV4s7AANfxNfWIQAQYATFOHQavHAByLCXYKuQWiK4EWQMkWB6yFV6h4CgkEgYEBVIQAHiEQV6SyFVhYDBIIivTAAd40SgB5vGAAKvE46rBAgQMBWKoABzmcvC0DVIYGFVywADQAsXAAkRiKrWVwivGVYQAFHgRWBVqtyAQV4uQFEXAKnCSYWiSgIHBW4IAY40rV4gAEVwSwFhSlHOhCfYADCvKKoSvVXht4VQN4p1IAAOcVSzFD5vGgAABWBC4IWgh7MUUcLAAatMegiuCV8YAEVwUkV4vNAAKqKABGjV4QARWIIACNggADVkKoDiIAFVQ6sGVokCgMAkgAQlUqWK1yzgAD0Wj0eizl4AAITEp150mdBQIVC43HCYVyCII+BVZIADMogALXhK9XiAAFVBCqGVwivBADrNRAAS5BAASuFp1yAAOWAYeWznAAAnGAAmjvMqWDDAYABwiKJAiqfYVISNZYVsX4OcpAuGNYoAGWrImMABx9LA=")) diff --git a/apps/boxclk/boxclk-1.json b/apps/boxclk/boxclk-1.json new file mode 100644 index 000000000..99e225f04 --- /dev/null +++ b/apps/boxclk/boxclk-1.json @@ -0,0 +1,88 @@ +{ + "time": { + "font": "6x8", + "fontSize": 3, + "outline": 2, + "color": "#0ff", + "outlineColor": "#00f", + "border": "#0f0", + "xPadding": -1, + "yPadding": -2.5, + "xOffset": 2, + "yOffset": 0, + "boxPos": { + "x": "0.33", + "y": "0.29" + } + }, + "meridian": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#FF9900", + "outlineColor": "fg", + "border": "#0ff", + "xPadding": -0.5, + "yPadding": -1.5, + "xOffset": 2, + "yOffset": 1, + "boxPos": { + "x": "0.34", + "y": "0.46" + }, + "short": false + }, + "dow": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": -0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.82" + } + }, + "step": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.71" + }, + "prefix": "Steps: " + }, + "batt": { + "font": "4x6", + "fontSize": 2, + "outline": 1, + "color": "#0ff", + "outlineColor": "#00f", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": -0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.87", + "y": "0.87" + }, + "suffix": "%" + }, + "bg": { + "img": "boxclk.space.img" + } +} diff --git a/apps/boxclk/boxclk-2.json b/apps/boxclk/boxclk-2.json new file mode 100644 index 000000000..1bdc89252 --- /dev/null +++ b/apps/boxclk/boxclk-2.json @@ -0,0 +1,87 @@ +{ + "time": { + "font": "6x8", + "fontSize": 5, + "outline": 3, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -2, + "yPadding": -4.5, + "xOffset": 3, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.739" + } + }, + "dow": { + "font": "6x8", + "fontSize": 3, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": 0.5, + "xOffset": 2, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.201" + }, + "short": false + }, + "date": { + "font": "6x8", + "fontSize": 2, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -0.5, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.074" + }, + "shortMonth": false, + "disableSuffix": true + }, + "step": { + "font": "4x6", + "fontSize": 3, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": 0.5, + "xOffset": 2, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.926" + }, + "prefix": "Steps: " + }, + "batt": { + "font": "4x6", + "fontSize": 3, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": -1, + "xOffset": 2, + "yOffset": 2, + "boxPos": { + "x": "0.8", + "y": "0.427" + }, + "suffix": "%" + } +} diff --git a/apps/boxclk/boxclk.json b/apps/boxclk/boxclk.json new file mode 100644 index 000000000..51a67ebd6 --- /dev/null +++ b/apps/boxclk/boxclk.json @@ -0,0 +1,60 @@ +{ + "time": { + "font": "BrunoAce", + "fontSize": 1, + "outline": 2, + "color": "#000", + "outlineColor": "#fff", + "border": "#000", + "xPadding": 0, + "yPadding": -4, + "xOffset": 0, + "yOffset": 3, + "boxPos": { "x": 0.633, "y": 0.16 } + }, + "dow": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#000", + "xPadding": -0.5, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { "x": 0.633, "y": 0.3 }, + "short": false + }, + "date": { + "font": "6x8", + "fontSize": 1, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#000", + "xPadding": 0, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { "x": 0.633, "y": 0.39 }, + "short": false + }, + "batt": { + "font": "4x6", + "fontSize": 2, + "outline": 1, + "color": "#0ff", + "outlineColor": "#000", + "border": "#fff", + "xPadding": -0.5, + "yPadding": -0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { "x": 0.9, "y": 0.95 }, + "suffix": "%" + }, + "bg": { + "img": "boxclk.beachhouse.img" + } +} diff --git a/apps/boxclk/boxclk.space.img b/apps/boxclk/boxclk.space.img new file mode 100644 index 000000000..1708b5c24 Binary files /dev/null and b/apps/boxclk/boxclk.space.img differ diff --git a/apps/boxclk/icon.js b/apps/boxclk/icon.js new file mode 100644 index 000000000..f6f4412c6 --- /dev/null +++ b/apps/boxclk/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEogA/AEB5TCwVAC6aOCoED/4AQmAXDh4XR+AX5+URgERl4XR+KGEj4XP+cAgMz/8ziEAn4XOkEBCIfziECC5ouBl/zkMAiU/+QwGC4/wE4MgLwQFCQgoXHiEfO4Mj/8yO4PxgIXMHwMggb+DgRQBC5fygIPBn4xBiYFCiDDEC43xgfyLQJfCGoMvmDCEC5ABCVIJlBCoIXM+EPAIRgBAQIIDC542DC53xT4MfC4cCBAanLBQaWDBIgXo+YXXdgoXQbQcBd5YXHiAYHC5wAC+UQe4IXTDAMABAQXEgYPEiDQEAATdBAYMwC4ZUCK4aLHSoIACC5IuHSogXKCxAXHogXTCwQAFC5YUIC7b/EC6TFFC6IwJC5itBcwQXUbI7vBC5bFGAAgXMDBQXNJIQXUGBEEBog=")) diff --git a/apps/boxclk/metadata.json b/apps/boxclk/metadata.json new file mode 100644 index 000000000..48f9f82ae --- /dev/null +++ b/apps/boxclk/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "boxclk", + "name": "Box Clock", + "version": "0.10", + "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", + "icon": "app.png", + "dependencies" : { "clockbg":"module" }, + "screenshots": [ + {"url":"screenshot.png"}, + {"url":"screenshot-1.png"}, + {"url":"screenshot-2.png"} + ], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"boxclk.app.js","url":"app.js"}, + {"name":"boxclk.settings.js","url":"settings.js"}, + {"name":"boxclk.img","url":"icon.js","evaluate":true}, + {"name":"boxclk.beachhouse.img","url":"beachhouse.js","evaluate":true} + ], + "data": [ + {"name":"boxclk.json","url":"boxclk.json"} + ] +} \ No newline at end of file diff --git a/apps/boxclk/screenshot-1.png b/apps/boxclk/screenshot-1.png new file mode 100644 index 000000000..c6e22d262 Binary files /dev/null and b/apps/boxclk/screenshot-1.png differ diff --git a/apps/boxclk/screenshot-2.png b/apps/boxclk/screenshot-2.png new file mode 100644 index 000000000..361185406 Binary files /dev/null and b/apps/boxclk/screenshot-2.png differ diff --git a/apps/boxclk/screenshot.png b/apps/boxclk/screenshot.png new file mode 100644 index 000000000..12ccee5e2 Binary files /dev/null and b/apps/boxclk/screenshot.png differ diff --git a/apps/boxclk/settings.js b/apps/boxclk/settings.js new file mode 100644 index 000000000..c4b41101b --- /dev/null +++ b/apps/boxclk/settings.js @@ -0,0 +1,94 @@ +(function () { + let storage = require("Storage"); + let fileRegex = /^boxclk-(\d+)\.json$/; + let selectedConfig; + let configs = {}; + let hasDefaultConfig = false; + + function getNextConfigNumber() { + let maxNumber = 0; + storage.list().forEach(file => { + let match = file.match(fileRegex); + if (match) { + let number = parseInt(match[1]); + if (number > maxNumber) { + maxNumber = number; + } + } + }); + return maxNumber + 1; + } + + function handleSelection(config) { + return function () { + selectedConfig = config === "Default" ? 0 : config; + menu["Cfg:"].value = selectedConfig === 0 ? "Default" : selectedConfig; + E.showMenu(menu); + + // Retrieve existing data and update selectedConfig + let defaultConfig = storage.readJSON("boxclk.json", 1) || {}; + defaultConfig.selectedConfig = selectedConfig; + storage.writeJSON("boxclk.json", defaultConfig); + }; + } + + let configFiles = []; + storage.list().forEach(file => { + let match = file.match(fileRegex); + if (match) { + configFiles.push({ file: file, number: parseInt(match[1]) }); + } else if (file === "boxclk.json") { + hasDefaultConfig = true; + let defaultConfig = storage.readJSON(file, 1); + if (defaultConfig && defaultConfig.selectedConfig != null) { + // Check if corresponding config file exists + let configFileName = 'boxclk-' + defaultConfig.selectedConfig + '.json'; + if (storage.read(configFileName)) { + // If it exists, assign selectedConfig + selectedConfig = defaultConfig.selectedConfig; + } else { + // If it does not exist, set selectedConfig to 0 and update boxclk.json + defaultConfig.selectedConfig = 0; + storage.writeJSON("boxclk.json", defaultConfig); + selectedConfig = 0; + } + } + } + }); + + // Sort the config files by number + configFiles.sort((a, b) => a.number - b.number); + + configFiles.forEach(configFile => { + configs[configFile.number] = handleSelection(configFile.number); + }); + + if (!selectedConfig) { + if (hasDefaultConfig) { + selectedConfig = "Default"; + } else { + let nextConfigNumber = getNextConfigNumber(); + selectedConfig = nextConfigNumber.toString(); + configs[selectedConfig] = handleSelection(selectedConfig); + } + } + + let menu = { + '': { 'title': '-- Box Clock --' }, + '< Back': () => Bangle.showClock(), + 'Cfg:': { + value: selectedConfig === 0 ? "Default" : selectedConfig, + format: () => selectedConfig === 0 ? "Default" : selectedConfig + } + }; + + if (hasDefaultConfig) { + menu['Default'] = handleSelection('Default'); + } + + Object.keys(configs).forEach(config => { + menu[config] = handleSelection(config); + }); + + E.showMenu(menu); +}) diff --git a/apps/bradbury/ChangeLog b/apps/bradbury/ChangeLog new file mode 100644 index 000000000..62542be60 --- /dev/null +++ b/apps/bradbury/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Minor code improvements diff --git a/apps/bradbury/app.js b/apps/bradbury/app.js index 147242689..ae018f87f 100644 --- a/apps/bradbury/app.js +++ b/apps/bradbury/app.js @@ -2,7 +2,7 @@ require("Font7x11Numeric7Seg").add(Graphics); require("Font5x9Numeric7Seg").add(Graphics); require("Font8x12").add(Graphics); require("FontDylex7x13").add(Graphics); -const X = 98, Y = 46; +//const X = 98, Y = 46; var wizible = 0; function getImg() { diff --git a/apps/bradbury/metadata.json b/apps/bradbury/metadata.json index 456daa381..6b4fb2171 100644 --- a/apps/bradbury/metadata.json +++ b/apps/bradbury/metadata.json @@ -3,7 +3,7 @@ "shortName":"Bradbury", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.01", + "version": "0.02", "description": "A watch face based on the classic Seiko model worn by one of my favorite authors. I didn't follow the original lcd layout exactly, opting for larger font for more easily readable time, and adding date, battery level, and step count; read from the device. Tapping the screen toggles visibility of widgets.", "type": "clock", "supports":["BANGLEJS2"], diff --git a/apps/breath/ChangeLog b/apps/breath/ChangeLog new file mode 100644 index 000000000..03c748ea5 --- /dev/null +++ b/apps/breath/ChangeLog @@ -0,0 +1,3 @@ +0.01: New app! +0.02: Minor code improvements +0.03: Minor code improvements diff --git a/apps/breath/app.js b/apps/breath/app.js index 380308739..f9abf9d6b 100644 --- a/apps/breath/app.js +++ b/apps/breath/app.js @@ -4,7 +4,7 @@ var max_radius = 70; var direction = 1; var display_HR = "--"; var first_signal = true; -var interval; +//var interval; var timeout; var settings; var status = 0; @@ -51,9 +51,9 @@ g.setFont("6x8", 2); function circle() { g.clear(); - adjusted_radius = max_radius * Math.abs(origin); + const adjusted_radius = max_radius * Math.abs(origin); g.drawCircle(120, 120, adjusted_radius); - radius = Math.abs(Math.sin(origin)); + //const radius = Math.abs(Math.sin(origin)); angle += 2; origin = angle * (Math.PI / 180); if (angle >= 0 && angle < 90) { @@ -63,7 +63,7 @@ function circle() { g.drawString("<<", 220, 40); status = 7; timeout = setTimeout(function () { - interval = restart_interval(); + /*interval =*/ restart_interval(); }, settings.exhale_pause * 1000); } direction = 0; @@ -77,7 +77,7 @@ function circle() { g.drawString("<<", 220, 40); status = 7; timeout = setTimeout(function () { - interval = restart_interval(); + /*interval =*/ restart_interval(); }, settings.inhale_pause * 1000); } direction = 1; @@ -100,7 +100,7 @@ function restart_interval() { if(direction == 1 && settings.ex_in_ratio == "5:6"){ calc -= calc*0.2; } - interval = setInterval(circle, calc); + /*interval =*/ setInterval(circle, calc); } function update_menu() { diff --git a/apps/breath/metadata.json b/apps/breath/metadata.json index 070a9a79a..c4280c4ad 100644 --- a/apps/breath/metadata.json +++ b/apps/breath/metadata.json @@ -2,7 +2,7 @@ "id": "breath", "name": "Breathing App", "shortName": "Breathing App", - "version": "0.01", + "version": "0.03", "description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR", "icon": "app-icon.png", "tags": "tools,health", diff --git a/apps/btadv/ChangeLog b/apps/btadv/ChangeLog new file mode 100644 index 000000000..c019a97b9 --- /dev/null +++ b/apps/btadv/ChangeLog @@ -0,0 +1,4 @@ +0.01: New app! +0.02: Advertise accelerometer data and sensor location +0.03: Use the bleAdvert module +0.04: Actually use the ble_advert module diff --git a/apps/btadv/README.md b/apps/btadv/README.md new file mode 100644 index 000000000..7b1afcefe --- /dev/null +++ b/apps/btadv/README.md @@ -0,0 +1,16 @@ +# Bluetooth Advert + +This app advertises and exports (over Bluetooth) live data from the bangle's sensors: + +- Heart Rate +- Accelerometer readings +- Pressure +- GPS information +- Magnetic flux + +Swipe in any direction to access settings, and tap a setting to toggle it. +Hit back to return to the details screen, which shows sensor data being exported. + +# TypeScript + +This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info diff --git a/apps/btadv/app.js b/apps/btadv/app.js new file mode 100644 index 000000000..457973e47 --- /dev/null +++ b/apps/btadv/app.js @@ -0,0 +1,453 @@ +{ + var __assign = Object.assign; + var Layout_1 = require("Layout"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + var HRM_MIN_CONFIDENCE_1 = 75; + var services_1 = [ + "0x180d", + "0x181a", + "0x1819", + "E95D0753251D470AA062FA1922DFA9A8", + ]; + var acc_1; + var bar_1; + var gps_1; + var hrm_1; + var hrmAny_1; + var mag_1; + var btnsShown_1 = false; + var prevBtnsShown_1 = undefined; + var hrmAnyClear_1; + var settings_1 = { + bar: false, + gps: false, + hrm: false, + mag: false, + }; + var idToName = { + bar: "Barometer", + gps: "GPS", + hrm: "HRM", + mag: "Magnetometer", + }; + var infoFont_1 = "6x8:2"; + var colour_1 = { + on: "#0f0", + off: "#fff", + }; + var makeToggle = function (id) { return function () { + settings_1[id] = !settings_1[id]; + var entry = btnLayout_1[id]; + var col = settings_1[id] ? colour_1.on : colour_1.off; + entry.btnBorder = entry.col = col; + btnLayout_1.update(); + btnLayout_1.render(); + enableSensors_1(); + }; }; + var btnStyle = { + font: "Vector:14", + fillx: 1, + filly: 1, + col: g.theme.fg, + bgCol: g.theme.bg, + btnBorder: "#fff", + }; + var btnLayout_1 = new Layout_1({ + type: "v", + c: [ + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle), + __assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle), + __assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign({ type: "btn", label: "Back", cb: function () { + setBtnsShown_1(false); + } }, btnStyle), + ] + } + ] + }, { + lazy: true, + back: function () { + setBtnsShown_1(false); + }, + }); + var setBtnsShown_1 = function (b) { + btnsShown_1 = b; + hook_1(!btnsShown_1); + setIntervals_1(); + redraw_1(); + }; + var drawInfo_1 = function (force) { + var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w; + var mid = x + w / 2; + var drawn = false; + if (!force && !bar_1 && !gps_1 && !hrm_1 && !mag_1) + return; + g.reset() + .clearRect(Bangle.appRect) + .setFont(infoFont_1) + .setFontAlign(0, -1); + if (bar_1) { + g.drawString("".concat(bar_1.altitude.toFixed(1), "m"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar_1.pressure.toFixed(1), " hPa"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar_1.temperature.toFixed(1), "C"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (gps_1) { + g.drawString("".concat(gps_1.lat.toFixed(4), " lat, ").concat(gps_1.lon.toFixed(4), " lon"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(gps_1.alt, "m (").concat(gps_1.satellites, " sat)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (hrm_1) { + g.drawString("".concat(hrm_1.bpm, " BPM (").concat(hrm_1.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + else if (hrmAny_1) { + g.drawString("~".concat(hrmAny_1.bpm, " BPM (").concat(hrmAny_1.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + if (!settings_1.hrm && !hrmAnyClear_1) { + hrmAnyClear_1 = setTimeout(function () { + hrmAny_1 = undefined; + hrmAnyClear_1 = undefined; + }, 10000); + } + } + if (mag_1) { + g.drawString("".concat(mag_1.x, " ").concat(mag_1.y, " ").concat(mag_1.z), mid, y); + y += g.getFontHeight(); + g.drawString("heading: ".concat(mag_1.heading.toFixed(1)), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (!drawn) { + if (!force || Object.values(settings_1).every(function (x) { return !x; })) { + g.drawString("swipe to enable", mid, y); + } + else { + g.drawString("events pending", mid, y); + } + y += g.getFontHeight(); + } + }; + var onTap_1 = function () { + setBtnsShown_1(true); + }; + var redraw_1 = function () { + if (btnsShown_1) { + if (!prevBtnsShown_1) { + prevBtnsShown_1 = btnsShown_1; + Bangle.removeListener("swipe", onTap_1); + btnLayout_1.setUI(); + btnLayout_1.forgetLazyState(); + g.clearRect(Bangle.appRect); + } + btnLayout_1.render(); + } + else { + if (prevBtnsShown_1) { + prevBtnsShown_1 = btnsShown_1; + Bangle.setUI(); + Bangle.on("swipe", onTap_1); + drawInfo_1(true); + } + else { + drawInfo_1(); + } + } + }; + var encodeHrm_1 = function (hrm) { + return [0, hrm.bpm]; + }; + encodeHrm_1.maxLen = 2; + var encodePressure_1 = function (data) { + return toByteArray_1(Math.round(data.pressure * 10), 4, false); + }; + encodePressure_1.maxLen = 4; + var encodeElevation_1 = function (data) { + return toByteArray_1(Math.round(data.altitude * 100), 3, true); + }; + encodeElevation_1.maxLen = 3; + var encodeTemp_1 = function (data) { + return toByteArray_1(Math.round(data.temperature * 10), 2, true); + }; + encodeTemp_1.maxLen = 2; + var encodeGps_1 = function (data) { + var speed = toByteArray_1(Math.round(1000 * data.speed / 36), 2, false); + var lat = toByteArray_1(Math.round(data.lat * 10000000), 4, true); + var lon = toByteArray_1(Math.round(data.lon * 10000000), 4, true); + var elevation = toByteArray_1(Math.round(data.alt * 100), 3, true); + var heading = toByteArray_1(Math.round(data.course * 100), 2, false); + return [ + 157, + 2, + speed[0], speed[1], + lat[0], lat[1], lat[2], lat[3], + lon[0], lon[1], lon[2], lon[3], + elevation[0], elevation[1], elevation[2], + heading[0], heading[1] + ]; + }; + encodeGps_1.maxLen = 17; + var encodeGpsHeadingOnly_1 = function (data) { + var heading = toByteArray_1(Math.round(data.heading * 100), 2, false); + return [ + 16, + 16, + heading[0], heading[1] + ]; + }; + encodeGpsHeadingOnly_1.maxLen = 17; + var encodeMag_1 = function (data) { + var x = toByteArray_1(data.x, 2, true); + var y = toByteArray_1(data.y, 2, true); + var z = toByteArray_1(data.z, 2, true); + return [x[0], x[1], y[0], y[1], z[0], z[1]]; + }; + encodeMag_1.maxLen = 6; + var encodeAcc_1 = function (data) { + var x = toByteArray_1(data.x * 1000, 2, true); + var y = toByteArray_1(data.y * 1000, 2, true); + var z = toByteArray_1(data.z * 1000, 2, true); + return [x[0], x[1], y[0], y[1], z[0], z[1]]; + }; + encodeAcc_1.maxLen = 6; + var toByteArray_1 = function (value, numberOfBytes, isSigned) { + var byteArray = new Array(numberOfBytes); + if (isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + for (var index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + return byteArray; + }; + var enableSensors_1 = function () { + Bangle.setBarometerPower(settings_1.bar, "btadv"); + if (!settings_1.bar) + bar_1 = undefined; + Bangle.setGPSPower(settings_1.gps, "btadv"); + if (!settings_1.gps) + gps_1 = undefined; + Bangle.setHRMPower(settings_1.hrm, "btadv"); + if (!settings_1.hrm) + hrm_1 = hrmAny_1 = undefined; + Bangle.setCompassPower(settings_1.mag, "btadv"); + if (!settings_1.mag) + mag_1 = undefined; + }; + var haveServiceData_1 = function (serv) { + switch (serv) { + case "0x180d": return !!hrm_1; + case "0x181a": return !!(bar_1 || mag_1); + case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1); + case "E95D0753251D470AA062FA1922DFA9A8": return !!acc_1; + } + }; + var serviceToAdvert_1 = function (serv, initial) { + var _a, _b, _c; + if (initial === void 0) { initial = false; } + switch (serv) { + case "0x180d": + if (hrm_1 || initial) { + var o = { + maxLen: encodeHrm_1.maxLen, + readable: true, + notify: true, + }; + var os = { + maxLen: 1, + readable: true, + notify: true, + }; + if (hrm_1) { + o.value = encodeHrm_1(hrm_1); + os.value = [2]; + hrm_1 = undefined; + } + return _a = {}, + _a["0x2a37"] = o, + _a["0x2a38"] = os, + _a; + } + return {}; + case "0x1819": + if (gps_1 || initial) { + var o = { + maxLen: encodeGps_1.maxLen, + readable: true, + notify: true, + }; + if (gps_1) { + o.value = encodeGps_1(gps_1); + gps_1 = undefined; + } + return _b = {}, _b["0x2a67"] = o, _b; + } + else if (mag_1) { + var o = { + maxLen: encodeGpsHeadingOnly_1.maxLen, + readable: true, + notify: true, + value: encodeGpsHeadingOnly_1(mag_1), + }; + return _c = {}, _c["0x2a67"] = o, _c; + } + return {}; + case "0x181a": { + var o = {}; + if (bar_1 || initial) { + o["0x2a6c"] = { + maxLen: encodeElevation_1.maxLen, + readable: true, + notify: true, + }; + o["0x2A1F"] = { + maxLen: encodeTemp_1.maxLen, + readable: true, + notify: true, + }; + o["0x2a6d"] = { + maxLen: encodePressure_1.maxLen, + readable: true, + notify: true, + }; + if (bar_1) { + o["0x2a6c"].value = encodeElevation_1(bar_1); + o["0x2A1F"].value = encodeTemp_1(bar_1); + o["0x2a6d"].value = encodePressure_1(bar_1); + bar_1 = undefined; + } + } + if (mag_1 || initial) { + o["0x2aa1"] = { + maxLen: encodeMag_1.maxLen, + readable: true, + notify: true, + }; + if (mag_1) { + o["0x2aa1"].value = encodeMag_1(mag_1); + } + } + return o; + } + case "E95D0753251D470AA062FA1922DFA9A8": { + var o = {}; + if (acc_1 || initial) { + o["E95DCA4B251D470AA062FA1922DFA9A8"] = { + maxLen: encodeAcc_1.maxLen, + readable: true, + notify: true, + }; + if (acc_1) { + o["E95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1); + acc_1 = undefined; + } + } + return o; + } + } + }; + var getBleAdvert_1 = function (map, all) { + if (all === void 0) { all = false; } + var advert = {}; + for (var _i = 0, services_2 = services_1; _i < services_2.length; _i++) { + var serv = services_2[_i]; + if (all || haveServiceData_1(serv)) { + advert[serv] = map(serv); + } + } + mag_1 = undefined; + return advert; + }; + var updateServices_1 = function () { + var newAdvert = getBleAdvert_1(serviceToAdvert_1); + NRF.updateServices(newAdvert); + }; + var onAccel_1 = function (newAcc) { return acc_1 = newAcc; }; + var onPressure_1 = function (newBar) { return bar_1 = newBar; }; + var onGPS_1 = function (newGps) { return gps_1 = newGps; }; + var onHRM_1 = function (newHrm) { + if (newHrm.confidence >= HRM_MIN_CONFIDENCE_1) + hrm_1 = newHrm; + hrmAny_1 = newHrm; + }; + var onMag_1 = function (newMag) { return mag_1 = newMag; }; + var hook_1 = function (enable) { + if (enable) { + Bangle.on("accel", onAccel_1); + Bangle.on("pressure", onPressure_1); + Bangle.on("GPS", onGPS_1); + Bangle.on("HRM", onHRM_1); + Bangle.on("mag", onMag_1); + } + else { + Bangle.removeListener("accel", onAccel_1); + Bangle.removeListener("pressure", onPressure_1); + Bangle.removeListener("GPS", onGPS_1); + Bangle.removeListener("HRM", onHRM_1); + Bangle.removeListener("mag", onMag_1); + } + }; + var setIntervals_1 = function (locked, connected) { + if (locked === void 0) { locked = Bangle.isLocked(); } + if (connected === void 0) { connected = NRF.getSecurityStatus().connected; } + changeInterval(redrawInterval_1, locked ? 15000 : 5000); + if (connected) { + var interval = btnsShown_1 ? 5000 : 1000; + if (bleInterval_1) { + changeInterval(bleInterval_1, interval); + } + else { + bleInterval_1 = setInterval(updateServices_1, interval); + } + } + else if (bleInterval_1) { + clearInterval(bleInterval_1); + bleInterval_1 = undefined; + } + }; + var redrawInterval_1 = setInterval(redraw_1, 1000); + Bangle.on("lock", function (locked) { return setIntervals_1(locked); }); + var bleInterval_1; + NRF.on("connect", function () { return setIntervals_1(undefined, true); }); + NRF.on("disconnect", function () { return setIntervals_1(undefined, false); }); + setIntervals_1(); + setBtnsShown_1(true); + enableSensors_1(); + { + var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true); + NRF.setServices(ad, { + uart: false, + }); + for (var id in ad) { + var serv = ad[id]; + var value = void 0; + for (var ch in serv) { + value = serv[ch].value; + break; + } + require("ble_advert").set(id, value || []); + } + } +} diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts new file mode 100644 index 000000000..0c1803fdb --- /dev/null +++ b/apps/btadv/app.ts @@ -0,0 +1,783 @@ +{ +// @ts-expect-error helper + +const __assign = Object.assign; + +const Layout = require("Layout"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const enum Intervals { + // BLE_ADVERT = 60 * 1000, + BLE = 1000, // info screen + BLE_BACKGROUND = 5000, // button screen + UI_INFO = 5 * 1000, // info refresh, wake + UI_INFO_SLEEP = 15 * 1000, // info refresh, asleep +} + +type Hrm = { bpm: number, confidence: number }; + +const HRM_MIN_CONFIDENCE = 75; + +// https://github.com/sputnikdev/bluetooth-gatt-parser/blob/master/src/main/resources/gatt/ +const enum BleServ { + // org.bluetooth.service.heart_rate + // contains: HRM + HRM = "0x180d", + + // org.bluetooth.service.environmental_sensing + // contains: Elevation, Temp(Celsius), Pressure, Mag + EnvSensing = "0x181a", + + // org.bluetooth.service.location_and_navigation + // contains: LocationAndSpeed + LocationAndNavigation = "0x1819", + + // org.microbit.service.accelerometer + // contains: Acc + Acc = "E95D0753251D470AA062FA1922DFA9A8", +} + +const services = [ + BleServ.HRM, + BleServ.EnvSensing, + BleServ.LocationAndNavigation, + BleServ.Acc, +]; + +const enum BleChar { + // org.bluetooth.characteristic.heart_rate_measurement + // + HRM = "0x2a37", + + // org.bluetooth.characteristic.body_sensor_location + // u8 + SensorLocation = "0x2a38", + + // org.bluetooth.characteristic.elevation + // s24, meters 0.01 + Elevation = "0x2a6c", + + // org.bluetooth.characteristic.temperature + // s16 *10^2 + Temp = "0x2a6e", + // org.bluetooth.characteristic.temperature_celsius + // s16 *10^2 + TempCelsius = "0x2A1F", + + // org.bluetooth.characteristic.pressure + // u32 *10 + Pressure = "0x2a6d", + + // org.bluetooth.characteristic.location_and_speed + // + LocationAndSpeed = "0x2a67", + + // org.bluetooth.characteristic.magnetic_flux_density_3d + // s16: x, y, z, tesla (10^-7) + MagneticFlux3D = "0x2aa1", + + // org.microbit.characteristic.accelerometer_data + // s16 x3, -1024 .. 1024 + // docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/ + Acc = "E95DCA4B251D470AA062FA1922DFA9A8", +} + +type BleCharAdvert = { + value?: Array, + readable?: true, + notify?: true, + indicate?: true, // notify + ACK + maxLen?: number, +}; + +type BleServAdvert = { + [key in BleChar]?: BleCharAdvert; +}; + +type LenFunc = { + (_: T): Array, + maxLen: number, +} + +const enum SensorLocations { + Other = 0, + Chest = 1, + Wrist = 2, + Finger = 3, + Hand = 4, + EarLobe = 5, + Foot = 6, +} + +let acc: undefined | AccelData; +let bar: undefined | PressureData; +let gps: undefined | GPSFix; +let hrm: undefined | Hrm; +let hrmAny: undefined | Hrm; +let mag: undefined | CompassData; +let btnsShown = false; +let prevBtnsShown: boolean | undefined = undefined; +let hrmAnyClear: undefined | number; + +type BtAdvType = "bar" | "gps" | "hrm" | "mag" | (IncludeAcc extends true ? "acc" : never); +type BtAdvMap = { [key in BtAdvType]: T }; + +const settings: BtAdvMap = { + bar: false, + gps: false, + hrm: false, + mag: false, +}; + +const idToName: BtAdvMap = { + bar: "Barometer", + gps: "GPS", + hrm: "HRM", + mag: "Magnetometer", +}; + +// 15 characters per line +const infoFont: FontNameWithScaleFactor = "6x8:2"; + +const colour = { + on: "#0f0", + off: "#fff", +} as const; + +const makeToggle = (id: BtAdvType) => () => { + settings[id] = !settings[id]; + + const entry = btnLayout[id]!; + const col = settings[id] ? colour.on : colour.off; + + entry.btnBorder = entry.col = col; + + btnLayout.update(); + btnLayout.render(); + + //require('Storage').writeJSON(SETTINGS_FILENAME, settings); + enableSensors(); +}; + +const btnStyle: { + font: FontNameWithScaleFactor, + fillx?: 1, + filly?: 1, + col: ColorResolvable, + bgCol: ColorResolvable, + btnBorder: ColorResolvable, +} = { + font: "Vector:14", + fillx: 1, + filly: 1, + col: g.theme.fg, + bgCol: g.theme.bg, + btnBorder: "#fff", +}; + +const btnLayout = new Layout( + { + type: "v", + c: [ + { + type: "h", + c: [ + { + type: "btn", + label: idToName.bar, + id: "bar", + cb: makeToggle('bar'), + ...btnStyle, + }, + { + type: "btn", + label: idToName.gps, + id: "gps", + cb: makeToggle('gps'), + ...btnStyle, + }, + ] + }, + { + type: "h", + c: [ + // hrm, mag + { + type: "btn", + label: idToName.hrm, + id: "hrm", + cb: makeToggle('hrm'), + ...btnStyle, + }, + { + type: "btn", + label: idToName.mag, + id: "mag", + cb: makeToggle('mag'), + ...btnStyle, + }, + ] + }, + { + type: "h", + c: [ + { + type: "btn", + label: "Back", + cb: () => { + setBtnsShown(false); + }, + ...btnStyle, + }, + ] + } + ] + }, + { + lazy: true, + back: () => { + setBtnsShown(false); + }, + }, +); + +const setBtnsShown = (b: boolean) => { + btnsShown = b; + + hook(!btnsShown); + setIntervals(); + + redraw(); +}; + +const drawInfo = (force?: true) => { + let { y, x, w } = Bangle.appRect; + const mid = x + w / 2 + let drawn = false; + + if (!force && !bar && !gps && !hrm && !mag) + return; + + g.reset() + .clearRect(Bangle.appRect) + .setFont(infoFont) + .setFontAlign(0, -1); + + if (bar) { + g.drawString(`${bar.altitude.toFixed(1)}m`, mid, y); + y += g.getFontHeight(); + + g.drawString(`${bar.pressure.toFixed(1)} hPa`, mid, y); + y += g.getFontHeight(); + + g.drawString(`${bar.temperature.toFixed(1)}C`, mid, y); + y += g.getFontHeight(); + + drawn = true; + } + + if (gps) { + g.drawString( + `${gps.lat.toFixed(4)} lat, ${gps.lon.toFixed(4)} lon`, + mid, + y, + ); + y += g.getFontHeight(); + + g.drawString( + `${gps.alt}m (${gps.satellites} sat)`, + mid, + y, + ); + y += g.getFontHeight(); + + drawn = true; + } + + if (hrm) { + g.drawString(`${hrm.bpm} BPM (${hrm.confidence}%)`, mid, y); + y += g.getFontHeight(); + + drawn = true; + } else if (hrmAny) { + g.drawString(`~${hrmAny.bpm} BPM (${hrmAny.confidence}%)`, mid, y); + y += g.getFontHeight(); + + drawn = true; + + if (!settings.hrm && !hrmAnyClear) { + // hrm is erased, but hrmAny will remain until cleared (or reset) + // if it runs via health check, we reset it here + hrmAnyClear = setTimeout(() => { + hrmAny = undefined; + hrmAnyClear = undefined; + }, 10000); + } + } + + if (mag) { + g.drawString( + `${mag.x} ${mag.y} ${mag.z}`, + mid, + y + ); + y += g.getFontHeight(); + + g.drawString( + `heading: ${mag.heading.toFixed(1)}`, + mid, + y + ); + y += g.getFontHeight(); + + drawn = true; + } + + if (!drawn) { + if (!force || Object.values(settings).every((x: boolean) => !x)) { + g.drawString(`swipe to enable`, mid, y); + } else { + g.drawString(`events pending`, mid, y); + } + y += g.getFontHeight(); + } +}; + +const onTap = (/* _: { ... } */) => { + setBtnsShown(true); +}; + +const redraw = () => { + if (btnsShown) { + if (!prevBtnsShown) { + prevBtnsShown = btnsShown; + + Bangle.removeListener("swipe", onTap); + + btnLayout.setUI(); + btnLayout.forgetLazyState(); + g.clearRect(Bangle.appRect); // in case btnLayout isn't full screen + } + + btnLayout.render(); + } else { + if (prevBtnsShown) { + prevBtnsShown = btnsShown; + + Bangle.setUI(); // remove all existing input handlers + Bangle.on("swipe", onTap); + + drawInfo(true); + } else { + drawInfo(); + } + } +}; + +const encodeHrm: LenFunc = (hrm: Hrm) => + // { + // flags: u8, + // bytes: [u8...] + // } + // flags { + // 1 << 0: 16bit bpm + // 1 << 1: sensor contact available + // 1 << 2: sensor contact boolean + // 1 << 3: energy expended, next 16 bits + // 1 << 4: "rr" data available, u16s, intervals + // } + [0, hrm.bpm]; +encodeHrm.maxLen = 2; + +const encodePressure: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.pressure * 10), 4, false); +encodePressure.maxLen = 4; + +const encodeElevation: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.altitude * 100), 3, true); +encodeElevation.maxLen = 3; + +const encodeTemp: LenFunc = (data: PressureData) => + toByteArray(Math.round(data.temperature * 10), 2, true); +encodeTemp.maxLen = 2; + +const encodeGps: LenFunc = (data: GPSFix) => { + // flags: 16 bits + // bit 0: Instantaneous Speed Present + // bit 1: Total Distance Present + // bit 2: Location Present + // bit 3: Elevation Present + // bit 4: Heading Present + // bit 5: Rolling Time Present + // bit 6: UTC Time Present + // + // bit 7-8: position status + // 0 (0b00): no position + // 1 (0b01): position ok + // 2 (0b10): estimated position + // 3 (0b11): last known position + // + // bit 9: speed & distance format + // 0: 2d + // 1: 3d + // + // bit 10-11: elevation source + // 0: Positioning System + // 1: Barometric Air Pressure + // 2: Database Service (or similiar) + // 3: Other + // + // bit 12: Heading Source + // 0: Heading based on movement + // 1: Heading based on magnetic compass + // + // speed: u16 (m/s), 1/100 + // distance: u24, 1/10 + // lat: s32, 1/10^7 + // lon: s32, 1/10^7 + // elevation: s24, 1/100 + // heading: u16 (deg), 1/100 + // rolling time: u8 (s) + // utc time: org.bluetooth.characteristic.date_time + + const speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false); + const lat = toByteArray(Math.round(data.lat * 10000000), 4, true); + const lon = toByteArray(Math.round(data.lon * 10000000), 4, true); + const elevation = toByteArray(Math.round(data.alt * 100), 3, true); + const heading = toByteArray(Math.round(data.course * 100), 2, false); + + return [ + 0b10011101, // speed, location, elevation, heading [...] + 0b00000010, // position ok, 3d speed/distance + speed[0]!, speed[1]!, + lat[0]!, lat[1]!, lat[2]!, lat[3]!, + lon[0]!, lon[1]!, lon[2]!, lon[3]!, + elevation[0]!, elevation[1]!, elevation[2]!, + heading[0]!, heading[1]! + ]; +}; +encodeGps.maxLen = 17; + +const encodeGpsHeadingOnly: LenFunc = (data: CompassData) => { + // see encodeGps() + const heading = toByteArray(Math.round(data.heading * 100), 2, false); + + return [ + 0b00010000, // heading present + 0b00010000, // heading source: mag + heading[0]!, heading[1]! + ]; +}; +encodeGpsHeadingOnly.maxLen = 17; + +const encodeMag: LenFunc = (data: CompassData) => { + const x = toByteArray(data.x, 2, true); + const y = toByteArray(data.y, 2, true); + const z = toByteArray(data.z, 2, true); + + return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; +}; +encodeMag.maxLen = 6; + +const encodeAcc: LenFunc = (data: AccelData) => { + const x = toByteArray(data.x * 1000, 2, true); + const y = toByteArray(data.y * 1000, 2, true); + const z = toByteArray(data.z * 1000, 2, true); + + return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ]; +}; +encodeAcc.maxLen = 6; + +const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => { + const byteArray: Array = new Array(numberOfBytes); + + if(isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + + for(let index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + + return byteArray; +}; + +const enableSensors = () => { + Bangle.setBarometerPower(settings.bar, "btadv"); + if (!settings.bar) + bar = undefined; + + Bangle.setGPSPower(settings.gps, "btadv"); + if (!settings.gps) + gps = undefined; + + Bangle.setHRMPower(settings.hrm, "btadv"); + if (!settings.hrm) + hrm = hrmAny = undefined; + + Bangle.setCompassPower(settings.mag, "btadv"); + if (!settings.mag) + mag = undefined; +}; + +// ---------------------------- + +const haveServiceData = (serv: BleServ): boolean => { + switch (serv) { + case BleServ.HRM: return !!hrm; + case BleServ.EnvSensing: return !!(bar || mag); + case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag); + case BleServ.Acc: return !!acc; + } +}; + +const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => { + switch (serv) { + case BleServ.HRM: + if (hrm || initial) { + const o: BleCharAdvert = { + maxLen: encodeHrm.maxLen, + readable: true, + notify: true, + }; + const os: BleCharAdvert = { + maxLen: 1, + readable: true, + notify: true, + }; + + if (hrm) { + o.value = encodeHrm(hrm); + os.value = [SensorLocations.Wrist]; + hrm = undefined; + } + + return { + [BleChar.HRM]: o, + [BleChar.SensorLocation]: os, + }; + } + return {}; + + case BleServ.LocationAndNavigation: + if (gps || initial) { + const o: BleCharAdvert = { + maxLen: encodeGps.maxLen, + readable: true, + notify: true, + }; + if (gps) { + o.value = encodeGps(gps); + gps = undefined; + } + + return { [BleChar.LocationAndSpeed]: o }; + } else if (mag) { + const o: BleCharAdvert = { + maxLen: encodeGpsHeadingOnly.maxLen, + readable: true, + notify: true, + value: encodeGpsHeadingOnly(mag), + }; + + return { [BleChar.LocationAndSpeed]: o }; + } + return {}; + + case BleServ.EnvSensing: { + const o: BleServAdvert = {}; + + if (bar || initial) { + o[BleChar.Elevation] = { + maxLen: encodeElevation.maxLen, + readable: true, + notify: true, + }; + o[BleChar.TempCelsius] = { + maxLen: encodeTemp.maxLen, + readable: true, + notify: true, + }; + o[BleChar.Pressure] = { + maxLen: encodePressure.maxLen, + readable: true, + notify: true, + }; + + if (bar) { + o[BleChar.Elevation]!.value = encodeElevation(bar); + o[BleChar.TempCelsius]!.value = encodeTemp(bar); + o[BleChar.Pressure]!.value = encodePressure(bar); + bar = undefined; + } + } + + if (mag || initial) { + o[BleChar.MagneticFlux3D] = { + maxLen: encodeMag.maxLen, + readable: true, + notify: true, + }; + + if (mag) { + o[BleChar.MagneticFlux3D]!.value = encodeMag(mag); + } + } + + return o; + } + + case BleServ.Acc: { + const o: BleServAdvert = {}; + + if (acc || initial) { + o[BleChar.Acc] = { + maxLen: encodeAcc.maxLen, + readable: true, + notify: true, + }; + + if (acc) { + o[BleChar.Acc]!.value = encodeAcc(acc); + acc = undefined; + } + } + + return o; + } + } +}; + +const getBleAdvert = (map: (s: BleServ) => T, all = false) => { + const advert: { [key in BleServ]?: T } = {}; + + for (const serv of services) { + if (all || haveServiceData(serv)) { + advert[serv] = map(serv); + } + } + + // clear mag only after both EnvSensing and LocationAndNavigation have run + mag = undefined; + + return advert; +}; + +// done via advertise in setServices() +//const updateBleAdvert = () => { +// require("ble_advert").set(...) +// +// let bleAdvert: ReturnType>; +// +// if (!(bleAdvert = (Bangle as any).bleAdvert)) { +// bleAdvert = getBleAdvert(_ => undefined); +// +// (Bangle as any).bleAdvert = bleAdvert; +// } +// +// try { +// NRF.setAdvertising(bleAdvert); +// } catch (e) { +// console.log("couldn't setAdvertising():", e); +// } +//}; + +const updateServices = () => { + const newAdvert = getBleAdvert(serviceToAdvert); + + NRF.updateServices(newAdvert); +}; + +const onAccel = (newAcc: NonNull) => acc = newAcc; +const onPressure = (newBar: NonNull) => bar = newBar; +const onGPS = (newGps: NonNull) => gps = newGps; +const onHRM = (newHrm: NonNull) => { + if (newHrm.confidence >= HRM_MIN_CONFIDENCE) + hrm = newHrm; + hrmAny = newHrm; +}; +const onMag = (newMag: NonNull) => mag = newMag; + +const hook = (enable: boolean) => { + // need to disable for perf reasons, when buttons are shown + if (enable) { + Bangle.on("accel", onAccel); + Bangle.on("pressure", onPressure); + Bangle.on("GPS", onGPS); + Bangle.on("HRM", onHRM); + Bangle.on("mag", onMag); + } else { + Bangle.removeListener("accel", onAccel); + Bangle.removeListener("pressure", onPressure); + Bangle.removeListener("GPS", onGPS); + Bangle.removeListener("HRM", onHRM); + Bangle.removeListener("mag", onMag); + } +} + +// --- intervals --- + +const setIntervals = ( + locked: ShortBoolean = Bangle.isLocked(), + connected: boolean = NRF.getSecurityStatus().connected, +) => { + changeInterval( + redrawInterval, + locked ? Intervals.UI_INFO_SLEEP : Intervals.UI_INFO, + ); + + if (connected) { + const interval = btnsShown ? Intervals.BLE_BACKGROUND : Intervals.BLE; + + if (bleInterval) { + changeInterval(bleInterval, interval); + } else { + bleInterval = setInterval(updateServices, interval); + } + } else if (bleInterval) { + clearInterval(bleInterval); + bleInterval = undefined; + } +}; + +const redrawInterval = setInterval(redraw, /*replaced*/1000); +Bangle.on("lock", locked => setIntervals(locked)); + +let bleInterval: undefined | IntervalId; +NRF.on("connect", () => setIntervals(undefined, true)); +NRF.on("disconnect", () => setIntervals(undefined, false)); + +setIntervals(); + +// turn things on +setBtnsShown(true); +enableSensors(); + +// set services/advert once at startup: +{ + // must have fixed services from the start: + const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true); + + NRF.setServices( + ad, + { + uart: false, + }, + ); + + for(const id in ad){ + const serv = ad[id as BleServ]; + let value; + + // pick the first characteristic to advertise + for(const ch in serv){ + value = serv[ch as BleChar]!.value; + break; + } + + require("ble_advert").set(id, value || []); + } +} +} diff --git a/apps/btadv/icon.js b/apps/btadv/icon.js new file mode 100644 index 000000000..03de6f5fd --- /dev/null +++ b/apps/btadv/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/gECoARQpARQpIRRkARQkgRRwBrPkmQBpIvDCIMEyQQIgIvDR4WSSRIvDCIUSSRIvDCISSCJRAvCCIoaDCIgvCGooaCiRNEDoRZFBwQRFgDCBPooOCOI0JkihDhApBHARxDgiSCyTFCHYQRGUIQRDHYIRCHYIRBiChDBAJKBHYICBpIRDyQyCSQQgBCJNBCIbCDCIZNDF4R0DEYwRCIIa5BI5ARDyAdCNZIFCCIKYBR5QRBVoJ6BWZY7CTwTXJWYQRFfZYRFRgQRCAoT4DCIgICCIQpCHARlCfYRBDCIhlDCIZuDGor1BCIgCBLgZZEAAiABEYIGCPooALUIYRQVQYRLdIQRPKAQROCBzjELJ4RPAHoA==")) diff --git a/apps/btadv/icon.png b/apps/btadv/icon.png new file mode 100644 index 000000000..28867f31e Binary files /dev/null and b/apps/btadv/icon.png differ diff --git a/apps/btadv/metadata.json b/apps/btadv/metadata.json new file mode 100644 index 000000000..71a0fedaf --- /dev/null +++ b/apps/btadv/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "btadv", + "name": "btadv", + "shortName": "btadv", + "version": "0.04", + "description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth", + "icon": "icon.png", + "tags": "health,tool,sensors,bluetooth", + "supports": ["BANGLEJS2"], + "readme":"README.md", + "storage": [ + {"name":"btadv.app.js","url":"app.js"}, + {"name":"btadv.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/bthome/ChangeLog b/apps/bthome/ChangeLog new file mode 100644 index 000000000..1920ee3a8 --- /dev/null +++ b/apps/bthome/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App! +0.02: Fix double-button press if you press the next button within 30s (#3243) +0.03: Cope with identical duplicate buttons (fix #3260) + Set 'n' for buttons in Bangle.btHomeData correctly (avoids adding extra buttons on end of advertising) +0.04: Fix duplicate button on edit->save +0.05: Use the bleAdvert module +0.06: button number can't be 0. Now generates number automatically diff --git a/apps/bthome/README.md b/apps/bthome/README.md new file mode 100644 index 000000000..d232e8d64 --- /dev/null +++ b/apps/bthome/README.md @@ -0,0 +1,26 @@ +# BTHome + +This uses BTHome (https://bthome.io/) to allow easy control of [Home Assistant](https://www.home-assistant.io/) via Bluetooth advertisements. + +Other apps like [the Home Assistant app](https://banglejs.com/apps/?id=ha) communicate with Home Assistant +via your phone so work from anywhere, but require being in range of your phone. + +## Usage + +When the app is installed, go to the `BTHome` app and click Settings. + +Here, you can choose if you want to advertise your Battery status, but can also click `Add Button`. + +You can then add a custom button event: + +* `Icon` - the picture for the button +* `Name` - the name associated with the button +* `Action` - the action that Home Assistant will see when this button is pressed +* `Button #` - the button event 'number' - keep this at 0 for now + +Once you've saved, you will then get your button shown in the BTHome app. Tapping it will make Bangle.js advertise via BTHome that the button has been pressed. + +## ClockInfo + +When you've added one or more buttons, they will appear in a ClockInfo under the main `Bangle.js` heading. You can just tap to select the ClockInfo, scroll down until a BTHome one is visible and then tap again. It will immediately send the Advertisement. + diff --git a/apps/bthome/app-icon.js b/apps/bthome/app-icon.js new file mode 100644 index 000000000..ecdc205bc --- /dev/null +++ b/apps/bthome/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA")) \ No newline at end of file diff --git a/apps/bthome/app.js b/apps/bthome/app.js new file mode 100644 index 000000000..6fce4ff0b --- /dev/null +++ b/apps/bthome/app.js @@ -0,0 +1,33 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function showMenu() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + var menu = []; + menu[""] = {title:"BTHome", back:load }; + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu.push({ + title : /*LANG*/"\0"+img+" "+button.name, + onchange : function() { + Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); + E.showMenu(); + E.showMessage("Sending Event"); + Bangle.buzz(); + setTimeout(showMenu, 500); + } + }); + }); + menu.push({ + title : /*LANG*/"Settings", + onchange : function() { + eval(require("Storage").read("bthome.settings.js"))(()=>showMenu()); + }}); + E.showMenu(menu); +} + +showMenu(); + + diff --git a/apps/bthome/boot.js b/apps/bthome/boot.js new file mode 100644 index 000000000..00e08df90 --- /dev/null +++ b/apps/bthome/boot.js @@ -0,0 +1,54 @@ +Bangle.btHomeData = []; +{ + require("BTHome").packetId = 0|(Math.random()*256); // random packet id so new packets show up + let settings = require("Storage").readJSON("bthome.json",1)||{}; + if (settings.showBattery) + Bangle.btHomeData.push({ + type : "battery", + v : E.getBattery() + }); + // If buttons defined, add events for them + if (settings.buttons instanceof Array) { + let n = settings.buttons.reduce((n,b)=>b.n>n?b.n:n,-1); + for (var i=0;i<=n;i++) + Bangle.btHomeData.push({type:"button_event",v:"none",n:i}); + } +} + +/* Global function to allow advertising BTHome adverts + extras = array of extra data, see require("BTHome").getAdvertisement - can add {n:0/1/2} for different instances + options = { + event : an event - advertise fast, and when connected + } +*/ +Bangle.btHome = function(extras, options) { + options = options||{}; + // clear any existing events + Bangle.btHomeData.forEach(d => {if (d.type=="button_event") d.v="none";}); + // update with extras + if (extras) { + extras.forEach(extra => { + var n = Bangle.btHomeData.find(b=>b.type==extra.type && b.n==extra.n); + if (n) Object.assign(n, extra); + else Bangle.btHomeData.push(extra); + }); + } + var bat = Bangle.btHomeData.find(b=>b.type=="battery"); + if (bat) bat.v = E.getBattery(); + var advert = require("BTHome").getAdvertisement(Bangle.btHomeData)[0xFCD2]; + // Add to the list of available advertising + var advOptions = {}; + var updateTimeout = 10*60*1000; // update every 10 minutes + if (options.event) { // if it's an event... + advOptions.interval = 50; + advOptions.whenConnected = true; + updateTimeout = 30000; // slow down in 30 seconds + } + require("ble_advert").set(0xFCD2, advert, advOptions); + if (Bangle.btHomeTimeout) clearTimeout(Bangle.btHomeTimeout); + Bangle.btHomeTimeout = setTimeout(function() { + delete Bangle.btHomeTimeout; + // update + Bangle.btHome(); + }, updateTimeout); +}; diff --git a/apps/bthome/clkinfo.js b/apps/bthome/clkinfo.js new file mode 100644 index 000000000..bcf2a6fa4 --- /dev/null +++ b/apps/bthome/clkinfo.js @@ -0,0 +1,17 @@ +(function() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + return { + name: "Bangle", + items: settings.buttons.map(button => { + return { name : button.name, + get : function() { return { text : button.name, + img : require("icons").getIcon(button.icon) }}, + show : function() {}, + hide : function() {}, + run : function() { Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); } + } + }) + }; +}) diff --git a/apps/bthome/icon.png b/apps/bthome/icon.png new file mode 100644 index 000000000..091784477 Binary files /dev/null and b/apps/bthome/icon.png differ diff --git a/apps/bthome/metadata.json b/apps/bthome/metadata.json new file mode 100644 index 000000000..bbfcfcfe5 --- /dev/null +++ b/apps/bthome/metadata.json @@ -0,0 +1,20 @@ +{ "id": "bthome", + "name": "BTHome", + "shortName":"BTHome", + "version":"0.06", + "description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth", + "icon": "icon.png", + "type": "app", + "tags": "clkinfo,bthome,bluetooth", + "supports" : ["BANGLEJS2"], + "dependencies": {"textinput":"type", "icons":"module"}, + "readme": "README.md", + "storage": [ + {"name":"bthome.img","url":"app-icon.js","evaluate":true}, + {"name":"bthome.clkinfo.js","url":"clkinfo.js"}, + {"name":"bthome.boot.js","url":"boot.js"}, + {"name":"bthome.app.js","url":"app.js"}, + {"name":"bthome.settings.js","url":"settings.js"} + ], + "data":[{"name":"bthome.json"}] +} diff --git a/apps/bthome/settings.js b/apps/bthome/settings.js new file mode 100644 index 000000000..19a854151 --- /dev/null +++ b/apps/bthome/settings.js @@ -0,0 +1,107 @@ +(function(back) { + var settings; + + function loadSettings() { + settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + } + + function saveSettings() { + require("Storage").writeJSON("bthome.json",settings) + } + + // Get id number for button that is sent to bthome + function getNewIdNumber(){ + return [1, 2, 3, 4, 5, 6, 7, 8, 9].find(id => settings.buttons.every(button => id != button.n)); + } + + function showButtonMenu(button, isNew) { + if (!button) { + button = {name:"home", icon:"home", n:getNewIdNumber(), v:"press"}; + isNew = true; + } + var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"]; + var menu = { + "":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back: () => { + loadSettings(); // revert changes + showMenu(); + }}, + /*LANG*/"Icon" : { + value : "\0"+require("icons").getIcon(button.icon), + onchange : () => { + require("icons").showIconChooser().then(function(iconName) { + button.icon = iconName; + button.name = iconName; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Name" : { + value : button.name, + onchange : () => { + require("textinput").input({text:button.name}).then(function(name) { + button.name = name; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Action" : { + value : Math.max(0,actions.indexOf(button.v)), min:0, max:actions.length-1, + format : v => actions[v], + onchange : v => button.v=actions[v] + }, + /*LANG*/"Save" : () => { + if (isNew) settings.buttons.push(button); + saveSettings(); + showMenu(); + } + }; + if (!isNew) menu[/*LANG*/"Delete"] = function() { + E.showPrompt("Delete Button?").then(function(yes) { + if (yes) { + settings.buttons.splice(settings.buttons.indexOf(button),1); + saveSettings(); + } + showMenu(); + }); + } + E.showMenu(menu); + } + + function showMenu() { + var menu = []; + menu[""] = {title:"BTHome", back:back}; + menu.push({ + title : /*LANG*/"Show Battery", + value : !!settings.showBattery, + onchange : v=>{ + settings.showBattery = v; + saveSettings(); + } + }); + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu.push({ + title : /*LANG*/"Button"+(img ? " \0"+img : (idx+1)), + onchange : function() { + showButtonMenu(button, false); + } + }); + }); + menu.push({ + title : /*LANG*/"Add Button", + onchange : function() { + showButtonMenu(undefined, true); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) \ No newline at end of file diff --git a/apps/bthometemp/ChangeLog b/apps/bthometemp/ChangeLog new file mode 100644 index 000000000..94a60aa3e --- /dev/null +++ b/apps/bthometemp/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Handle the case where other apps have set bleAdvert to an array +0.03: Use the ble_advert module diff --git a/apps/bthometemp/README.md b/apps/bthometemp/README.md new file mode 100644 index 000000000..1a8212ea4 --- /dev/null +++ b/apps/bthometemp/README.md @@ -0,0 +1,9 @@ +# BTHome Temperature and Pressure + +This app displays temperature and pressure and advertises them over bluetooth using BTHome.io standard (along with battery level) + +This can be used to integrate with [Home Assistant](https://www.home-assistant.io/), so you can use your Bangle as a wireless temperature/pressure sensor. + +More info on the standard at https://bthome.io + +And the data format used is https://bthome.io/format/ diff --git a/apps/bthometemp/app-icon.js b/apps/bthometemp/app-icon.js new file mode 100644 index 000000000..e2dff3eb9 --- /dev/null +++ b/apps/bthometemp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///1N6BIPf//1gMIwdE8sG2me+9Y/8C/2snXsoUNpdnzdt/xj/AH4AYgMRAAUQCyoYSCQNXs1muoFBFyHm1X//+qtwwPiMX1+YmczxP6uIwNFwN6yeDnGDmc504wNFwOpnGYC4OJweaGBsR9WTmYtBmc4GAOuC5ZGBt4SBAAQEBwf2JBcBiupnIuCmedxGTzVRC5cX1AuDnPZF4OKuIXLi3zIoedMgMzn9hC5uICQON5IDBxAXSznYC6RdDPQYXNO4JcB7pdCO56nBnGZ7p6DU5zXBXgSqDa5sAiPqIgOZd4c510RCxQXBi+pRQIXBxODzVxC5hIBvR1DnE505GMGAevzAvC/QuNGAfm1X//+qtwuOGAURq9ms11AoIWOGAQAEFw1EDBwWFggBCkUgAQMigUAAIIAJoABDCgIXQFwYXBCYYBDHAMCEAIkCFgcEAIIKCCoQFCkAhBAQIlCkAsBOoIXCBoIvEAwQTCAYI2BIwgXIF4YXDQwIVCC4YIBMIwfCAQRfGYBSPNC6TBFACgwBACouWAH4AiA=")) diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js new file mode 100644 index 000000000..f18e33c20 --- /dev/null +++ b/apps/bthometemp/app.js @@ -0,0 +1,58 @@ +// history of temperature/pressure readings +var history = []; + +// When we get temperature... +function onTemperature(p) { + // Average the last 5 temperature readings + while (history.length>4) history.shift(); + history.push(p); + var avrTemp = history.reduce((i,h)=>h.temperature+i,0) / history.length; + var avrPressure = history.reduce((i,h)=>h.pressure+i,0) / history.length; + var t = require('locale').temp(avrTemp).replace("'","°"); + // Draw + var rect = Bangle.appRect; + g.reset(1).clearRect(rect.x, rect.y, rect.x2, rect.y2); + var x = (rect.x+rect.x2)/2; + var y = (rect.y+rect.y2)/2 + 10; + g.setFont("6x15").setFontAlign(0,0).drawString("Temperature:", x, y - 65); + g.setFontVector(50).setFontAlign(0,0).drawString(t, x, y-25); + g.setFont("6x15").setFontAlign(0,0).drawString("Pressure:", x, y+15 ); + g.setFont("12x20").setFontAlign(0,0).drawString(Math.round(avrPressure)+" hPa", x, y+40); + // Set Bluetooth Advertising + // https://bthome.io/format/ + var temp100 = Math.round(avrTemp*100); + var pressure100 = Math.round(avrPressure*100); + + var advert = [ 0x40, /* BTHome Device Information + bit 0: "Encryption flag" + bit 1-4: "Reserved for future use" + bit 5-7: "BTHome Version" */ + + 0x01, // Battery, 8 bit + E.getBattery(), + + 0x02, // Temperature, 16 bit + temp100&255,temp100>>8, + + 0x04, // Pressure, 16 bit + pressure100&255,(pressure100>>8)&255,pressure100>>16 + ]; + + require("ble_advert").set(0xFCD2, advert); +} + +// Gets the temperature in the most accurate way with pressure sensor +function drawTemperature() { + Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); +} + +setInterval(function() { + drawTemperature(); +}, 10000); // update every 10s +Bangle.loadWidgets(); +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); +E.showMessage("Reading temperature..."); +drawTemperature(); diff --git a/apps/bthometemp/app.png b/apps/bthometemp/app.png new file mode 100644 index 000000000..6c8eb3f14 Binary files /dev/null and b/apps/bthometemp/app.png differ diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json new file mode 100644 index 000000000..3e96d95f8 --- /dev/null +++ b/apps/bthometemp/metadata.json @@ -0,0 +1,14 @@ +{ "id": "bthometemp", + "name": "BTHome Temperature and Pressure", + "shortName":"BTHome T", + "version":"0.03", + "description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard", + "icon": "app.png", + "tags": "bthome,bluetooth,temperature", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"bthometemp.app.js","url":"app.js"}, + {"name":"bthometemp.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 7ca8319b6..1480698a2 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -22,3 +22,28 @@ Restructure the settings menu 0.08: Allow scanning for devices in settings 0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655) +0.10: Use default Bangle formatter for booleans +0.11: App now shows status info while connecting + Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central) +0.12: Fix HRM fallback handling + Use default boolean formatter in custom menu and directly apply config if useful + Allow recording unmodified internal HR + Better connection retry handling +0.13: Less time used during boot if disabled +0.14: Allow bonding (Debug menu) + Prevent mixing of BT and internal HRM events if both are enabled + Always use a grace period (default 0 ms) to decouple some connection steps + Device not found errors now utilize increasing timeouts +0.15: Fix recording internal sensor + Handle fallback to internal sensor consistently if BT bpm is 0 + Power internal sensor down if not needed for fallback +0.16: Set powerdownRequested correctly on BTHRM power on + Additional logging on errors + Add debug option for disabling active scanning +0.17: New GUI based on layout library +0.18: Minor code improvements +0.19: Move caching of characteristics into settings app + Changed default of active scanning to false + Fix setHRMPower method not returning new state + Only buzz for disconnect after switching on if there already was an actual connection + Fix recorder not switching BTHRM on and off diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md index 8d5872670..570072dbf 100644 --- a/apps/bthrm/README.md +++ b/apps/bthrm/README.md @@ -1,6 +1,6 @@ # Bluetooth Heart Rate Monitor -When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one. +When this app is installed it overrides Bangle.js's built in heart rate monitor with an external Bluetooth one. HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM')` event as if it came from the on board monitor. @@ -19,7 +19,18 @@ Just install the app, then install an app that uses the heart rate monitor. Once installed you will have to go into this app's settings while your heart rate monitor is available for bluetooth pairing and scan for devices. -**To disable this and return to normal HRM, uninstall the app** +**To disable this and return to normal HRM, uninstall the app or change the settings** + +The characteristics of your selected sensor are cached in the settings. That means if your sensor changes, e.g. by firmware updates or similar, you will need to re-scan in the settings to update the cache of characteristics. This is done to take some complexity (and time) out of the boot process. + +Scanning in the settings will do 10 retries and then give up on adding the sensor. Usually that works fine, if it does not for you just try multiple times. Currently saved sensor information is only replaced on a successful pairing. There are additional options in the Debug entry of the menu that can help with specific sensor oddities. Bonding and active scanning can help with connecting, but can also prevent some sensors from working. The "Grace Periods" just add some additional time at certain steps in the connection process which can help with stability or reconnect speed of some finicky sensors. Defaults should be fine for most. + +### Modes + +* Off - Internal HRM is used, no attempt on connecting to BT HRM. +* Default - Replaces internal HRM with BT HRM and falls back to internal HRM if no valid measurements received. +* Both - The BT HRM needs to be started explicitly by an app that wants to use it. BT HRM has its own event and is completely separated from the internal HRM. Apps not supporting the BT HRM will not see the BT HRM measurements. +* Custom - Combine low level settings as you see fit. ## Compatible Heart Rate Monitors @@ -35,6 +46,10 @@ So far it has been tested on: * Polar OH1 * Wahoo TICKR X 2 +## Recorder plugin + +The recorder plugin can record the BT HRM event (blue) and the original unchanged HRM event (green). This is mainly useful for debugging purposes or comparing the BT with the internal HRM, as the resulting "merged" HRM can be recorded using the default HRM recorder. + ## Internals This replaces `Bangle.setHRMPower` with its own implementation. @@ -46,3 +61,7 @@ This replaces `Bangle.setHRMPower` with its own implementation. ## Creator Gordon Williams + +## Contributer + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index e9e640563..3e3d35737 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -1,567 +1 @@ -(function() { - var settings = Object.assign( - require('Storage').readJSON("bthrm.default.json", true) || {}, - require('Storage').readJSON("bthrm.json", true) || {} - ); - - var log = function(text, param){ - if (settings.debuglog){ - var logline = new Date().toISOString() + " - " + text; - if (param){ - logline += " " + JSON.stringify(param); - } - print(logline); - } - }; - - log("Settings: ", settings); - - if (settings.enabled){ - - var clearCache = function() { - return require('Storage').erase("bthrm.cache.json"); - }; - - var getCache = function() { - var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; - if (settings.btid && settings.btid === cache.id) return cache; - clearCache(); - return {}; - }; - - var addNotificationHandler = function(characteristic) { - log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler); - characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); - }; - - var writeCache = function(cache) { - var oldCache = getCache(); - if (oldCache !== cache) { - log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache); - } else { - log("No changes, don't write cache"); - } - }; - - var characteristicsToCache = function(characteristics) { - log("Cache characteristics"); - var cache = getCache(); - if (!cache.characteristics) cache.characteristics = {}; - for (var c of characteristics){ - //"handle_value":16,"handle_decl":15 - log("Saving handle " + c.handle_value + " for characteristic: ", c); - cache.characteristics[c.uuid] = { - "handle": c.handle_value, - "uuid": c.uuid, - "notify": c.properties.notify, - "read": c.properties.read - }; - } - writeCache(cache); - }; - - var characteristicsFromCache = function() { - log("Read cached characteristics"); - var cache = getCache(); - if (!cache.characteristics) return []; - var restored = []; - for (var c in cache.characteristics){ - var cached = cache.characteristics[c]; - var r = new BluetoothRemoteGATTCharacteristic(); - log("Restoring characteristic ", cached); - r.handle_value = cached.handle; - r.uuid = cached.uuid; - r.properties = {}; - r.properties.notify = cached.notify; - r.properties.read = cached.read; - addNotificationHandler(r); - log("Restored characteristic: ", r); - restored.push(r); - } - return restored; - }; - - log("Start"); - - var lastReceivedData={ - }; - - var supportedServices = [ - "0x180d", // Heart Rate - "0x180f", // Battery - ]; - - var supportedCharacteristics = { - "0x2a37": { - //Heart rate measurement - handler: function (dv){ - var flags = dv.getUint8(0); - - var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit - - var sensorContact; - - if (flags & 2){ - sensorContact = !!(flags & 4); - } - - var idx = 2 + (flags&1); - - var energyExpended; - if (flags & 8){ - energyExpended = dv.getUint16(idx,1); - idx += 2; - } - var interval; - if (flags & 16) { - interval = []; - var maxIntervalBytes = (dv.byteLength - idx); - log("Found " + (maxIntervalBytes / 2) + " rr data fields"); - for(var i = 0 ; i < maxIntervalBytes / 2; i++){ - interval[i] = dv.getUint16(idx,1); // in milliseconds - idx += 2; - } - } - - var location; - if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){ - location = lastReceivedData["0x180d"]["0x2a38"]; - } - - var battery; - if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){ - battery = lastReceivedData["0x180f"]["0x2a19"]; - } - - if (settings.replace){ - var repEvent = { - bpm: bpm, - confidence: (sensorContact || sensorContact === undefined)? 100 : 0, - src: "bthrm" - }; - - log("Emitting HRM: ", repEvent); - Bangle.emit("HRM", repEvent); - } - - var newEvent = { - bpm: bpm - }; - - if (location) newEvent.location = location; - if (interval) newEvent.rr = interval; - if (energyExpended) newEvent.energy = energyExpended; - if (battery) newEvent.battery = battery; - if (sensorContact) newEvent.contact = sensorContact; - - log("Emitting BTHRM: ", newEvent); - Bangle.emit("BTHRM", newEvent); - } - }, - "0x2a38": { - //Body sensor location - handler: function(dv){ - if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; - lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); - } - }, - "0x2a19": { - //Battery - handler: function (dv){ - if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; - lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); - } - } - }; - - var device; - var gatt; - var characteristics = []; - var blockInit = false; - var currentRetryTimeout; - var initialRetryTime = 40; - var maxRetryTime = 60000; - var retryTime = initialRetryTime; - - var connectSettings = { - minInterval: 7.5, - maxInterval: 1500 - }; - - var waitingPromise = function(timeout) { - return new Promise(function(resolve){ - log("Start waiting for " + timeout); - setTimeout(()=>{ - log("Done waiting for " + timeout); - resolve(); - }, timeout); - }); - }; - - if (settings.enabled){ - Bangle.isBTHRMOn = function(){ - return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0); - }; - - Bangle.isBTHRMConnected = function(){ - return gatt && gatt.connected; - }; - } - - if (settings.replace){ - var origIsHRMOn = Bangle.isHRMOn; - - Bangle.isHRMOn = function() { - if (settings.enabled && !settings.replace){ - return origIsHRMOn(); - } else if (settings.enabled && settings.replace){ - return Bangle.isBTHRMOn(); - } - return origIsHRMOn() || Bangle.isBTHRMOn(); - }; - } - - var clearRetryTimeout = function() { - if (currentRetryTimeout){ - log("Clearing timeout " + currentRetryTimeout); - clearTimeout(currentRetryTimeout); - currentRetryTimeout = undefined; - } - }; - - var retry = function() { - log("Retry"); - - if (!currentRetryTimeout){ - - var clampedTime = retryTime < 100 ? 100 : retryTime; - - log("Set timeout for retry as " + clampedTime); - clearRetryTimeout(); - currentRetryTimeout = setTimeout(() => { - log("Retrying"); - currentRetryTimeout = undefined; - initBt(); - }, clampedTime); - - retryTime = Math.pow(clampedTime, 1.1); - if (retryTime > maxRetryTime){ - retryTime = maxRetryTime; - } - } else { - log("Already in retry..."); - } - }; - - var buzzing = false; - var onDisconnect = function(reason) { - log("Disconnect: " + reason); - log("GATT: ", gatt); - log("Characteristics: ", characteristics); - retryTime = initialRetryTime; - clearRetryTimeout(); - switchInternalHrm(); - blockInit = false; - if (settings.warnDisconnect && !buzzing){ - buzzing = true; - Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;}); - } - if (Bangle.isBTHRMOn()){ - retry(); - } - }; - - var createCharacteristicPromise = function(newCharacteristic) { - log("Create characteristic promise: ", newCharacteristic); - var result = Promise.resolve(); - // For values that can be read, go ahead and read them, even if we might be notified in the future - // Allows for getting initial state of infrequently updating characteristics, like battery - if (newCharacteristic.readValue){ - result = result.then(()=>{ - log("Reading data for " + JSON.stringify(newCharacteristic)); - return newCharacteristic.readValue().then((data)=>{ - if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { - supportedCharacteristics[newCharacteristic.uuid].handler(data); - } - }); - }); - } - if (newCharacteristic.properties.notify){ - result = result.then(()=>{ - log("Starting notifications for: ", newCharacteristic); - var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started for ", newCharacteristic)); - if (settings.gracePeriodNotification > 0){ - log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); - startPromise = startPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodNotification); - }); - } - return startPromise; - }); - } - return result.then(()=>log("Handled characteristic: ", newCharacteristic)); - }; - - var attachCharacteristicPromise = function(promise, characteristic) { - return promise.then(()=>{ - log("Handling characteristic:", characteristic); - return createCharacteristicPromise(characteristic); - }); - }; - - var createCharacteristicsPromise = function(newCharacteristics) { - log("Create characteristics promise: ", newCharacteristics); - var result = Promise.resolve(); - for (var c of newCharacteristics){ - if (!supportedCharacteristics[c.uuid]) continue; - log("Supporting characteristic: ", c); - characteristics.push(c); - if (c.properties.notify){ - addNotificationHandler(c); - } - - result = attachCharacteristicPromise(result, c); - } - return result.then(()=>log("Handled characteristics")); - }; - - var createServicePromise = function(service) { - log("Create service promise: ", service); - var result = Promise.resolve(); - result = result.then(()=>{ - log("Handling service: " + service.uuid); - return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); - }); - return result.then(()=>log("Handled service" + service.uuid)); - }; - - var attachServicePromise = function(promise, service) { - return promise.then(()=>createServicePromise(service)); - }; - - var initBt = function () { - log("initBt with blockInit: " + blockInit); - if (blockInit){ - retry(); - return; - } - - blockInit = true; - - var promise; - var filters; - - if (!device){ - if (settings.btid){ - log("Configured device id", settings.btid); - filters = [{ id: settings.btid }]; - } else { - return; - } - log("Requesting device with filters", filters); - promise = NRF.requestDevice({ filters: filters, active: true }); - - if (settings.gracePeriodRequest){ - log("Add " + settings.gracePeriodRequest + "ms grace period after request"); - } - - promise = promise.then((d)=>{ - log("Got device: ", d); - d.on('gattserverdisconnected', onDisconnect); - device = d; - }); - - promise = promise.then(()=>{ - log("Wait after request"); - return waitingPromise(settings.gracePeriodRequest); - }); - } else { - promise = Promise.resolve(); - log("Reuse device: ", device); - } - - promise = promise.then(()=>{ - if (gatt){ - log("Reuse GATT: ", gatt); - } else { - log("GATT is new: ", gatt); - characteristics = []; - var cachedId = getCache().id; - if (device.id !== cachedId){ - log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); - clearCache(); - } - var newCache = getCache(); - newCache.id = device.id; - writeCache(newCache); - gatt = device.gatt; - } - - return Promise.resolve(gatt); - }); - - promise = promise.then((gatt)=>{ - if (!gatt.connected){ - var connectPromise = gatt.connect(connectSettings); - if (settings.gracePeriodConnect > 0){ - log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); - connectPromise = connectPromise.then(()=>{ - log("Wait after connect"); - return waitingPromise(settings.gracePeriodConnect); - }); - } - return connectPromise; - } else { - return Promise.resolve(); - } - }); - -/* promise = promise.then(() => { - log(JSON.stringify(gatt.getSecurityStatus())); - if (gatt.getSecurityStatus()['bonded']) { - log("Already bonded"); - return Promise.resolve(); - } else { - log("Start bonding"); - return gatt.startBonding() - .then(() => console.log(gatt.getSecurityStatus())); - } - });*/ - - promise = promise.then(()=>{ - if (!characteristics || characteristics.length === 0){ - characteristics = characteristicsFromCache(); - } - }); - - promise = promise.then(()=>{ - var characteristicsPromise = Promise.resolve(); - if (characteristics.length === 0){ - characteristicsPromise = characteristicsPromise.then(()=>{ - log("Getting services"); - return gatt.getPrimaryServices(); - }); - - characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services:", services); - var result = Promise.resolve(); - for (var service of services){ - if (!(supportedServices.includes(service.uuid))) continue; - log("Supporting service: ", service.uuid); - result = attachServicePromise(result, service); - } - if (settings.gracePeriodService > 0) { - log("Add " + settings.gracePeriodService + "ms grace period after services"); - result = result.then(()=>{ - log("Wait after services"); - return waitingPromise(settings.gracePeriodService); - }); - } - return result; - }); - } else { - for (var characteristic of characteristics){ - characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); - } - } - - return characteristicsPromise; - }); - - return promise.then(()=>{ - log("Connection established, waiting for notifications"); - characteristicsToCache(characteristics); - clearRetryTimeout(); - }).catch((e) => { - characteristics = []; - log("Error:", e); - onDisconnect(e); - }); - }; - - Bangle.setBTHRMPower = function(isOn, app) { - // Do app power handling - if (!app) app="?"; - if (Bangle._PWR===undefined) Bangle._PWR={}; - if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; - if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); - if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app); - isOn = Bangle._PWR.BTHRM.length; - // so now we know if we're really on - if (isOn) { - if (!Bangle.isBTHRMConnected()) initBt(); - } else { // not on - log("Power off for " + app); - if (gatt) { - if (gatt.connected){ - log("Disconnect with gatt: ", gatt); - try{ - gatt.disconnect().then(()=>{ - log("Successful disconnect"); - }).catch((e)=>{ - log("Error during disconnect promise", e); - }); - } catch (e){ - log("Error during disconnect attempt", e); - } - } - } - } - }; - - var origSetHRMPower = Bangle.setHRMPower; - - if (settings.startWithHrm){ - - Bangle.setHRMPower = function(isOn, app) { - log("setHRMPower for " + app + ": " + (isOn?"on":"off")); - if (settings.enabled){ - Bangle.setBTHRMPower(isOn, app); - } - if ((settings.enabled && !settings.replace) || !settings.enabled){ - origSetHRMPower(isOn, app); - } - }; - } - - var fallbackInterval; - - var switchInternalHrm = function() { - if (settings.allowFallback && !fallbackInterval){ - log("Fallback to HRM enabled"); - origSetHRMPower(1, "bthrm_fallback"); - fallbackInterval = setInterval(()=>{ - if (Bangle.isBTHRMConnected()){ - origSetHRMPower(0, "bthrm_fallback"); - clearInterval(fallbackInterval); - fallbackInterval = undefined; - log("Fallback to HRM disabled"); - } - }, settings.fallbackTimeout); - } - }; - - if (settings.replace){ - log("Replace HRM event"); - if (Bangle._PWR && Bangle._PWR.HRM){ - for (var i = 0; i < Bangle._PWR.HRM.length; i++){ - var app = Bangle._PWR.HRM[i]; - log("Moving app " + app); - origSetHRMPower(0, app); - Bangle.setBTHRMPower(1, app); - if (Bangle._PWR.HRM===undefined) break; - } - } - switchInternalHrm(); - } - - E.on("kill", ()=>{ - if (gatt && gatt.connected){ - log("Got killed, trying to disconnect"); - gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); - } - }); - } -})(); +if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable(); diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index dd9230386..14ef531c8 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -1,5 +1,5 @@ -var intervalInt; -var intervalBt; +const BPM_FONT_SIZE="19%"; +const VALUE_TIMEOUT=3000; var BODY_LOCS = { 0: 'Other', @@ -7,83 +7,151 @@ var BODY_LOCS = { 2: 'Wrist', 3: 'Finger', 4: 'Hand', - 5: 'Ear Lobe', + 5: 'Earlobe', 6: 'Foot', +}; + +var Layout = require("Layout"); + +function border(l,c) { + g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4); } -function clear(y){ - g.reset(); - g.clearRect(0,y,g.getWidth(),y+75); -} - -function draw(y, type, event) { - clear(y); - var px = g.getWidth()/2; - var str = event.bpm + ""; - g.reset(); - g.setFontAlign(0,0); - g.setFontVector(40).drawString(str,px,y+20); - str = "Event: " + type; - if (type === "HRM") { - str += " Confidence: " + event.confidence; - g.setFontVector(12).drawString(str,px,y+40); - str = " Source: " + (event.src ? event.src : "internal"); - g.setFontVector(12).drawString(str,px,y+50); +function getRow(id, text, additionalInfo){ + let additional = []; + let l = { + type:"h", c: [ + { + type:"v", + width: g.getWidth()*0.4, + c: [ + {type:"txt", halign:1, font:"8%", label:text, id:id+"text" }, + {type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg } + ] + },{ + type:undefined, fillx:1 + },{ + type:"v", + valign: -1, + width: g.getWidth()*0.45, + c: additional + },{ + type:undefined, width:g.getWidth()*0.05 + } + ] + }; + for (let i of additionalInfo){ + let label = {type:"txt", font:"6x8", label:i + ":" }; + let value = {type:"txt", font:"6x8", label:"--", id:id + i }; + additional.push({type:"h", halign:-1, c:[ label, {type:undefined, fillx:1}, value ]}); } - if (type === "BTHRM"){ - if (event.battery) str += " Bat: " + (event.battery ? event.battery : ""); - g.setFontVector(12).drawString(str,px,y+40); - str= ""; - if (event.location) str += "Loc: " + BODY_LOCS[event.location]; - if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(","); - g.setFontVector(12).drawString(str,px,y+50); - str= ""; - if (event.contact) str += " Contact: " + event.contact; - if (event.energy) str += " kJoule: " + event.energy.toFixed(0); - g.setFontVector(12).drawString(str,px,y+60); - } - + + return l; } -var firstEventBt = true; -var firstEventInt = true; +var layout = new Layout( { + type:"v", c: [ + getRow("int", "INT", ["Confidence"]), + getRow("agg", "HRM", ["Confidence", "Source"]), + getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]), + { type:undefined, height:8 } //dummy to protect debug output + ] +}, { + lazy:false +}); + +var int,agg,bt; +var firstEvent = true; + +function draw(){ + if (!(int || agg || bt)) return; + + if (firstEvent) { + g.clearRect(Bangle.appRect); + firstEvent = false; + } + + let now = Date.now(); + + if (int && int.time > (now - VALUE_TIMEOUT)){ + layout.int.label = int.bpm; + if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence; + } else { + layout.int.label = "--"; + layout.intConfidence.label = "--"; + } + + if (agg && agg.time > (now - VALUE_TIMEOUT)){ + layout.agg.label = agg.bpm; + if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence; + if (agg.src) layout.aggSource.label = agg.src; + } else { + layout.agg.label = "--"; + layout.aggConfidence.label = "--"; + layout.aggSource.label = "--"; + } + + if (bt && bt.time > (now - VALUE_TIMEOUT)) { + layout.bt.label = bt.bpm; + if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%"; + if (bt.rr) layout.btRR.label = bt.rr.join(","); + if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location]; + if (bt.contact !== undefined) layout.btContact.label = bt.contact ? /*LANG*/"Yes":/*LANG*/"No"; + if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; + } else { + layout.bt.label = "--"; + layout.btBattery.label = "--"; + layout.btRR.label = "--"; + layout.btLocation.label = "--"; + layout.btContact.label = "--"; + layout.btEnergy.label = "--"; + } + layout.clear(); + layout.render(); + let first = true; + for (let c of layout.l.c){ + if (first) { + first = false; + continue; + } + if (c.type && c.type == "h") + border(c,g.theme.fg); + } +} + + +// This can get called for the boot code to show what's happening +global.showStatusInfo = function(txt) { + var R = Bangle.appRect; + g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8"); + txt = g.wrapString(txt, R.w)[0]; + g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2); +}; function onBtHrm(e) { - if (firstEventBt){ - clear(24); - firstEventBt = false; - } - draw(100, "BTHRM", e); - if (e.bpm === 0){ - Bangle.buzz(100,0.2); - } - if (intervalBt){ - clearInterval(intervalBt); - } - intervalBt = setInterval(()=>{ - clear(100); - }, 2000); + bt = e; + bt.time = Date.now(); + draw(); } -function onHrm(e) { - if (firstEventInt){ - clear(24); - firstEventInt = false; - } - draw(24, "HRM", e); - if (intervalInt){ - clearInterval(intervalInt); - } - intervalInt = setInterval(()=>{ - clear(24); - }, 2000); +function onInt(e) { + int = e; + int.time = Date.now(); + draw(); } +function onAgg(e) { + agg = e; + agg.time = Date.now(); + draw(); +} var settings = require('Storage').readJSON("bthrm.json", true) || {}; Bangle.on('BTHRM', onBtHrm); -Bangle.on('HRM', onHrm); +Bangle.on('HRM_int', onInt); +Bangle.on('HRM', onAgg); + Bangle.setHRMPower(1,'bthrm'); if (!(settings.startWithHrm)){ @@ -95,10 +163,10 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); if (Bangle.setBTHRMPower){ g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24); + g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2); } else { g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32); + g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2); } E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm')); diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json index fb284bcd2..e1464a9d4 100644 --- a/apps/bthrm/default.json +++ b/apps/bthrm/default.json @@ -15,6 +15,7 @@ "custom_fallbackTimeout": 10, "gracePeriodNotification": 0, "gracePeriodConnect": 0, - "gracePeriodService": 0, - "gracePeriodRequest": 0 + "gracePeriodRequest": 0, + "bonding": false, + "active": false } diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js new file mode 100644 index 000000000..b81bd6118 --- /dev/null +++ b/apps/bthrm/lib.js @@ -0,0 +1,547 @@ +exports.enable = () => { + let settings = Object.assign( + require('Storage').readJSON("bthrm.default.json", true) || {}, + require('Storage').readJSON("bthrm.json", true) || {} + ); + + let log = function(text, param){ + if (global.showStatusInfo) + global.showStatusInfo(text); + if (settings.debuglog){ + let logline = new Date().toISOString() + " - " + text; + if (param) logline += ": " + JSON.stringify(param); + print(logline); + } + }; + + log("Settings: ", settings); + + //this is for compatibility with 0.18 and older + let oldCache = require('Storage').readJSON("bthrm.cache.json", true); + if(oldCache){ + settings.cache = oldCache; + require('Storage').writeJSON("bthrm.json", settings); + require('Storage').erase("bthrm.cache.json"); + } + + if (settings.enabled && settings.cache){ + + log("Start"); + + let addNotificationHandler = function(characteristic) { + log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); + characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); + }; + + + let characteristicsFromCache = function(device) { + let service = { device : device }; // fake a BluetoothRemoteGATTService + log("Read cached characteristics"); + let cache = settings.cache; + if (!cache.characteristics) return []; + let restored = []; + for (let c in cache.characteristics){ + let cached = cache.characteristics[c]; + let r = new BluetoothRemoteGATTCharacteristic(); + log("Restoring characteristic ", cached); + r.handle_value = cached.handle; + r.uuid = cached.uuid; + r.properties = {}; + r.properties.notify = cached.notify; + r.properties.read = cached.read; + r.service = service; + addNotificationHandler(r); + log("Restored characteristic: ", r); + restored.push(r); + } + return restored; + }; + + let supportedCharacteristics = { + "0x2a37": { + //Heart rate measurement + active: false, + handler: function (dv){ + let flags = dv.getUint8(0); + + let bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit + supportedCharacteristics["0x2a37"].active = bpm > 0; + log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active); + switchFallback(); + if (bpmTimeout) clearTimeout(bpmTimeout); + bpmTimeout = setTimeout(()=>{ + bpmTimeout = undefined; + supportedCharacteristics["0x2a37"].active = false; + startFallback(); + }, 3000); + + let sensorContact; + + if (flags & 2){ + sensorContact = !!(flags & 4); + } + + let idx = 2 + (flags&1); + + let energyExpended; + if (flags & 8){ + energyExpended = dv.getUint16(idx,1); + idx += 2; + } + let interval; + if (flags & 16) { + interval = []; + let maxIntervalBytes = (dv.byteLength - idx); + log("Found " + (maxIntervalBytes / 2) + " rr data fields"); + for(let i = 0 ; i < maxIntervalBytes / 2; i++){ + interval[i] = dv.getUint16(idx,1); // in milliseconds + idx += 2; + } + } + + let location; + if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){ + location = lastReceivedData["0x180d"]["0x2a38"]; + } + + let battery; + if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){ + battery = lastReceivedData["0x180f"]["0x2a19"]; + } + + if (settings.replace && bpm > 0){ + let repEvent = { + bpm: bpm, + confidence: (sensorContact || sensorContact === undefined)? 100 : 0, + src: "bthrm" + }; + + log("Emitting HRM_R(bt)", repEvent); + Bangle.emit("HRM_R", repEvent); + } + + let newEvent = { + bpm: bpm + }; + + if (location) newEvent.location = location; + if (interval) newEvent.rr = interval; + if (energyExpended) newEvent.energy = energyExpended; + if (battery) newEvent.battery = battery; + if (sensorContact) newEvent.contact = sensorContact; + + log("Emitting BTHRM", newEvent); + Bangle.emit("BTHRM", newEvent); + } + }, + "0x2a38": { + //Body sensor location + handler: function(dv){ + if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; + log("Got location", dv); + lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); + } + }, + "0x2a19": { + //Battery + handler: function (dv){ + if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; + log("Got battery", dv); + lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); + } + } + }; + + let lastReceivedData={ + }; + + let bpmTimeout; + + let device; + let gatt; + let characteristics = []; + let blockInit = false; + let currentRetryTimeout; + let initialRetryTime = 40; + let maxRetryTime = 60000; + let retryTime = initialRetryTime; + + let waitingPromise = function(timeout) { + return new Promise(function(resolve){ + log("Start waiting for " + timeout); + setTimeout(()=>{ + log("Done waiting for " + timeout); + resolve(); + }, timeout); + }); + }; + + if (settings.enabled){ + Bangle.isBTHRMActive = function (){ + return supportedCharacteristics["0x2a37"].active; + }; + + Bangle.isBTHRMOn = function(){ + return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0); + }; + + Bangle.isBTHRMConnected = function(){ + return gatt && gatt.connected; + }; + } + + if (settings.replace){ + Bangle.origIsHRMOn = Bangle.isHRMOn; + + Bangle.isHRMOn = function() { + if (settings.enabled && !settings.replace){ + return Bangle.origIsHRMOn(); + } else if (settings.enabled && settings.replace){ + return Bangle.isBTHRMOn(); + } + return Bangle.origIsHRMOn() || Bangle.isBTHRMOn(); + }; + } + + let clearRetryTimeout = function(resetTime) { + if (currentRetryTimeout){ + log("Clearing timeout " + currentRetryTimeout); + clearTimeout(currentRetryTimeout); + currentRetryTimeout = undefined; + } + if (resetTime) { + log("Resetting retry time"); + retryTime = initialRetryTime; + } + }; + + let retry = function() { + log("Retry"); + + if (!currentRetryTimeout && !powerdownRequested){ + + let clampedTime = retryTime < 100 ? 100 : retryTime; + + log("Set timeout for retry as " + clampedTime); + clearRetryTimeout(); + currentRetryTimeout = setTimeout(() => { + log("Retrying"); + currentRetryTimeout = undefined; + initBt(); + }, clampedTime); + + retryTime = Math.pow(clampedTime, 1.1); + if (retryTime > maxRetryTime){ + retryTime = maxRetryTime; + } + } else { + log("Already in retry..."); + } + }; + + let initialDisconnects = true; + let buzzing = false; + let onDisconnect = function(reason) { + log("Disconnect: " + reason); + log("GATT", gatt); + log("Characteristics", characteristics); + + let retryTimeResetNeeded = true; + retryTimeResetNeeded &= reason != "Connection Timeout"; + retryTimeResetNeeded &= reason != "No device found matching filters"; + clearRetryTimeout(retryTimeResetNeeded); + supportedCharacteristics["0x2a37"].active = false; + if (!powerdownRequested) startFallback(); + blockInit = false; + if (settings.warnDisconnect && !buzzing && !initialDisconnects){ + buzzing = true; + Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;}); + } + if (Bangle.isBTHRMOn()){ + retry(); + } + }; + + let createCharacteristicPromise = function(newCharacteristic) { + log("Create characteristic promise", newCharacteristic); + let result = Promise.resolve(); + // For values that can be read, go ahead and read them, even if we might be notified in the future + // Allows for getting initial state of infrequently updating characteristics, like battery + if (newCharacteristic.readValue){ + result = result.then(()=>{ + log("Reading data", newCharacteristic); + return newCharacteristic.readValue().then((data)=>{ + if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { + supportedCharacteristics[newCharacteristic.uuid].handler(data); + } + }); + }); + } + if (newCharacteristic.properties.notify){ + result = result.then(()=>{ + log("Starting notifications", newCharacteristic); + let startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); + + if (settings.gracePeriodNotification){ + log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); + startPromise = startPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodNotification); + }); + } + return startPromise; + }); + } + return result.then(()=>log("Handled characteristic", newCharacteristic)); + }; + + let attachCharacteristicPromise = function(promise, characteristic) { + return promise.then(()=>{ + log("Handling characteristic:", characteristic); + return createCharacteristicPromise(characteristic); + }); + }; + + let initBt = function () { + log("initBt with blockInit: " + blockInit); + if (blockInit && !powerdownRequested){ + retry(); + return; + } + + blockInit = true; + + let promise; + let filters; + + if (!device){ + if (settings.btid){ + log("Configured device id", settings.btid); + filters = [{ id: settings.btid }]; + } else { + return; + } + log("Requesting device with filters", filters); + try { + promise = NRF.requestDevice({ filters: filters, active: settings.active }); + } catch (e){ + log("Error during initial request:", e); + onDisconnect(e); + return; + } + + if (settings.gracePeriodRequest){ + log("Add " + settings.gracePeriodRequest + "ms grace period after request"); + promise = promise.then((d)=>{ + log("Wait after request"); + return waitingPromise(settings.gracePeriodRequest).then(()=>Promise.resolve(d)); + }); + } + + promise = promise.then((d)=>{ + log("Got device", d); + d.on('gattserverdisconnected', onDisconnect); + device = d; + }); + } else { + promise = Promise.resolve(); + log("Reuse device", device); + } + + promise = promise.then(()=>{ + gatt = device.gatt; + return Promise.resolve(gatt); + }); + + promise = promise.then((gatt)=>{ + if (!gatt.connected){ + log("Connecting..."); + let connectPromise = gatt.connect().then(function() { + log("Connected."); + }); + if (settings.gracePeriodConnect){ + log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); + connectPromise = connectPromise.then(()=>{ + log("Wait after connect"); + return waitingPromise(settings.gracePeriodConnect); + }); + } + return connectPromise; + } else { + return Promise.resolve(); + } + }); + + promise = promise.then(()=>{ + if (!characteristics || characteristics.length == 0){ + characteristics = characteristicsFromCache(device); + } + let characteristicsPromise = Promise.resolve(); + for (let characteristic of characteristics){ + characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); + } + + return characteristicsPromise; + }); + + return promise.then(()=>{ + log("Connection established, waiting for notifications"); + initialDisconnects = false; + clearRetryTimeout(true); + }).catch((e) => { + characteristics = []; + log("Error:", e); + onDisconnect(e); + }); + }; + + let powerdownRequested = false; + + Bangle.setBTHRMPower = function(isOn, app) { + // Do app power handling + if (!app) app="?"; + if (Bangle._PWR===undefined) Bangle._PWR={}; + if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[]; + if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app); + if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app); + isOn = Bangle._PWR.BTHRM.length; + // so now we know if we're really on + if (isOn) { + initialDisconnects = true; + powerdownRequested = false; + switchFallback(); + if (!Bangle.isBTHRMConnected()) initBt(); + } else { // not on + log("Power off for " + app); + powerdownRequested = true; + clearRetryTimeout(true); + stopFallback(); + if (gatt) { + if (gatt.connected){ + log("Disconnect with gatt", gatt); + try{ + gatt.disconnect().then(()=>{ + log("Successful disconnect"); + }).catch((e)=>{ + log("Error during disconnect promise", e); + }); + } catch (e){ + log("Error during disconnect attempt", e); + } + } + } + } + }; + + if (settings.replace){ + // register a listener for original HRM events and emit as HRM_int + Bangle.on("HRM", (o) => { + let e = Object.assign({},o); + log("Emitting HRM_int", e); + Bangle.emit("HRM_int", e); + if (fallbackActive){ + // if fallback to internal HRM is active, emit as HRM_R to which everyone listens + o.src = "int"; + log("Emitting HRM_R(int)", o); + Bangle.emit("HRM_R", o); + } + }); + + // force all apps wanting to listen to HRM to actually get events for HRM_R + Bangle.on = ( o => (name, cb) => { + o = o.bind(Bangle); + if (name == "HRM") o("HRM_R", cb); + else o(name, cb); + })(Bangle.on); + + Bangle.removeListener = ( o => (name, cb) => { + o = o.bind(Bangle); + if (name == "HRM") o("HRM_R", cb); + else o(name, cb); + })(Bangle.removeListener); + } else { + Bangle.on("HRM", (o)=>{ + o.src = "int"; + let e = Object.assign({},o); + log("Emitting HRM_int", e); + Bangle.emit("HRM_int", e); + }); + } + + Bangle.origSetHRMPower = Bangle.setHRMPower; + + if (settings.startWithHrm){ + Bangle.setHRMPower = function(isOn, app) { + log("setHRMPower for " + app + ": " + (isOn?"on":"off")); + if (settings.enabled){ + Bangle.setBTHRMPower(isOn, app); + if (Bangle._PWR && Bangle._PWR.HRM && Object.keys(Bangle._PWR.HRM).length == 0) { + Bangle._PWR.BTHRM = []; + Bangle.setBTHRMPower(0); + if (!isOn) stopFallback(); + } + return Bangle.isBTHRMOn() || Bangle.isHRMOn(); + } + if ((settings.enabled && !settings.replace) || !settings.enabled){ + return Bangle.origSetHRMPower(isOn, app); + } + }; + } + + let fallbackActive = false; + let inSwitch = false; + + let stopFallback = function(){ + if (fallbackActive){ + Bangle.origSetHRMPower(0, "bthrm_fallback"); + fallbackActive = false; + log("Fallback to HRM disabled"); + } + }; + + let startFallback = function(){ + if (!fallbackActive && settings.allowFallback) { + fallbackActive = true; + Bangle.origSetHRMPower(1, "bthrm_fallback"); + log("Fallback to HRM enabled"); + } + }; + + let switchFallback = function() { + log("Check falling back to HRM"); + if (!inSwitch){ + inSwitch = true; + if (Bangle.isBTHRMActive()){ + stopFallback(); + } else { + startFallback(); + } + } + inSwitch = false; + }; + + if (settings.replace){ + log("Replace HRM event"); + if (Bangle._PWR && Bangle._PWR.HRM){ + for (let i = 0; i < Bangle._PWR.HRM.length; i++){ + let app = Bangle._PWR.HRM[i]; + log("Moving app " + app); + Bangle.origSetHRMPower(0, app); + Bangle.setBTHRMPower(1, app); + if (Bangle._PWR.HRM===undefined) break; + } + } + } + + E.on("kill", ()=>{ + if (gatt && gatt.connected){ + log("Got killed, trying to disconnect"); + try { + gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect promise on kill", e)); + } catch (e) { + log("Error during disconnnect on kill", e) + } + } + }); + } +}; diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 39c1ff8bb..56e1beffd 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,9 +2,10 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.09", + "version": "0.19", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", + "screenshots": [{"url":"screen.png"}], "type": "app", "tags": "health,bluetooth,hrm,bthrm", "supports": ["BANGLEJS","BANGLEJS2"], @@ -15,6 +16,8 @@ {"name":"bthrm.0.boot.js","url":"boot.js"}, {"name":"bthrm.img","url":"app-icon.js","evaluate":true}, {"name":"bthrm.settings.js","url":"settings.js"}, + {"name":"bthrm","url":"lib.js"}, {"name":"bthrm.default.json","url":"default.json"} - ] + ], + "data": [{"name":"bthrm.json"}] } diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js index 21345a907..ec880d553 100644 --- a/apps/bthrm/recorder.js +++ b/apps/bthrm/recorder.js @@ -16,7 +16,7 @@ name : "BT HR", fields : ["BT Heartrate", "BT Battery", "Energy expended", "Contact", "RR"], getValues : () => { - result = [bpm,bat,energy,contact,rr]; + const result = [bpm,bat,energy,contact,rr]; bpm = ""; rr = ""; bat = ""; @@ -26,14 +26,48 @@ }, start : () => { Bangle.on('BTHRM', onHRM); - if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder"); + if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(1,"recorder"); }, stop : () => { Bangle.removeListener('BTHRM', onHRM); - if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder"); + if (Bangle.setBTHRMPower) Bangle.setBTHRMPower(0,"recorder"); }, - draw : (x,y) => g.setColor((bpm != "")?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) }; - } + }; + recorders.hrmint = function() { + var active = false; + var bpmTimeout; + var bpm = "", bpmConfidence = ""; + function onHRM(h) { + bpmConfidence = h.confidence; + bpm = h.bpm; + if (h.bpm > 0){ + active = true; + if (bpmTimeout) clearTimeout(bpmTimeout); + bpmTimeout = setTimeout(()=>{ + active = false; + },3000); + } + } + return { + name : "HR int", + fields : ["Int Heartrate", "Int Confidence"], + getValues : () => { + var r = [bpm,bpmConfidence]; + bpm = ""; bpmConfidence = ""; + return r; + }, + start : () => { + Bangle.on('HRM_int', onHRM); + if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder"); + }, + stop : () => { + Bangle.removeListener('HRM_int', onHRM); + if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder"); + }, + draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y) + }; + }; }) diff --git a/apps/bthrm/screen.png b/apps/bthrm/screen.png new file mode 100644 index 000000000..6b6b85227 Binary files /dev/null and b/apps/bthrm/screen.png differ diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js index b376d6a2d..310816dda 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -1,6 +1,6 @@ (function(back) { function writeSettings(key, value) { - var s = require('Storage').readJSON(FILE, true) || {}; + let s = require('Storage').readJSON(FILE, true) || {}; s[key] = value; require('Storage').writeJSON(FILE, s); readSettings(); @@ -13,12 +13,24 @@ ); } - var FILE="bthrm.json"; - var settings; + let FILE="bthrm.json"; + let settings; readSettings(); + let log = ()=>{}; + if (settings.debuglog) + log = print; + + function applyCustomSettings(){ + writeSettings("enabled",true); + writeSettings("replace",settings.custom_replace); + writeSettings("startWithHrm",settings.custom_startWithHrm); + writeSettings("allowFallback",settings.custom_allowFallback); + writeSettings("fallbackTimeout",settings.custom_fallbackTimeout); + } + function buildMainMenu(){ - var mainmenu = { + let mainmenu = { '': { 'title': 'Bluetooth HRM' }, '< Back': back, 'Mode': { @@ -35,7 +47,6 @@ case 1: writeSettings("enabled",true); writeSettings("replace",true); - writeSettings("debuglog",false); writeSettings("startWithHrm",true); writeSettings("allowFallback",true); writeSettings("fallbackTimeout",10); @@ -43,17 +54,11 @@ case 2: writeSettings("enabled",true); writeSettings("replace",false); - writeSettings("debuglog",false); writeSettings("startWithHrm",false); writeSettings("allowFallback",false); break; case 3: - writeSettings("enabled",true); - writeSettings("replace",settings.custom_replace); - writeSettings("debuglog",settings.custom_debuglog); - writeSettings("startWithHrm",settings.custom_startWithHrm); - writeSettings("allowFallback",settings.custom_allowFallback); - writeSettings("fallbackTimeout",settings.custom_fallbackTimeout); + applyCustomSettings(); break; } writeSettings("mode",v); @@ -62,12 +67,13 @@ }; if (settings.btname || settings.btid){ - var name = "Clear " + (settings.btname || settings.btid); + let name = "Clear " + (settings.btname || settings.btid); mainmenu[name] = function() { E.showPrompt("Clear current device?").then((r)=>{ if (r) { writeSettings("btname",undefined); writeSettings("btid",undefined); + writeSettings("cache", undefined); } E.showMenu(buildMainMenu()); }); @@ -80,31 +86,165 @@ return mainmenu; } - var submenu_debug = { + let submenu_debug = { '' : { title: "Debug"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Alert on disconnect': { value: !!settings.warnDisconnect, - format: v => settings.warnDisconnect ? "On" : "Off", onchange: v => { writeSettings("warnDisconnect",v); } }, 'Debug log': { value: !!settings.debuglog, - format: v => settings.debuglog ? "On" : "Off", onchange: v => { writeSettings("debuglog",v); } }, + 'Use bonding': { + value: !!settings.bonding, + onchange: v => { + writeSettings("bonding",v); + } + }, + 'Use active scanning': { + value: !!settings.active, + onchange: v => { + writeSettings("active",v); + } + }, 'Grace periods': function() { E.showMenu(submenu_grace); } }; + let supportedServices = [ + "0x180d", // Heart Rate + "0x180f", // Battery + ]; + + let supportedCharacteristics = [ + "0x2a37", // Heart Rate + "0x2a38", // Body sensor location + "0x2a19", // Battery + ]; + + var characteristicsToCache = function(characteristics) { + log("Cache characteristics"); + let cache = {}; + if (!cache.characteristics) cache.characteristics = {}; + for (var c of characteristics){ + //"handle_value":16,"handle_decl":15 + log("Saving handle " + c.handle_value + " for characteristic: ", c.uuid); + cache.characteristics[c.uuid] = { + "handle": c.handle_value, + "uuid": c.uuid, + "notify": c.properties.notify, + "read": c.properties.read + }; + } + writeSettings("cache", cache); + }; + + let createCharacteristicPromise = function(newCharacteristic) { + log("Create characteristic promise", newCharacteristic.uuid); + return Promise.resolve().then(()=>log("Handled characteristic", newCharacteristic.uuid)); + }; + + let attachCharacteristicPromise = function(promise, characteristic) { + return promise.then(()=>{ + log("Handling characteristic:", characteristic.uuid); + return createCharacteristicPromise(characteristic); + }); + }; + + let characteristics; + + let createCharacteristicsPromise = function(newCharacteristics) { + log("Create characteristics promise ", newCharacteristics.length); + let result = Promise.resolve(); + for (let c of newCharacteristics){ + if (!supportedCharacteristics.includes(c.uuid)) continue; + log("Supporting characteristic", c.uuid); + characteristics.push(c); + + result = attachCharacteristicPromise(result, c); + } + return result.then(()=>log("Handled characteristics")); + }; + + let createServicePromise = function(service) { + log("Create service promise", service.uuid); + let result = Promise.resolve(); + result = result.then(()=>{ + log("Handling service", service.uuid); + return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); + }); + return result.then(()=>log("Handled service", service.uuid)); + }; + + let attachServicePromise = function(promise, service) { + return promise.then(()=>createServicePromise(service)); + }; + + function cacheDevice(deviceId){ + let promise; + let filters; + let gatt; + characteristics = []; + filters = [{ id: deviceId }]; + + log("Requesting device with filters", filters); + promise = NRF.requestDevice({ filters: filters, active: settings.active }); + + promise = promise.then((d)=>{ + log("Got device", d); + gatt = d.gatt; + log("Connecting..."); + return gatt.connect().then(function() { + log("Connected."); + }); + }); + + if (settings.bonding){ + promise = promise.then(() => { + log(JSON.stringify(gatt.getSecurityStatus())); + if (gatt.getSecurityStatus().bonded) { + log("Already bonded"); + return Promise.resolve(); + } else { + log("Start bonding"); + return gatt.startBonding() + .then(() => log("Security status after bonding" + gatt.getSecurityStatus())); + } + }); + } + + promise = promise.then(()=>{ + log("Getting services"); + return gatt.getPrimaryServices(); + }); + + promise = promise.then((services)=>{ + log("Got services", services.length); + let result = Promise.resolve(); + for (let service of services){ + if (!(supportedServices.includes(service.uuid))) continue; + log("Supporting service", service.uuid); + result = attachServicePromise(result, service); + } + return result; + }); + + return promise.then(()=>{ + log("Connection established, saving cache"); + characteristicsToCache(characteristics); + }); + } + function createMenuFromScan(){ E.showMenu(); E.showMessage("Scanning for 4 seconds"); - var submenu_scan = { + let submenu_scan = { '< Back': function() { E.showMenu(buildMainMenu()); } }; NRF.findDevices(function(devices) { @@ -115,18 +255,41 @@ return; } else { devices.forEach((d) => { - print("Found device", d); - var shown = (d.name || d.id.substr(0, 17)); + log("Found device", d); + let shown = (d.name || d.id.substr(0, 17)); submenu_scan[shown] = function () { - E.showPrompt("Set " + shown + "?").then((r) => { + E.showPrompt("Connect to\n" + shown + "?", {title: "Pairing"}).then((r) => { if (r) { - writeSettings("btid", d.id); - // Store the name for displaying later. Will connect by ID - if (d.name) { - writeSettings("btname", d.name); - } + E.showMessage("Connecting...", {img:require("Storage").read("bthrm.img")}); + let count = 0; + const successHandler = ()=>{ + E.showPrompt("Success!", { + img:require("Storage").read("bthrm.img"), + buttons: { "OK":true } + }).then(()=>{ + writeSettings("btid", d.id); + // Store the name for displaying later. Will connect by ID + if (d.name) { + writeSettings("btname", d.name); + } + E.showMenu(buildMainMenu()); + }); + }; + const errorHandler = (e)=>{ + count++; + log("ERROR", e); + if (count <= 10){ + E.showMessage("Error during caching\nRetry " + count + "/10", e); + return cacheDevice(d.id).then(successHandler).catch(errorHandler); + } else { + E.showAlert("Error during caching", e).then(()=>{ + E.showMenu(buildMainMenu()); + }); + } + }; + + return cacheDevice(d.id).then(successHandler).catch(errorHandler); } - E.showMenu(buildMainMenu()); }); }; }); @@ -135,28 +298,28 @@ }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]}); } - var submenu_custom = { + let submenu_custom = { '' : { title: "Custom mode"}, '< Back': function() { E.showMenu(buildMainMenu()); }, 'Replace HRM': { value: !!settings.custom_replace, - format: v => settings.custom_replace ? "On" : "Off", onchange: v => { writeSettings("custom_replace",v); + if (settings.mode == 3) applyCustomSettings(); } }, 'Start w. HRM': { value: !!settings.custom_startWithHrm, - format: v => settings.custom_startWithHrm ? "On" : "Off", onchange: v => { writeSettings("custom_startWithHrm",v); + if (settings.mode == 3) applyCustomSettings(); } }, 'HRM Fallback': { value: !!settings.custom_allowFallback, - format: v => settings.custom_allowFallback ? "On" : "Off", onchange: v => { writeSettings("custom_allowFallback",v); + if (settings.mode == 3) applyCustomSettings(); } }, 'Fallback Timeout': { @@ -167,11 +330,12 @@ format: v=>v+"s", onchange: v => { writeSettings("custom_fallbackTimout",v*1000); + if (settings.mode == 3) applyCustomSettings(); } }, }; - var submenu_grace = { + let submenu_grace = { '' : { title: "Grace periods"}, '< Back': function() { E.showMenu(submenu_debug); }, 'Request': { @@ -203,18 +367,8 @@ onchange: v => { writeSettings("gracePeriodNotification",v); } - }, - 'Service': { - value: settings.gracePeriodService, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - onchange: v => { - writeSettings("gracePeriodService",v); - } } }; E.showMenu(buildMainMenu()); -}); +}) diff --git a/apps/bthrmlite/ChangeLog b/apps/bthrmlite/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/bthrmlite/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/bthrmlite/README.md b/apps/bthrmlite/README.md new file mode 100644 index 000000000..5bfdc9a83 --- /dev/null +++ b/apps/bthrmlite/README.md @@ -0,0 +1,44 @@ +# Bluetooth Heart Rate Monitor - Lite + +When this app is installed it overrides Bangle.js's built in heart rate monitor with an external Bluetooth one. + +The [`bthrm` app](https://banglejs.com/apps/?id=bthrm) is a much more sophisticated version of this app, however is uses a lot more +RAM so may not be suitable for Bangle.js 1. + +HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor. + +This means it's compatible with many Bangle.js apps including: + +* [Heart Rate Widget](https://banglejs.com/apps/#widhrt) +* [Heart Rate Recorder](https://banglejs.com/apps/#heart) + +It it NOT COMPATIBLE with [Heart Rate Monitor](https://banglejs.com/apps/#hrm) +as that requires live sensor data (rather than just BPM readings). + +## Usage + +Just install the app, then install an app that uses the heart rate monitor. + +Once an app requests Heart Rate `bthrmlite` will automatically try and connect to the first bluetooth +heart rate monitor it finds. + +**To disable this and return to normal HRM, uninstall the app** + +## Compatible Heart Rate Monitors + +This works with any heart rate monitor providing the standard Bluetooth +Heart Rate Service (`180D`) and characteristic (`2A37`). + +So far it has been tested on: + +* CooSpo Bluetooth Heart Rate Monitor + +## Internals + +This replaces `Bangle.setHRMPower` with its own implementation. + +To get debug info, you can call `Bangle.enableBTHRMLog()` in the IDE to enable log messages. + +## Creator + +Gordon Williams \ No newline at end of file diff --git a/apps/bthrmlite/app-icon.js b/apps/bthrmlite/app-icon.js new file mode 100644 index 000000000..04a5ee610 --- /dev/null +++ b/apps/bthrmlite/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///g3yy06AoIZNitUAg8AgtVqtQAgoRCAwITBAggABAoIABAgsAgIGDoIEDoApDAAwwBFIV1BYo1E+oLTAgQLGJon9BZNXBatdBYRVFBYN/r9fHoxTBBYYlEL4QLFq/a1WUgE///fr4xBv/+1Wq1EAh/3/tX6/fv/6BYOqwCzBBYf9tWq9QLF79X+oLBDIOgKgILEEIIxBGAMVNAP/BYf/BYUFBYJSB6wLC9QLBeAQLBqwLCGAL9BBYmr9X+GAILBbIIlBBYP6/wwBBYMFBYZGB/4XDGAILD34vEcwYLB15HBBYYkBBYWrFwILDKoRTCVIQLCEgQXIEgVaF44YCoRHHAAMUgQuBNgILFgECO4W/BZCPFBYinGBY6/CAArXFBY7vDAAsq1QuB0ALIOwOABY0KEgJGGGAguHDAYDBA==")) diff --git a/apps/bthrmlite/app.png b/apps/bthrmlite/app.png new file mode 100644 index 000000000..40c2ab024 Binary files /dev/null and b/apps/bthrmlite/app.png differ diff --git a/apps/bthrmlite/boot.js b/apps/bthrmlite/boot.js new file mode 100644 index 000000000..d10266789 --- /dev/null +++ b/apps/bthrmlite/boot.js @@ -0,0 +1,89 @@ +{ + let log = function() {};//print + let gatt; + + Bangle.enableBTHRMLog = function() { + log = print; + }; + + Bangle.setHRMPower = function(isOn, app) { + // Do app power handling + if (!app) app="?"; + log("setHRMPower ->", isOn, app); + if (Bangle._PWR===undefined) Bangle._PWR={}; + if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[]; + if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app); + if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app); + isOn = Bangle._PWR.HRM.length; + // so now we know if we're really on + if (isOn) { + log("setHRMPower on", app); + if (!gatt) { + log("HRM not already on"); + NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) { + log("Found device "+device.id); + device.on('gattserverdisconnected', function(reason) { + gatt = undefined; + }); + return device.gatt.connect(); + }).then(function(g) { + log("Connected"); + gatt = g; + return gatt.getPrimaryService(0x180D); + }).then(function(service) { + return service.getCharacteristic(0x2A37); + }).then(function(characteristic) { + log("Got characteristic"); + characteristic.on('characteristicvaluechanged', function(event) { + var dv = event.target.value; + var flags = dv.getUint8(0); + // 0 = 8 or 16 bit + // 1,2 = sensor contact + // 3 = energy expended shown + // 4 = RR interval + var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit + /* var idx = 2 + (flags&1); // index of next field + if (flags&8) idx += 2; // energy expended + if (flags&16) { + var interval = dv.getUint16(idx,1); // in milliseconds + }*/ + Bangle.emit('HRM',{ + bpm:bpm, + confidence:100 + }); + }); + return characteristic.startNotifications(); + }).then(function() { + log("Ready"); + console.log("Done!"); + }).catch(function(err) { + console.log("BTHRM Error",err); + gatt = undefined; + // retry connection if we got a connect timeout + if (err == "Connection Timeout") + setTimeout(Bangle.setHRMPower, 1000, isOn, app); + }); + } + } else { // not on + log("setHRMPower off", app); + if (gatt) { + log("HRM connected - disconnecting"); + try {gatt.disconnect();}catch(e) { + log("HRM disconnect error", e); + } + gatt = undefined; + } + } + }; +// disconnect when swapping apps +E.on("kill", function() { + if (gatt) { + log("HRM connected - disconnecting"); + try {gatt.disconnect();}catch(e) { + log("HRM disconnect error", e); + } + gatt = undefined; + } +}); + +} \ No newline at end of file diff --git a/apps/bthrmlite/metadata.json b/apps/bthrmlite/metadata.json new file mode 100644 index 000000000..e4b95cca8 --- /dev/null +++ b/apps/bthrmlite/metadata.json @@ -0,0 +1,15 @@ +{ "id": "bthrmlite", + "name": "Bluetooth Heart Rate Monitor (Lite)", + "shortName":"BT HRM", + "version":"0.01", + "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one. Cut-down version that uses less RAM.", + "icon": "app.png", + "tags": "health,bluetooth", + "type": "bootloader", + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"bthrmlite.boot.js","url":"boot.js"}, + {"name":"bthrmlite.img","url":"app-icon.js","evaluate":true} + ] +} \ No newline at end of file diff --git a/apps/bthrv/ChangeLog b/apps/bthrv/ChangeLog index eefadac78..e4dc0f14d 100644 --- a/apps/bthrv/ChangeLog +++ b/apps/bthrv/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Write available data on reset or kill 0.03: Buzz short on every finished measurement and longer if all are done +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/bthrv/app.js b/apps/bthrv/app.js index fbd0e2d05..8378ba025 100644 --- a/apps/bthrv/app.js +++ b/apps/bthrv/app.js @@ -1,4 +1,4 @@ -var btm = g.getHeight()-1; +//var btm = g.getHeight()-1; var ui = false; function clear(y){ @@ -10,7 +10,7 @@ var startingTime; var currentSlot = 0; var hrvSlots = [10,20,30,60,120,300]; var hrvValues = {}; -var rrRmsProgress; +//var rrRmsProgress; var rrNumberOfValues = 0; var rrSquared = 0; @@ -120,7 +120,7 @@ function resetHrv(){ } -var settings = require('Storage').readJSON("bthrm.json", true) || {}; +//var settings = require('Storage').readJSON("bthrm.json", true) || {}; g.clear(); Bangle.loadWidgets(); diff --git a/apps/bthrv/metadata.json b/apps/bthrv/metadata.json index 7c57be682..e20e6bfb8 100644 --- a/apps/bthrv/metadata.json +++ b/apps/bthrv/metadata.json @@ -2,7 +2,7 @@ "id": "bthrv", "name": "Bluetooth Heart Rate variance calculator", "shortName": "BT HRV", - "version": "0.03", + "version": "0.05", "description": "Calculates HRV from a a BT HRM with interval data", "icon": "app.png", "type": "app", diff --git a/apps/bthrv/recorder.js b/apps/bthrv/recorder.js index 0fce6971e..f1e1d5040 100644 --- a/apps/bthrv/recorder.js +++ b/apps/bthrv/recorder.js @@ -31,7 +31,7 @@ } } } - result = [hrv]; + const result = [hrv]; hrv = ""; rrHistory = []; return result; diff --git a/apps/bttfclock/ChangeLog b/apps/bttfclock/ChangeLog new file mode 100644 index 000000000..2d7ac2abd --- /dev/null +++ b/apps/bttfclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Back to the future clock first version. +0.02: Added more icons to the status field. Made it posible to custemize the step goal thrue the Health Tracking app. +0.03: Added charging screen. \ No newline at end of file diff --git a/apps/bttfclock/README.md b/apps/bttfclock/README.md new file mode 100644 index 000000000..5417328c0 --- /dev/null +++ b/apps/bttfclock/README.md @@ -0,0 +1,40 @@ +# Back to the future Clock + +![](screenshot.png) + +A watchface inspierd by this garmin watchface.
+ +## Todo + +- Improving quality of Fonts. +- More status icons. + +## Functionalities + +- Current time +- Current day and month +- Battery +- Steps +- Step goal can be set white the Health Tracking app defult is 10000 +- Bluetooth connected icon +- Alarm icon +- Notification icon + +## Screenshots +Clock:
+![](screenshot.png)
+Clock when charging:
+![](screenshotCharging.png)
+ +## Usage +Install it and enjoy + + +## Links +### code ispired by +advCasioBangleClock https://github.com/dotgreg/advCasioBangleClock + +93dub https://github.com/espruino/BangleApps/tree/master/apps/93dub + +### Creator +https://github.com/NoobEjby diff --git a/apps/bttfclock/app-icon.js b/apps/bttfclock/app-icon.js new file mode 100644 index 000000000..f2d2f8aed --- /dev/null +++ b/apps/bttfclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwyEAhN0AMF1AIl2AKAXFH8Jbny4BKu5j/LZWWJ4W4AIXYhPZAJAPDzBnIMc3GjABFLZBZB25XDgP5gUaAIU7AJUbgP6AIRnB3JlEMYw/KLbAjIGIJbBWIJZDrcCvkCzkC3oBMzsCvsCrkCnhnDZomXH6BfU9HkAIIfGLoP4gU6LYRZB7sC/sDj0DnwBCrwBEBIYPBj0C7xnCMoZjD3I/QLqwhJLYpJCKIOege/gffAIX/AIwJBB4IBBv8Dn5lEZYNcH6ZfPDo4hHLYl+I4X/gkfgk/glfAJgPBAIMfM4WfgdfMYW+H6ZffW4W+LI1/gmfAKATBAIIZBMYLNBMYM/L8bfPToJbYMpZjD74/TL5ZhLBIv9vJdCLbpjJn4tBH58JupfTAJP+oJhCLsYBCFIItBH55fOBoIBBywbHF4YBDMMotJLZGYhN3L6GWhO3gUagWcgcdgeegkfGYvvmxhhEIIlBRY0fgefgcegV+gUcgO6L4V2MJhfD7ECrcC3sDr0D/8Er43FAIZhdEplfHIMDr8C70CnsB7bBCL5YJBAIO3hP5gV7gX9XocEv47JTIhdYcYaDIGoLBCgX+gV9gP8hO5YIRfLNoO4gUagWcb4MD78En4/LfYxdWDp0/HoMDn8Cz0CjkB3RfOy0J7ECncC3sDrzjBc4JDPUJBdMbYZ7Or49BgdfgW+gU9gPbhOYWYRfNrcC7pfPI4oBDMJoVVL6DBHA4OXhPZL4l+gkfgl/VKKrDJZLVDLqQBBv5fE70CrpfS/JfXJ4oBDJ4oNNL9C/FrwhBL6JTHL5ZdSAINfL4m+gU9L4WZL6E7gW9L4lfHKRVC982AIIFBA44jTL/ZhFL4pdWL4ffgc/gWeL4mYL5gLB3ECjcCzsDnwhBgk/HqzDID61/HIMD38Dj5fCjsB3cJvBfPgP7gV9gX+gYnBj4pCL+sfHoMC/8Cv0B/pfSy8B7UCrkC70DcYP/c4JfX/1BAIJfYHIQ9BgW/gVegP8gOaL5wBBy8J3MCjcCzsDjzBZL7i9EgcfgWegUdgPchOZhN3KohfFYIu4gP7gVdYInfgk/MKZfaFoM/GoMDn69E/sB3a9ML4+XgPagUcgV9gX+MK5ZB982AIJfSFIIxB/41BgX/gV+XocBzS9CL54BBCYOYgPbgU9cIQrBv5hTL6y7DGIRdCz8Cny9KL5bBFMIO5gP8MIyRB38Ej6XBMZZfSXIQlBRYK7ELoNeLomZXqDBHCoLBB3RhISoOfS4LFCMZBZB/1BAIJfILYa5DEoJdGgUdgPcgOaXqhhHPIJhB3ZhEv0CSoKZBYoJjDY4JlDr5fIBYS3ELYO/EIUfFIJdMIYJdTMIp5BDoN4MIkdGIQ1BMY4HBMoRfHK4QBBCIK3CLYm/RYU+gP9LoW6Loi9DLqZhL3QtBMIU+HIQ9BIIJNBn5LCr/GjABFBYQPBn4VBW4l/EoS5BLoO8LsRhLzTFCMYrHBMoOfLY4BHLIlfDoUeXIgrBzJdkMI5jDzDFCMYQ/BMoZVF9HkAIJfGLIf+LY2aLZBdhMIrFGhOZY4pdJMJMB7sB3gbC3UJvJduMI5jHvD7BLpZhHW4hbLLtJjPu5fTLIhb3MZxPDMJINFLIRb9MZhhJLoxZFLf4BIKooBJJ/5jbI/4B/AP4B/AP4BLA")) \ No newline at end of file diff --git a/apps/bttfclock/app.js b/apps/bttfclock/app.js new file mode 100644 index 000000000..9689cfb56 --- /dev/null +++ b/apps/bttfclock/app.js @@ -0,0 +1,308 @@ +require("Font8x16").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); +require("Font5x7Numeric7Seg").add(Graphics); +require("Font4x5").add(Graphics); + +const timeTextY = 4; +const timeDataY = timeTextY+19; +const DateTextY = 48; +const DateDataY = DateTextY+19; +const stepGoalBatTextY = 100; +const stepGoalBatdataY = stepGoalBatTextY+19; +const statusTextY = 140; +const statusDataY = statusTextY+19; +let stepGoal = (require("Storage").readJSON("health.json",1)||10000).stepGoal; +let steps = 0; +let alarmStatus = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on); +let charching = Bangle.isCharging(); +let chargeAniFrame= 0; + +const chargeAni =[require("heatshrink").decompress(atob("2GwgcEiFBAX4CbhEgwQC/ATeAUP4CegSh/ATyh/AT0AUP4CeUP4CeoCh/AT0BUP4CeUP4CewCh/AT0CUP4CeUP4CegCh/ATyh/AT1AUP4CegKh/ATyh/AT2AUP4CJgUBgAXSoBxNgEgaLUAAAMCCh8kKB4kCgKDXgAuBwAdLiRBCAoJEOgEEyFAAYJBVoECpMkyUIIJWChJEBAQJEBI4ZlIoIjBpJBQXI0AiRBCDoK/HQYQ+EyFIkiMBQxBlDpLuBQasADQIdDYRA4BIJAICKwsIMokkgB0GAR0EIIgdBBwh0BHApBIdIwjFwCDVDQZBDBYdIHBBBJwUBIJGQgBBTgBBMHxwIDdJJBWiJBGwDjCQCBBHdI2SEYT+JAQ+AII4pCHZwIGIJOQIKmBIIwdBF4KARBAhBBEY2SYryDXpBoBYo5BdBYI+UBAbFHyTFUwBBIHywIDII2QQYkAIP4CRDoxZBYrOCoBlGIKqDGgA+YBARBGyECX5oCGQYsggDFbgESdIyDUL4sAgECHy4IBiAcBU4pBWL4dIgHYgCDZwE2gCnFYSQCDgEJDoMAg3bAwKDYgO24EBEYNAgCATAQkALgJBCsEBIK8ghpBBEILKBEAJBYAAMSgHbtBIBQCgIBgFt2w/CAAI+WAQpBBjVtUgKDWgdp2EABAI4ThEgwQCGFgMIgdNEwZBToEbpuAMQIsJASiqDtKqCYqkA7VscAZBdEAcN23AgQ+RBAOAg3bsEBIMApDgO27EAYqeAm3bgAgEfxICSF4cAFISDTLJAyLgSDOF4qtBoEBIKMAboZBQQakSgHTtEAYqMAts2gEQBYr+ROIJBFGQ1AjVtwALHpJEHhEDtOwgALFQbZiFoEDposBPQpBCQY1AjdNKxA7URgiwJ2ywFkGShIFBbQ3btjaIYqICIeo0Ahu24ECYQmSoMkIgK5Eg3bsDsBaI47RRIxBHgkB23YgALBQARBDpIICgmAm3bgEIUIrFWAQggGIgMAm3agALBQYI+CAQeCCIJTFcw4yJgSJMMQ6zDzdAgJ6BII7IChuma4IdHYqYCGMQ4IBgHTtkAHw4CCoMAts2gEQDpFIfZgCLMQwmCoEatuAghBJpEDtOwgAdJHCBBTgdtGQJBJoEbKATjIBAQvDoDFRUhAICWwO2gESIJEA7bUBcZTFGIL0N23AgRBHyEG7dggKhKBAIvDCILFQMpckgO26EAII+Am3bgDCKIIwCFI5hBMwE07Q1BII0B2nYgAaKBAR9QIKcGzdAgJBFkEN0zRBIOOSgHTtkAIIsAts2gESpLFWARhlLkGSoEatuAghBDpEDtuggAIBIOI4C2A4CIINAjZKEUJi/QgBBDYpZxBXge2XgTOC7bOEILoCBoBBRkEN22Ag1Jk2Qg3bsEBIIShLBAJxCgJEQMRQIBGQcB03QgHJ0uAmnagAOEIJ8IILlBGQeAm2bgEPI4O07EAZYRBSkBBgX4ObsBBBhum4ECIIjmLIIoCPMRYFBGQWSgHTtkHkFt2kAiRB4oEbIIewgALDIMYgKyAyEyVIgdtw/jtuAghB5pMAhuAgdggALFIOvkgAACh5B78+eIANz55BGDpIIBiFBggCTQaX//Ef+fJIPVPk+fIPyDB/1AgZB9nnz43DzwLGkg7FEAuChACTIKN5k+Sg3AjxBpUI4jDAoKDFz027BB9knwIIMCIPuQIJTgGEAovCgMEiFBARpfHEYhBIzBBXwA+OIK80QbA+PIKESIJ47HEAg7PagQCCMRdBQaJBXgA7DIKeCIJ4dLGQ4CPYpcgQY026BB3ghBHQY7CKBATFKARbpMYoxBtU5iDPL5jFkyBBODRQICF4WAYr8SoJBFzBBFDRjFMgCDXwQFBHAc0QYyALYrKDNhJBMLhi/SAQwmNO4JBJC5aDHgJBSExpBKTZiDpkGSZAOQm3QIIYXNyBBtQYcSTZsSYQQCXFJ1BkkQIITOCIK8BQbwICwBBEC57FZiAsRIISAOBATFaQaDFBzECCiKDsmiDBINiwQwBBCYqkBQf4CGfxACIFiJBXHaICGQaE26BBQHa5BXQaJBdVpgICwBBCYRoICiFAIPqAcAQYvNiDFRoKDBATqDPzCDRwBBsmjFPQDwCDF5pBCYpr+YARKDngRB2QcZxNm3QYpr+aARKDMmyDcgJBxQcsQYphBBYpb+cARRBewCGhWxU2zBBLYUgCEQZM0QZgaBgDImQZJBLHcoCEGQKDTYVICDQaY+pgSGCAoI1Gm3QII7CpAQ5BGQY7CsZBWAIITCuARZ6DQYw+zQwsQgEAIIUSdjJEfkBABAAI+ZAUcBkmSgEBgkCIPMApJBBpEAFkjFVoESIIVJkDpdATlAHwJBCkkAIPMAINbdUghBFwECfzICeIP8gwA+DAQWQIMraTII8AfzQCdIP5B/IP5B/AQ8EIIuAFklAkGChACQII8CDSICmgBBFgBB/IIIsjwDaViRBDkD+cAT0AIIVIgBB8gJBBAYIphgTdZgAACfzgChIAJfaAX4CDcEIC8oCh/AT0BUP4CeUP4CewBxPA=")), +require("heatshrink").decompress(atob("2GwgcEiFBAX4CbhEgwQC/ATeAUP4CegSh/ATyh/AT0AUP4CeUP4CeoCh/AT0BUP4CeUP4CewCh/AT0CUP4CeUP4CegCh/ATyh/AT1AUP4CegKh/ATyh/AT2AUP4CJgUBgAXSoBxNgEgaLUAAAMCCh8kKB4kCgKDXgAuBwAdLiRBCAoJEOgEEyFAAYJBVoECpMkyUIIJWChJEBAQJEBI4ZlIoIjBpJBQXI0AiRBCDoK/HQYQ+EyFIkiMBQxBlDpLuBQasADQIdDYRA4BIJAICKwsIMokkgB0GAR0EIIgdBBwh0BHApBIdIwjFwCDVDQZBDBYdIHBBBJwUBIJGQgBBTgBBMHxwIDdJJBWiJBGwDjCQCBBHdI2SEYT+JAQ+AII4pCHZwIGIJOQIKmBIIwdBF4KARBAhBBEY2SYryDXpBoBYo5BdBYI+UBAbFHyTFUwBBIHywIDII2QQYkAIP4CRDoxZBYrOCoBlGIKqDGgA+YBARBGyECX5oCGQYsggDFbgESdIyDUL4sAgECHy4IBiAcBU4pBWL4dIgHYgCDZwE2gCnFYSQCDgEJDoMAg3bAwKDYgO24EBEYNAgCATAQkALgJBCsEBIK8ghpBBEILKBEAJBYAAMSgHbthIBQCgIBgFt2w/CAAI+WAQpBBjdtUgKDWgdt2EABAI4ThEgwQCGFgMIgdN0AmCIKdAjdNwBiBFhICUVQdp2iqBYqkA7TgEILogDhs24ECHyIIBwEG6dggJBgFIcB03YgDFTwE27UAEAj+JASQvDgE27cAQacB2xZGGRcCQZwvFg3bVoJBRgEN2zdBIKCDUiUA7dsgDFRgFt20AiALFfyJxBIIoyGoEbpuABY9JIg8IgdN2EABYqDbMQtAFgOggB6FIISDGKxY7URgiwItKwGkGShIFBbQ3abRLFRARD1GgENm3AgTCEyVBkhEBXIkG7VgdgLRHHaKJGII8EgO27EABYKACIIdJBAUEwE27cAhChFYqwCEEAxEBgAvCBYKDBHwQCDwQRBKYrmHGRMCRJhiHWYfbWYJ6BII7IChu2a4IdHYqYCGMQ4IBgHatkAHw4CCoMAtO2gEQDpFIfZgCLMQwmCoEatsAghBJpEDtOwgAdJHCBBTgdNGQJBJoEbpuAgjjIBAQvDoDFRUhAICWwcSIJEA7dMgDjKYoxBehu24ECII+Qg3bsEBUJQIBF4YRBYqBlLkkB23YgBBHwE27cAYRRBGAQpHMIJg1EIIxNDDRQICPqBBTg3TXIJBFkENmzRBIOOSgHTtEAIIsAts2gESpLFWARhlLkGSoEatuAghBDpEDtOwgAIBIOI4Bpo4DIINAjdpJQahMX6EAIIbFLOIK8D2y8CZwXbtjODILoCBoBBRkEN23AgVJk+Qg3bsEBIIShLBAJxCgJEQMRQIBGQcB23YgFJ8mAm3bgAOEIJ8IILlBGQY7B6cAh5HB2hHCIKkgIMC/BzdAjtkhs2wDLBBwbmLIIoCPMRYFBGQWSgHatkHkFtm0AiRB4oEaIIcwgALDIMYgKyAyEyVIgdtw/jtuAghB5pMAhuAgdggALFIOvkgAACh5B78+eIANz55BGDpIIBiFBggCTQaX//Ef+fJIPVPk+fIPyDB/1AgZB9nnz43DzwLGkg7FEAuChACTIKN5k+Sg3AjxBpUI4jDAoKDFz027BB9knwm2YgRB9yBBKcAwgFF4UBgkQoICNL44jEIMOAHxxBXmnYIK4+PIKESIJ47HEAg7PagQCCMRdBYpHQIL8AHYZBTwRBGQZAdLGQ4CPYpcgQY5B4ghBPYRQICYpQCLdJjFPIMinMQY+YIIxfMYsmQIIs0QY4aKBAQvCwDFfiVBIJgaMYpkAQa+CAoJBLQBbFZQZsJIJhcMX6QCGExp3BIIU26BBEC5aDHgJBSExpBFQYibMQdMgyTIBIIwXNyBBxiSbNiTCCAS4pOoMkiBBCZwRBXgKDeBAWAm2YIIQXPYrMQFiM0QYKAOBATFaQaEQIIQURQd5BsWCGAYq8BQdE26CDbfxACIFiM2Yqw7RAQyDRIKI7XIP4CIVpYICwBBCYRoICiFAINmYIJ6AcAQYvNiE0YqFBQYICdQZxBCQaGAIPqAeAQbFefzACJQZs26CDXgRBnYpyDjOJpBCYpj+aARKDqgJBxQcsQYpmYYpj+cARRBKmiDBIKGAQ0K2KIJrCkAQiDXDQMAZEyDVHcoCEGQKDIm3QIJDCpAQaDImyDJH1MCQwQFBQY5BIYVICHIJzCsZBWAIITCuARZ6Dm2YQYg+zQwsQgEAIIUSdjJEfkBABAAI+ZAUcBkmSgEBgkCIPMApJBBpEAFkjFVoESIIVJkDpdATlAHwJBCkkAIPMAINbdUghBFwECfzICeIP8gwA+DAQWQIMraTII8AfzQCdIP5B/IP5B/AQ8EIIuAFklAkGChACQII8CDSICmgBBFgBB/IIIsjwDaViRBDkD+cAT0AIIVIgBB8gJBBAYIphgTdZgAACfzgChIAJfaAX4CDcEIC8oCh/AT0BUP4CeUP4CewBxP")), +require("heatshrink").decompress(atob("2GwgcEiFBAX4CbhEgwQC/ATeAUP4CegSh/ATyh/AT0AUP4CeUP4CeoCh/AT0BUP4CeUP4CewCh/AT0CUP4CeUP4CegCh/ATyh/AT1AUP4CegKh/ATyh/AT2AUP4CJgUBgAXSoBxNgEgaLUAAAMCCh8kKB4kCgKDXgAuBwAdLiRBCAoJEOgEEyFAAYJBVoECpMkyUIIJWChJEBAQJEBI4ZlIoIjBpJBQXI0AiRBCDoK/HQYQ+EyFIkiMBQxBlDpLuBQasADQIdDYRA4BIJAICKwsIMokkgB0GAR0EIIgdBBwh0BHApBIdIwjFwCDVDQZBDBYdIHBBBJwUBIJGQgBBTgBBMHxwIDdJJBWiJBGwDjCQCBBHdI2SEYT+JAQ+AII4pCHZwIGIJOQIKmBIIwdBF4KARBAhBBEY2SYryDXpBoBYo5BdBYI+UBAbFHyTFUwBBIHywIDII2QQYkAIP4CRDoxZBYrOCoBlGIKqDGgA+YBARBGyECX5oCGQYsggDFbgESdIyDUL4sAgECHy4IBiAcBU4pBWL4dIgHQgCDZwE2gCnFYSQCDgEJDoMAg2bAwKDYgO04EBEYNAgCATAQkALgJBCsEBIK8ghpBBEILKBEAJBYAAMSgHbthIBQCgIBgFt2w/CAAI+WAQpBBjdtwCDXgdt2EABAI4ThEgwQCGFgMIEwpBToBcDEAIsIASiqE0yqBYqkA6bgEILogDhs2wECHyIIBwEG6dggJBgFIcB03YgDFTwE2zcAEAj+JASQvDgE07cAQacB22YLIoyLgSDOF4sG7atBIKMAhu24ECIKCDUiUA7dsgDFRgFt20AiALFfyJxBIIoyGoEbtuABY9JIg8Igdt2EABYqDbMQtAFgMwgB6FIISDGoEaKxI7URgiwItO0WAsgyUJAoLaG7TaJYqICIeo0Ahs24ECYQmSoMkIgK5Eg3TsDsBaI47RRIxBHgkB03YgALBQARBDpIICgmAm3TgEIUIrFWAQggGIgMAm3bgALBQYI+CAQeCCIMB2xTDcw4yJgSJMMQ6zD7azBPQJBHZAUN2zXBDo7FTAQxiHBAMA7dsgA+HAQVBgFt20AiAdIpD7MARZiGEwVAjdpwEEIJNIgdN2EADpI4QIKYyB0EAIJJQBppQBcZAICF4dAYqKkIBAUAtK2BiRBIgHaagLjKYoxBehs24ECII+Qg3TsEBUJQIBF4YRBYqBlLkkB23YgBBHwE27cAYRRBGAQpHMIJg1EIIxNDDRQICPqBBTg3bXIJBFkEN2zRBIOOSgHbtEAIIsAtu2gESpLFWARhlLkGSoEatsAghBDpEDtOwgAIBIOI4Bpo4DIINAjdNwBKCUJi/QgBBDYpZxBXgVpXgbOC7VsZwZBdAQNAIKMghu24ECpMnyEG7dggJBCUJYIBOIUBIiBiKBAIyDgO27EApPkwE27cABwhBPhBBcoIyDHYcPI4pBUkBBgX4VAjtkhs2ZYQODcxZBFAR5iLAoIyCyUA6dog9gtO0gESIPFAjdMg8gtswgALDIMYgKyAyEyVIgdpw/jpuAghB5pMAhuAgdggALFIOvkgAACh5B78+eIANz55BGDpIIBiFBggCTQaX//Ef+fJIPVPk+fIPyDB/1AgZB9nnz43DzwLGkg7FEAuChACTIKN5k+Sg3AjxBpUI4jDAoKDFz007BB9knwIIMCIPuQm2YIJDgGEAovCgMEiFBARpfHEYhBhwA+OILHQIK4+PIKESII3YII47HEAg7PagQCCMRdBQZBBggA7DIKeCIJ4dLGQ4CPYpcgQaBBvghBHzBBGYRQICYpQCLdJjFGmiDHIMinMQYxBIL5jFkyBBODRQICF4WAYr8SoJBMDRjFMgCDXwQFBHAc26BBFQBbFZQZsJIIqDGLhi/SAQwmNO4JBJC5aDHgJBSExpBKTZiDpkGSZAJBGC5uQINuYIIUSTZsSYQQCXFJ1BkkQmiDBZwRBXgKDeBAWAIIgXPYrMQFiJBCQBwICYrSDQYoYURQdk26BBtWCGAmzFWgKDpYrj+IARAsRIK47RAQyDjHa5BXzBBuVpgICwE0QYLCNBAUQoBB9QDgCDF5sQIITFOoKDBATqDiwBBsm3QIJyAeAQYvNmzFPfzACJQZzFPNZECIOyDjYrr+aARKDNzCDbgJBimjFNQcsQYpZBCYpb+cARRBewCGhWxRBNYUgCEQZM26BBNgDImQZM2QZQ7lAQgyBQZJBJYVICDQaY+pgSGCAoKDQYVICHII2YIIzCsZBWAmiDBYVwCLPQZBCQYY+zQwsQgEAIIUSdjJEfkBABAAI+ZAUcBkmSgEBgkCIPMApJBBpEAFkjFVoESIIVJkDpdATlAHwJBCkkAIPMAINbdUghBFwECfzICeIP8gwA+DAQWQIMraTII8AfzQCdIP5B/IP5B/AQ8EIIuAFklAkGChACQII8CDSICmgBBFgBB/IIIsjwDaViRBDkD+cAT0AIIVIgBB8gJBBAYIphgTdZgAACfzgChIAJfaAX4CDcEIC8oCh/AT0BUP4CeUP4CewBxPA==")), +require("heatshrink").decompress(atob("2GwgcEiFBAX4CbhEgwQC/ATeAUP4CegSh/ATyh/AT0AUP4CeUP4CeoCh/AT0BUP4CeUP4CewCh/AT0CUP4CeUP4CegCh/ATyh/AT1AUP4CegKh/ATyh/AT2AUP4CJgUBgAXSoBxNgEgaLUAAAMCCh8kKB4kCgKDXgAuBwAdLiRBCAoJEOgEEyFAAYJBVoECpMkyUIIJWChJEBAQJEBI4ZlIoIjBpJBQXI0AiRBCDoK/HQYQ+EyFIkiMBQxBlDpLuBQasADQIdDYRA4BIJAICKwsIMokkgB0GAR0EIIgdBBwh0BHApBIdIwjFwCDVDQZBDBYdIHBBBJwUBIJGQgBBTgBBMHxwIDdJJBWiJBGwDjCQCBBHdI2SEYT+JAQ+AII4pCHZwIGIJOQIKmBIIwdBF4KARBAhBBEY2SYryDXpBoBYo5BdBYI+UBAbFHyTFUwBBIHywIDII2QQYkAIP4CRDoxZBYrOCoBlGIKqDGgA+YBARBGyECX5oCGQYsggDFbgESdIyDUL4sAgECHy4IBiAcBU4pBWL4dIgHQgCDZwE2gCnFYSQCDgEJDoMAg2bAwKDYgOm4EBEYNAgCATAQkALgJBCoEBIK8ghpBBEILKBEAJBYAAMSgHTthIBQCgIBgFtYoI/BAAI+WAQpBBjVtwCDXgdt0EABAI4ThEgwQCGFgMIEwOwEwRBToEbLgQgBFhACUVQm2VQLFUgHbcAhBdEAcN22AgQ+RBAOAg3bsEBIMApDgOm6EAYqeAm2bgAgEfxICSF4cAmnagCDTgO07BZFGRcCQZwvFg2bVoJBRgEN03AgRBQQakSgHTtkAYqMAtu0gEQBYr+ROIJBFGQ1AjdtwALHpJEHhEDtuwgALFQbZiFoAsDPQpBCQYxWLHaiMEWCEgyUJAoLaG7baJYqICIeo0Ahs2wECYQmSoMkIgK5Eg3TsDsBaI47RRIxBHgkB03QgALBQARBDpIICgmAm2bgEIUIrFWAQggGIgMAmnbgALBQYI+CAQeCCIMB2nYKYTmHGRMCRJhiHWYebWYJ6BII7IChu2a4IdHYqYCGMQ4IBgHbtkAHw4CCoMAtu2gEQDpFIfZgCLMQwmCoEbtuAghBJpEDtuwgAdJHCBBTGQOggBBJKAjjIBAQvDoDFRUhAICgFp2kAiRBIgHaagLjKYoxBehs2wECII+Qg3TsEBUJQIBF4YRBYqBlLkkB03YgBBHwE2zcAYRRBGAQpHMIJmAmnbGoJBGgO26EADRQICPqBBTg3bXIJBFkEN23AgRBxyUA7dsgBBFgFt20AiVJYqwCMMpcgyVAjdtgEEIIdIgdt2EABAJBxHANN0A4CIIJKBpuAJQShMX6EAIIbFLOIK8CtO0XgPJZwXaZwhBdAQNAIKMghs24ECpMnyEG6dggJBCUJYIBOIUBIiBiKBAIyDgOm7EApPkwE26cABwhBPhBBcoIyDHYPbgEPI4O2I4RBUkBBgX4PbsBBBhu2ZYQODcxZBFAR5iLAoIyCyUA7dsg8gtu2gESIPFAjdMIIWggALDIMYgKyAyEyVYgdNw/jpuAghB5pMAhuAgdAgFkIPWsgAACh4LFIOvnzxABufPIIwdJBAMQoMEASZBR8n//Ef+fJIPVPk+fIPyDB/1AgZB9nnz42D3VZIIw7FEAuChACTIKNpk+Wg2AnSDHIMKhHEYYFBGok1zU2zEeR4xB1knwmnYgRB9yE2IJLgGEAovCgMEiFBARpfHEYhBhwA+OIOI+PIKESIJ47HEAg7PagQCCMRdBQaJBXgA7DIKeCII2YII4dLGQ4CPYpcgQY00QZBBvghBPYRQICYpQCLdJjFGINqnMQZ5fMYsmQIIs26BBGDRQICF4WAYr8SoJBFQYwaMYpkAQa+CAoJBLQBbFZQZsJIJhcMX6QCGExp3BIJIXLQY8BIKQmNIIuYIIabMQdMgyTIByE0QYgXNyBBxiSbNiTCCAS4pOoMkiBBCZwRBXgKDeBAWAIIgXPYrMQFiM26ECQBwICYrSDQiE2QYIURQdpBuWCGAIITFUgKD/AQz+IARAsRm2YIKo7RAQyDQmiDRHa5B/ARCtLBAWAIITCNBAUQoBB9QDgCDF5sQm3QYp9BQYICdQZ02QaWAIPqAeAQbFOIILFNfzACJQc8CIM+YIJqDjOJs0Ypz+aARKDMIISDagJBxQcsQYrT+cARRBKm3QIKOAQ0K2KmyDMYUgCEQZRBOgDImQao7lAQgyBQabCpAQaDJzBBygSGCAoI1GmiDIYVICHIJzCsZBWAIITCuARZ6DQYw+zQwsQgEAIIUSdjJEfkBABAAI+ZAUcBkmSgEBgkCIPMApJBBpEAFkjFVoESIIVJkDpdATlAHwJBCkkAIPMAINbdUghBFwECfzICeIP8gwA+DAQWQIMraTII8AfzQCdIP5B/IP5B/AQ8EIIuAFklAkGChACQII8CDSICmgBBFgBB/IIIsjwDaViRBDkD+cAT0AIIVIgBB8gJBBAYIphgTdZgAACfzgChIAJfaAX4CDcEIC8oCh/AT0BUP4CeUP4CewBxPA==")), +require("heatshrink").decompress(atob("2GwgcEiFBAX4CbhEgwQC/ATeAUP4CegSh/ATyh/AT0AUP4CeUP4CeoCh/AT0BUP4CeUP4CewCh/AT0CUP4CeUP4CegCh/ATyh/AT1AUP4CegKh/ATyh/AT2AUP4CJgUBgAXSoBxNgEgaLUAAAMCCh8kKB4kCgKDXgAuBwAdLiRBCAoJEOgEEyFAAYJBVoECpMkyUIIJWChJEBAQJEBI4ZlIoIjBpJBQXI0AiRBCDoK/HQYQ+EyFIkiMBQxBlDpLuBQasADQIdDYRA4BIJAICKwsIMokkgB0GAR0EIIgdBBwh0BHApBIdIwjFwCDVDQZBDBYdIHBBBJwUBIJGQgBBTgBBMHxwIDdJJBWiJBGwDjCQCBBHdI2SEYT+JAQ+AII4pCHZwIGIJOQIKmBIIwdBF4KARBAhBBEY2SYryDXpBoBYo5BdBYI+UBAbFHyTFUwBBIHywIDII2QQYkAIP4CRDoxZBYrOCoBlGIKqDGgA+YBARBGyECX5oCGQYsggDFbgESdIyDUL4sAgECHy4IBiAcBU4pBWL4dIgHYgCDZwE2gCnFYSQCDgEJDoMAg3bAwKDYgO24EBEYNAgCATAQkALgJBB6dggJBXkENm3AEILKBEAJBYAAMSgHTtBIBQCgIBgFtYoI/BAAI+WAQpBBjVtwCDXgdp2EABAI4ThEgwQCGFgMIgdNEwZBToEbtJcBEAIsIASiqE2yqBYqkA7dscAZBdEAcN23AgQ+RBAOAg3bsEBIMApDgO27EAYqeAm3bgAgEfxICSF4cAm2bgCDTgOmLIwyLgSDOF4sGzdAgJBRgEN0zdBIKCDUiUA6dsgDFRgFtm0AiALFfyJxBIIoyGoEatuABY9JIg8IgdtmEABYqDbMQtAFgOwgB6FIISDGoEbKxI7URgiwJ2ywFkGShIFBbQ3bbRLFRARD1GgEN23AgTCEyVBkhEBXIkG7dgdgLRHHaKJGII8EgO2zEABYKACIIdJBAUEwE07cAhChFYqwCEEAxEBgE07UABYKDBHwQCDwQRBgO07BTCcw4yJgSJMMQ6zDzazBPQJBHZAUN0zXBDo7FTAQxiHBAMA6dsgA+HAQVBgFt00AiAdIpD7MARZiGEwVAjdtwEEIJNIgdt2EADpI4QIKYyDIJJQEcZAICF4dAYqKkIBAS2B20AiRBIgHbagLjKYoxBehu04ECII+Qg2bsEBUJQIBF4YRBYqBlLkkB03QgBBHwE2zcAYRRBGAQpHMIJmAmnbGoJBGgO07EADRQICPqBBTXIZBFkDRDIOOSgHbtkAIIsAtu2gESpLFWARhlLkGSoEbtuAghBDpEDtuwgAIBIOI4GIIJKGUJi/QgBBDYpZxBXgemXgTOC7TOEILoCBoBBRkENm2Ag1Jk2Qg3TsEBIIShLBAJxCgJEQMRQIBGQcB03YgFJ8mAm2bgAOEIJ8IILlBGQeAmnbgEPI4O2zBHBIKkgIMC/B7dgIIMN23AgRBEcxZBFAR5iLAoIyCyUA7dsg8gtu2gESIPFAjZBD2EABYZBjEBWQGQmSpEDtsH8dNwEEIPNJgENwEDoEAshB61kAAAUPBYpB18+eIANz55BGDpIIBiFBggCTIKPk//4j/z5JB6p8nz5B+QYP+oEDIPs8+fGwe6rJBGHYogFwUIASZBRtMny0GwE6QY5BhUI4jDAoI1Emuam3YjyPGIOsk+BBBgRB9yBBKcAwgFF4UBgkQoICNL44jEIMOAHxxBxHx5BQiRBGzBBHHY4gEHZ7UCAQRiLoKDHmiDIIK8AHYZBTwRBPDpYyHAR7FLkCDQIN8EIJ7CKBATFKARbpMYo026BBrU5iDGmyDHL5jFkyBBODRQICF4WAYr8SoJBMDRjFMgCDXwQFBIJaALYrKDNhJBFzBBFLhi/SAQwmNO4JBCmiDFC5aDHgJBSExpBKTZiDpkGSZAJBGC5uQIOMSTZsSYQQCXFJ1BkkQm3QgTOCIK8BQbwICwE2QYI7LYr8QFiJBCQBwICYrSDQYoJBBCiKDvINiwQwE2zDFVgKDomiDcfxACIFiJBXHaICGQcY7XIP4CIVpYICwE26ECYRoICiFAINiDBIJyAcAQYvNiBBCYp1BQYICdQcWAIPqAeAQbFOzDFOfzACJQZs0QbECIOyDjOJpBCYpj+aARKDqgJBim3QIJiDliDFLmzFNfzgCKIL2AQ0K2KIJrCkAQiDXDQMAZEyDKzBBJHcoCEGQKDImiDJYVICDQZBB0gSGCAoKDQYVICHIJzCsZBWAm3QgTCuARZ6DmyDFH2aGFiEAgBBCiTsZIj8gIAIABHzICjgMkyUAgMEgRB5gFJIINIgAskYqtAiRBCpMgdLoCcoA+BIIUkgBB5gBBrbqkEIIuAgT+ZATxB/kGAHwYCCyBBlbSZBHgD+aATpB/IP5B/IP4CHghBFwAskoEgwUIASBBHgQaRAU0AIIsAIP5BBFkeAbSsSIIcgfzgCegBBCpEAIPkBIIIDBFMMCbrMAAAT+cAUJABL7QC/AQbghAXlAUP4CegKh/ATyh/AT2AOJ4=")) + ]; + +const bluetoothOnIcon = require("heatshrink").decompress(atob("iEQwYROg3AAokYAgUMg0DAoUBwwFDgE2CIYdHAogREDoopFGoodGABI=")); + +const bluetoothOffIcon = require("heatshrink").decompress(atob("iEQwYLIgwFF4ADBgYFBjAKCsEGBAIABhgFEgOA7AdDmApKmwpCC4OGFIYjFGoVgIIkMEZAAD")); + +const alarmIcon = require("heatshrink").decompress(atob("iEQyBC/AA3/8ABBB7INHA4YLLDqIHVApJRJCZodNCJ4dPHqqPJGp4RLOaozZT8btLF64hJFJpFbAEYA=")); + +const notificationIcon = require("heatshrink").decompress(atob("iEQyBC/AB3/8ABBD+4bHEa4VJD6YTNEKIf/D/rTDAJ7jTADo5hK+IA==")); + + +//the following 2 sections are used from waveclk to schedule minutely updates +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw(time) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + //draw; + //console.log(drawTimeout); + }, time - (Date.now() % time)); + +} + +function getSteps() { + steps = Bangle.getHealthStatus("day").steps; +} + +function drawBackground() { + //g.setBgColor(1,1,1); + g.setBgColor('#555555'); + g.setColor(1,1,1); + g.clear(); + //g.drawImage(imgBg,0,0); + g.reset(); +} +function drawBlackBox() { + g.reset(); + g.setBgColor(1,0,0); + g.setColor(0,0,0); + + //Hour, Min and Sec + g.fillRect(50, timeDataY,50+33,timeDataY+22); + g.fillRect(90, timeDataY,90+33, timeDataY+22); + g.fillRect(128, timeDataY+8,130+24, timeDataY+8+14); + //Day, Month, Day and Year + g.fillRect(9, DateDataY,9+24, DateDataY+15); + g.fillRect(42, DateDataY,42+40, DateDataY+15); + g.fillRect(91, DateDataY,91+24, DateDataY+15); + g.fillRect(124, DateDataY,124+43, DateDataY+15); + //Present day + g.fillRect(60, 86,60+47, 86+7); + //Middle line + g.drawLine(0,95,176,95); + //Step and bat + g.fillRect(3, stepGoalBatdataY-1, 62, stepGoalBatdataY+15); + g.fillRect(121, stepGoalBatdataY-1, 150, stepGoalBatdataY+15); + + //Status + g.fillRect(62, statusDataY-1, 62+49, statusDataY+15); +} + + +function draw(){ + let time = 60000; + if(charching){ + drawCharging(); + time = 500; + }else{ + drawWatchface(); + } + //console.log(charching); + queueDraw(time); +} + +function drawGoal() { + var goal = stepGoal <= steps; + g.reset(); + g.setColor(0,0,0); + + g.fillRect(84, stepGoalBatdataY-1, 92, stepGoalBatdataY+15); + + if (goal){ + g.reset(); + g.setColor(0,1,0); + g.fillRect(84, stepGoalBatdataY, 92, stepGoalBatdataY+7); + } else { + g.reset(); + g.setColor(1,0,0); + g.fillRect(84, stepGoalBatdataY+7, 92, stepGoalBatdataY+14); + } +} +function drawRedkBox() { + g.reset(); + g.setBgColor(1,0,0); + g.setColor(1,0,0); + //Hour, Min and Sec + g.fillRect(50, timeTextY,50+33,timeTextY+15); + g.fillRect(90, timeTextY,90+33, timeTextY+15); + g.fillRect(128, timeTextY+8,130+24, timeTextY+8+15); + //Day, Month, Day and Year + g.fillRect(9, DateTextY,9+24, DateTextY+15); + g.fillRect(42, DateTextY,42+40, DateTextY+15); + g.fillRect(91, DateTextY,91+24, DateTextY+15); + g.fillRect(124, DateTextY,124+43, DateTextY+15); + //Step, Goal and Bat + g.fillRect(2, stepGoalBatTextY,2+61, stepGoalBatTextY+15); + g.fillRect(70, stepGoalBatTextY,72+33, stepGoalBatTextY+15); + g.fillRect(120, stepGoalBatTextY,120+31, stepGoalBatTextY+15); + //Status + g.fillRect(62, statusTextY,62+49, statusTextY+15); +} + +function drawCharging(){ + g.drawImage(chargeAni[chargeAniFrame], 0, 0); + chargeAniFrame+=1; + if(chargeAniFrame>=chargeAni.length){ + chargeAniFrame=0; + } + + + var date = new Date(); + var h = date.getHours(), m = date.getMinutes(); + + if (h<10) { + h = ("0"+h).substr(-2); + } + if (m<10) { + m = ("0"+m).substr(-2); + } + + //time + g.reset(); + g.setBgColor(0,0,0); + g.setColor(1,0,0); + g.setFont("7x11Numeric7Seg",3); + g.drawString(h, 40, 105); + g.drawString(m, 95, 105); + + + var bat = E.getBattery(); + var batl = bat.toString().length-1; + var batDrawX = 80-(11*batl); + //80 69 58 + g.drawString(bat, batDrawX, 20); + //g.drawLine(77,18,94,18); + +} + +function drawWatchface(){ + drawBackground(); + getSteps(); + drawBlackBox(); + drawRedkBox(); + drawGoal(); + var date = new Date(); + var h = date.getHours(), m = date.getMinutes(), s = date.getSeconds(); + var d = date.getDate(), y = date.getFullYear();//, w = date.getDay(); + + if (h<10) { + h = ("0"+h).substr(-2); + } + if (m<10) { + m = ("0"+m).substr(-2); + } + if (s<10) { + s = ("0"+s).substr(-2); + } + if (d<10) { + d = ("0"+d).substr(-2); + } + + g.reset(); + g.setBgColor(1,0,0); + g.setColor(1,1,1); + //Draw text + g.setFont("8x16"); + g.drawString('HOUR', 51, timeTextY+1); + g.drawString('MIN', 96, timeTextY+1); + g.drawString('SEC', 130, timeTextY+9); + + g.drawString('DAY', 10, DateTextY+1); + g.drawString('MONTH', 43, DateTextY+1); + g.drawString('DAY', 92, DateTextY+1); + g.drawString(' YEAR ', 125, DateTextY+1); + + g.drawString('STEPS', 15, stepGoalBatTextY+1); + g.drawString('GOAL', 72, stepGoalBatTextY+1); + g.drawString(' BAT ', 120, stepGoalBatTextY+1); + g.drawString('STATUS', 64, statusTextY+1); + + //time + g.reset(); + g.setBgColor(0,0,0); + g.setColor(1,0,0); + g.setFont("5x7Numeric7Seg",2); + g.drawString(s, 131, timeDataY+8); + g.setFont("7x11Numeric7Seg",2); + g.drawString(h, 53, timeDataY); + g.drawString(m, 93, timeDataY); + //Date + g.reset(); + g.setBgColor(0,0,0); + g.setColor(0,1,0); + g.setFont("5x7Numeric7Seg",2); + g.drawString(d, 13, DateDataY); + g.drawString(y, 127, DateDataY); + g.setFont("8x16"); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 52, DateDataY); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 92, DateDataY); + + + //status + g.reset(); + g.setBgColor(0,0,0); + g.setColor(1,1,0); + g.setFont("5x7Numeric7Seg",2); + var step = steps; + var stepl = steps.toString().length; + var stepdDrawX = 4+(36-(stepl*6))+(4*(6-stepl)); + g.drawString(step, stepdDrawX, stepGoalBatdataY); + var bat = E.getBattery(); + var batl = bat.toString().length; + var batDrawX = 122+(18-(batl*6))+(4*(3-batl)); + g.drawString(bat, batDrawX, stepGoalBatdataY); + + //status + var b = bluetoothOffIcon; + if (NRF.getSecurityStatus().connected){ + b = bluetoothOnIcon; + } + g.drawImage(b, 62, statusDataY-1); + if (alarmStatus){ + g.drawImage(alarmIcon, 78, statusDataY-1); + } + if ((require('Storage').readJSON('messages.json',1)||[]).some(messag=>messag.new==true)){ + g.drawImage(notificationIcon, 94, statusDataY-1); + } + + g.reset(); + g.setBgColor(0,0,0); + g.setColor(1,1,1); + g.setFont("4x5"); + g.drawString('Present day', 62, 88); + +} + +/** + * This watch is mostly dark, it does not make sense to respect the + * light theme as you end up with a white strip at the top for the + * widgets and black watch. So set the colours to the dark theme. + * + */ +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); +//draw(); +//the following section is from waveclk +Bangle.on('lcdPower',function(on) { + if (on) { + draw(); // draw immediately, queue redraw + console.log(drawTimeout); + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.on('charging', function(charging) { + Bangle.setLCDTimeout(!charging); + Bangle.setLCDPower(1); + charching = charging; + draw(); +}); + + +Bangle.setUI("clock"); +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe +g.clear(1); +//queueDraw(); +draw(); \ No newline at end of file diff --git a/apps/bttfclock/app.png b/apps/bttfclock/app.png new file mode 100644 index 000000000..e33ad3e84 Binary files /dev/null and b/apps/bttfclock/app.png differ diff --git a/apps/bttfclock/metadata.json b/apps/bttfclock/metadata.json new file mode 100644 index 000000000..eb66c30ef --- /dev/null +++ b/apps/bttfclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "bttfclock", + "name": "Back To The Future", + "version": "0.03", + "description": "The watch of Marty McFly", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"bttfclock.app.js","url":"app.js"}, + {"name":"bttfclock.img","url":"app-icon.js","evaluate":true} + ] +} \ No newline at end of file diff --git a/apps/bttfclock/screenshot.png b/apps/bttfclock/screenshot.png new file mode 100644 index 000000000..c756bffca Binary files /dev/null and b/apps/bttfclock/screenshot.png differ diff --git a/apps/bttfclock/screenshotCharging.png b/apps/bttfclock/screenshotCharging.png new file mode 100644 index 000000000..852ddbf2d Binary files /dev/null and b/apps/bttfclock/screenshotCharging.png differ diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json deleted file mode 100644 index aaae0a0cb..000000000 --- a/apps/buffgym/.eslintrc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": true, - "es6": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - "indent": [ - "error", - 2, - { "SwitchCase": 1 } - ], - "linebreak-style": [ - "error", - "windows" - ], - "quotes": [ - "error", - "double" - ] - /*, - "semi": [ - "error", - "always" - ]*/ - } -} \ No newline at end of file diff --git a/apps/burn/.gitignore b/apps/burn/.gitignore new file mode 100644 index 000000000..656b79624 --- /dev/null +++ b/apps/burn/.gitignore @@ -0,0 +1 @@ +.prettierignore \ No newline at end of file diff --git a/apps/burn/ChangeLog b/apps/burn/ChangeLog new file mode 100644 index 000000000..66c4f98bd --- /dev/null +++ b/apps/burn/ChangeLog @@ -0,0 +1,7 @@ +0.01: New App! +0.02: Added README.md +0.03: Icon update +0.04: Icon Fix +0.05: Misc cleanup for PR +0.06: Implementing fixes from PR comments +0.07: Bug fix diff --git a/apps/burn/README.md b/apps/burn/README.md new file mode 100644 index 000000000..44f1e260f --- /dev/null +++ b/apps/burn/README.md @@ -0,0 +1,30 @@ +# Burn: Calorie Counter + +Burn is a simple calorie counter application. It is based on the original Counter app and has been enhanced with additional features (I recommend using +it with the "Digital Clock Widget", if you intend to keep it running). + +## Features + +- **Persistent counter**: The counter value is saved to flash, so it persists even when the app is closed or the device is restarted. +- **Daily reset**: The counter resets each day, allowing you to track your calorie intake on a daily basis. +- **Adjustable increment value**: You can adjust the increment value to suit your needs. + +## Controls + +### Bangle.js 1 + +- **BTN1**: Increase (or tap right) +- **BTN3**: Decrease (or tap left) +- **Press BTN2**: Change increment + +### Bangle.js 2 + +- **Swipe up**: Increase +- **Swipe down**: Decrease +- **Press BTN**: Change increment + +## How it Works + +The counter value and the date are stored in a file named "kcal.txt". The counter value is read from the file when the app starts and written to the file whenever the counter value is updated. + +The app uses the current date to determine whether to reset the counter. If the date has changed since the last time the counter was updated, the counter is reset to 0. diff --git a/apps/burn/app-icon.js b/apps/burn/app-icon.js new file mode 100644 index 000000000..8cd3b7ca1 --- /dev/null +++ b/apps/burn/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/gUA///j32o8h9v6glA+P9+/3tu27YCLvICBgEGCJlFmwRBgALFxQRIgIdF28JmIIEknG7cMyVJk4nBC4PJk4dBC4OJkmSrYRCkuACQM26/88wRGgQHB2iGCm33//8GoXtonbraYGgwRB/+bNY4AEg9/CIPbth3BxYRJn4RB/YRBgEUTwIRGne275IBCIQABjYRGrpCB+VK1gJDgYQFgXN23YQwIjEAA0WMwPQ0mSqgRK2QRBy6cBCJUFGIO12wjBpgRMlsAqmSqTOCAA0sCINogEIyVKCJdLoEAhQRNpQFCyVII5IRGqARQNZUECIcGyRLBPpPSCIQWBsCzK3VJoEB0mTCBUAitplEA0WYCJb7B1EBCYIAsjJjCknDpMkyUAmlKNwuEyEAgMSwwQBpNhAQM4CAcDkgRBe4ODogOB4MT0MldgcxCIXEyWAi3axNgykECIcBxIRBEwIRBYoK3BykGkw1DxPEyEZksSCIMEpcDjAIBGocbhMQiMzCIUyqALB5KmFhMghMk0VLQANYPoLeBCI0SRgOKJYOAgOSpihFCIMaCIOTgMk4ACBqVICIyTBKIMZkkAhpyBo4RHgOk4EZPAIjByVFNYYIBAoU2AoOwAQO5kmMf7QALpMg2VJ23Mn////8OIVThmUCIs27FvCIP+eQOSpUIyXAgFNEYfQCIX8P4OSp0MCIVZgEWFoNgrMktuwgdt23YOIQ")) \ No newline at end of file diff --git a/apps/burn/app-icon.png b/apps/burn/app-icon.png new file mode 100644 index 000000000..23d4a13e6 Binary files /dev/null and b/apps/burn/app-icon.png differ diff --git a/apps/burn/app-screenshot.png b/apps/burn/app-screenshot.png new file mode 100644 index 000000000..fef0e701e Binary files /dev/null and b/apps/burn/app-screenshot.png differ diff --git a/apps/burn/app.js b/apps/burn/app.js new file mode 100644 index 000000000..348a19153 --- /dev/null +++ b/apps/burn/app.js @@ -0,0 +1,243 @@ +/* + * Burn: Calories Counter for Bangle.js (Espruino). Based on the original Counter app. + * Features: + * - Persistent counter: saved to a file. + * - Daily reset: counter resets each day. + * - Adjustable increment value. + * + * Bangle.js 1 Controls: + * - BTN1: Increase (or tap right) + * - BTN3: Decrease (or tap left) + * - Press BTN2: Change increment + * + * Bangle.js 2 Controls: + * - Swipe up: Increase + * - Swipe down: Decrease + * - Press BTN: Change increment + */ + +// File variable to handle file operations +let file; + +// Check if the hardware version is Bangle.js 2 +const BANGLEJS2 = process.env.HWVERSION == 2; + +// Importing the Storage module for file operations +const Storage = require("Storage"); + +// File path for the counter data +const PATH = "kcal.txt"; + +// Function to get the current date as a string +function dayString() { + const date = new Date(); + // Month is 0-indexed, so we add 1 to get the correct month number + return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; +} + +// Counter object to keep track of the count and the date +let counter = { count: 0, date: dayString() }; + +// Function to read the counter from the file +function readCounterFromFile() { + try { + // Open the file in read mode + file = Storage.open(PATH, "r"); + let line = file.readLine(); + + // If the file has content, parse it and update the counter + if (line) { + let splitLine = line.trim().split(","); + counter = { count: parseInt(splitLine[0]), date: splitLine[1] }; + } + } catch (err) { + // If the file does not exist, the counter will remain 0 + } +} + +// Function to write the counter to the file +function writeCounterToFile() { + // Open the file in write mode + file = Storage.open(PATH, "w"); + // Write the counter and date to the file + file.write(counter.count.toString() + "," + counter.date + "\n"); +} + +// Function to reset the counter +function resetCounter() { + // Reset the counter to 0 and update the date + counter = { count: 0, date: dayString() }; +} + +// Function to update the counter value +function updateCounterValue(value) { + // Update the counter with the new value, ensuring it's not less than 0 + counter = { count: Math.max(0, value), date: dayString() }; +} + +// Function to update the counter +function updateCounter(value) { + // If the date has changed, reset the counter + if (counter.date != dayString()) { + resetCounter(); + } else { + // Otherwise, update the counter value + updateCounterValue(value); + } + + // Write the updated counter to the file + writeCounterToFile(); + // Update the screen with the new counter value + updateScreen(); +} + +// Function to set a watch on a button to update the counter when pressed +function counterButtonWatch(button, increment) { + setWatch( + () => { + // If the button is for incrementing, or the counter is greater than 0, update the counter + if (increment || counter.count > 0) { + updateCounter( + counter.count + (increment ? getInterval() : -getInterval()) + ); + // Update the screen with the new counter value + updateScreen(); + } + }, + button, + { repeat: true } + ); +} + +// Function to create interval functions +const createIntervalFunctions = function () { + // Array of intervals + const intervals = [50, 100, 200, 10]; + // Current location in the intervals array + let location = 0; + + // Function to get the current interval + const getInterval = function () { + return intervals[location]; + }; + + // Function to rotate the increment + const rotateIncrement = function () { + // Update the location to the next index in the intervals array, wrapping around if necessary + location = (location + 1) % intervals.length; + // Update the screen with the new increment + updateScreen(); + }; + + // Return the getInterval and rotateIncrement functions + return { getInterval, rotateIncrement }; +}; + +// Create the interval functions +const intervalFunctions = createIntervalFunctions(); +const getInterval = intervalFunctions.getInterval; +const rotateIncrement = intervalFunctions.rotateIncrement; + +// Function to update the screen +function updateScreen() { + // Clear the screen area for the counter + g.clearRect(0, 50, 250, BANGLEJS2 ? 130 : 150) + .setBgColor(g.theme.bg) + .setColor(g.theme.fg) + .setFont("Vector", 40) + .setFontAlign(0, 0) + // Draw the counter value + .drawString(Math.floor(counter.count), g.getWidth() / 2, 100) + .setFont("6x8") + // Clear the screen area for the increment + .clearRect(g.getWidth() / 2 - 50, 140, g.getWidth() / 2 + 50, 160) + // Draw the increment value + .drawString("Increment: " + getInterval(), g.getWidth() / 2, 150); + + // If the hardware version is Bangle.js 1, draw the increment and decrement buttons + if (!BANGLEJS2) { + g.drawString("-", 45, 100).drawString("+", 185, 100); + } +} + +// If the hardware version is Bangle.js 2, set up the drag handling and button watch + +let drag; + +if (BANGLEJS2) { + // Set up drag handling + Bangle.on("drag", (e) => { + // If this is the start of a drag, record the initial coordinates + if (!drag) { + drag = { x: e.x, y: e.y }; + return; + } + + // If the button is still being pressed, ignore this event + if (e.b) return; + + // Calculate the change in x and y from the start of the drag + const dx = e.x - drag.x; + const dy = e.y - drag.y; + // Reset the drag start coordinates + drag = null; + + // Determine if the drag is primarily horizontal or vertical + const isHorizontalDrag = Math.abs(dx) > Math.abs(dy) + 10; + const isVerticalDrag = Math.abs(dy) > Math.abs(dx) + 10; + + // If the drag is primarily horizontal, ignore it + if (isHorizontalDrag) { + return; + } + + // If the drag is primarily vertical, update the counter + if (isVerticalDrag) { + // If the drag is downwards and the counter is greater than 0, decrease the counter + if (dy > 0 && counter.count > 0) { + updateCounter(counter.count - getInterval()); + } else if (dy < 0) { + // If the drag is upwards, increase the counter + updateCounter(counter.count + getInterval()); + } + // Update the screen with the new counter value + updateScreen(); + } + }); + + // Set a watch on the button to rotate the increment when pressed + setWatch(rotateIncrement, BTN1, { repeat: true }); +} else { + // If the hardware version is Bangle.js 1, set up the button watches + + // Set watch on button to increase the counter + counterButtonWatch(BTN1, true); + counterButtonWatch(BTN5, true); // screen tap + // Set watch on button to decrease the counter + counterButtonWatch(BTN3, false); + counterButtonWatch(BTN4, false); // screen tap + + // Set a watch on button to rotate the increment when pressed + setWatch( + () => { + rotateIncrement(); + }, + BTN2, + { repeat: true } + ); +} + +// clear the screen +g.clear(); + +// Set the background and foreground colors +g.setBgColor(g.theme.bg).setColor(g.theme.fg); + +// Load and draw the widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Read the counter from the file +readCounterFromFile(); +// Update the screen with the counter value +updateScreen(); diff --git a/apps/burn/metadata.json b/apps/burn/metadata.json new file mode 100644 index 000000000..c032058c8 --- /dev/null +++ b/apps/burn/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "burn", + "name": "Burn", + "version": "0.07", + "description": "Simple Calorie Counter -- saves to flash and resets at midnight. I often keep mine running while the digital clock widget is at the top", + "icon": "app-icon.png", + "tags": "tool", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "screenshots": [{"url":"app-screenshot.png"}], + "allow_emulator": true, + "storage": [ + {"name":"burn.app.js","url":"app.js"}, + {"name":"burn.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index ecd0c355f..74b80b73b 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -6,4 +6,31 @@ 0.06: Design and usability improvements. 0.07: Improved positioning. 0.08: Select the color of widgets correctly. Additional settings to hide colon. -0.09: Larger font size if colon is hidden to improve readability further. \ No newline at end of file +0.09: Larger font size if colon is hidden to improve readability further. +0.10: HomeAssistant integration if HomeAssistant is installed. +0.11: Performance improvements. +0.12: Implements a 2D menu. +0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled. +0.14: Adds humidity to weather data. +0.15: Added option for a dynamic mode to show widgets only if unlocked. +0.16: You can now show your agenda if your calendar is synced with Gadgetbridge. +0.17: Fix - Step count was no more shown in the menu. +0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set. +0.19: Fix - Compatibility with "Digital clock widget" +0.20: Better handling of async data such as getPressure. +0.21: On the default menu the week of year can be shown. +0.22: Use the new clkinfo module for the menu. +0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. +0.24: Update clock_info to avoid a redraw +0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16. + ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.26: Use clkinfo.addInteractive instead of a custom implementation +0.27: Clean out some leftovers in the remove function after switching to +clkinfo.addInteractive that would cause ReferenceError. +0.28: Option to show (1) time only and (2) week of year. +0.29: use setItem of clockInfoMenu to change the active item +0.30: Use widget_utils +0.31: Use clock_info module as an app +0.32: Make the border of the clock_info box extend all the way to the right of the screen. +0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) +0.34: Support 12-hour time format diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index f6a1c6522..882d525f6 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -1,17 +1,27 @@ # BW Clock +A very minimalistic clock. ![](screenshot.png) ## Features -- Fullscreen on/off -- Tab left/right of screen to show steps, temperature etc. -- Enable / disable lock icon in the settings. -- If the "sched" app is installed tab top / bottom of the screen to set the timer. -- The design is adapted to the theme of your bangle. -- The colon (e.g. 7:35 = 735) can be hidden now in the settings. +The BW clock implements features that are exposed by other apps through the `clkinfo` module. +For example, if you install the HomeAssistant app, this menu item will be shown if you first +touch the bottom of the screen and then swipe left/right to the home assistant menu. To select +sub-items simply swipe up/down. To run an action (e.g. trigger home assistant), simply select the clkinfo (border) and touch on the item again. See also the screenshot below: + +![](screenshot_3.png) + +Note: Check out the settings to change different themes. + +## Settings +- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). +- Enable/disable lock icon in the settings. Useful if fullscreen mode is on. +- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. +- Your bangle uses the sys color settings so you can change the color too. ## Thanks to -Icons created by Flaticon +- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. +- Icons created by Flaticon ## Creator -- [David Peer](https://github.com/peerdavid) +[David Peer](https://github.com/peerdavid) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 5bfec4097..5053dafbb 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -1,57 +1,62 @@ -/* +{ // must be inside our own scope here so that when we are unloaded everything disappears + +/************************************************ * Includes */ const locale = require('locale'); const storage = require('Storage'); +const clock_info = require("clock_info"); +const widget_utils = require("widget_utils"); -/* - * Statics +/************************************************ + * Globals */ const SETTINGS_FILE = "bwclk.setting.json"; -const TIMER_IDX = "bwclk"; const W = g.getWidth(); const H = g.getHeight(); -/* +/************************************************ * Settings */ let settings = { - fullscreen: false, + screen: "Normal", showLock: true, hideColon: false, - showInfo: 0, + menuPosX: 0, + menuPosY: 0, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { - settings[key] = saved_settings[key] + settings[key] = saved_settings[key]; } - -/* - * Assets - */ - -// Manrope font -Graphics.prototype.setLargeFont = function(scale) { - // Actual height 48 (49 - 2) - this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AFcH+AHFh/gA4sf4AHFn+AA4t/E43+AwsB/gHFgf4PH4AMgJ9Ngf/Pot//6bF/59F///PokfA4J9DEgIABEwYkB/7DDEgIlFCoRMDEgQsEDoRLEEgpoBA4JhGOIsHZ40PdwwA/L4SjHNAgGCP4cHA4wWDA4aVCA4gGDA4SNBe4IiBA4MPHYRBBEwScCA4d/EQUBaoRKDA4UBLQYECgb+EAgMHYYcHa4MPHoLBCBgMfYgcfBgM/PIc/BgN/A4YECIIQEDHwkDHwQHDGwQHENQUHA4d/QIQnCRIJJCSgYTCA4hqCA4hqCA4hiCA4ZCEA4RFBGYbrFAHxDGSohdDcgagFAAjPCEzicDToU/A4jPCAwbQCBwgrBgIHEFYKrDWoa7DaggA/AC0PAYV+AYSBCgKpCg4DDVIUfAYZ9BToIDDPoKVBAYfARoQDDXgMPFwTIBdYSYCv4LCv7zCXgYKCXAK8CHoUPXgY9Cn/vEYMPEwX/z46Bj4mBgf+n77CDwX4v54EIIIzCOgX/4I+CAQI9BHYQCCQ4I7CRASDBHYQHCv/Aj4+BGYIeBGAI+Bj/8AIIRBQIZjCRIiWBXgYHCPQgHBBgJ6DA4IEBPQaKBGYQ+BbgiCCAGZFDIIUBaAZBCgYHCQAQTBA4SACUwS8DDYQHBQAbVCQAYwBA4SABgYEBPoQCBFgU/CQWACgRDCHwKVCIYX+aYRDCHwMPAgY+Cn4EDHwX/AgY+B8bEFj/HA4RGCn+f94MBv45Cv+fA4J6C//+j5gBGIMBFoJWBQoRMB8E//4DBHIJcBv4HBEwJUCA4ImCj5MBA4KZCPYQHBZgRBCE4LICvwaCXAYA5PgQAEMIQAEUwQADQAJlCAARlBWYIACT4JtDAAMPA4IWESgg8CAwI+EEoPhHwYlCgY+DEoP4g4+DEoPAh4+CEoReBHwUfLYU/CwgMBXARqBHYQCCGoIjBgI+CgZSCHwcHAYY+Ch4lBJ4IbCjhACPwqUBPwqFCPwhQBIQZ+DOAKVFXooHCXop9DFAi8EFAT0GPoYAygwFEgOATISLDwBWDTQc/A4L6CTQKkCVQX+BYIHBDwX+BYIHBVQX8B4KqD+/wA4aBBj/AgK8CQIIJBA4a/BBIMBAgL/BAgUDYgL/BAII7BAQXgAII7BAQXAYQQxBYARrCMwQ0BAgV/HwYECHwgEBgY+EA4MPGwI8BA4UfGwI8BgYHBPofAQYOHPoeAR4QmBHwQHCEwI+CA4RVBHwQHCaggnBDwQHEHoIAEEQIA6v5NFfgSECBwZtEf4IHFOYQHEj4HGDwYHCDwPgv/jA4UHXQS8E/ED/AHDZ4MPSYKlCv+AYwIHDDwL7EgL7DAgTzCEwIpCeYTZBg4CBeYIJBAgICBFgIJBAgICBeYIEDHII0BAgg+EgI5CMocHGwJBCA4MfGwMD/h/BwF/PoQHC451CJIMDSgIjBA4PAA4QmBA4IhBA4JVBgEMA4bUDV4QeCAAf/HoIAENIIApOoIAEW4QAEW4QAEW4QAEWQRSFNIcDfYQMDny8DO4Q7BAQQjCewh+EHwcPToQ+Dv//ewkHUoI+En68DeIS0EHwMf/46CeYYlCHwQ0BKIY+BGgJ4Dh/nGgZZCAwKPEHYLpFDoKuFGgj4JgY0EHwQ0EYhIA6MAkf+BRBLIa5BQAJSCBgP4R4iVB/YHERoIACA4QGDE4SFBAoV/A4MH/ggBWIL7C8EfVoL4DwBHBFYIHBfYIRBAgT7CDgQEBgP4BgUBEIMDDgIMBgYMBg/gBgS5Ch/ABgUPFIMf4EHA4IEBHwUPCgJGCIIM/CgLgCAQJlBFIQFB44HBEIUBQYc/EIIHDAAIuBA4oeBRoSfBLAIHC/gHBEwIXC+AHBZghHBDwQADj4WCAHEPAwpWBKYYOCLwIHELYJUBghlDA4UcQogHBvgeDD4K0DDwIHBWgQeB4CyBh68CUAMf8DeCdIYHDdIfAfYjxCAgj2BAgbHCvwJCIIYCBBIMDHIX4BgUHFwMD+AMCA4Q0BAgg5CHwxICAQY5BdgQHBEgMDIYV/DgR1CA4PwP4KvDRgIACEYIHFWggABMQQHEZwd/Dwq1DHoTFEdooA/ACrBBcAZmC8DTCAATGBaYR+DwDTCRwbYDAASLBCIIGCFgQRBAG4='))), - 46, - atob("EhooGyUkJiUnISYnFQ=="), - 63+(scale<<8)+(1<<16) - ); - return this; +let isFullscreen = function() { + var s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked(); + } else { + return s == "full"; + } }; -Graphics.prototype.setXLargeFont = function(scale) { - // Actual height 53 (55 - 3) +let getLineY = function(){ + return H/5*2 + (isFullscreen() ? 0 : 8); +} + +/************************************************ + * Assets + */ +// Manrope font +Graphics.prototype.setLargeFont = function(scale) { + // Actual height 47 (48 - 2) this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))), + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAD/AAAAAAAAP/wAAAAAAAf/8AAAAAAB///AAAAAAH///wAAAAAf///8AAAAB/////AAAAH////8AAAAP////wAAAA/////AAAAB////+AAAAA////4AAAAAP///gAAAAAD//+AAAAAAA//4AAAAAAAP/gAAAAAAAD/AAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///+AAAAAB////8AAAAB/////wAAAA/////+AAAA//////wAAAf/////+AAAH//////wAAD//////+AAB/+AAAf/gAAf+AAAA/8AAH/AAAAH/AAD/gAAAA/4AA/wAAAAH+AAP8AAAAB/gAD+AAAAAf4AA/gAAAAH+AAP4AAAAA/gAD+AAAAAf4AA/wAAAAH+AAP8AAAAB/gAD/AAAAA/4AA/4AAAAP+AAH/AAAAH/AAB/4AAAH/wAAP/wAAP/4AAD//////+AAAf//////AAAD//////gAAAf/////wAAAD/////4AAAAf////4AAAAB////4AAAAAB///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/AAAAAAAAD/gAAAAAAAA/4AAAAAAAAf8AAAAAAAAH+AAAAAAAAD/gAAAAAAAB/wAAAAAAAAf8AAAAAAAAP///////AAD///////wAA///////8AAP///////AAD///////wAA///////8AAP///////AAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAB/AAAA/gAAA/wAAA/4AAAf8AAAf+AAAP/AAAP/gAAH/wAAH/4AAD/8AAD/+AAB//AAA//gAA//wAAf/AAAP/8AAH/AAAH//AAD/gAAD//wAA/wAAB//8AAP8AAA///AAD/AAAf+fwAA/gAAP/n8AAP4AAH/x/AAD+AAD/4fwAA/gAB/8H8AAP8AAf+B/AAD/AAP/AfwAA/4AH/gH8AAH/AH/wB/AAB/8H/4AfwAAP///8AH8AAD////AB/AAAf///gAfwAAD///wAH8AAAf//4AB/AAAD//4AAfwAAAP/8AAH8AAAAf4AAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAADgAAAfwAAAB+AAAH8AAAAfwAAB/AAAAH+AAAfwAAAB/wAAH8AAAA/+AAB/AAAAP/gAAfwA4AA/8AAH8AfgAH/AAB/AP8AA/4AAfwD/gAH+AAH8B/4AB/gAB/A/8AAf4AAfwf/AAD+AAH8P/wAA/gAB/H/8AAf4AAfz//gAH+AAH8//4AB/gAB/f//AA/4AAf/+/4Af8AAH//P/AP/AAB//j////gAAf/wf///4AAH/4H///8AAB/8A///+AAAf+AH///AAAH/AA///gAAB/gAD//wAAAfwAAP/wAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAH/wAAAAAAAH/8AAAAAAAH//AAAAAAAH//wAAAAAAH//8AAAAAAH///AAAAAAH///wAAAAAH///8AAAAAP//9/AAAAAP//8fwAAAAP//4H8AAAAP//4B/AAAAP//4AfwAAAP//4AH8AAAD//4AB/AAAA//4AAfwAAAP/4AAH8AAAD/wAAB/AAAA/wAAAfwAAAPwAH////AADwAB////wAAwAAf///8AAAAAH////AAAAAB////wAAAAAf///8AAAAAH////AAAAAA////wAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAGAHwAAAB///gB+AAAH///8AfwAAB////AP+AAAf///wD/wAAH///+A/+AAB////gP/gAAf///4A/8AAH/8P8AH/AAB/AD+AA/4AAfwA/gAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/AAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/gAH+AAH8Af4AB/gAB/AH/AA/wAAfwB/4Af8AAH8AP/AP/AAB/AD////gAAfwAf///wAAH8AD///8AAB/AA///+AAAfwAH///AAAAAAA///gAAAAAAD//gAAAAAAAP/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAAAH////wAAAAH/////AAAAD/////4AAAB//////AAAA//////4AAAf//////AAAP//////4AAD/8D/w/+AAB/4B/wD/wAAf8A/wAf8AAP+AP4AD/gAD/AD+AAf4AA/wB/AAH+AAP4AfwAB/gAD+AH8AAf4AA/gB/AAH+AAP4AfwAB/gAD+AH+AAf4AA/wB/gAH+AAP8Af8AD/gAD/gH/gB/wAAf8A/8A/8AAH/AP///+AAB/gB////gAAPwAP///wAAB4AD///4AAAMAAf//8AAAAAAD//+AAAAAAAP/+AAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAABwAAfwAAAAB8AAH8AAAAD/AAB/AAAAD/wAAfwAAAH/8AAH8AAAH//AAB/AAAP//wAAfwAAP//8AAH8AAf//+AAB/AAf//8AAAfwA///8AAAH8A///4AAAB/A///4AAAAfx///wAAAAH9///wAAAAB////gAAAAAf///gAAAAAH///AAAAAAB///AAAAAAAf/+AAAAAAAH/+AAAAAAAB/8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAf/AAAAAP+Af/8AAAAP/4P//wAAAP//P//+AAAH//////wAAB//////8AAA///////gAAf//////8AAH////gP/AAD/wf/wA/wAA/4D/4AP+AAP8Af8AB/gAD/AH/AAf4AA/gA/wAH+AAP4AP4AA/gAD+AD/AAP4AA/gA/wAH+AAP8Af8AB/gAD/AH/AAf4AA/4D/4AP+AAP/B//AH/AAB////4D/wAAf//////8AAD//////+AAAf//////AAAH//////wAAA//8///4AAAD/+D//8AAAAP+Af/8AAAAAAAB/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAB//AAAAAAAB//8AAAAAAB///gAAgAAA///8AAcAAAf///gAPAAAH///8AH4AAD////AD/AAB/+H/4B/wAAf+Af+Af8AAP+AB/wD/gAD/gAf8Af4AA/wAD/AH+AAP8AA/wB/gAD+AAH8AP4AA/gAB/AD+AAP4AAfwB/gAD+AAH8Af4AA/wAD/AH+AAP8AA/gD/gAD/gAf4A/wAAf8AP8A/8AAH/gH/Af/AAA///////gAAP//////wAAB//////8AAAP/////+AAAB//////AAAAP/////AAAAA/////gAAAAD////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), 46, - atob("FR4uHyopKyksJSssGA=="), - 70+(scale<<8)+(1<<16) + atob("ExspGyUkJiQnISYnFQ=="), + 62+(scale<<8)+(1<<16) ); + return this; }; Graphics.prototype.setMediumFont = function(scale) { @@ -62,205 +67,129 @@ Graphics.prototype.setMediumFont = function(scale) { Graphics.prototype.setSmallFont = function(scale) { // Actual height 28 (27 - 0) - this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//84D//zgP/+GAAAAAAAAAAAAAAAAAAAD4AAAPgAAA+AAAAAAAAAAAAA+AAAD4AAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAg4AAHDgAAcOCABw54AHD/gAf/8AD/8AB//gAP8OAA9w4YCHD/gAcf+AB//gAf/gAP/uAA/w4ADnDgAAcOAABw4AAHAAAAcAAAAAAAAAAAAAAAIAA+A4AH8HwA/4PgHjgOAcHAcBwcBw/BwH78DgfvwOB8HA4HAOBw8A+HngB4P8ADgfgAAAYAAAAAAAAAAB4AAAf4AQB/gDgOHAeA4cDwDhweAOHDwA88eAB/nwAD88AAAHgAAA8AAAHn4AA8/wAHnvgA8cOAHhg4A8GDgHgcOA8B74BgD/AAAH4AAAAAAAAAAAAAAAAAMAAAH8AD8/4Af/3wB/8HgODwOA4HA4DgODgOAcOA4A44DwDzgHAH8AMAPwAQP+AAA/8AAAB4AAADAAAAAA+AAAD4AAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AD//+A/+/+H4AD98AAB3gAADIAAAAAAAAAAAAAIAAABwAAAXwAAHPwAB8P8D/gP//4AH/8AAAAAAAAAAAAAAAAAAAAAAAAGAAAA4gAAB/AAAH8AAD/AAAP8AAAH4AAAfwAADiAAAOAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAD/+AAP/4AABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAADkAAAPwAAA/AAAAAAAAAAAAAAAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAAAAAAAAAAAAAAAAAAAAADgAAAOAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAA4AAA/gAA/+AA//AA//AAP/AAA/AAADAAAAAAAAAAAAAAAAAAA//gAP//gB///AHgA8A8AB4DgADgOAAOA4AA4DgADgPAAeAeADwB///AD//4AD/+AAAAAAAAAAAAAAAA4AAAHgAAAcAAADwAAAP//+A///4D///gAAAAAAAAAAAAAAAAAAAAAAYAeADgD4AeAfAD4DwAfgOAD+A4Ae4DgDzgOAeOA4Dw4DweDgH/wOAP+A4AfwDgAAAAAAAAAAAAIAOAA4A4ADwDggHAOHgOA48A4DnwDgO/AOA7uA4D84HgPh/8A8H/gDgH8AAACAAAAAAAAAAAAAHgAAB+AAA/4AAP7gAD+OAA/g4AP4DgA+AOADAA4AAB/+AAH/4AAf/gAADgAAAOAAAAAAAAAAAAAAAAD4cAP/h4A/+HwDw4HgOHAOA4cA4DhwDgOHAOA4cA4Dh4HAOD58A4H/gAAP8AAAGAAAAAAAAAAAAAAAAD/+AAf/8AD//4AePDwDw4HgOHAOA4cA4DhwDgOHAOA4cB4Bw8PAHD/8AIH/gAAH4AAAAAAAAAADgAAAOAAAA4AAYDgAHgOAD+A4B/wDgf4AOP+AA7/AAD/gAAP4AAA8AAAAAAAAAAAAAAAAAAeH8AD+/4Af//wDz8HgOHgOA4OA4Dg4DgODgOA4eA4Dz8HgH//8AP7/gAeH8AAAAAAAAAAAAAAAA+AAAH+AgB/8HAHh4cA8Dg4DgODgOAcOA4Bw4DgODgPA4eAeHDwB///AD//4AD/+AAAAAAAAAAAAAAAAAAAAAAAAAAODgAA4OAADg4AAAAAAAAAAAAAAAAAAAAAAAAAAAAABwA5AHAD8AcAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAB8AAAP4AAB5wAAPDgAB4HAAHAOAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAABjAAAGMAAAYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAEAAcA4AB4HAADw4AADnAAAH4AAAPAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAB8AAAHgAAA4AAADgDzgOA/OA4D84DgeAAPHwAAf+AAA/wAAB8AAAAAAAAAAAAAAAAAAD+AAB/+AAP/8AB4B4AOABwBwADgHB8OA4P4cDhxxwMGDDAwYMMDBgwwOHHHA4f4cDh/xwHAHCAcAMAA8AwAB8PAAD/4AAD/AAAAAAAAAAAAAACAAAB4AAB/gAA/8AAf+AAP/wAH/nAA/gcADwBwAPwHAA/4cAA/9wAAf/AAAP/AAAD/gAAB+AAAA4AAAAAAAAAAAAAAD///gP//+A///4DgcDgOBwOA4HA4DgcDgOBwOA4HA4Dg8DgPHwOAf/h4A///AB8f4AAAfAAAAAAAP+AAD/+AAf/8AD4D4AeADwBwAHAOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOAcABwB4APAD4D4AHgPAAOA4AAAAAAAAAAAAAAAP//+A///4D///gOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOA8AB4BwAHAHwB8AP//gAP/4AAP+AAAAAAAAAAAAAAAA///4D///gP//+A4HA4DgcDgOBwOA4HA4DgcDgOBwOA4HA4DgcDgOBgOA4AA4AAAAAAAAAAAAAAD///gP//+A///4DgcAAOBwAA4HAADgcAAOBwAA4HAADgcAAOAwAA4AAAAAAAAAf+AAD/+AA//+ADwB4AeADwDwAHgOAAOA4AA4DgADgOAAOA4AA4DgMDgPAweAcDBwB8MfADw/4AHD/AAAPwAAAAAAAAAAAAAAAP//+A///4D///gABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAA///4D///gP//+AAAAAAAAAAAAAAAAAAAD///gP//+A///4AAAAAAAAAAAADgAAAPAAAA+AAAA4AAADgAAAOAAAA4AAAHgP//8A///wD//8AAAAAAAAAAAAAAAAAAAA///4D///gP//+AAHAAAA+AAAP8AAB54AAPDwAB4HgAPAPAB4AfAPAA+A4AA4DAABgAAACAAAAAAAAAAP//+A///4D///gAAAOAAAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAAAAAAAAAAAAP//+A///4D///gD+AAAD+AAAB+AAAB/AAAB/AAAB/AAAB+AAAH4AAB+AAA/gAAP4AAD+AAA/AAAfwAAD///gP//+A///4AAAAAAAAAAAAAAAAAAAP//+A///4D///gHwAAAPwAAAPgAAAfgAAAfAAAAfAAAA/AAAA+AAAB+AAAB8A///4D///gP//+AAAAAAAAAAAP+AAD/+AAf/8AD4D4AeADwBwAHAOAAOA4AA4DgADgOAAOA4AA4DgADgOAAOAcABwB4APAD4D4AH//AAP/4AAP+AAAAAAAAAAAP//+A///4D///gOAcAA4BwADgHAAOAcAA4BwADgHAAOAcAA4DgAD4eAAH/wAAP+AAAPgAAAAAAAA/4AAP/4AB//wAPgPgB4APAHAAcA4AA4DgADgOAAOA4AA4DgADgOAAOA4AO4BwA/AHgB8APgPwAf//gA//uAA/4QAAAAAAAAAA///4D///gP//+A4BwADgHAAOAcAA4BwADgHAAOAcAA4B8ADgP8APh/8Af/H4A/4HgA+AGAAAAAAAAAAAABgAHwHAA/g+AH/A8A8cBwDg4DgODgOA4OA4DgcDgOBwOA4HA4DwODgHg4cAPh/wAcH+AAwPwAAAAADgAAAOAAAA4AAADgAAAOAAAA4AAAD///gP//+A///4DgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAAAAAAAAAAAAAAAP//AA///AD//+AAAB8AAABwAAADgAAAOAAAA4AAADgAAAOAAAA4AAAHgAAA8A///gD//8AP//gAAAAAAAAAAIAAAA8AAAD+AAAH/AAAD/wAAB/4AAA/8AAAf4AAAPgAAB+AAA/4AAf+AAP/AAH/gAD/wAAP4AAA4AAAAAAAAPAAAA/gAAD/4AAA/+AAAf/AAAH/gAAB+AAAf4AAf/AAf/AAP/gAD/gAAPwAAA/4AAA/+AAAf/AAAH/wAAB/gAAB+AAB/4AA/+AA/+AA/+AAD/AAAPAAAAgAAAAAAAAMAAGA4AA4D4APgHwB8APwfAAPn4AAf+AAAfwAAB/AAAf+AAD4+AA/B8AHwB8A+AD4DgADgMAAGAwAAADwAAAPwAAAPwAAAfgAAAfgAAAf/4AAf/gAH/+AB+AAAPwAAD8AAA/AAADwAAAMAAAAgAAAAAAAAMAACA4AA4DgAPgOAD+A4Af4DgH7gOB+OA4Pw4Dj8DgO/AOA/4A4D+ADgPgAOA4AA4DAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////gAAAOAAAA4AAADAAAAAAAAAAAAAAAAAAAAAAA4AAAD+AAAP/gAAH/4AAB/+AAAf+AAAH4AAABgAAAAAAAAADAAAAOAAAA4AAADgAAAP////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgAAAcAAADgAAAcAAADgAAAcAAAB4AAADwAAADgAAAHAAAAOAAAAYAAAAAAAAAAAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAHH8AA8/4AHzjgAcMOABxwYAHHBgAccOABxwwAHGHAAP/4AA//4AA//gAAAAAAAAAAAAAAAAAAA///4D///gP//+AA4BwAHADgAcAOABwA4AHADgAcAOAB4B4ADwPAAP/8AAf/AAAf4AAAAAAAAAAAAPwAAD/wAAf/gADwPAAeAeABwA4AHADgAcAOABwA4AHADgAeAeAA8DwABwOAADAwAAAAAAAAAAAA/AAAP/AAD//AAPA8AB4B4AHADgAcAOABwA4AHADgAcAOAA4BwD///gP//+A///4AAAAAAAAAAAAAAAAPwAAD/wAAf/gAD2PAAeYeABxg4AHGDgAcYOABxg4AHGDgAeYeAA/jwAB+OAAD4wAABgAAAAAAAAAAABgAAAGAAAB//+Af//4D///gPcAAA5gAADGAAAMYAAAAAAAAAPwAAD/wMA//w4DwPHgeAePBwA4cHADhwcAOHBwA4cHADhwOAcPB///4H///Af//wAAAAAAAAAAAAAAAAAAD///gP//+AA//4ADgAAAcAAABwAAAHAAAAcAAABwAAAHgAAAP/+AAf/4AA//gAAAAAAAAAAAAAAMf/+A5//4Dn//gAAAAAAAAAAAAAAAAAAHAAAAfn///+f//+5///wAAAAAAAAAAAAAAAAAAP//+A///4D///gAAcAAAD8AAAf4AADzwAAeHgAHwPAAeAeABgA4AEABgAAAAAAAAAD///gP//+A///4AAAAAAAAAAAAAAAAAAAAf/+AB//4AH//gAOAAABwAAAHAAAAcAAABwAAAHgAAAP/+AA//4AB//gAOAAABwAAAHAAAAcAAABwAAAHgAAAf/+AA//4AA//gAAAAAAAAAAAAAAAf/+AB//4AD//gAOAAABwAAAHAAAAcAAABwAAAHAAAAeAAAA//4AB//gAD/+AAAAAAAAAAAAAAAAD8AAA/8AAH/4AA8DwAHgHgAcAOABwA4AHADgAcAOABwA4AHgHgAPh8AAf/gAA/8AAA/AAAAAAAAAAAAAAAAB///8H///wf///A4BwAHADgAcAOABwA4AHADgAcAOAB4B4ADwPAAP/8AAf/AAAf4AAAAAAAAAAAAPwAAD/wAA//wADwPAAeAeABwA4AHADgAcAOABwA4AHADgAOAcAB///8H///wf///AAAAAAAAAAAAAAAAAAAH//gAf/+AB//4ADwAAAcAAABwAAAHAAAAcAAAAAAAAAAMAAHw4AA/jwAH+HgAcYOABxw4AHHDgAcMOABw44AHjjgAPH+AA8fwAAw+AAAAAABgAAAGAAAAcAAAf//wB///AH//+ABgA4AGADgAYAOABgA4AAAAAAAAAAAAAAAH/AAAf/wAB//wAAB/AAAAeAAAA4AAADgAAAOAAAA4AAADgAAAcAB//4AH//gAf/+AAAAAAAAAAAAAAABwAAAH4AAAf8AAAP8AAAH+AAAD+AAAD4AAA/gAAf8AAP+AAH/AAAfgAABwAAAAAAAAAAAABwAAAH8AAAf+AAAP/gAAD/gAAB+AAAf4AAP8AAP+AAB/AAAH4AAAf8AAAP+AAAD/gAAB+AAAf4AAf/AAP/AAB/gAAHgAAAQAAABAAIAHADgAeAeAA8HwAB8+AAD/gAAD8AAAPwAAD/gAAfPgADwfAAeAeABwA4AEAAgAAAAABAAAAHgAAAfwAAA/wAAAf4BwAP4/AAP/8AAP+AAD/AAB/wAA/4AAP8AAB+AAAHAAAAQAAAAAAIAHADgAcAeABwD4AHA/gAcHuABx84AHPDgAf4OAB/A4AHwDgAeAOABgA4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAH4Af//////n//AAAA4AAADgAAAAAAAAAAAAAAAAAP//+A///4D///gAAAAAAAAAAAAAAAAAAA4AAADgAAAOAAAA//5/9////wAH4AAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAeAAAD4AAAOAAAA4AAADgAAAHAAAAcAAAA4AAADgAAAOAAAD4AAAPAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 32, atob("BgkMGhEZEgYMDAwQCAwICxILEBAREBEOEREJCREVEQ8ZEhEUExAOFBQHDREPGBMUERQSEhEUERsREBIMCwwTEg4QERAREQoREQcHDgcYEREREQoPDBEPFg8PDwwIDBMc"), 28+(scale<<8)+(1<<16)); + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/+cB//5wH//nAAAAAAAAAAAAAAAAAAAB8AAAHwAAAfAAAAAAAAAAAAAfAAAB8AAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAQcAADhwAAOHBAA4c8ADh/wAP/+AB/+AA//wAH+HAAe4cMBDh/wAOP/AA//wAP/wAH/3AAf4cABzhwAAOHAAA4cAADgAAAOAAAAAAAAAAAAAAAAAAAAwAH8HwA/4PgD/geAePA8BwcBw/BwH78DgfvwOB+HA4HAeBwcA8HDgB4f+ADg/wAGB+AAAAAAAAAAAAAAAH4AAA/wBwHngPAcOB4Bw4PAHDh4AcOPAA/x4AD/PAADx4AAAPAAAB5wAAPPwAB5/gAPOPAB4wcAPDBwB4MHAPA4cA4B/gBAH8AAAHAAAAAAAAAAAAAPAAHD/AB/f+AP/x4B4+DwHB4HAcDwcBwHhwHAPHAcAccB4A5wDgB+AGA/4AAH/AAAf+AAAA8AAABgAAAAAfAAAB8AAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AD//+A/+/+H4AD98AAB3gAADIAAAAAAAAAAAAAIAAABwAAAXwAAHPwAB8P8D/gP//4AH/8AAAAAAAAAAAAAAAAAAAAAAAAHAAAAcwAAA/gAAb8AAB/gAAH+AAAD+AAAOwAABxAAADAAAAAAAAAAAAAADAAAAMAAAAwAAADAAAAMAAAAwAAB//AAH/8AAAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAABwAAAHIAAAfgAAB8AAAAAAAAAAAAAAAAAAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAAAAAAAAAAABwAAAHAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAA/wAA//AA//AA//AAH/AAAfAAABAAAAAAAAAAAAAAAAAAAf/wAH//wA///gDgAOAcAAcBwABwHAAHAcAAcBwABwHgAPAPAB4Af//AA//4AA/+AAAAAAAAAAAAAAAAMAAABwAAAOAAAB4AAAH///Af//8B///wAAAAAAAAAAAAAAAAAAAAwAcAPADwB8AfAPAB8B4APwHAB/AcAPcBwB5wHAPHAcB4cA8PBwD/4HAH/AcAHwBwAAAAAAAAAAAAGAHAAcAcAB4BwYDwHDwHAceAcBz4BwHfgHAf3AcB+eDwHw/+AeB/wBwD+AAAAAAAAAAAAAAAAABwAAAfAAAP8AAD/wAA/nAAP4cAD+BwAfgHAB4AcAEA//AAD/8AAP/wAABwAAAHAAAAMAAAAAAAAAAAAAEAH/w4Af/D4B/8HgHDgPAcOAcBw4BwHDgHAcOAcBw8DwHB4eAcH/wBgP+AAAPwAAAAAAAAAAAAAAAB//AAf//AD//+AOHB4Bw4BwHDgHAcOAcBw4BwHDgHAcPA8A4eHgDh/8AEB/gAAD4AAAAAAAAAABwAAAHAAAAcAAMBwADwHAB/AcA/4BwP8AHH/AAd/gAB/wAAH8AAAeAAAAAAAAAAAAAAAEAAPD+AB/f8AP//4B4+DwHDwHAcHAcBwcBwHBwHAcPAcB/+DgD//+AH5/wACB8AAAAAAAAAAAAAAAAEAAAD+AAAf+DAD74OAODw8BwHBwHAOHAcA4cBwDBwHAcHAeBw8A+ePgB//8AD//gAB/wAAAAAAAAAAAAAAAAAAAAAHBwAAcHAABwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AcgDgB+AOAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAeAAAD8AAAf4AADzwAAeHgADwPAAGAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAADGAAAMYAAAxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABACAAOAcAA8DgAB4cAABzgAAD8AAAHgAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAHgAAA+AAAHgAAAcAAABwD5wHAfnAcD8cBweAAHzwAAP+AAAfwAAAcAAAAAAAAAAAAAAAAAAB/AAA//AAH/+AA8A8AHAA4A4ABwDg+HAcH8OBw444GDBhgYMGGBgwYYHDjjgcP8OBw/44DgDhAOAGAAeAYAA+HgAB/8AAB/gAAAAAAAAAAAAABAAAA8AAAfwAAP/AAH/gAD/4AB/zgAf4OAB8A4AHwDgAf4OAA/84AAP/gAAH/AAAD/gAAB/AAAA8AAAAQAAAAAAAAAB///wH///Af//8BwOBwHA4HAcDgcBwOBwHA4HAcDgcBweBwHj4HAP/58Afz/gAcH8AAAPAAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB8B8ADwHgADAYAAAAAAAAAAAAAAAH///Af//8B///wHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAeAA8A8AHgB8B+AD//gAH/8AAD/AAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8BwOAAHA4AAcDgABwOAAHA4AAcDgABwOAAHAAAAcAAAAAAAAAP/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwGBwHgYPAOBg4A+GPgB4f8ADh/gAAH4AAAAAAAAAAAAAAAH///Af//8B///wAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAADgAAAOAAAA4AAf//8B///wH///AAAAAAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAAABgAAAHgAAAeAAAA8AAABwAAAHAAAAcAAABwH///Af//4B///AAAAAAAAAAAAAAAAAAAAf//8B///wH///AAHgAAA/AAAH+AAA88AAHh8AA8D4AHgDwA8AHgHgAPAYAAcBAAAwAAABAAAAAAAAAAH///Af//8B///wAAAHAAAAcAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAAAAAAAAAAAAH///Af//8B///wB/AAAB/AAAA/AAAA/gAAA/gAAA/gAAA/AAAD8AAA/AAAfwAAH8AAB/AAAfgAAP4AAB///wH///Af//8AAAAAAAAAAAAAAAAAAAH///Af//8B///wD8AAAD4AAAH4AAAHwAAAPwAAAPgAAAPgAAAfAAAAfAAAA/Af//8B///wH///AAAAAAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAH///Af//8B///wHAOAAcA4ABwDgAHAOAAcA4ABwDgAHAeAAeBwAA+fAAD/4AAD/AAADgAAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAnAcAHcA4AfgDwA+AH4P4AP//wAf/3AAP4AAAAAAAAAAAf//8B///wH///AcA4ABwDgAHAOAAcA4ABwDgAHAOAAcB+AB4H+AD59/AP/h8AP8BwAOABAAAAAAAAAAAAAwAD4HwA/4fAD/geAePA8BwcBwHBwHAcDgcBwOBwHA4HAcDgcA4HDwD4eeAHw/4AOD/AAIDwAAAAABwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAH///Af//8B///wHAAAAcAAABwAAAHAAAAcAAABwAAAGAAAAAAAAAAAAAH//wAf//gB///AAAAeAAAA8AAABwAAAHAAAAcAAABwAAAHAAAAcAAADgAAAeAf//wB//+AH//gAAAAAAAAAAGAAAAfAAAB/gAAB/wAAA/4AAAf8AAAP/AAAH8AAADwAAA/AAAf8AAP+AAP/AAH/gAB/wAAH4AAAcAAABAAAAHwAAAf4AAA/+AAAP/gAAH/wAAB/wAAA/AAAf8AAf/AAP/gAP/gAB/gAAH4AAAf+AAAf/AAAH/wAAB/8AAAfwAAB/AAB/8AA/+AA/+AAf+AAB/AAAHAAAAAAAAAAAAQGAADAeAA8B8AHwD8B+AD4PgAH74AAH/AAAPwAAA/gAAP/gAD8fAAfA/AH4A+AeAA8BwABwEAABAQAAABwAAAHwAAAPwAAAfwAAAfgAAAfgAAAf/wAB//AAf/8AH8AAA/AAAPwAAB8AAAHAAAAQAAAAAAAAAAABAcAAcBwADwHAA/AcAP8BwD/wHAfnAcH4cBx+BwHPwHAf8AcB/ABwH4AHAeAAcBgABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////gAAAOAAAA4AAADAAAAAAAAAAAAAAAAAAAAAAAeAAAB/gAAH/4AAB/+AAAf/gAAH/AAAB8AAAAQAAAAAAAAAAAAAAOAAAA4AAADgAAAP/////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAADgAAAcAAADgAAAcAAADgAAAcAAAB4AAADwAAADgAAAHAAAAOAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAADj+AAef8AD5xwAOGHAA44MADjgwAOOHAA44YADjDgAH/8AAf/8AAf/wAAAAAAAAAAAAAAAAAAAf//8B///wH//+AAcA4ADgBwAOAHAA4AcADgBwAOAHAA8A8AB8PgAD/8AAH/gAAH4AAAAAAAAAAAAH4AAB/4AAP/wAB4HgAPAPAA4AcADgBwAOAHAA4AcADgBwAPAPAAeB4AA4HAABgYAAAAAAAAAAAAfgAAH/gAB//gAHgeAA8A8ADgBwAOAHAA4AcADgBwAOAHAAcA4B///wH///Af//8AAAAAAAAAAAAAAAAH4AAB/4AAP/wAB7HgAPMPAA4wcADjBwAOMHAA4wcADjBwAPMPAAfx4AA/HAAB8YAAAwAAAAAAAAAAAAwAAADAAAB///AP//8B///wHMAAAYwAABjAAAGMAAAAAAAAAPwAAD/wMA//w4DwPHgeAePBwA4cHADhwcAOHBwA4cHADhwOAcPB///4H///Af//wAAAAAAAAAAAAAAAAAAB///wH///AAf/8ABwAAAOAAAA4AAADgAAAOAAAA4AAADwAAAH//AAP/8AAf/wAAAAAAAAAAAAAAAAAAAc//8Bz//wHP//AAAAAAAAAAAAAAHAAAAcAAAH+f///5///7H//8AAAAAAAAAAAAAAH///Af//8B///wAAPAAAB+AAAP8AAB54AAfDwAD4HgAOAPAAwAcACAAwAAAAAAAAAB///wH///Af//8AAAAAAAAAAAAAAAAAAAAP//AA//8AB//wAHAAAA4AAADgAAAOAAAA4AAAD4AAAH//AAP/8AB//wAHAAAA4AAADgAAAOAAAA4AAADwAAAH//AAP/8AAf/wAAAAAAAAAAAAAAAP//AA//8AB//wAHAAAA4AAADgAAAOAAAA4AAADgAAAPAAAAf/8AA//wAB//AAAAAAAAAAAAAAAAB+AAAf+AAD/8AAeB4ADwDwAOAHAA4AcADgBwAOAHAA4AcADwDwAHw+AAP/wAAf+AAAfgAAAAAAAAAAAAAAAB///8H///wP///A4BwAHADgAcAOABwA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAAAAAAAAPwAAD/wAA//wADwPAAeAeABwA4AHADgAcAOABwA4AHADgAOAcAB///8H///wf///AAAAAAAAAAAAAAAAAAAD//wAP//AAf/8ABwAAAOAAAA4AAADgAAAOAAAAAAAAAYGAAD4cAAfx4AD3DwAOOHAA44cADjhwAOGHAA4ccADxzwAHj+AAOP4AAYOAAAAAAAwAAADAAAAMAAAP//wA///gD///AAwAcADABwAMAHAAwAcADAAwAAAAAAAAAAD/gAAP/4AA//4AAA/gAAAPAAAAcAAABwAAAHAAAAcAAABwAAAOAA//8AD//wAP//AAAAAAAAAAAIAAAA4AAAD8AAAH+AAAH/AAAD/gAAB/AAAB8AAA/wAAf8AAP+AAD/AAAPgAAAwAAAAAAAAIAAAA8AAAD/AAAH/gAAD/wAAA/wAAA/AAAf8AAP+AAP+AAA/AAAD+AAAH/AAAD/gAAA/wAAA/AAAf8AAP/AAP/AAA/gAADgAAAAAAAAAAEADAAwAOAHAA+B8AB8PgAB74AAD/AAAH4AAA/wAAHvgAB8PgAPgfAA4AcADAAwAAABABAAAAHAAAAfgAAA/wAAA/wAwAf4fAAP/8AAP/AAB/gAA/wAAf4AAP+AAB/AAAHgAAAQAAAAAAEADAAwAOAPAA4B8ADgPwAOD/AA4ecADnxwAO8HAA/gcAD8BwAPAHAA4AcACAAwAAAAAAAAAAAAAAAAAAAAAAAAAA8AB////f//////n/+AAAA4AAADgAAAAAAAAAAAAAAAAAH///Af//8B///wAAAAAAAAAAAAAAAAAAA4AAADgAAAOAAAA//5/9////z////AAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAB8AAAHwAAAcAAABwAAAHgAAAOAAAA8AAABwAAAHAAAB8AAAHwAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAGAAABwAAAOAAABwAAAHAAAAcAAAA4AAABwAAABgAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAOAB4B4ADwPAAHh4AAPPAAAf4AAA/AAAB4AAAPwAAB/gAAPPAAB4eAAPA8AB4B4AHADgAIAEAAAAAAADAAAAMAAAAwAAADAAAAMAAAAwAAHDDgA8MPADww8AGDBgAAMAAAAwAAADAAAAMAAAAwAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADn//gOf/+A5//4AAAAAAAAAAAAAAAAAAAD/AAA//AAH/+AA+B8ADgBwAOAHAHwAPgfAA+B8AD4A4AcADwDwAHgeAAOBwAAQCAAAAAAAAAAAADgcAAOBwAA4HAD//8A///wD///AeDgcBwOBwHA4HAcDgcB4GBwD4AHAHgAcAOAAAAAAAAAAAAAMAGAB7+8AD//gAHx8AAcBwADgDgAOAOAA4A4ADgDgAOAOAA4A4ABwHAAHg8AA//4AH//wAMOGAAAAAAQAAABwAAAHwMYAPwxgAfjGAAfsYAAf7gAAf/wAB//AAf/8AH7GAA/MYAPwxgB8DGAHAAAAQAAAAAAAAAAAAAf/D/5/8P/n/w/+AAAAAAAAAAAAAAAAAAAABwAAffhwD//Hgf+cfBzwwcGHDhwYcOHBxw4cHDhxwfOPvA8//4Bx//AADwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAP/wAB//gAPAPAB4AOAPDw8A4/xwHH/jgc4HOBzgc4HMAzgcwDOBzgc4HPDjgOcOeA4whwBwAOAHwD4APw/AAf/4AAf+AAAPAAAAAAAATgAAD/AAANsAAA2wAADTAAAP8AAAfwAAAAAAAAAAAAAAAAAAgAAAPAAAB+AAAOeAADw8AAOIwAADxAAAfgAADngAA8PAADgMAAEAQAAAAAAAAAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAABgAAAGAAAAYAAAB+AAAH4AAAAAAAAAAAAAAAAAAAAD8AAA/8AAHh4AAYDgAD/3AAN/MAA0QwADRjAAN/MAA7hwABwOAADhwAAH+AAAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAD/AAAeeAABw4AAGDgAAYOAABw4AAH/AAAP8AAAfAAAAAAAAAAAAAAAAAAAwYAADBgAAMGAAAwYAADBgAAMGAAP+YAA/5gAD/mAAAwYAADBgAAMGAAAwYAADBgAAAAAAAAAAAAAAAMDAABwcAAPDwAAwPAADB8AAMOwAA5zAAB+MAADwwAAAAAAAAAAAIBAAAwGAADMcAANwwAA/DAAD8MAAO/wAAx+AAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///B///8H//AAAAeAAAA4AAADgAAAOAAAA4AAADgAAAcAB//gAH//gAf/+AAAAAAAAAAAAAAAAAAAAP4AAB/4AAP/gAB//AAH/8AAf/wAB//AAH///8f///x////AAAAAAAAAB////H///8f///wAAAAAAAAAAAAAAABAAAAOAAAB4AAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzAAAPMAAA/wAAAeAAAAAAAAAAAAAAAAAAAIAAABgAAAMAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAA8AAAP4AAAwwAADDAAAMMAAA5wAAB+AAADwAAAAAAAAAAAAAAAAAMAwAA8HAAB44AAD/AAAD4AADGMAAOBwAAeOAAA/wAAA+AAABgAAAAAAAAAAAAAAABAAAAMAAABwAAAH/8CAf/wcAAAHgAAA8AAAHgAAB4AAAPAAAB4AAAeAAADwAAA+AAAHgCAA8A8APAfwB4H7AHB+MAAHAwAAQ/wAAD/AAAAwAAADAAAAAAAAAAAAAAAAAAAAEAAAAwAAAHAAAAf/wIB//BwAAAeAAADwAAAeAAAHgAAA8AAAHgAAB4AAAPAAAD4AAAeAAADwAAA8GAwHg4HAcHA8AAYHwABg7AAGHMAAf4wAA/DAAA4MAAAAAAAAAAYBgABgHAAGMOAAZwYABvBgAH8OCAe/wcBx+HgABg8AAAHgAAB4AAAPAAAB4AAAeAAADwAAA+AAAHgHAA8B8APAfwB4HzADB8MAAHAwAAQ/wAAD/AAAAwAAAAAAAAAAAAAAAAAA4AAAP4AAB/wAAPHgABwOA4/A4Dn4DgOfAOAAAA4AAAHgAAB8AAAHgAAAYAAAAAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAfAOAB/g4AD/zgAA/+AAAf8AAAP+AAAH8AAADwAAABAAAAAAAAABwAAA/AAAf8AAP/AAH/wfD/nD+/wcMb4BwxvgHD+/wcHx/5wEAf/AAAP+AAAH/AAAD8AAABwAAAAAAAEAAADwAAB/AAA/8AAf+AAP/gAH/OAB/g4AHwDgAcAOABwA4AHADgAf//8B///wHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAYAAMAAAAAAAAAAA/4AAP/4AD//4APAHgB4APAPAAeA4AA4DgADg+AAPz4AA//gAD/+AAOe4AA4BwAHAHgA8APgPgAeA8AAYDAAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8BwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcAAcBwABwAAAAAAAAAAAAAAH///Af//8B///wHA4HAcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwABwHAAHAAAAAAAAAAAAAAAf//8B///wH///AcDgcBwOBwHA4HAcDgcBwOBwHA4HAcDgcBwOBwHAAHAcAAcAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAAAAAAAAAAH///Af//8B///wAAAAAAAAAAAAAAAAAAAf//8B///wH///AAAAAAAAAAAAAAAAAAAB///wH///Af//8AAAAAAAAAAABgAAAGAAH///Af//8B///wHAYHAcBgcBwGBwHAYHAcBgcBwABwHAAHAeAA8A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAAAAAAf//8B///wH///APwAAAPgAAAfgAAAfAAAA/AAAA+AAAA+AAAB8AAAB8AAAD8B///wH///Af//8AAAAAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAHAcAAcA4ADgDwAeAH4PwAP/+AAf/wAAP4AAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAB/wAAf/wAH//wAeAPADwAeAeAA8BwABwHAAHAcAAcBwABwHAAHAcAAcBwABwDgAOAPAB4Afg/AA//4AB//AAA/gAAAAAAAf8AAH/8AB//8AHgDwA8AHgHgAPAcAAcBwABwHAAHAcAAcBwABwHAAHAcAAcA4ADgDwAeAH4PwAP/+AAf/wAAP4AAAAAAAH/AAB//AAf//AB4A8APAB4B4ADwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHAOAA4A8AHgB+D8AD//gAH/8AAD+AAAAAAAAAAAAGDgAA8eAAB7wAAD+AAAHwAAAfAAAD+AAAe8AADw4AAGBAAAAAAAAAAAAAAAAAf8MAH//4B///AHgD4A8AfgHgD/AcAecBwDxwHAeHAcDwcBw+BwHHgHAc8AcA/gDgD8AeAH4PwA//+AH//wAMP4AAAAAAAAAAAf//AB//+AH//8AAAB4AAADwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAOAAAB4B///AH//4Af/+AAAAAAAAAAAAAAAAAAAAH//wAf//gB///AAAAeAAAA8AAABwAAAHAAAAcAAABwAAAHAAAAcAAADgAAAeAf//wB//+AH//gAAAAAAAAAAAAAAAAAAAB//8AH//4Af//wAAAHgAAAPAAAAcAAABwAAAHAAAAcAAABwAAAHAAAA4AAAHgH//8Af//gB//4AAAAAAAAAAAAAAAAAAAAf//AB//+AH//8AAAB4AAADwAAAHAAAAcAAABwAAAHAAAAcAAABwAAAOAAAB4B///AH//4Af/+AAAAAAAAAAAQAAABwAAAHwAAAPwAAAfwAAAfgAAAfgAAAf/wAB//AAf/8AH8AAA/AAAPwAAB8AAAHAAAAQAAAAAAAAAAAAAf//8B///wH///ABwHAAHAcAAcBwABwHAAHAcAAcBwABwHAAHg8AAP/gAAf8AAA/gAAAAAAAAAAAAAAAA///AP//8A///wHgAAAcAAcBwABwHBwHAcHAcB4+BwD/4PAH954APn/gAAP8AAAOAAAAAAAAAAAAAD4AAcfwADz/gAfOOCBww4PHHBg+ccGAZxw4AHHDAAcYcAA//gAD//gAD/+AAAAAAAAAAAAAAAAAPgABx/AAPP+AB844AHDDgAccGAZxwYPnHDg8ccMDBxhwAD/+AAP/+AAP/4AAAAAAAAAAAAAAAAA+AAHH8AA8/4BnzjgOcMOBxxwYOHHBg4ccOBxxwwDnGHAGP/4AA//4AA//gAAAAAAAAAAAAAAAAHwAA4/gAHn/A8+ccDzhhwMOODA444MBjjhwHOOGAM4w4Dx//AOH//AAH/8AAAAAAAAAAAAAAAAAfAADj+AAef8Bz5xwHOGHAc44MADjgwAOOHAY44YBzjDgHH/8AAf/8AAf/wAAAAAAAAAAAAAAAAAfAADj+AAef8AD5xweOGHD844MMzjgwzOOHD844YHjjDgAH/8AAf/8AAf/wAAAAAAAAAAAAAAAAHwAAx/gAHn/AAc4cADjhwAOMDAA4wcADjBwAOMHAA4w4AB//AAH/4AAP/wAB/fgAPMPAA4wcADjBwAOMHAA4wcADjBwAPMPAAfx4AA/HAAB8YAAAAAAAAAAAA/AAAP/AAB/+AAPA8AB4B4AHADgwcAPzBwA/8HADngcAOMB4B4ADwPAAHA4AAMDAAAAAAAAAAAAA/AAAP/AAB/+AAPY8AB5h4OHGDg+cYOB5xg4AnGDgAcYOAB5h4AD+PAAH44AAPjAAAGAAAAAAAAAAAAAPwAAD/wAAf/gAD2PAAeYeABxg4AHGDgOcYOD5xg4OHGDggeYeAA/jwAB+OAAD4wAABgAAAAAAAAAAAAD8AAA/8AAH/4AY9jwDnmHgecYODhxg4OHGDg8cYOB5xg4BnmHgCP48AAfjgAA+MAAAYAAAAAAAAAAAAB+AAAf+AAD/8Acex4BzzDwHOMHAA4wcADjBwAOMHAc4wcBzzDwGH8eAAPxwAAfGAAAMAAAAAAAAAAAOAAAA+f/+A5//4An//gAAAAAAAAAAAAAAAAAAAJ//4Dn//g+f/+DgAAAAAAAAMAAABwAAAOP//Aw//8Dj//wHAAAAMAAABwAAAHAAAAA//8AD//wAP//AcAAABwAAAAAAAAAA/gAAP/AAB//AAPA8AA4A4DDgDgPMAOA/wA4D7ADgPOAOB+8B4C/+/AA//4AB//AAAHAAAAAAAAAAAAP//AA//8Bx//wPHAAAw4AADjgAAGOAAAc4AAAzgAAPPAAA4f/8AA//wAB//AAAAAAAAAAAAAAAAA/AAAP/AAB/+AAPA8CB4B4OHADg+cAOA5wA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAAAAAAAAPwAAD/wAAf/gADwPAAeAeABwA4AnADgecAOD5wA4OHADgAeAeAA+HwAB/+AAD/wAAD8AAAAAAAAAAAAD8AAA/8AAH/4AY8DwDngHgecAODhwA4OHADg8cAOB5wA4BngHgCPh8AAf/gAA/8AAA/AAAAAAAAAAAAB+AAAf+AAD/8AceB4DzwDwMOAHA44AcBjgBwHOAHAM4AcDzwDwOHw+AAP/wAAf+AAAfgAAAAAAAAAAAAfgAAH/gAA//AHHgeAc8A8BzgBwAOAHAA4AcADgBwHOAHAc8A8Bh8PgAD/8AAH/gAAH4AAAAAAAAAAAAMAAAAwAAADAAAAMAAAAwAAADAAADtwAAO3AAA7cAAAMAAAAwAAADAAAAMAAAAAAAAAAAAAH5gAB//AAP/4AB4PgAPB/AA4PcADh5wAOPHAA54cADvBwAP4PAAfD4AB//AAP/4AAZ+AAAAAAAAAAAAf8AAB//AAH//AAAH8AAAB4OAADg+AAOB4AA4AgADgAAAOAAABwAH//gAf/+AB//4AAAAAAAAAAAAAAAH/AAAf/wAB//wAAB/AAAAeAAAA4BgADgeAAOD4AA4MAADgAAAcAB//4AH//gAf/+AAAAAAAAAAAAAAAB/wAAH/8AAf/8AYAfwDgAHgcAAODgAA4OAADg8AAOB4AA4BgAHACf/+AB//4AH//gAAAAAAAAAAAAAAA/4AAD/+AAP/+AcAP4BwADwHAAHAAAAcAAABwAAAHAcAAcBwADgGP//AA//8AD//wAAAAAAAAAABAAAAHAAAAfgAAA/wAAA/wAAAf4cAAP/zgAP/+AB/jgA/wAAf4AAP+AAB/AAAHgAAAQAAAAAAAAAAAA//////////////A4BwAHADgAcAOABwA4AHADgAcAOAB4B4AD4fAAH/4AAP/AAAPwAAAAAABAAAAHAAAAfgAAw/wADg/wA+Af4fAAP/8AAP/AAB/g4A/wDgf4AOP+AAB/AAAHgAAAQAAA=='), + 32, + atob("BgkMGhEZEgYMDAwQCAwICxILEBAREBEOEREJCREVEQ8ZEhEUExAOFBQHDREPGBMUERQSEhEUERsREBIMCwwTEg4QERAREQoREQcHDgcYEREREQoPDBEPFg8PDwwIDBMcCgoAAAAAAAAAAAAAACERESEAAAAAAAAAAAAAAAAhIQAGCRAQEhAIDw8XCQ8RABIODRELCw4REwcLCQoPHBscDxISEhISEhoUEBAQEAcHBwcTExQUFBQUDhQUFBQUEBEREBAQEBAQGhARERERBwcHBxAREREREREPEREREREPEQ8="), + 28+(scale<<8)+(1<<16) + ); return this; }; -var imgLock = { - width : 16, height : 16, bpp : 1, - transparent : 0, - buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) +Graphics.prototype.setMiniFont = function(scale) { + // Actual height 16 (15 - 0) + this.setFontCustom( + atob('AAAAAAAAAAAAAP+w/5AAAAAA4ADgAOAA4AAAAAAAAAABgBmAGbAb8D+A+YDZ8B/wf4D5gJmAGQAQAAAAAAAeOD8cMwzxj/GPMYwc/Az4AAAAAHAA+DDIYMjA+YBzAAYADeA7MHMw4zDD4ADAAAAz4H/wzjDHMMMwwbBj4APgADAAAAAA4ADgAAAAAAAAAAfwH/54B+ABAAAAAOABeAcf/gfwAAAAACAAaAD4APgAOABgAAAAAAACAAIAAgA/wAMAAgACAAAAAAAAPAA4AAAAAAIAAgACAAIAAgAAAAAAADAAMAAAAAAAcAfwf4D4AIAAAAA/wH/gwDDAMMAwwDB/4D/AAAAAAGAAwAD/8P/wAAAAAHAw8HDA8MHww7DnMH4wGBAAAMBgyHDcMPww/DDv4MfAAAAAAAHgD+A+YPhgwGAH8AfwAEAAAAAA/GD8cMwwzDDMMM5wx+ABgAAAP8B/4MwwzDDMMMwwx+ADwAAAgADAAMBwwfDPgP4A8ADAAAAAe+D/8M4wxjDGMP5wf+ABwAAAfAB+cMYwwjDCMMYwf+A/wAAAAAAAAAxgBCAAAAAAAAAYPBA4AAAAAAAAAgAHAA+AHMAYYAAAAAAAAAAAAAAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAAABhgHMAPgAcAAgAAAAAAAABgAOAAwbDDsMYA/AA4AAAAAAAD4A/wGBgxzDPsMyQjJDPkM+wYIBxgD+AAAAAAABAA8A/gf8DwwODA/sAfwAHwADAAAP/w//DGMMYwxjDOMP9we+ABwA8AP8Bw4MAwwDDAMMAwwDDgcHDgMMAAAAAA//D/8MAwwDDAMMAw4HB/4D/AAAAAAP/w//DGMMYwxjDGMMQwgBAAAP/w//DDAMMAwwDDAMAADwA/wHDgwDDAMMAwwDDCMOJwc+ADwAAA//D/8AMAAwADAAMAAwD/8P/wAAAAAP/w//AAAABgAHAAMAAwAHD/4P+AAAAAAP/w//AOAB+AOcBw4MBwgDAAEAAA//D/8AAwADAAMAAwADAAAP/w//A8AA8AA+AA8AHwB8AeAHgA//D/8AAAAAD/8P/wcAAcAA8AA4AB4P/w//AAAA8AP8Bw4MAwwDDAMMAwwDDgcH/gP8AAAAAA//D/8MMAwwDDAMYA7gB8ABgADwA/wHDgwDDAMMAwwDDA8ODwf/A/8AAAAAD/8P/wwwDDAMMAx4Dv4HxwEBAAAHjg/HDMMMYwxjDGMONwc+ABwMAAwADAAMAA//D/8MAAwADAAIAAAAD/wP/gAHAAMAAwADAAMAHg/8AAAMAA+AA/AAfgAPAA8AfgPwD4AMAAwAD4AD+AA/AA8A/g/gDwAP4AH8AB8APwH8D8AMAAgBDAMPDgO8APAB8AOcDw8MAwgBCAAOAAeAAeAAfwH/B4AOAAwAAAAMAwwPDB8Mew3jD4MPAwwDAAAAAAAAB//3//QABAAAAAAADgAP4AH+AB8AAQAABAAEAAf/9//wAAAAAAAAAAGAAwAGAAwABgADAAGAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAQA3wHbAZMBswGzAf4A/wAAAAAP/w//AYMBgwGDAYMA/gB8AAAAEAD+Ae8BgwGDAYMBgwDGAAAAMAD+Ae8BgwGDAYMBhw//D/8AAAAYAP4B/wGTAZMBkwGTAP4AcAEAAYAP/w//CQAJAAAwAP4hz3GDMQMxAzGHcf/h/8AAAAAP/w//AYABgAGAAYAA/wB/AAAAAA3/Df8AAAAAOf/9//AAAAAP/w//ADgAfADGAYMBAQAAD/8P/wAAAAAB/wH/AYABgAGAAf8A/wGAAYABgAH/AP8AAAAAAf8B/wGAAYABgAGAAP8AfwAAADAA/gHvAYMBgwGDAYMA/gB8AAAAAAH/8f/xgwGDAYMBgwD+AHwAAAAwAP4B7wGDAYMBgwGHAf/x//AAAAAB/wH/AYABgAEAAAAA5gHzAbMBkwGbAd8AzgEAAYAP/wf/AQMBAwAAAAAB/gH/AAMAAwADAAcB/wH/AAABAAHgAPwAHwAPAH4B8AGAAQAB8AB+AA8APwHwAeAA/AAPAD8B+AHAAQEBgwHOAHwAOAD+AccBAwAAAQAB4AD4EB/wB8A/APgBwAAAAAEBgwGPAZ8B8wHjAcMBAQAAAAAABgf/9/n2AAAAAAAP/w//AAAEAAYAB/nz//AGAAAAAAAAAAAAcABgAGAAcAAwAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), + 32, + atob("AwUHDwoOCwQHBwcJBAcEBgoGCQkKCQoICQoFBQoMCgkPCgoMCwkICwsECAoIDgsMCgwKCgoLCg8KCQoHBgcLCwgJCgkKCQYKCgQECAQOCgoKCgYIBwoIDAkJCAcEBwsQ"), + 16+(scale<<8)+(1<<16) + ); + return this; }; -var imgSteps = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/H///wv4CBn4CD8ACCj4IBj8f+Eeh/wjgCBngCCg/4nEH//4h/+jEP/gRBAQX+jkf/wgB//8GwP4FoICDHgICCBwIA==")) -}; - -var imgBattery = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/4AN4EAg4TBgd///9oEAAQv8ARQRDDQQgCEwQ4OA")) -}; - -var imgBpm = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/4AOn4CD/wCCjgCCv/8jF/wGYgOA5MB//BC4PDAQnjAQPnAQgANA")) -}; - -var imgTemperature = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA==")) -}; - -var imgWind = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/0f//8h///Pn//zAQXzwf/88B//mvGAh18gEevn/DIICB/PwgEBAQMHBAIADFwM/wEAGAP/54CD84CE+eP//wIQU/A==")) -}; - -var imgTimer = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/+B/4CD84CEBAPygFP+F+h/x/+P+fz5/n+HnAQNn5/wuYCBmYCC5kAAQfOgFz80As/ngHn+fD54mC/F+j/+gF/HAQA==")) -}; - -var imgCharging = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA=")) -}; - -var imgWatch = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC")) -}; - - -/* - * INFO ENTRIES - */ -var infoArray = [ - function(){ return [ null, null, "left" ] }, - function(){ return [ "Bangle", imgWatch, "right" ] }, - function(){ return [ E.getBattery() + "%", imgBattery, "left" ] }, - function(){ return [ getSteps(), imgSteps, "left" ] }, - function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm, "left"] }, - function(){ return [ getWeather().temp, imgTemperature, "left" ] }, - function(){ return [ getWeather().wind, imgWind, "left" ] }, -]; -const NUM_INFO=infoArray.length; - - -function getInfoEntry(){ - if(isAlarmEnabled()){ - return [getAlarmMinutes() + " min.", imgTimer, "left"] - } else if(Bangle.isCharging()){ - return [E.getBattery() + "%", imgCharging, "left"] - } else{ - return infoArray[settings.showInfo](); - } -} - - -/* - * Helper - */ -function getSteps() { - var steps = 0; - try{ - if (WIDGETS.wpedom !== undefined) { - steps = WIDGETS.wpedom.getSteps(); - } else if (WIDGETS.activepedom !== undefined) { - steps = WIDGETS.activepedom.getSteps(); - } else { - steps = Bangle.getHealthStatus("day").steps; - } - } catch(ex) { - // In case we failed, we can only show 0 steps. - } - - steps = Math.round(steps/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead - return steps + "k"; -} - - -function getWeather(){ - var weatherJson; - - try { - weatherJson = storage.readJSON('weather.json'); - var weather = weatherJson.weather; - - // Temperature - weather.temp = locale.temp(weather.temp-273.15); - - // Humidity - weather.hum = weather.hum + "%"; - - // Wind - const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - weather.wind = Math.round(wind[1]) + " km/h"; - - return weather - - } catch(ex) { - // Return default - } - +let imgLock = function() { return { - temp: "? °C", - hum: "-", - txt: "-", - wind: "? km/h", - wdir: "-", - wrose: "-" + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) }; -} - -function isAlarmEnabled(){ - try{ - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); - if(alarmObj===undefined || !alarmObj.on){ - return false; - } - - return true; - - } catch(ex){ } - return false; -} - -function getAlarmMinutes(){ - if(!isAlarmEnabled()){ - return -1; - } - - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); - return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); -} - -function increaseAlarm(){ - try{ - var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; - var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, { - timer : (minutes+5)*60*1000, - }); - alarm.reload(); - } catch(ex){ } -} - -function decreaseAlarm(){ - try{ - var minutes = getAlarmMinutes(); - minutes -= 5; - - var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, undefined); - - if(minutes > 0){ - alarm.setAlarm(TIMER_IDX, { - timer : minutes*60*1000, - }); - } - - alarm.reload(); - } catch(ex){ } -} +}; -/* - * DRAW functions +/************************************************ + * Clock Info */ +let clockInfoItems = clock_info.load(); -function draw() { +// Add some custom clock-infos +let weekOfYear = function() { + var date = new Date(); + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + // January 4 is always in week 1. + var week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7); +} + +clockInfoItems[0].items.unshift({ name : "weekofyear", + get : function() { return { text : "Week " + weekOfYear(), + img : null}}, + show : function() {}, + hide : function() {}, +}) + +// Empty for large time +clockInfoItems[0].items.unshift({ name : "nop", + get : function() { return { text : null, + img : null}}, + show : function() {}, + hide : function() {}, +}) + + + +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app: "bwclk", + x : 0, + y: 135, + w: W+1, + h: H-135, + draw : (itm, info, options) => { + var hideClkInfo = info.text == null; + + g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h); + g.setFontAlign(0,0).setColor(g.theme.bg); + + if (options.focus){ + var y = hideClkInfo ? options.y+20 : options.y+2; + var h = hideClkInfo ? options.h-20 : options.h-2; + g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused + g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused + } + + // In case we hide the clkinfo, we show the time again as the time should + // be drawn larger. + if(hideClkInfo){ + drawTime(); + return; + } + + // Set text and font + var image = info.img; + var text = String(info.text); + if(text.split('\n').length > 1){ + g.setMiniFont(); + } else { + g.setSmallFont(); + } + + // Compute sizes + var strWidth = g.stringWidth(text); + var imgWidth = image == null ? 0 : 24; + var midx = options.x+options.w/2; + + // Draw + if (image) { + var scale = imgWidth / image.width; + g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); + } + g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); + + // In case we are in focus and the focus box changes (fullscreen yes/no) + // we draw the time again. Otherwise it could happen that a while line is + // not cleared correctly. + if(options.focus) drawTime(); + } +}); + + +/************************************************ + * Draw + */ +let draw = function() { // Queue draw again queueDraw(); @@ -269,17 +198,17 @@ function draw() { drawTime(); drawLock(); drawWidgets(); -} +}; -function drawDate(){ +let drawDate = function() { // Draw background - var y = H/5*2; - g.reset().clearRect(0,0,W,W); + var y = getLineY() + g.reset().clearRect(0,0,W,y); // Draw date - y = parseInt(y/2); - y += settings.fullscreen ? 2 : 15; + y = parseInt(y/2)+4; + y += isFullscreen() ? 0 : 8; var date = new Date(); var dateStr = date.getDate(); dateStr = ("0" + dateStr).substr(-2); @@ -293,186 +222,150 @@ function drawDate(){ var fullDateW = dateW + 10 + dayW; g.setFontAlign(-1,0); - g.setMediumFont(); - g.setColor(g.theme.fg); - g.drawString(dateStr, W/2 - fullDateW / 2, y+1); - - g.setSmallFont(); g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12); g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11); -} + + g.setMediumFont(); + g.setColor(g.theme.fg); + g.drawString(dateStr, W/2 - fullDateW / 2, y+2); +}; -function drawTime(){ +let drawTime = function() { + var hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0; + // Draw background - var y = H/5*2 + (settings.fullscreen ? 0 : 8); - g.setColor(g.theme.fg); - g.fillRect(0,y,W,H); + var y1 = getLineY(); + var y = y1; var date = new Date(); - // Draw time - g.setColor(g.theme.bg); - g.setFontAlign(0,0); - - var hours = String(date.getHours()); - var minutes = date.getMinutes(); - minutes = minutes < 10 ? String("0") + minutes : minutes; - var colon = settings.hideColon ? "" : ":"; - var timeStr = hours + colon + minutes; + var timeStr = locale.time(date, 1); + if (settings.hideColon) + timeStr = timeStr.replace(":", ""); // Set y coordinates correctly y += parseInt((H - y)/2) + 5; - var infoEntry = getInfoEntry(); - var infoStr = infoEntry[0]; - var infoImg = infoEntry[1]; - var printImgLeft = infoEntry[2] == "left"; - - // Show large or small time depending on info entry - if(infoStr == null){ - if(settings.hideColon){ - g.setXLargeFont(); - } else { - g.setLargeFont(); - } + if (hideClkInfo){ + g.setLargeFont(); } else { y -= 15; g.setMediumFont(); } - g.drawString(timeStr, W/2, y); - // Draw info if set - if(infoStr == null){ - return; - } + // Clear region and draw time + g.setColor(g.theme.fg); + g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0)); - y += 35; + g.setColor(g.theme.bg); g.setFontAlign(0,0); - g.setSmallFont(); - var imgWidth = 0; - if(infoImg !== undefined){ - imgWidth = infoImg.width; - var strWidth = g.stringWidth(infoStr); - g.drawImage( - infoImg, - W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - infoImg.width/2, - y - infoImg.height/2 - ); - } - g.drawString(infoStr, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3); -} + g.drawString(timeStr, W/2, y); +}; -function drawLock(){ +let drawLock = function() { if(settings.showLock && Bangle.isLocked()){ g.setColor(g.theme.fg); - g.drawImage(imgLock, W-16, 2); + g.drawImage(imgLock(), W-16, 2); } -} +}; -function drawWidgets(){ - if(settings.fullscreen){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +let drawWidgets = function() { + if(isFullscreen()){ + widget_utils.hide(); } else { Bangle.drawWidgets(); } -} +}; - -/* - * Draw timeout +/************************************************ + * Listener */ // timeout used to update every minute -var drawTimeout; +let drawTimeout; // schedule a draw for the next minute -function queueDraw() { +let queueDraw = function() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); -} +}; // Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +let lcdListenerBw = function(on) { if (on) { draw(); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } -}); +}; +Bangle.on('lcdPower', lcdListenerBw); -Bangle.on('lock', function(isLocked) { +let lockListenerBw = function(isLocked) { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; + + if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ + // If we have to show the widgets again, we load it from our + // cache and not through Bangle.loadWidgets as its much faster! + widget_utils.show(); + } + draw(); -}); +}; +Bangle.on('lock', lockListenerBw); -Bangle.on('charging',function(charging) { - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - draw(); -}); +let charging = function(charging){ + // Jump to battery + clockInfoMenu.setItem(0, 2); + drawTime(); +} +Bangle.on('charging', charging); -Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.2); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.2); - var lower = g.getHeight() - upper; +let kill = function(){ + clockInfoMenu.remove(); + delete clockInfoMenu; +}; +E.on("kill", kill); - var is_left = e.x < left; - var is_right = e.x > right; - var is_upper = e.y < upper; - var is_lower = e.y > lower; - - if(is_upper){ - Bangle.buzz(40, 0.6); - increaseAlarm(); - drawTime(); - } - - if(is_lower){ - Bangle.buzz(40, 0.6); - decreaseAlarm(); - drawTime(); - } - - if(is_right){ - Bangle.buzz(40, 0.6); - settings.showInfo = (settings.showInfo+1) % NUM_INFO; - drawTime(); - } - - if(is_left){ - Bangle.buzz(40, 0.6); - settings.showInfo = settings.showInfo-1; - settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo; - drawTime(); - } -}); - - -E.on("kill", function(){ - storage.write(SETTINGS_FILE, settings); -}); - - -/* - * Draw clock the first time +/************************************************ + * Startup Clock */ + // The upper part is inverse i.e. light if dark and dark if light theme // is enabled. In order to draw the widgets correctly, we invert the // dark/light theme as well as the colors. +let themeBackup = g.theme; g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear(); +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', lcdListenerBw); + Bangle.removeListener('lock', lockListenerBw); + Bangle.removeListener('charging', charging); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // save settings + kill(); + E.removeListener("kill", kill); + g.setTheme(themeBackup); + widget_utils.show(); + } +}); + // Load widgets and draw clock the first time Bangle.loadWidgets(); + +// Draw first time draw(); -// Show launcher when middle button pressed -Bangle.setUI("clock"); +} // End of app scope diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index eba1449a6..de84ba947 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,18 +1,20 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.09", - "description": "BW Clock.", + "version": "0.34", + "description": "A very minimalistic clock.", "readme": "README.md", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}], "type": "clock", - "tags": "clock", + "tags": "clock,clkinfo", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "allow_emulator": true, "storage": [ {"name":"bwclk.app.js","url":"app.js"}, {"name":"bwclk.img","url":"app-icon.js","evaluate":true}, {"name":"bwclk.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"bwclk.setting.json"}] } diff --git a/apps/bwclk/screenshot.png b/apps/bwclk/screenshot.png index 550913422..37acf7cc0 100644 Binary files a/apps/bwclk/screenshot.png and b/apps/bwclk/screenshot.png differ diff --git a/apps/bwclk/screenshot_2.png b/apps/bwclk/screenshot_2.png index ccbc9aae1..8d2f1717f 100644 Binary files a/apps/bwclk/screenshot_2.png and b/apps/bwclk/screenshot_2.png differ diff --git a/apps/bwclk/screenshot_3.png b/apps/bwclk/screenshot_3.png index 5bf7083f0..d52057569 100644 Binary files a/apps/bwclk/screenshot_3.png and b/apps/bwclk/screenshot_3.png differ diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index a421e81a9..8bcf0ae0f 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -4,7 +4,7 @@ // initialize with default settings... const storage = require('Storage') let settings = { - fullscreen: false, + screen: "Normal", showLock: true, hideColon: false, }; @@ -17,21 +17,21 @@ storage.write(SETTINGS_FILE, settings) } - + var screenOptions = ["Normal", "Dynamic", "Full"]; E.showMenu({ '': { 'title': 'BW Clock' }, '< Back': back, - 'Fullscreen': { - value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), - onchange: () => { - settings.fullscreen = !settings.fullscreen; + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: 2, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; save(); }, }, 'Show Lock': { value: settings.showLock, - format: () => (settings.showLock ? 'Yes' : 'No'), onchange: () => { settings.showLock = !settings.showLock; save(); @@ -39,7 +39,6 @@ }, 'Hide Colon': { value: settings.hideColon, - format: () => (settings.hideColon ? 'Yes' : 'No'), onchange: () => { settings.hideColon = !settings.hideColon; save(); diff --git a/apps/bwclklite/ChangeLog b/apps/bwclklite/ChangeLog new file mode 100644 index 000000000..bba66928b --- /dev/null +++ b/apps/bwclklite/ChangeLog @@ -0,0 +1,39 @@ +0.01: New App. +0.02: Use build in function for steps and other improvements. +0.03: Adapt colors based on the theme of the user. +0.04: Steps can be hidden now such that the time is even larger. +0.05: Included icons for information. +0.06: Design and usability improvements. +0.07: Improved positioning. +0.08: Select the color of widgets correctly. Additional settings to hide colon. +0.09: Larger font size if colon is hidden to improve readability further. +0.10: HomeAssistant integration if HomeAssistant is installed. +0.11: Performance improvements. +0.12: Implements a 2D menu. +0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled. +0.14: Adds humidity to weather data. +0.15: Added option for a dynamic mode to show widgets only if unlocked. +0.16: You can now show your agenda if your calendar is synced with Gadgetbridge. +0.17: Fix - Step count was no more shown in the menu. +0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set. +0.19: Fix - Compatibility with "Digital clock widget" +0.20: Better handling of async data such as getPressure. +0.21: On the default menu the week of year can be shown. +0.22: Use the new clkinfo module for the menu. +0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. +0.24: Update clock_info to avoid a redraw +0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16. + ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.26: Use clkinfo.addInteractive instead of a custom implementation +0.27: Clean out some leftovers in the remove function after switching to +clkinfo.addInteractive that would cause ReferenceError. +0.28: Option to show (1) time only and (2) week of year. +0.29: use setItem of clockInfoMenu to change the active item +0.30: Use widget_utils +0.31: Use clock_info module as an app +0.32: Diverge from BW Clock. Change out the custom font for a standard bitmap one to speed up loading times. + Remove invertion of theme as this doesn'twork very well with fastloading. + Do an quick inital fillRect on theclock info area. +0.33: Make the border of the clock_info box extend all the way to the right of the screen. +0.34: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) +0.35: Support 12-hour time format diff --git a/apps/bwclklite/README.md b/apps/bwclklite/README.md new file mode 100644 index 000000000..1ad320894 --- /dev/null +++ b/apps/bwclklite/README.md @@ -0,0 +1,30 @@ +# BW Clock Lite +This is a fork of a very minimalistic clock. + +![](screenshot.png) + +## Features +The BW clock implements features that are exposed by other apps through the `clkinfo` module. +For example, if you install the Simple Timer app, this menu item will be shown if you first +touch the bottom of the screen and then swipe left/right to the Simple Timer menu. To select +sub-items simply swipe up/down. To run an action (e.g. add 5 min), simply select the clkinfo (border) and touch on the item again. See also the screenshot below: + +![](screenshot_3.png) + +Note: Check out the settings to change different themes. + +## Settings +- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). +- Enable/disable lock icon in the settings. Useful if fullscreen mode is on. +- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. +- Your bangle uses the sys color settings so you can change the color too. + +## Thanks to +- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. +- Icons created by Flaticon + +## Creator +[David Peer](https://github.com/peerdavid) + +## Contributors +thyttan diff --git a/apps/bwclklite/app-icon.js b/apps/bwclklite/app-icon.js new file mode 100644 index 000000000..1df0fa6a5 --- /dev/null +++ b/apps/bwclklite/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIcah0EgEB/H8iFsAoOY4kMBYMDhmGgXkAoUGiWkAoQQBoAFCjgnCAoM4hgFDuEI+wpC8EKyg1C/0eAoMAsEAiQvBAAeAApQAB/4Ao+P4v/wn0P8Pgn/wnkH4Pjv/j/nn9PH//n/nj/IFF4F88AXBAoM88EcAoPHj//jlDAoOf/+Y+YFHjnnjAjBEIIjD+BHDO9IALA==")) diff --git a/apps/bwclklite/app.js b/apps/bwclklite/app.js new file mode 100644 index 000000000..794b39aa9 --- /dev/null +++ b/apps/bwclklite/app.js @@ -0,0 +1,326 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + +/************************************************ + * Includes + */ +const locale = require('locale'); +const storage = require('Storage'); +const clock_info = require("clock_info"); +const widget_utils = require("widget_utils"); + +/************************************************ + * Globals + */ +const SETTINGS_FILE = "bwclklite.setting.json"; +const W = g.getWidth(); +const H = g.getHeight(); + +/************************************************ + * Settings + */ +let settings = { + screen: "Normal", + showLock: true, + hideColon: false, + menuPosX: 0, + menuPosY: 0, +}; + +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key]; +} + +let isFullscreen = function() { + let s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked(); + } else { + return s == "full"; + } +}; + +let getLineY = function(){ + return H/5*2 + (isFullscreen() ? 0 : 8); +}; + +/************************************************ + * Assets + */ +let imgLock = function() { + return { + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) + }; +}; + + +/************************************************ + * Clock Info + */ +let clockInfoItems = clock_info.load(); + +// Add some custom clock-infos +let weekOfYear = function() { + let date = new Date(); + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + // January 4 is always in week 1. + let week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7); +}; + +clockInfoItems[0].items.unshift({ name : "weekofyear", + get : function() { return { text : "Week " + weekOfYear(), + img : null};}, + show : function() {}, + hide : function() {}, +}); + +// Empty for large time +clockInfoItems[0].items.unshift({ name : "nop", + get : function() { return { text : null, + img : null};}, + show : function() {}, + hide : function() {}, +}); + + + +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app: "bwclklite", + x : 0, + y: 135, + w: W+1, + h: H-135, + draw : (itm, info, options) => { + let hideClkInfo = info.text == null; + + g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h); + g.setFontAlign(0,0).setColor(g.theme.bg); + + if (options.focus){ + let y = hideClkInfo ? options.y+20 : options.y+2; + let h = hideClkInfo ? options.h-20 : options.h-2; + g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused + g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused + } + + // In case we hide the clkinfo, we show the time again as the time should + // be drawn larger. + if(hideClkInfo){ + drawTime(); + return; + } + + // Set text and font + let image = info.img; + let text = String(info.text); + if(text.split('\n').length > 1){ + g.setFont("6x8"); //g.setMiniFont(); + } else { + g.setFont("6x8:3"); //g.setSmallFont(); + } + + // Compute sizes + let strWidth = g.stringWidth(text); + let imgWidth = image == null ? 0 : 24; + let midx = options.x+options.w/2; + + // Draw + if (image) { + let scale = imgWidth / image.width; + g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); + } + g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); + + // In case we are in focus and the focus box changes (fullscreen yes/no) + // we draw the time again. Otherwise it could happen that a while line is + // not cleared correctly. + if(options.focus) drawTime(); + } +}); + + +/************************************************ + * Draw + */ +let draw = function() { + // Queue draw again + queueDraw(); + + // Draw clock + drawDate(); + drawTime(); + drawLock(); + drawWidgets(); +}; + + +let drawDate = function() { + // Draw background + let y = getLineY(); + g.reset().clearRect(0,0,W,y); + + // Draw date + y = parseInt(y/2)+4; + y += isFullscreen() ? 0 : 8; + let date = new Date(); + let dateStr = date.getDate(); + dateStr = ("0" + dateStr).substr(-2); + g.setFont("6x8:4"); //g.setMediumFont(); // Needed to compute the width correctly + let dateW = g.stringWidth(dateStr); + + g.setFont("6x8:3"); //g.setSmallFont(); + let dayStr = locale.dow(date, true); + let monthStr = locale.month(date, 1); + let dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr)); + let fullDateW = dateW + 10 + dayW; + + g.setFontAlign(-1,0); + g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12); + g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11); + + g.setFont("6x8:4"); //g.setMediumFont(); + g.setColor(g.theme.fg); + g.drawString(dateStr, W/2 - fullDateW / 2, y+2); +}; + + +let drawTime = function() { + let hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0; + + // Draw background + let y1 = getLineY(); + let y = y1; + let date = new Date(); + + var timeStr = locale.time(date, 1); + if (settings.hideColon) + timeStr = timeStr.replace(":", ""); + + // Set y coordinates correctly + y += parseInt((H - y)/2) + 5; + + if (hideClkInfo){ + g.setFont("6x8:5"); //g.setLargeFont(); + } else { + y -= 15; + g.setFont("6x8:4"); //g.setMediumFont(); + } + + // Clear region and draw time + g.setColor(g.theme.fg); + g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0)); + + g.setColor(g.theme.bg); + g.setFontAlign(0,0); + g.drawString(timeStr, W/2, y); +}; + + +let drawLock = function() { + if(settings.showLock && Bangle.isLocked()){ + g.setColor(g.theme.fg); + g.drawImage(imgLock(), W-16, 2); + } +}; + + +let drawWidgets = function() { + if(isFullscreen()){ + widget_utils.hide(); + } else { + Bangle.drawWidgets(); + } +}; + + +/************************************************ + * Listener + */ +// 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)); +}; + + +// Stop updates when LCD is off, restart when on +let lcdListenerBw = function(on) { + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}; +Bangle.on('lcdPower', lcdListenerBw); + +let lockListenerBw = function(isLocked) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ + // If we have to show the widgets again, we load it from our + // cache and not through Bangle.loadWidgets as its much faster! + widget_utils.show(); + } + + draw(); +}; +Bangle.on('lock', lockListenerBw); + +let charging = function(charging){ + // Jump to battery + clockInfoMenu.setItem(0, 2); + drawTime(); +}; +Bangle.on('charging', charging); + +let kill = function(){ + clockInfoMenu.remove(); + delete clockInfoMenu; +}; +E.on("kill", kill); + +/************************************************ + * Startup Clock + */ + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', lcdListenerBw); + Bangle.removeListener('lock', lockListenerBw); + Bangle.removeListener('charging', charging); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // save settings + kill(); + E.removeListener("kill", kill); + Bangle.removeListener('charging', charging); + widget_utils.show(); + } +}); + +// Load widgets and draw clock the first time +Bangle.loadWidgets(); + +// Draw first time +g.setColor(g.theme.fg).fillRect(0,135,W,H); // Otherwise this rect will wait for clock_info before updating +draw(); + +} // End of app scope diff --git a/apps/bwclklite/app.png b/apps/bwclklite/app.png new file mode 100644 index 000000000..5073f0ed0 Binary files /dev/null and b/apps/bwclklite/app.png differ diff --git a/apps/bwclklite/metadata.json b/apps/bwclklite/metadata.json new file mode 100644 index 000000000..14af7131c --- /dev/null +++ b/apps/bwclklite/metadata.json @@ -0,0 +1,48 @@ +{ + "id": "bwclklite", + "name": "BW Clock Lite", + "version": "0.35", + "description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [ + { + "url": "screenshot.png" + }, + { + "url": "screenshot_2.png" + }, + { + "url": "screenshot_3.png" + } + ], + "type": "clock", + "tags": "clock,clkinfo", + "supports": [ + "BANGLEJS2" + ], + "dependencies": { + "clock_info": "module" + }, + "allow_emulator": true, + "storage": [ + { + "name": "bwclklite.app.js", + "url": "app.js" + }, + { + "name": "bwclklite.img", + "url": "app-icon.js", + "evaluate": true + }, + { + "name": "bwclklite.settings.js", + "url": "settings.js" + } + ], + "data": [ + { + "name": "bwclklite.setting.json" + } + ] +} diff --git a/apps/bwclklite/screenshot.png b/apps/bwclklite/screenshot.png new file mode 100644 index 000000000..28983c9c4 Binary files /dev/null and b/apps/bwclklite/screenshot.png differ diff --git a/apps/bwclklite/screenshot_2.png b/apps/bwclklite/screenshot_2.png new file mode 100644 index 000000000..8d2f1717f Binary files /dev/null and b/apps/bwclklite/screenshot_2.png differ diff --git a/apps/bwclklite/screenshot_3.png b/apps/bwclklite/screenshot_3.png new file mode 100644 index 000000000..573675d28 Binary files /dev/null and b/apps/bwclklite/screenshot_3.png differ diff --git a/apps/bwclklite/settings.js b/apps/bwclklite/settings.js new file mode 100644 index 000000000..4c59198c6 --- /dev/null +++ b/apps/bwclklite/settings.js @@ -0,0 +1,48 @@ +(function(back) { + const SETTINGS_FILE = "bwclklite.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + screen: "Normal", + showLock: true, + hideColon: false, + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + var screenOptions = ["Normal", "Dynamic", "Full"]; + E.showMenu({ + '': { 'title': 'BW Clock' }, + '< Back': back, + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: 2, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; + save(); + }, + }, + 'Show Lock': { + value: settings.showLock, + onchange: () => { + settings.showLock = !settings.showLock; + save(); + }, + }, + 'Hide Colon': { + value: settings.hideColon, + onchange: () => { + settings.hideColon = !settings.hideColon; + save(); + }, + } + }); + }) diff --git a/apps/c25k/ChangeLog b/apps/c25k/ChangeLog new file mode 100644 index 000000000..0e7594334 --- /dev/null +++ b/apps/c25k/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add rep info to time screen +0.03: Add option to pause/resume workout (Bangle.js 1 only) +0.04: Add possibility of creating a custom exercise diff --git a/apps/c25k/README.md b/apps/c25k/README.md new file mode 100644 index 000000000..8237199d5 --- /dev/null +++ b/apps/c25k/README.md @@ -0,0 +1,96 @@ +# C25K + +Unofficial app for the Couch to 5k training plan. +From being a couch-potato to running 5k in 8 weeks! + +Each week has 3 training days, ideally with rest days between them. + +Each day's programme consists of running for a certain time with occasional walking/resting phases. +When walking is part of the programme, the (run+walk) stages are repeated a number of times. + +![](c25k-scrn1.png) +![](c25k-scrn2.png) +![](c25k-scrn3.png) + +## Features + +- Show remaining time in seconds for each phase +- Vibrates on phase changes +- Keeps screen on to allow quickly glancing at the time while running +- Shows time on button press + +## Usage + +If you know the week and day of the programme you'd like to start, set `Week` and `Day` to the appropriate values in the main menu and press `Start`. + +**Example**: +To start the programme of the **second day** of **week 4**: +![](c25k-scrn4.png) + +--- + +Alternatively, you can go to the `View plan` menu to look at all the programmes and select the one you'd like to start. + +**Example**: +Go to the `View plan` menu: +![](c25k-scrn5.png) + +Select the programme to start it: +![](c25k-scrn6.png) + +--- + +The format of the `View menu` is `w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps})`. + +For example `w6d1(r:6|w:3|x2)` means: +`it's the programme of day 1 on week 6`, +`it consists of running for 6 minutes`, +`followed by walking for 3`, +`done 2 times back to back`. + +--- + +### Create a custom excercise + +Under the `Custom run` menu, it's possible to create a custom excercise. +![](c25k-scrn9.png) + +Some important details/limitations: + +- To disable walking: set `walk` to `0` +- When walking is set to `0`, the repetition count is set to `1`. +- When repetition is set to `2` or higher, `walk` is set to `1`. + +**Unfortunately, the value in the menu do not update to reflect the changes, so I recommend setting the values with the rules above in mind.** + +--- + +### Show extra info: + +If you ever need to peek at the time, just press the middle (or only) physical button on the watch: +![](c25k-scrn7.png) + +This view also shows `current rep / total reps` at the top. + +--- + +### Pause/resume workout: + +**This is currently only available on Bangle.js 1.** + +Press the top button to pause or to resume the active programme: +![](c25k-scrn8.png) + +--- + +## Disclaimer + +This app was hacked together in a day with no JS knowledge. +It's probably inefficient and buggy, but it does what I needed it to do: allow me to follow the C25K programme without a phone. + +The app was designed with a Bangle.js 1 in mind, as that's the one I have. +It *should* work fine on the Bangle.js 2, but I couldn't test it on real hardware. + +--- + +Made with <3 by [Erovia](https://github.com/Erovia/BangleApps/tree/c25k) diff --git a/apps/c25k/app-icon.js b/apps/c25k/app-icon.js new file mode 100644 index 000000000..6b85dbf29 --- /dev/null +++ b/apps/c25k/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoPk9G9gsj14lZhWq0AEBgtVqALmhQJBAQMFBIICCBc4ADBQYLnAQQKEBcibETQIABHggLiAEQqEh/wgACCBcpXDBAIKDBcqJDh//BQYLkHwg7GBcY7FU5ALgAEQA=")) diff --git a/apps/c25k/app.js b/apps/c25k/app.js new file mode 100644 index 000000000..eed918e46 --- /dev/null +++ b/apps/c25k/app.js @@ -0,0 +1,290 @@ +var week = 1; // Stock plan: programme week +var day = 1; // Stock plan: programe day +var run = 1; // Custom plan: running time +var walk = 0; // Custom plan: walking time +var reps = 1; // Custom plan: repetition count + +var time; // To store the date + +var loop; // To store how many times we will have to do a countdown +var rep; // The current rep counter +var counter; // The seconds counter +var currentMode; // Either "run" or "walk" +var mainInterval; // Ticks every second, checking if a new countdown is needed +var activityInterval; // Ticks every second, doing the countdown +var extraInfoWatch; // Watch for button presses to show additional info +var paused = false; // Track pause state +var pauseOrResumeWatch; // Watch for button presses to pause/resume countdown +var defaultFontSize = (process.env.HWVERSION == 2) ? 7 : 9; // Default font size, Banglejs 2 has smaller +var activityBgColour; // Background colour of current activity +var currentActivity; // To store the current activity + +function outOfTime() { + buzz(); + + // Once we're done + if (loop == 0) { + clearWatch(extraInfoWatch); // Don't watch for button presses anymore + if (pauseOrResumeWatch) clearWatch(pauseOrResumeWatch); // Don't watch for button presses anymore + g.setBgColor("#75C0E0"); // Blue background for the "Done" text + drawText("Done", defaultFontSize); // Write "Done" to screen + g.reset(); + setTimeout(E.showMenu, 5000, mainmenu); // Show the main menu again after 5secs + clearInterval(mainInterval); // Stop the main interval from starting a new activity + mainInterval = undefined; + currentMode = undefined; + } +} + +// Buzz 3 times on state transitions +function buzz() { + Bangle.buzz(500) + .then(() => new Promise(resolve => setTimeout(resolve, 200))) + .then(() => Bangle.buzz(500)) + .then(() => new Promise(resolve => setTimeout(resolve, 200))) + .then(() => Bangle.buzz(500)); +} + +function drawText(text, size){ + g.clear(); + g.setFontAlign(0, 0); // center font + g.setFont("6x8", size); + g.drawString(text, g.getWidth() / 2, g.getHeight() / 2); +} + +function countDown() { + if (!paused) { + var text = ""; + var size = defaultFontSize; + if (time) { + var total = ("walk" in currentActivity) ? currentActivity.repetition : 1; + text += rep + "/" + total + "\n"; // Show the current/total rep count when time is shown + size -= 2; // Use smaller font size to fit everything nicely on the screen + } + text += (currentMode === "run") ? "Run\n" + counter : "Walk\n" + counter; // Switches output text + if (time) text += "\n" + time; + drawText(text, size); // draw the current mode and seconds + Bangle.setLCDPower(1); // keep the watch LCD lit up + + counter--; // Reduce the seconds + + // If the current activity is done + if (counter < 0) { + clearInterval(activityInterval); + activityInterval = undefined; + outOfTime(); + return; + } + } +} + +function startTimer() { + // If something is already running, do nothing + if (activityInterval) return; + + // Switches between the two modes + if (!currentMode || currentMode === "walk") { + currentMode = "run"; + rep++; // Increase the rep counter every time a "run" activity starts + counter = currentActivity.run * 60; + activityBgColour = "#ff5733"; // Red background for running + } + else { + currentMode = "walk"; + counter = currentActivity.walk * 60; + activityBgColour = "#4da80a"; // Green background for walking + + } + + g.setBgColor(activityBgColour); + countDown(); + if (!activityInterval) { + loop--; // Reduce the number of iterations + activityInterval = setInterval(countDown, 1000); // Start a new activity + } +} + +function showTime() { + if (time) return; // If clock is already shown, don't do anything even if the button was pressed again + // Get the time and format it with a leading 0 if necessary + var d = new Date(); + var h = d.getHours(); + var m = d.getMinutes(); + time = h + ":" + m.toString().padStart(2, 0); + setTimeout(function() { time = undefined; }, 5000); // Hide clock after 5secs +} + +// Populate the PLAN menu +function populatePlan() { + for (var i = 0; i < PLAN.length; i++) { + for (var j = 0; j < PLAN[i].length; j++) { + // Ever line will have the following format: + // w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps}) + var name = "w" + (i + 1) + "d" + (j + 1); + if (process.env.HWVERSION == 2) name += "\n"; // Print in 2 lines to accomodate the Bangle.js 2 screen + name += "(r:" + PLAN[i][j].run; + if ("walk" in PLAN[i][j]) name += "|w:" + PLAN[i][j].walk; + if ("repetition" in PLAN[i][j]) name += "|x" + PLAN[i][j].repetition; + name += ")"; + // Each menu item will have a function that start the program at the selected day + planmenu[name] = getFunc(i, j); + } + } +} + +// Helper function to generate functions for the activePlan menu +function getFunc(i, j) { + return function() { + currentActivity = PLAN[i][j]; + startActivity(); + }; +} + +function startActivity() { + loop = ("walk" in currentActivity) ? currentActivity.repetition * 2 : 1; + rep = 0; + + E.showMenu(); // Hide the main menu + extraInfoWatch = setWatch(showTime, (process.env.HWVERSION == 2) ? BTN1 : BTN2, {repeat: true}); // Show the clock on button press + if (process.env.HWVERSION == 1) pauseOrResumeWatch = setWatch(pauseOrResumeActivity, BTN1, {repeat: true}); // Pause or resume on button press (Bangle.js 1 only) + buzz(); + mainInterval = setInterval(function() {startTimer();}, 1000); // Check every second if we need to do something +} + +// Pause or resume current activity +function pauseOrResumeActivity() { + paused = !paused; + buzz(); + if (paused) { + g.setBgColor("#fdd835"); // Yellow background for pause screen + drawText("Paused", (process.env.HWVERSION == 2) ? defaultFontSize - 3 : defaultFontSize - 2); // Although the font size is configured here, this feature does not work on Bangle.js 2 as the only physical button is tied to the extra info screen already + } + else { + g.setBgColor(activityBgColour); + } +} + +const PLAN = [ + [ + {"run": 1, "walk": 1.5, "repetition": 8}, + {"run": 1, "walk": 1.5, "repetition": 8}, + {"run": 1, "walk": 1.5, "repetition": 8}, + ], + [ + {"run": 1.5, "walk": 2, "repetition": 6}, + {"run": 1.5, "walk": 2, "repetition": 6}, + {"run": 1.5, "walk": 2, "repetition": 6}, + ], + [ + {"run": 2, "walk": 2, "repetition": 5}, + {"run": 2.5, "walk": 2.5, "repetition": 4}, + {"run": 2.5, "walk": 2.5, "repetition": 4}, + ], + [ + {"run": 3, "walk": 2, "repetition": 5}, + {"run": 3, "walk": 2, "repetition": 5}, + {"run": 4, "walk": 2.5, "repetition": 3}, + ], + [ + {"run": 5, "walk": 2, "repetition": 3}, + {"run": 8, "walk": 5, "repetition": 2}, + {"run": 20}, + ], + [ + {"run": 6, "walk": 3, "repetition": 2}, + {"run": 10, "walk": 3, "repetition": 2}, + {"run": 25}, + ], + [ + {"run": 25}, + {"run": 25}, + {"run": 25}, + ], + [ + {"run": 30}, + {"run": 30}, + {"run": 30}, + ], +]; + +var customRun = {"run": 1}; + +// Main menu +var mainmenu = { + "": { "title": "-- C25K --" }, + "Week": { + value: week, + min: 1, max: PLAN.length, step: 1, + onchange : v => { week = v; } + }, + "Day": { + value: day, + min: 1, max: 3, step: 1, + onchange: v => { day = v; } + }, + "View plan": function() { E.showMenu(planmenu); }, + "Custom run": function() { E.showMenu(custommenu); }, + "Start": function() { + currentActivity = PLAN[week - 1][day -1]; + startActivity(); + }, + "Exit": function() { load(); }, +}; + +// Plan view +var planmenu = { + "": { title: "-- Plan --" }, + "< Back": function() { E.showMenu(mainmenu);}, +}; + +// Custom view +var custommenu = { + "": { title : "-- Cust. run --" }, + "< Back": function() { E.showMenu(mainmenu);}, + "Run (mins)": { + value: run, + min: 1, max: 150, step: 1, + wrap: true, + onchange: v => { customRun.run = v; } + }, + "Walk (mins)": { + value: walk, + min: 0, max: 10, step: 1, + onchange: v => { + if (v > 0) { + if (reps == 1) { reps = 2; } // Walking only makes sense with multiple reps + customRun.repetition = reps; + customRun.walk = v; + } + else { + // If no walking, delete both the reps and walk data + delete customRun.repetition; + delete customRun.walk; + } + walk = v; + } + }, + "Reps": { + value: reps, + min: 1, max: 10, step: 1, + onchange: v => { + if (v > 1) { + if (walk == 0) { walk = 1; } // Multiple reps only make sense with walking phases + customRun.walk = walk; + customRun.repetition = v; + } + else { + // If no multiple reps, delete both the reps and walk data + delete customRun.repetition; + delete customRun.walk; + } + reps = v; + } + }, + "Start": function() { currentActivity = customRun; startActivity(); } +}; + +// Populate the activePlan menu view +populatePlan(); +// Actually display the menu +E.showMenu(mainmenu); diff --git a/apps/c25k/app.png b/apps/c25k/app.png new file mode 100644 index 000000000..6b3a9ba95 Binary files /dev/null and b/apps/c25k/app.png differ diff --git a/apps/c25k/c25k-scrn1.png b/apps/c25k/c25k-scrn1.png new file mode 100644 index 000000000..c4d9ea24b Binary files /dev/null and b/apps/c25k/c25k-scrn1.png differ diff --git a/apps/c25k/c25k-scrn2.png b/apps/c25k/c25k-scrn2.png new file mode 100644 index 000000000..ba064200e Binary files /dev/null and b/apps/c25k/c25k-scrn2.png differ diff --git a/apps/c25k/c25k-scrn3.png b/apps/c25k/c25k-scrn3.png new file mode 100644 index 000000000..6901abf31 Binary files /dev/null and b/apps/c25k/c25k-scrn3.png differ diff --git a/apps/c25k/c25k-scrn4.png b/apps/c25k/c25k-scrn4.png new file mode 100644 index 000000000..ad64da947 Binary files /dev/null and b/apps/c25k/c25k-scrn4.png differ diff --git a/apps/c25k/c25k-scrn5.png b/apps/c25k/c25k-scrn5.png new file mode 100644 index 000000000..ca32abdfa Binary files /dev/null and b/apps/c25k/c25k-scrn5.png differ diff --git a/apps/c25k/c25k-scrn6.png b/apps/c25k/c25k-scrn6.png new file mode 100644 index 000000000..53f5221d7 Binary files /dev/null and b/apps/c25k/c25k-scrn6.png differ diff --git a/apps/c25k/c25k-scrn7.png b/apps/c25k/c25k-scrn7.png new file mode 100644 index 000000000..407afd48b Binary files /dev/null and b/apps/c25k/c25k-scrn7.png differ diff --git a/apps/c25k/c25k-scrn8.png b/apps/c25k/c25k-scrn8.png new file mode 100644 index 000000000..1cd92d876 Binary files /dev/null and b/apps/c25k/c25k-scrn8.png differ diff --git a/apps/c25k/c25k-scrn9.png b/apps/c25k/c25k-scrn9.png new file mode 100644 index 000000000..53dbaad1f Binary files /dev/null and b/apps/c25k/c25k-scrn9.png differ diff --git a/apps/c25k/metadata.json b/apps/c25k/metadata.json new file mode 100644 index 000000000..876926a0c --- /dev/null +++ b/apps/c25k/metadata.json @@ -0,0 +1,30 @@ +{ + "id": "c25k", + "name": "C25K", + "icon": "app.png", + "version":"0.04", + "description": "Unofficial app for the Couch to 5k training plan", + "readme": "README.md", + "type": "app", + "tags": "running,c25k,tool,outdoors,exercise", + "allow_emulator": true, + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "storage": [ + {"name": "c25k.app.js", "url": "app.js"}, + {"name": "c25k.img", "url": "app-icon.js", "evaluate": true} + ], + "screenshots": [ + {"url": "c25k-scrn1.png"}, + {"url": "c25k-scrn2.png"}, + {"url": "c25k-scrn3.png"}, + {"url": "c25k-scrn4.png"}, + {"url": "c25k-scrn5.png"}, + {"url": "c25k-scrn6.png"}, + {"url": "c25k-scrn7.png"}, + {"url": "c25k-scrn8.png"}, + {"url": "c25k-scrn9.png"} + ] +} diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog new file mode 100644 index 000000000..effa87c4b --- /dev/null +++ b/apps/calclock/ChangeLog @@ -0,0 +1,9 @@ +0.01: Initial version +0.02: More compact rendering & app icon +0.03: Tell clock widgets to hide. +0.04: Improve current time readability in light theme. +0.05: Show calendar colors & improved all day events. +0.06: Improved multi-line locations & titles +0.07: Buzz 30, 15 and 1 minute before an event +0.08: No buzz during quiet hours & tweaked 30-minute buzz +0.09: Minor code improvements diff --git a/apps/calclock/README.md b/apps/calclock/README.md new file mode 100644 index 000000000..2b4e93a0c --- /dev/null +++ b/apps/calclock/README.md @@ -0,0 +1,9 @@ +# Calendar Clock - Your day at a glance + +This clock shows a chronological view of your current and future events. +It uses events synced from Gadgetbridge to achieve this. + +The current time and date is highlighted in cyan. + +## Screenshot +![](screenshot.png) diff --git a/apps/calclock/calclock-icon.js b/apps/calclock/calclock-icon.js new file mode 100644 index 000000000..9d5514d80 --- /dev/null +++ b/apps/calclock/calclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgpm5gAB4AVRhgWCAAQWWDCARC/4ACJR4uB54WDAAP8DBotFGIgXLFwv4GAouQC4gwMLooXF/gXJOowXGJBIXBCIgXQxgXLMAIXXMAmIC5OIx4XJhH/wAXIxnIC78IxGIHoIABI44MBC4wQBEQIDB5gXGPAJgEC6IxBC5oABC4wwDa4YTCxAWD5nPDAzvGFYgAB5AXWJBK+GcAq5CGBIuBC5X4GBIJBdoQXB/GIx4CDPJAuEC5JoCDAgWBFwYXJxCBIFwYXKYwoACCwZ3IPQoWIC5YABGYIABCwpHKAQYMBCwwX/C5QAMC8R3/R/4XNhAXNwAXHgGIABgWIAFwA==")) diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js new file mode 100644 index 000000000..32cce5fdb --- /dev/null +++ b/apps/calclock/calclock.js @@ -0,0 +1,150 @@ +var calendar = []; +var current = []; +var next = []; + +function updateCalendar() { + calendar = require("Storage").readJSON("android.calendar.json",true)||[]; + calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp); + calendar.sort((a,b) => a.timestamp - b.timestamp); + + current = calendar.filter(isActive); + next = calendar.filter(e=>!isActive(e)); +} + +function isActive(event) { + var timeActive = getTime() - event.timestamp; + return timeActive >= 0 && timeActive <= event.durationInSeconds; +} +function zp(str) { + return ("0"+str).substr(-2); +} + +function drawEventHeader(event, y) { + var x = 0; + var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000); + + //Don't need to know what time the event is at if its all day + if (isActive(event) || !event.allDay) { + g.setFont("Vector", 24); + var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); + g.drawString(timeStr, 0, y); + y += 3; + x = 13*timeStr.length+5; + } + + g.setFont("12x20", 1); + + if (isActive(event)) { + g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y); + } else { + var offset = 0-time.getTimezoneOffset()/1440; + var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset); + if(days > 0 || event.allDay) { + var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days"; + g.drawString(daysStr,x,y); + } + } + y += 21; + return y; +} + +function drawEventBody(event, y) { + g.setFont("12x20", 1); + var lines = g.wrapString(event.title, g.getWidth()-15); + var yStart = y; + if (lines.length > 2) { + lines = lines.slice(0,2); + lines[1] += "..."; + } + g.drawString(lines.join('\n'),10,y); + y+=20 * lines.length; + if(event.location) { + g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y); + var loclines = g.wrapString(event.location, g.getWidth()-30); + if(loclines.length>1) loclines[0] += "..."; + g.drawString(loclines[0],25,y); + y+=20; + } + if (event.color) { + var oldColor = g.getColor(); + g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0")); + g.fillRect(0,yStart,5,y-3); + g.setColor(oldColor); + } + y+=5; + return y; +} + +function drawEvent(event, y) { + y = drawEventHeader(event, y); + y = drawEventBody(event, y); + return y; +} + +var curEventHeight = 0; + +function drawCurrentEvents(y) { + g.setColor(g.theme.dark ? "#0ff" : "#00f"); + g.clearRect(0,y,g.getWidth()-5,y+curEventHeight); + curEventHeight = y; + + if(current.length === 0) { + y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y); + } else { + y = drawEventHeader(current[0],y); + for (var e of current) { + y = drawEventBody(e,y); + } + } + curEventHeight = y-curEventHeight; + return y; +} + +function drawFutureEvents(y) { + g.setColor(g.theme.fg); + for (var e of next) { + y = drawEvent(e, y); + if(y>g.getHeight())break; + } + return y; +} + +function fullRedraw() { + g.clearRect(0,24,g.getWidth()-5,g.getHeight()); + updateCalendar(); + var y = 30; + y = drawCurrentEvents(y); + drawFutureEvents(y); +} + +function buzzForEvents() { + let nextEvent = next[0]; if (!nextEvent) return; + // No buzz for all day events or events before 7am + // TODO: make this configurable + if (nextEvent.allDay || (new Date(nextEvent.timestamp * 1000)).getHours() < 7) return; + let minToEvent = Math.round((nextEvent.timestamp - getTime()) / 60.0); + switch (minToEvent) { + case 30: require("buzz").pattern(":"); break; + case 15: require("buzz").pattern(", ,"); break; + case 1: require("buzz").pattern(": : :"); break; + } +} + +function redraw() { + g.reset(); + if (current.find(e=>!isActive(e)) || next.find(isActive)) { + fullRedraw(); + } else { + drawCurrentEvents(30); + } + buzzForEvents(); +} + +g.clear(); +fullRedraw(); +buzzForEvents(); +/*var minuteInterval =*/ setInterval(redraw, 60 * 1000); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/calclock/calclock.png b/apps/calclock/calclock.png new file mode 100644 index 000000000..5f953c1ee Binary files /dev/null and b/apps/calclock/calclock.png differ diff --git a/apps/calclock/location.png b/apps/calclock/location.png new file mode 100644 index 000000000..619e55775 Binary files /dev/null and b/apps/calclock/location.png differ diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json new file mode 100644 index 000000000..31dcb88d0 --- /dev/null +++ b/apps/calclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "calclock", + "name": "Calendar Clock", + "shortName": "CalClock", + "version": "0.09", + "description": "Show the current and upcoming events synchronized from Gadgetbridge", + "icon": "calclock.png", + "type": "clock", + "tags": "clock,agenda", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"calclock.app.js","url":"calclock.js"}, + {"name":"calclock.img","url":"calclock-icon.js","evaluate":true} + ], + "screenshots": [{"url":"screenshot.png"}] +} diff --git a/apps/calclock/screenshot.patch b/apps/calclock/screenshot.patch new file mode 100644 index 000000000..3fdbf79d1 --- /dev/null +++ b/apps/calclock/screenshot.patch @@ -0,0 +1,32 @@ +diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js +index cb8c6100e..2092c1a4e 100644 +--- a/apps/calclock/calclock.js ++++ b/apps/calclock/calclock.js +@@ -3,9 +3,24 @@ var current = []; + var next = []; + + function updateCalendar() { +- calendar = require("Storage").readJSON("android.calendar.json",true)||[]; +- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp); +- calendar.sort((a,b) => a.timestamp - b.timestamp); ++ calendar = [ ++ { ++ t: "calendar", ++ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200, ++ title: "Capture Screenshot", ++ description: "Capture Screenshot", ++ location: "", ++ calName: "", ++ color: -7151168, allDay: true }, ++ { ++ t: "calendar", ++ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100, ++ title: "Upload to BangleApps", ++ description: "", ++ location: "", ++ calName: "", ++ color: -509406, allDay: false } ++ ]; + + current = calendar.filter(isActive); + next = calendar.filter(e=>!isActive(e)); diff --git a/apps/calclock/screenshot.png b/apps/calclock/screenshot.png new file mode 100644 index 000000000..8b2e39784 Binary files /dev/null and b/apps/calclock/screenshot.png differ diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index a08a0f5a7..7b47d3a4c 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -3,3 +3,6 @@ 0.03: Support for different screen sizes and touchscreen 0.04: Display current operation on LHS 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) +0.06: Bangle.js 2: Exit with a short press of the physical button +0.07: Bangle.js 2: Exit by pressing upper left corner of the screen +0.08: truncate long numbers (and append '...' to displayed value) diff --git a/apps/calculator/README.md b/apps/calculator/README.md index b25d355bf..29edd433a 100644 --- a/apps/calculator/README.md +++ b/apps/calculator/README.md @@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus. ## Controls +Bangle.js 1 - UP: BTN1 - DOWN: BTN3 - LEFT: BTN4 - RIGHT: BTN5 - SELECT: BTN2 +Bangle.js 2 +- Swipe up or down to go back to the number input +- Swipe to the left for operators, swipe to the right for the special functions +- Exit by pressing the physical button or the upper left corner of screen to exit (where the red back button would be) ## Creator + +## Contributors +[thyttan](https://github.com/thyttan) diff --git a/apps/calculator/app.js b/apps/calculator/app.js index 40953254e..5f4e77a47 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -3,14 +3,16 @@ * * Original Author: Frederic Rousseau https://github.com/fredericrous * Created: April 2020 + * + * Contributors: thyttan https://github.com/thyttan */ g.clear(); require("Font7x11Numeric7Seg").add(Graphics); -var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R'; -var RIGHT_MARGIN = 20; +var DEFAULT_SELECTION_NUMBERS = '5'; var RESULT_HEIGHT = 40; +var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14); var COLORS = { // [normal, selected] DEFAULT: ['#7F8183', '#A6A6A7'], @@ -86,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) { } function drawKey(name, k, selected) { - var rMargin = 0; - var bMargin = 0; var color = k.color || COLORS.DEFAULT; g.setColor(color[selected ? 1 : 0]); g.setFont('Vector', 20).setFontAlign(0,0); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.setColor(-1); - // correct margins to center the texts - if (name == '0') { - rMargin = (RIGHT_MARGIN * 2) - 7; - } else if (name === '/') { - rMargin = 5; - } else if (name === '*') { - bMargin = 5; - rMargin = 3; - } else if (name === '-') { - rMargin = 3; - } else if (name === 'R' || name === 'N') { - rMargin = k.val === 'C' ? 0 : -9; - } else if (name === '%') { - rMargin = -3; - } g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); } @@ -136,29 +121,21 @@ function drawGlobal() { screen[k] = specials[k]; } drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawNumbers() { screen = numbers; screenColor = COLORS.DEFAULT; drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawOperators() { screen = operators; screenColor =COLORS.OPERATOR; drawKeys(); - var selected = DEFAULT_SELECTION_OPERATORS; - var prevSelected = DEFAULT_SELECTION_OPERATORS; } function drawSpecials() { screen = specials; screenColor = COLORS.SPECIAL; drawKeys(); - var selected = DEFAULT_SELECTION_SPECIALS; - var prevSelected = DEFAULT_SELECTION_SPECIALS; } function getIntWithPrecision(x) { @@ -216,8 +193,6 @@ function doMath(x, y, operator) { } function displayOutput(num) { - var len; - var minusMarge = 0; g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setColor(-1); if (num === Infinity || num === -Infinity || isNaN(num)) { @@ -228,9 +203,7 @@ function displayOutput(num) { num = '-INFINITY'; } else { num = 'NOT A NUMBER'; - minusMarge = -25; } - len = (num + '').length; currNumber = null; results = null; isDecimal = false; @@ -259,6 +232,9 @@ function displayOutput(num) { num = num.toString(); num = num.replace("-","- "); // fix padding for '-' g.setFont('7x11Numeric7Seg', 2); + if (num.length > RESULT_MAX_LEN) { + num = num.substr(0, RESULT_MAX_LEN - 1)+'...'; + } } g.setFontAlign(1,0); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); @@ -367,7 +343,7 @@ function buttonPress(val) { } hasPressedNumber = false; break; - default: + default: { specials.R.val = 'C'; if (!swipeEnabled) drawKey('R', specials.R); const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity); @@ -383,6 +359,7 @@ function buttonPress(val) { hasPressedNumber = currNumber; displayOutput(currNumber); break; + } } } @@ -402,43 +379,42 @@ if (process.env.HWVERSION==1) { swipeEnabled = false; drawGlobal(); } else { // touchscreen? - selected = "NONE"; + selected = "NONE"; swipeEnabled = true; prepareScreen(numbers, numbersGrid, COLORS.DEFAULT); prepareScreen(operators, operatorsGrid, COLORS.OPERATOR); prepareScreen(specials, specialsGrid, COLORS.SPECIAL); drawNumbers(); - Bangle.on('touch',(n,e)=>{ - for (var key in screen) { - if (typeof screen[key] == "undefined") break; - var r = screen[key].xy; - if (e.x>=r[0] && e.y>=r[1] && - e.x{ + for (var key in screen) { + if (typeof screen[key] == "undefined") break; + var r = screen[key].xy; + if (e.x>=r[0] && e.y>=r[1] && e.x { - if (!e.b) { - if (lastX > 50) { // right + }, + swipe : (LR, UD) => { + if (LR == 1) { // right drawSpecials(); - } else if (lastX < -50) { // left + } + if (LR == -1) { // left drawOperators(); - } else if (lastY > 50) { // down - drawNumbers(); - } else if (lastY < -50) { // up + } + if (UD == 1) { // down + drawNumbers(); + } + if (UD == -1) { // up drawNumbers(); } - lastX = 0; - lastY = 0; - } else { - lastX = lastX + e.dx; - lastY = lastY + e.dy; } }); + } - displayOutput(0); diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json index e78e4d54f..a88444e11 100644 --- a/apps/calculator/metadata.json +++ b/apps/calculator/metadata.json @@ -2,7 +2,7 @@ "id": "calculator", "name": "Calculator", "shortName": "Calculator", - "version": "0.05", + "version": "0.08", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "icon": "calculator.png", "screenshots": [{"url":"screenshot_calculator.png"}], diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index ea8934f84..b5a5fbe2f 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -7,3 +7,17 @@ 0.07: Fix off-by-one-error on previous month 0.08: Do not register as watch, manually start clock on button read start of week from system settings +0.09: Fix scope of let variables +0.10: Use default Bangle formatter for booleans +0.11: Fix off-by-one-error on next year +0.12: Mark dated events on a day +0.13: Switch to swipe left/right for month and up/down for year selection + Display events for current month on touch +0.14: Add support for holidays +0.15: Edit holidays on device in settings +0.16: Add menu to fast open settings to edit holidays + Display Widgets in menus +0.17: Load holidays before events so the latter is not overpainted +0.18: Minor code improvements +0.19: Read events synchronized from Gadgetbridge +0.20: Correct start time of all-day events synchronized from Gadgetbridge diff --git a/apps/calendar/README.md b/apps/calendar/README.md index 4fc6962cf..7fa7bea1c 100644 --- a/apps/calendar/README.md +++ b/apps/calendar/README.md @@ -1,14 +1,17 @@ # Calendar -Basic calendar +Monthly calendar, displays holidays uploaded from the web interface and scheduled events. ## Usage -- Use `BTN4` (left screen tap) to go to the previous month -- Use `BTN5` (right screen tap) to go to the next month +- Swipe left to go to the previous month +- Swipe right to go to the next month +- Swipe up (Bangle.js 2 only) to go to the previous year +- Swipe down (Bangle.js 2 only) to go to the next year +- Touch to display events for current month +- Press the button (button 3 on Bangle.js 1) to exit +- Holidays have same color as weekends and can be edited with the 'Download'-interface, e.g. by uploading an iCalendar file. ## Settings -- Starts Sunday: whether the calendar should start on Sunday (default is Monday). -- B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. - +B2 Colors: use non-dithering colors (default, recommended for Bangle 2) or the original color scheme. diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index fc7e93cf5..d6eefce39 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -1,3 +1,4 @@ +{ const maxX = g.getWidth(); const maxY = g.getHeight(); const fontSize = g.getWidth() > 200 ? 2 : 1; @@ -16,95 +17,137 @@ const white = "#ffffff"; const red = "#d41706"; const blue = "#0000ff"; const yellow = "#ffff00"; +const cyan = "#00ffff"; +let bgColor; +let bgColorMonth; +let bgColorDow; +let bgColorWeekend; +let fgOtherMonth; +let fgSameMonth; +let bgEvent; +let bgOtherEvent; +const eventsPerDay=6; // how much different events per day we can display +const date = new Date(); -let settings = require('Storage').readJSON("calendar.json", true) || {}; +const timeutils = require("time_utils"); let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0; -if (settings.ndColors === undefined) - if (process.env.HWVERSION == 2) { - settings.ndColors = true; +let events; +const dowLbls = function() { + const days = startOnSun ? [0, 1, 2, 3, 4, 5, 6] : [1, 2, 3, 4, 5, 6, 0]; + const d = new Date(); + return days.map(i => { + d.setDate(d.getDate() + (i + 7 - d.getDay()) % 7); + return require("locale").dow(d, 1); + }); +}(); + +const loadEvents = () => { + // add holidays & other events + events = (require("Storage").readJSON("calendar.days.json",1) || []).map(d => { + const date = new Date(d.date); + const o = {date: date, msg: d.name, type: d.type}; + if (d.repeat) { + o.repeat = d.repeat; + } + return o; + }); + // all alarms that run on a specific date + events = events.concat((require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => { + const date = new Date(a.date); + const time = timeutils.decodeTime(a.t); + date.setHours(time.h); + date.setMinutes(time.m); + date.setSeconds(time.s); + return {date: date, msg: a.msg, type: "e"}; + })); + // all events synchronized from Gadgetbridge + events = events.concat((require("Storage").readJSON("android.calendar.json",1) || []).map(a => { + // All-day events always start at 00:00:00 UTC, so we need to "undo" the + // timezone offsetting to make sure that the day is correct. + const offset = a.allDay ? new Date().getTimezoneOffset() * 60 : 0 + const date = new Date((a.timestamp+offset) * 1000); + return {date: date, msg: a.title, type: a.allDay ? "o" : "e"}; + })); +}; + +const loadSettings = () => { + let settings = require('Storage').readJSON("calendar.json", true) || {}; + if (settings.ndColors === undefined) { + settings.ndColors = !g.theme.dark; + } + if (settings.ndColors === true) { + bgColor = white; + bgColorMonth = blue; + bgColorDow = black; + bgColorWeekend = yellow; + fgOtherMonth = blue; + fgSameMonth = black; + bgEvent = color2; + bgOtherEvent = cyan; } else { - settings.ndColors = false; + bgColor = color4; + bgColorMonth = color1; + bgColorDow = color2; + bgColorWeekend = color3; + fgOtherMonth = gray1; + fgSameMonth = white; + bgEvent = blue; + bgOtherEvent = "#ff8800"; } +}; -if (settings.ndColors === true) { - let bgColor = white; - let bgColorMonth = blue; - let bgColorDow = black; - let bgColorWeekend = yellow; - let fgOtherMonth = blue; - let fgSameMonth = black; -} else { - let bgColor = color4; - let bgColorMonth = color1; - let bgColorDow = color2; - let bgColorWeekend = color3; - let fgOtherMonth = gray1; - let fgSameMonth = white; -} +const sameDay = function(d1, d2) { + "jit"; + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +}; -function getDowLbls(locale) { - let dowLbls; - //TODO: Find some clever way to generate this programmatically from locale lib - switch (locale) { - case "de_AT": - case "de_CH": - case "de_DE": - if (startOnSun) { - dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; - } else { - dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; - } +const drawEvent = function(ev, curDay, x1, y1, x2, y2) { + "ram"; + switch(ev.type) { + case "e": { // alarm/event + const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0; + const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59 + const height = (y2-2) - (y1+2); // height of a cell + const sliceHeight = height/eventsPerDay; + const ystart = (y1+2) + slice*sliceHeight; + g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight); break; - case "nl_NL": - if (startOnSun) { - dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"]; - } else { - dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"]; - } + } + case "h": // holiday + g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1); break; - case "fr_BE": - case "fr_CH": - case "fr_FR": - if (startOnSun) { - dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; - } else { - dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; - } - break; - case "sv_SE": - if (startOnSun) { - dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; - } else { - dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; - } - break; - case "it_CH": - case "it_IT": - if (startOnSun) { - dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"]; - } else { - dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"]; - } - break; - case "oc_FR": - if (startOnSun) { - dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"]; - } else { - dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"]; - } - break; - default: - if (startOnSun) { - dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; - } else { - dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; - } + case "o": // other + g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1); break; } - return dowLbls; -} +}; -function drawCalendar(date) { +const calcDays = (month, monthMaxDayMap, dowNorm) => { + "jit"; + const maxDay = colN * (rowN - 1) + 1; + const days = []; + let nextMonthDay = 1; + let thisMonthDay = 51; + let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1; + + for (let i = 0; i < maxDay; i++) { + if (i < dowNorm) { + days.push(prevMonthDay); + prevMonthDay++; + } else if (thisMonthDay <= monthMaxDayMap[month] + 50) { + days.push(thisMonthDay); + thisMonthDay++; + } else { + days.push(nextMonthDay); + nextMonthDay++; + } + } + return days; +}; + +const drawCalendar = function(date) { g.setBgColor(bgColor); g.clearRect(0, 0, maxX, maxY); g.setBgColor(bgColorMonth); @@ -142,8 +185,6 @@ function drawCalendar(date) { true ); - g.setFont("6x8", fontSize); - let dowLbls = getDowLbls(require('locale').name); dowLbls.forEach((lbl, i) => { g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2); }); @@ -167,36 +208,52 @@ function drawCalendar(date) { 11: 31 }; - let days = []; - let nextMonthDay = 1; - let thisMonthDay = 51; - let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm + 1; - for (let i = 0; i < colN * (rowN - 1) + 1; i++) { - if (i < dowNorm) { - days.push(prevMonthDay); - prevMonthDay++; - } else if (thisMonthDay <= monthMaxDayMap[month] + 50) { - days.push(thisMonthDay); - thisMonthDay++; - } else { - days.push(nextMonthDay); - nextMonthDay++; + const days = calcDays(month, monthMaxDayMap, dowNorm); + const weekBeforeMonth = new Date(date.getTime()); + weekBeforeMonth.setDate(weekBeforeMonth.getDate() - 7); + const week2AfterMonth = new Date(date.getFullYear(), date.getMonth()+1, 0); + week2AfterMonth.setDate(week2AfterMonth.getDate() + 14); + events.forEach(ev => { + if (ev.repeat === "y") { + ev.date.setFullYear(ev.date.getMonth() < 6 ? week2AfterMonth.getFullYear() : weekBeforeMonth.getFullYear()); } - } + }); + const eventsThisMonthPerDay = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth).reduce((acc, ev) => { + const day = ev.date.getDate(); + if (!acc[day]) { + acc[day] = []; + } + acc[day].push(ev); + return acc; + }, []); let i = 0; - for (y = 0; y < rowN - 1; y++) { - for (x = 0; x < colN; x++) { + g.setFont("8x12", fontSize); + for (let y = 0; y < rowN - 1; y++) { + for (let x = 0; x < colN; x++) { i++; const day = days[i]; - const isToday = - today.year === year && today.month === month && today.day === day - 50; + const curMonth = day < 15 ? month+1 : day < 50 ? month-1 : month; + const curDay = new Date(year, curMonth, day > 50 ? day-50 : day); + const isToday = sameDay(curDay, new Date()); + const x1 = x * colW; + const y1 = y * rowH + headerH + rowH; + const x2 = x * colW + colW; + const y2 = y * rowH + headerH + rowH + rowH; + + const eventsThisDay = eventsThisMonthPerDay[curDay.getDate()]; + if (eventsThisDay && eventsThisDay.length > 0) { + // Display events for this day + eventsThisDay.forEach((ev, idx) => { + if (sameDay(ev.date, curDay)) { + drawEvent(ev, curDay, x1, y1, x2, y2); + eventsThisDay.splice(idx, 1); // this event is no longer needed + } + }); + } + if (isToday) { g.setColor(red); - let x1 = x * colW; - let y1 = y * rowH + headerH + rowH; - let x2 = x * colW + colW; - let y2 = y * rowH + headerH + rowH + rowH; g.drawRect(x1, y1, x2, y2); g.drawRect( x1 + 1, @@ -205,41 +262,107 @@ function drawCalendar(date) { y2 - 1 ); } - require("Font8x12").add(Graphics); - g.setFont("8x12", fontSize); + g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); g.drawString( (day > 50 ? day - 50 : day).toString(), x * colW + colW / 2, headerH + rowH + y * rowH + rowH / 2 ); - } - } -} + } // end for (x = 0; x < colN; x++) + } // end for (y = 0; y < rowN - 1; y++) +}; // end function drawCalendar -const date = new Date(); -const today = { - day: date.getDate(), - month: date.getMonth(), - year: date.getFullYear() +const showMenu = function() { + const menu = { + "" : { + title : "Calendar", + remove: () => { + require("widget_utils").show(); + } + }, + "< Back": () => { + require("widget_utils").hide(); + E.showMenu(); + setUI(); + }, + /*LANG*/"Exit": () => load(), + /*LANG*/"Settings": () => + eval(require('Storage').read('calendar.settings.js'))(() => { + loadSettings(); + loadEvents(); + showMenu(); + }), + }; + if (require("Storage").read("alarm.app.js")) { + menu[/*LANG*/"Launch Alarms"] = () => { + load("alarm.app.js"); + }; + } + require("widget_utils").show(); + E.showMenu(menu); }; -drawCalendar(date); -clearWatch(); -Bangle.on("touch", area => { - const month = date.getMonth(); - let prevMonth; - if (area == 1) { - let prevMonth = month > 0 ? month - 1 : 11; - if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); - date.setMonth(prevMonth); - } else { - let prevMonth = month < 11 ? month + 1 : 0; - if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1); - date.setMonth(month + 1); - } - drawCalendar(date); -}); -// Show launcher when button pressed -setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" }); -// No space for widgets! +const setUI = function() { + require("widget_utils").hide(); // No space for widgets! + drawCalendar(date); + + Bangle.setUI({ + mode : "custom", + swipe: (dirLR, dirUD) => { + if (dirLR<0) { // left + const month = date.getMonth(); + let prevMonth = month > 0 ? month - 1 : 11; + if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1); + date.setMonth(prevMonth); + drawCalendar(date); + } else if (dirLR>0) { // right + const month = date.getMonth(); + let nextMonth = month < 11 ? month + 1 : 0; + if (nextMonth === 0) date.setFullYear(date.getFullYear() + 1); + date.setMonth(nextMonth); + drawCalendar(date); + } else if (dirUD<0) { // up + date.setFullYear(date.getFullYear() - 1); + drawCalendar(date); + } else if (dirUD>0) { // down + date.setFullYear(date.getFullYear() + 1); + drawCalendar(date); + } + }, + btn: (n) => { + if (process.env.HWVERSION === 2 || n === 2) { + showMenu(); + } else if (n === 3) { + // directly exit only on Bangle.js 1 + load(); + } + }, + touch: (n,e) => { + events.sort((a,b) => a.date - b.date); + const menu = events.filter(ev => ev.date.getFullYear() === date.getFullYear() && ev.date.getMonth() === date.getMonth()).map(e => { + const dateStr = require("locale").date(e.date, 1); + const timeStr = require("locale").time(e.date, 1); + return { title: `${dateStr} ${e.type === "e" ? timeStr : ""}` + (e.msg ? " " + e.msg : "") }; + }); + if (menu.length === 0) { + menu.push({title: /*LANG*/"No events"}); + } + menu[""] = { title: require("locale").month(date) + " " + date.getFullYear() }; + menu["< Back"] = () => { + require("widget_utils").hide(); + E.showMenu(); + setUI(); + }; + require("widget_utils").show(); + E.showMenu(menu); + } + }); +}; + +loadSettings(); +loadEvents(); +Bangle.loadWidgets(); +require("Font8x12").add(Graphics); +setUI(); +} diff --git a/apps/calendar/interface.html b/apps/calendar/interface.html new file mode 100644 index 000000000..8fa624a40 --- /dev/null +++ b/apps/calendar/interface.html @@ -0,0 +1,240 @@ + + + + + + + + + +

Holidays

+ +
+ +
+ + + + + + + + + + + + + +
DateHolidayTypeRepeat
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + + + diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 5f968b364..36713a487 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,18 +1,19 @@ { "id": "calendar", "name": "Calendar", - "version": "0.08", - "description": "Simple calendar", + "version": "0.20", + "description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], - "tags": "calendar", + "tags": "calendar,tool", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "allow_emulator": true, + "interface": "interface.html", "storage": [ {"name":"calendar.app.js","url":"calendar.js"}, {"name":"calendar.settings.js","url":"settings.js"}, {"name":"calendar.img","url":"calendar-icon.js","evaluate":true} ], - "data": [{"name":"calendar.json"}] + "data": [{"name":"calendar.json"}, {"name":"calendar.days.json"}] } diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js index 192d2ece0..051826646 100644 --- a/apps/calendar/settings.js +++ b/apps/calendar/settings.js @@ -1,28 +1,155 @@ (function (back) { - var FILE = "calendar.json"; - var settings = require('Storage').readJSON(FILE, true) || {}; - if (settings.ndColors === undefined) + const FILE = "calendar.json"; + const HOLIDAY_FILE = "calendar.days.json"; + const settings = require('Storage').readJSON(FILE, true) || {}; + if (settings.ndColors === undefined) { if (process.env.HWVERSION == 2) { settings.ndColors = true; } else { settings.ndColors = false; } + } + const holidays = (require("Storage").readJSON(HOLIDAY_FILE,1)||[]).sort((a,b) => new Date(a.date) - new Date(b.date)) || []; function writeSettings() { require('Storage').writeJSON(FILE, settings); } - E.showMenu({ - "": { "title": "Calendar" }, - "< Back": () => back(), - 'B2 Colors': { - value: settings.ndColors, - format: v => v ? "Yes" : "No", - onchange: v => { - settings.ndColors = v; - writeSettings(); - } - }, - }); -}) + function writeHolidays() { + holidays.sort((a,b) => new Date(a.date) - new Date(b.date)); + require('Storage').writeJSON(HOLIDAY_FILE, holidays); + } + function formatDate(d) { + return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0'); + } + + const editdate = (i) => { + const holiday = holidays[i]; + const date = new Date(holiday.date); + //const dateStr = require("locale").date(date, 1); + const menu = { + "": { "title" : holiday.name}, + "< Back": () => { + writeHolidays(); + editdates(); + }, + /*LANG*/"Day": { + value: date ? date.getDate() : null, + min: 1, + max: 31, + wrap: true, + onchange: v => { + date.setDate(v); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Month": { + value: date ? date.getMonth() + 1 : null, + format: v => require("date_utils").month(v), + onchange: v => { + date.setMonth((v+11)%12); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Year": { + value: date ? date.getFullYear() : null, + min: 1900, + max: 2100, + onchange: v => { + date.setFullYear(v); + holiday.date = formatDate(date); + } + }, + /*LANG*/"Name": () => { + require("textinput").input({text:holiday.name}).then(result => { + holiday.name = result; + editdate(i); + }); + }, + /*LANG*/"Type": { + value: function() { + switch(holiday.type) { + case 'h': return 0; + case 'o': return 1; + } + return 0; + }(), + min: 0, max: 1, + format: v => [/*LANG*/"Holiday", /*LANG*/"Other"][v], + onchange: v => { + holiday.type = function() { + switch(v) { + case 0: return 'h'; + case 1: return 'o'; + } + }(); + } + }, + /*LANG*/"Repeat": { + value: !!holiday.repeat, + format: v => v ? /*LANG*/"Yearly" : /*LANG*/"Never", + onchange: v => { + holiday.repeat = v ? 'y' : undefined; + } + }, + /*LANG*/"Delete": () => E.showPrompt(/*LANG*/"Delete" + " " + menu[""].title + "?").then(function(v) { + if (v) { + holidays.splice(i, 1); + writeHolidays(); + editdates(); + } else { + editday(i); + } + } + ), + }; + try { + require("textinput"); + } catch(e) { + // textinput not installed + delete menu[/*LANG*/"Name"]; + } + + E.showMenu(menu); + }; + + const editdates = () => { + const menu = holidays.map((holiday,i) => { + const date = new Date(holiday.date); + const dateStr = require("locale").date(date, 1); + return { + title: dateStr + ' ' + holiday.name, + onchange: v => setTimeout(() => editdate(i), 10), + }; + }); + + menu[''] = { 'title': 'Holidays' }; + menu['< Back'] = ()=>settingsmenu(); + E.showMenu(menu); + }; + + const settingsmenu = () => { + E.showMenu({ + "": { "title": "Calendar" }, + "< Back": () => back(), + 'B2 Colors': { + value: settings.ndColors, + onchange: v => { + settings.ndColors = v; + writeSettings(); + } + }, + /*LANG*/"Edit Holidays": () => editdates(), + /*LANG*/"Add Holiday": () => { + holidays.push({ + "date":formatDate(new Date()), + "name":/*LANG*/"New", + "type":'h', + }); + editdate(holidays.length-1); + }, + }); + }; + settingsmenu(); +}) diff --git a/apps/calibration/ChangeLog b/apps/calibration/ChangeLog new file mode 100644 index 000000000..3813bfdf8 --- /dev/null +++ b/apps/calibration/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Use fractional numbers and scale the points to keep working consistently on whole screen +0.03: Use default Bangle formatter for booleans +0.04: Minor code improvements diff --git a/apps/calibration/README.md b/apps/calibration/README.md index 37f637d21..ed1a29d9e 100644 --- a/apps/calibration/README.md +++ b/apps/calibration/README.md @@ -6,6 +6,7 @@ A simple calibration app for the touchscreen Once lauched touch the cross that appear on the screen to make another spawn elsewhere. -each new touch on the screen will help to calibrate the offset -of your finger on the screen. After five or more input, press -the button to save the calibration and close the application. \ No newline at end of file +Each new touch on the screen will help to calibrate the offset +of your finger on the screen. After four or more inputs, press +the button to save the calibration and close the application. Quality +of the calibration gets better with every touch on a cross. diff --git a/apps/calibration/app.js b/apps/calibration/app.js index d3823de63..b5faa8f81 100644 --- a/apps/calibration/app.js +++ b/apps/calibration/app.js @@ -1,40 +1,60 @@ class BanglejsApp { constructor() { + this.maxSamples = 16; + this.target = { + xMin: Math.floor(0.1 * g.getWidth()), + xMax: Math.floor(0.9 * g.getWidth()), + yMin: Math.floor(0.1 * g.getHeight()), + yMax: Math.floor(0.9 * g.getHeight()), + }; this.x = 0; this.y = 0; + this.step = 0; this.settings = { - xoffset: 0, - yoffset: 0, + xoffset: [0], + yoffset: [0], + xMaxActual: [this.target.xMax], + yMaxActual: [this.target.yMax], }; } load_settings() { let settings = require('Storage').readJSON('calibration.json', true) || {active: false}; - // do nothing if the calibration is deactivated - if (settings.active === true) { - // cancel the calibration offset - Bangle.on('touch', function(button, xy) { - xy.x += settings.xoffset; - xy.y += settings.yoffset; - }); - } - if (!settings.xoffset) settings.xoffset = 0; - if (!settings.yoffset) settings.yoffset = 0; - console.log('loaded settings:'); console.log(settings); return settings; } - save_settings() { - this.settings.active = true; - this.settings.reload = false; - require('Storage').writeJSON('calibration.json', this.settings); + getMedian(array){ + array.sort(); + let i = Math.floor(array.length/2); + if ( array.length % 2 && array.length > 1 ){ + return (array[i]+array[i+1])/2; + } else { + return array[i]; + } + } - console.log('saved settings:'); - console.log(this.settings); + getMedianSettings(){ + let medianSettings = { + xoffset: this.getMedian(this.settings.xoffset), + yoffset: this.getMedian(this.settings.yoffset) + }; + + medianSettings.xscale = this.target.xMax / (medianSettings.xoffset + this.getMedian(this.settings.xMaxActual)); + medianSettings.yscale = this.target.yMax / (medianSettings.yoffset + this.getMedian(this.settings.yMaxActual)); + return medianSettings; + } + + save_settings() { + let settingsToSave = this.getMedianSettings(); + settingsToSave.active = true; + settingsToSave.reload = false; + require('Storage').writeJSON('calibration.json', settingsToSave); + + console.log('saved settings:', settingsToSave); } explain() { @@ -46,29 +66,78 @@ class BanglejsApp { } drawTarget() { - this.x = 16 + Math.floor(Math.random() * (g.getWidth() - 32)); - this.y = 40 + Math.floor(Math.random() * (g.getHeight() - 80)); + switch (this.step){ + case 0: + this.x = this.target.xMin; + this.y = this.target.yMin; + break; + case 1: + this.x = this.target.xMax; + this.y = this.target.yMin; + break; + case 2: + this.x = this.target.xMin; + this.y = this.target.yMax; + break; + case 3: + this.x = this.target.xMax; + this.y = this.target.yMax; + break; + } - g.clearRect(0, 24, g.getWidth(), g.getHeight() - 24); + g.clearRect(0, 0, g.getWidth(), g.getHeight()); + g.setColor(g.theme.fg); g.drawLine(this.x, this.y - 5, this.x, this.y + 5); g.drawLine(this.x - 5, this.y, this.x + 5, this.y); g.setFont('Vector', 10); - g.drawString('current offset: ' + this.settings.xoffset + ', ' + this.settings.yoffset, 0, 24); + let medianSettings = this.getMedianSettings(); + g.drawString('current offset: ' + medianSettings.xoffset.toFixed(3) + ', ' + medianSettings.yoffset.toFixed(3), 2, (g.getHeight()/2)-6); + g.drawString('current scale: ' + medianSettings.xscale.toFixed(3) + ', ' + medianSettings.yscale.toFixed(3), 2, (g.getHeight()/2)+6); } setOffset(xy) { - this.settings.xoffset = Math.round((this.settings.xoffset + (this.x - Math.floor((this.x + xy.x)/2)))/2); - this.settings.yoffset = Math.round((this.settings.yoffset + (this.y - Math.floor((this.y + xy.y)/2)))/2); + switch (this.step){ + case 0: + this.settings.xoffset.push(this.x - xy.x); + this.settings.yoffset.push(this.y - xy.y); + break; + case 1: + this.settings.xMaxActual.push(xy.x); + this.settings.yoffset.push(this.y - xy.y); + break; + case 2: + this.settings.xoffset.push(this.x - xy.x); + this.settings.yMaxActual.push(xy.y); + break; + case 3: + this.settings.xMaxActual.push(xy.x); + this.settings.yMaxActual.push(xy.y); + break; + } + + for (let c in this.settings){ + if (this.settings[c].length > this.maxSamples) this.settings[c] = this.settings[c].slice(1, this.maxSamples); + } + } + + nextStep() { + this.step++; + if ( this.step == 4 ) this.step = 0; } } E.srand(Date.now()); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -calibration = new BanglejsApp(); +const calibration = new BanglejsApp(); calibration.load_settings(); +Bangle.disableCalibration = true; + +function touchHandler (btn, xy){ + if (xy) calibration.setOffset(xy); + calibration.nextStep(); + calibration.drawTarget(); +} let modes = { mode : 'custom', @@ -76,10 +145,7 @@ let modes = { calibration.save_settings(this.settings); load(); }, - touch : function(btn, xy) { - calibration.setOffset(xy); - calibration.drawTarget(); - }, + touch : touchHandler, }; Bangle.setUI(modes); calibration.drawTarget(); diff --git a/apps/calibration/boot.js b/apps/calibration/boot.js index 237fb2e0d..03b17a03a 100644 --- a/apps/calibration/boot.js +++ b/apps/calibration/boot.js @@ -1,7 +1,7 @@ let cal_settings = require('Storage').readJSON("calibration.json", true) || {active: false}; Bangle.on('touch', function(button, xy) { // do nothing if the calibration is deactivated - if (cal_settings.active === false) return; + if (cal_settings.active === false || Bangle.disableCalibration) return; // reload the calibration offset at each touch event /!\ bad for the flash memory if (cal_settings.reload === true) { @@ -9,6 +9,6 @@ Bangle.on('touch', function(button, xy) { } // apply the calibration offset - xy.x += cal_settings.xoffset; - xy.y += cal_settings.yoffset; + xy.x = E.clip(Math.round((xy.x + (cal_settings.xoffset || 0)) * (cal_settings.xscale || 1)),0,g.getWidth()); + xy.y = E.clip(Math.round((xy.y + (cal_settings.yoffset || 0)) * (cal_settings.yscale || 1)),0,g.getHeight()); }); diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json index 122a2c175..11fc73828 100644 --- a/apps/calibration/metadata.json +++ b/apps/calibration/metadata.json @@ -2,8 +2,8 @@ "name": "Touchscreen Calibration", "shortName":"Calibration", "icon": "calibration.png", - "version":"1.00", - "description": "A simple calibration app for the touchscreen", + "version": "0.04", + "description": "(NOT RECOMMENDED) A simple calibration app for the touchscreen. Please use the Touchscreen Calibration in the Settings app instead.", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "tags": "tool", diff --git a/apps/calibration/settings.js b/apps/calibration/settings.js index 6db8dd3bb..08c728d96 100644 --- a/apps/calibration/settings.js +++ b/apps/calibration/settings.js @@ -13,7 +13,6 @@ "< Back" : () => back(), 'Active': { value: !!settings.active, - format: v => v? "On":"Off", onchange: v => { settings.active = v; writeSettings(); diff --git a/apps/carcrazy/ChangeLog b/apps/carcrazy/ChangeLog index f697617b4..f4fd9ab7b 100644 --- a/apps/carcrazy/ChangeLog +++ b/apps/carcrazy/ChangeLog @@ -1,3 +1,4 @@ 0.01: Car Crazy is now avialable for testing in beta! 0.02: 10 Levels are now added making the game harder as it goes along. Some of the levels include multiple cars and faster cars. More levels coming soon. 0.03: Settings are now added so that you can reset your high score. +0.04: Minor code improvements. diff --git a/apps/carcrazy/app.js b/apps/carcrazy/app.js index 0fb765871..2fdfc67e7 100644 --- a/apps/carcrazy/app.js +++ b/apps/carcrazy/app.js @@ -66,10 +66,8 @@ function moveEnemyPosition(){ enemyPositonCenterX2 = 120; }else if((randomRoadPositionIndicator2 == 3)){ enemyPositonCenterX2 = 155; - }else if(level == 7||level == 8){ - } - } + } // TODO: else if(level == 7) } function collision(){ @@ -168,17 +166,17 @@ var playerCarRightX; var playerCarFrontY; var playerCarFrontY; -var playerCarBackY; +//var playerCarBackY; var playerCarLeftX; var playerCarRightX; var enemyCarFrontY; -var enemyCarBackY; +//var enemyCarBackY; var enemyCarLeftX; var enemyCarRightX; var enemyCarFrontY2; -var enemyCarBackY2; +//var enemyCarBackY2; var enemyCarLeftX2; var enemyCarRightX2; @@ -239,17 +237,17 @@ function draw(){ } playerCarFrontY = playerCarCenterY-carHeight/2; - playerCarBackY = playerCarCenterY+carHeight/2; + //playerCarBackY = playerCarCenterY+carHeight/2; playerCarLeftX = playerCarCenterX-carWidth/2; playerCarRightX = playerCarCenterX+carWidth/2; enemyCarFrontY = enemyPositonCenterY+carHeight/2; - enemyCarBackY = enemyPositonCenterY-carHeight/2; + //enemyCarBackY = enemyPositonCenterY-carHeight/2; enemyCarLeftX = enemyPositonCenterX-carWidth/2; enemyCarRightX = enemyPositonCenterX+carWidth/2; enemyCarFrontY2 = enemyPositonCenterY2+carHeight/2; - enemyCarBackY2 = enemyPositonCenterY2-carHeight/2; + //enemyCarBackY2 = enemyPositonCenterY2-carHeight/2; enemyCarLeftX2 = enemyPositonCenterX2-carWidth/2; enemyCarRightX2 = enemyPositonCenterX2+carWidth/2; diff --git a/apps/carcrazy/metadata.json b/apps/carcrazy/metadata.json index 3898de962..4a1b359c8 100644 --- a/apps/carcrazy/metadata.json +++ b/apps/carcrazy/metadata.json @@ -2,7 +2,7 @@ "id": "carcrazy", "name": "Car Crazy", "shortName": "Car Crazy", - "version": "0.03", + "version": "0.04", "description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.", "icon": "carcrash.png", "tags": "game", diff --git a/apps/carcrazy/settings.js b/apps/carcrazy/settings.js index ee3bbd417..48301a865 100644 --- a/apps/carcrazy/settings.js +++ b/apps/carcrazy/settings.js @@ -17,4 +17,4 @@ } }; E.showMenu(menu); -}); +}) diff --git a/apps/cards/Barcode.js b/apps/cards/Barcode.js new file mode 100644 index 000000000..ea2448ca5 --- /dev/null +++ b/apps/cards/Barcode.js @@ -0,0 +1,35 @@ +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +class Barcode{ + constructor(data, options){ + this.data = data; + this.text = options.text || data; + this.options = options; + } +} + +module.exports = Barcode; diff --git a/apps/cards/ChangeLog b/apps/cards/ChangeLog new file mode 100644 index 000000000..b35947cda --- /dev/null +++ b/apps/cards/ChangeLog @@ -0,0 +1,5 @@ +0.01: Simple app to display loyalty cards +0.02: Hiding widgets while showing the code +0.03: Added option to use max brightness when showing code +0.04: Minor code improvements +0.05: Add EAN & UPC codes diff --git a/apps/cards/EAN.js b/apps/cards/EAN.js new file mode 100644 index 000000000..177874494 --- /dev/null +++ b/apps/cards/EAN.js @@ -0,0 +1,73 @@ +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); + +// Standard start end and middle bits +const SIDE_BIN = '101'; +const MIDDLE_BIN = '01010'; + +// Base class for EAN8 & EAN13 +class EAN extends Barcode { + constructor(data, options) { + super(data, options); + } + + leftText(from, to) { + return this.text.substr(from, to); + } + + leftEncode(data, structure) { + return encode(data, structure); + } + + rightText(from, to) { + return this.text.substr(from, to); + } + + rightEncode(data, structure) { + return encode(data, structure); + } + + encode() { + const data = [ + SIDE_BIN, + this.leftEncode(), + MIDDLE_BIN, + this.rightEncode(), + SIDE_BIN + ]; + + return { + data: data.join(''), + text: this.text + }; + } + +} + +module.exports = EAN; \ No newline at end of file diff --git a/apps/cards/EAN13.js b/apps/cards/EAN13.js new file mode 100644 index 000000000..c91e385e3 --- /dev/null +++ b/apps/cards/EAN13.js @@ -0,0 +1,92 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Binary_encoding_of_data_digits_into_EAN-13_barcode +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const EAN = require("cards.EAN.js"); + +const EAN13_STRUCTURE = [ + 'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', + 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL' +]; + +// Calculate the checksum digit +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit +const checksum = (number) => { + const res = number + .substr(0, 12) + .split('') + .map((n) => +n) + .reduce((sum, a, idx) => ( + idx % 2 ? sum + a * 3 : sum + a + ), 0); + + return (10 - (res % 10)) % 10; +}; + +class EAN13 extends EAN { + + constructor(data, options) { + // Add checksum if it does not exist + if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) { + data += checksum(data); + } + + super(data, options); + + // Adds a last character to the end of the barcode + this.lastChar = options.lastChar; + } + + valid() { + return ( + /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + +this.data[12] === checksum(this.data) + ); + } + + leftText() { + return super.leftText(1, 6); + } + + leftEncode() { + const data = this.data.substr(1, 6); + const structure = EAN13_STRUCTURE[this.data[0]]; + return super.leftEncode(data, structure); + } + + rightText() { + return super.rightText(7, 6); + } + + rightEncode() { + const data = this.data.substr(7, 6); + return super.rightEncode(data, 'RRRRRR'); + } + +} + +module.exports = EAN13; \ No newline at end of file diff --git a/apps/cards/EAN8.js b/apps/cards/EAN8.js new file mode 100644 index 000000000..382ee647a --- /dev/null +++ b/apps/cards/EAN8.js @@ -0,0 +1,82 @@ +// Encoding documentation: +// http://www.barcodeisland.com/ean8.phtml +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const EAN = require("cards.EAN.js"); + +// Calculate the checksum digit +const checksum = (number) => { + const res = number + .substr(0, 7) + .split('') + .map((n) => +n) + .reduce((sum, a, idx) => ( + idx % 2 ? sum + a : sum + a * 3 + ), 0); + + return (10 - (res % 10)) % 10; +}; + +class EAN8 extends EAN { + + constructor(data, options) { + // Add checksum if it does not exist + if (/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)) { + data += checksum(data); + } + + super(data, options); + } + + valid() { + return ( + /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + +this.data[7] === checksum(this.data) + ); + } + + leftText() { + return super.leftText(0, 4); + } + + leftEncode() { + const data = this.data.substr(0, 4); + return super.leftEncode(data, 'LLLL'); + } + + rightText() { + return super.rightText(4, 4); + } + + rightEncode() { + const data = this.data.substr(4, 4); + return super.rightEncode(data, 'RRRR'); + } + +} + +module.exports = EAN8; diff --git a/apps/cards/README.md b/apps/cards/README.md new file mode 100644 index 000000000..001595452 --- /dev/null +++ b/apps/cards/README.md @@ -0,0 +1,32 @@ +# Cards + +Simple app to display loyalty cards synced from Catima through GadgetBridge. +The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported. + +To come back to the visualization of the card's details from the code view, simply press the button. + +Beware that the small screen of the Banglejs 2 cannot render properly complex barcodes (in fact the resolution is very limited to render most barcodes). + +### Supported codes types + +* `CODE_39` +* `CODABAR` +* `QR_CODE` + +### Disclaimer + +This app is a proof of concept, many codes are too complex to be rendered by the bangle's screen or hardware (at least with the current logic), keep that in mind. + +### How to sync + +We can synchronize cards with GadgetBridge and Catima, refer to those projects for further information. +The feature is currently available on nightly builds only. +It should be released from version 0.77 (not yet out at the time of writing). + +GadgetBridge syncronizes all cards at once, if you have too many cards you may want to explicitly select which ones to syncronize, keep in mind the limitations of the Banglejs. + +### Credits + +Barcode generation adapted from [lindell/JsBarcode](https://github.com/lindell/JsBarcode) + +QR code generation adapted from [ricmoo/QRCode](https://github.com/ricmoo/QRCode) diff --git a/apps/cards/UPC.js b/apps/cards/UPC.js new file mode 100644 index 000000000..6f581ca18 --- /dev/null +++ b/apps/cards/UPC.js @@ -0,0 +1,79 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); + +class UPC extends Barcode{ + constructor(data, options){ + // Add checksum if it does not exist + if(/^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + data += checksum(data); + } + + super(data, options); + } + + valid(){ + return /^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(this.data) && + this.data[11] == checksum(this.data); + } + + encode(){ + var result = ""; + + result += "101"; + result += encode(this.data.substr(0, 6), "LLLLLL"); + result += "01010"; + result += encode(this.data.substr(6, 6), "RRRRRR"); + result += "101"; + + return { + data: result, + text: this.text + }; + } +} + +// Calulate the checksum digit +// https://en.wikipedia.org/wiki/International_Article_Number_(EAN)#Calculation_of_checksum_digit +function checksum(number){ + var result = 0; + + var i; + for(i = 1; i < 11; i += 2){ + result += parseInt(number[i]); + } + for(i = 0; i < 11; i += 2){ + result += parseInt(number[i]) * 3; + } + + return (10 - (result % 10)) % 10; +} + +module.exports = { UPC, checksum }; diff --git a/apps/cards/UPCE.js b/apps/cards/UPCE.js new file mode 100644 index 000000000..9aaa464b7 --- /dev/null +++ b/apps/cards/UPCE.js @@ -0,0 +1,134 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#Encoding +// +// UPC-E documentation: +// https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const encode = require("cards.encode.js"); +const Barcode = require("cards.Barcode.js"); +const upc = require("cards.UPC.js"); + +const EXPANSIONS = [ + "XX00000XXX", + "XX10000XXX", + "XX20000XXX", + "XXX00000XX", + "XXXX00000X", + "XXXXX00005", + "XXXXX00006", + "XXXXX00007", + "XXXXX00008", + "XXXXX00009" +]; + +const PARITIES = [ + ["EEEOOO", "OOOEEE"], + ["EEOEOO", "OOEOEE"], + ["EEOOEO", "OOEEOE"], + ["EEOOOE", "OOEEEO"], + ["EOEEOO", "OEOOEE"], + ["EOOEEO", "OEEOOE"], + ["EOOOEE", "OEEEOO"], + ["EOEOEO", "OEOEOE"], + ["EOEOOE", "OEOEEO"], + ["EOOEOE", "OEEOEO"] +]; + +class UPCE extends Barcode{ + constructor(data, options){ + // Code may be 6 or 8 digits; + // A 7 digit code is ambiguous as to whether the extra digit + // is a UPC-A check or number system digit. + super(data, options); + this.isValid = false; + if(/^[0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + this.middleDigits = data; + this.upcA = expandToUPCA(data, "0"); + this.text = options.text || + `${this.upcA[0]}${data}${this.upcA[this.upcA.length - 1]}`; + this.isValid = true; + } + else if(/^[01][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$/.test(data)){ + this.middleDigits = data.substring(1, data.length - 1); + this.upcA = expandToUPCA(this.middleDigits, data[0]); + + if(this.upcA[this.upcA.length - 1] === data[data.length - 1]){ + this.isValid = true; + } + else{ + // checksum mismatch + return; + } + } + } + + valid(){ + return this.isValid; + } + + encode(){ + var result = ""; + + result += "101"; + result += this.encodeMiddleDigits(); + result += "010101"; + + return { + data: result, + text: this.text + }; + } + + encodeMiddleDigits() { + const numberSystem = this.upcA[0]; + const checkDigit = this.upcA[this.upcA.length - 1]; + const parity = PARITIES[parseInt(checkDigit)][parseInt(numberSystem)]; + return encode(this.middleDigits, parity); + } +} + +function expandToUPCA(middleDigits, numberSystem) { + const lastUpcE = parseInt(middleDigits[middleDigits.length - 1]); + const expansion = EXPANSIONS[lastUpcE]; + + let result = ""; + let digitIndex = 0; + for(let i = 0; i < expansion.length; i++) { + let c = expansion[i]; + if (c === 'X') { + result += middleDigits[digitIndex++]; + } else { + result += c; + } + } + + result = `${numberSystem}${result}`; + return `${result}${upc.checksum(result)}`; +} + +module.exports = UPCE; \ No newline at end of file diff --git a/apps/cards/app-icon.js b/apps/cards/app-icon.js new file mode 100644 index 000000000..3ec6948c4 --- /dev/null +++ b/apps/cards/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AYoIAjF/4v/F/4v/F/4v/FAdNAAsoADgv/F/4v/F/4vqu4AjF/4v/F/4v6poAjF/4AfFAYAGF/4v/F/4v/F/4v/F94A/AH4A/AH4A/ABo")) diff --git a/apps/cards/app.js b/apps/cards/app.js new file mode 100644 index 000000000..45e72831c --- /dev/null +++ b/apps/cards/app.js @@ -0,0 +1,265 @@ +/* CARDS is a list of: + {id:int, + name, + value, + type, + expiration, + color, + balance, + note, + ... + } +*/ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// get brightness +let brightness; + +function loadBrightness() { + const getBrightness = require('Storage').readJSON("setting.json", 1) || {}; + brightness = getBrightness.brightness || 0.1; +} + +//may make it configurable in the future +const WHITE=-1 +const BLACK=0 + +const Locale = require("locale"); +const widget_utils = require('widget_utils'); + +//var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +//var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; + +var CARDS = require("Storage").readJSON("android.cards.json",true)||[]; +var settings = require("Storage").readJSON("cards.settings.json",true)||{}; + +function getDate(timestamp) { + return new Date(timestamp*1000); +} +function formatDay(date) { + let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); + if (!settings.useToday) { + return formattedDate; + } + const today = new Date(Date.now()); + if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth()) + return /*LANG*/"Today "; + else { + const tomorrow = new Date(Date.now() + 86400 * 1000); + if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) { + return /*LANG*/"Tomorrow "; + } + return formattedDate; + } +} + +function getColor(intColor) { + return "#"+(0x1000000+Number(intColor)).toString(16).padStart(6,"0"); +} +function isLight(color) { + var r = +("0x"+color.slice(1,3)); + var g = +("0x"+color.slice(3,5)); + var b = +("0x"+color.slice(5,7)); + var threshold = 0x88 * 3; + return (r+g+b) > threshold; +} + +function printSquareCode(binary, size) { + var padding = 5; + var ratio = (g.getWidth()-(2*padding))/size; + for (var y = 0; y < size; y++) { + for (var x = 0; x < size; x++) { + if (binary[x + y * size]) { + g.setColor(BLACK).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + } else { + g.setColor(WHITE).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + } + } + } +} +function printLinearCode(binary) { + var padding = 5; + var yFrom = 15; + var yTo = 28; + var width = (g.getWidth()-(2*padding))/binary.length; + for(var b = 0; b < binary.length; b++){ + var x = b * width; + if(binary[b] === "1"){ + g.setColor(BLACK).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + } + else if(binary[b]){ + g.setColor(WHITE).fillRect({x:x+padding, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + } + } +} + +function showCode(card) { + // set to full bright when the setting is true + if(settings.fullBrightness) { + Bangle.setLCDBrightness(1); + } + widget_utils.hide(); + E.showScroller(); + // keeping it on rising edge would come back twice.. + setWatch(()=>showCard(card), BTN, {edge:"falling"}); + // theme independent + g.setColor(WHITE).fillRect(0, 0, g.getWidth(), g.getHeight()); + switch (card.type) { + case "QR_CODE": { + const getBinaryQR = require("cards.qrcode.js"); + let code = getBinaryQR(card.value); + printSquareCode(code.data, code.size); + break; + } + case "CODE_39": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const CODE39 = require("cards.code39.js"); + let code = new CODE39(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "CODABAR": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const codabar = require("cards.codabar.js"); + let code = new codabar(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "EAN_8": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const EAN8 = require("cards.EAN8.js"); + let code = new EAN8(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "EAN_13": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const EAN13 = require("cards.EAN13.js"); + let code = new EAN13(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "UPC_A": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const UPC = require("cards.UPC.js"); + let code = new UPC.UPC(card.value, {}); + printLinearCode(code.encode().data); + break; + } + case "UPC_E": { + g.setFont("Vector:20"); + g.setFontAlign(0,1).setColor(BLACK); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); + const UPCE = require("cards.UPCE.js"); + let code = new UPCE(card.value, {}); + printLinearCode(code.encode().data); + break; + } + default: + g.clear(true); + g.setFont("Vector:30"); + g.setFontAlign(0,0); + g.drawString(card.value, g.getWidth()/2, g.getHeight()/2); + } +} + +function showCard(card) { + // reset brightness to old value after maxing it out + if(settings.fullBrightness) { + Bangle.setLCDBrightness(brightness); + } + var lines = []; + var bodyFont = fontBig; + if(!card) return; + g.setFont(bodyFont); + //var lines = []; + if (card.name) lines = g.wrapString(card.name, g.getWidth()-10); + var titleCnt = lines.length; + lines = lines.concat("", /*LANG*/"View code"); + var valueLine = lines.length - 1; + if (card.expiration) + lines = lines.concat("",/*LANG*/"Expires"+": ", g.wrapString(formatDay(getDate(card.expiration)), g.getWidth()-10)); + if(card.balance) + lines = lines.concat("",/*LANG*/"Balance"+": ", g.wrapString(card.balance, g.getWidth()-10)); + if(card.note && card.note.trim()) + lines = lines.concat("",g.wrapString(card.note, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); + var titleBgColor = card.color ? getColor(card.color) : g.theme.bg2; + var titleColor = g.theme.fg2; + if (card.color) + titleColor = isLight(titleBgColor) ? BLACK : WHITE; + widget_utils.show(); + E.showScroller({ + h : g.getFontHeight(), // height of each menu item in pixels + c : lines.length, // number of menu items + // a function to draw a menu item + draw : function(idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx=lines.length-2) + showList(); + else if (idx==valueLine) + showCode(card); + }, + back : () => showList() + }); +} + +// https://github.com/metafloor/bwip-js +// https://github.com/lindell/JsBarcode + +function showList() { + if(CARDS.length == 0) { + E.showMessage(/*LANG*/"No cards"); + return; + } + E.showScroller({ + h : 52, + c : Math.max(CARDS.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var card = CARDS[idx]; + g.setColor(g.theme.fg); + g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); + if (!card) return; + var isPast = false; + var x = r.x+2, name = card.name; + var body = card.expiration ? formatDay(getDate(card.expiration)) : ""; + if (card.balance) body += "\n" + card.balance; + if (name) g.setFontAlign(-1,-1).setFont(fontBig) + .setColor(isPast ? "#888" : g.theme.fg).drawString(name, x+4,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg); + g.drawString(body, x+10,r.y+20); + } + g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items + if(card.color) { + g.setColor(getColor(card.color)); + g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4); + } + }, + select : idx => showCard(CARDS[idx]), + back : () => load() + }); +} +if(settings.fullBrightness) { + loadBrightness(); +} +showList(); diff --git a/apps/cards/app.png b/apps/cards/app.png new file mode 100644 index 000000000..b2bfa59f4 Binary files /dev/null and b/apps/cards/app.png differ diff --git a/apps/cards/codabar.js b/apps/cards/codabar.js new file mode 100644 index 000000000..2d245e091 --- /dev/null +++ b/apps/cards/codabar.js @@ -0,0 +1,88 @@ +// Encoding specification: +// http://www.barcodeisland.com/codabar.phtml +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const Barcode = require("cards.Barcode.js"); + +class codabar extends Barcode{ + constructor(data, options){ + if (/^[0-9\-\$\:\.\+\/]+$/.test(data)) { + data = "A" + data + "A"; + } + + super(data.toUpperCase(), options); + + this.text = this.options.text || this.text.replace(/[A-D]/g, ''); + } + + valid(){ + return /^[A-D][0-9\-\$\:\.\+\/]+[A-D]$/.test(this.data) + } + + encode(){ + var result = []; + var encodings = this.getEncodings(); + for(var i = 0; i < this.data.length; i++){ + result.push(encodings[this.data.charAt(i)]); + // for all characters except the last, append a narrow-space ("0") + if (i !== this.data.length - 1) { + result.push("0"); + } + } + return { + text: this.text, + data: result.join('') + }; + } + + getEncodings(){ + return { + "0": "101010011", + "1": "101011001", + "2": "101001011", + "3": "110010101", + "4": "101101001", + "5": "110101001", + "6": "100101011", + "7": "100101101", + "8": "100110101", + "9": "110100101", + "-": "101001101", + "$": "101100101", + ":": "1101011011", + "/": "1101101011", + ".": "1101101101", + "+": "1011011011", + "A": "1011001001", + "B": "1001001011", + "C": "1010010011", + "D": "1010011001" + }; + } +} + +module.exports = codabar diff --git a/apps/cards/code39.js b/apps/cards/code39.js new file mode 100644 index 000000000..c9b81d55c --- /dev/null +++ b/apps/cards/code39.js @@ -0,0 +1,130 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Code_39#Encoding +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const Barcode = require("cards.Barcode.js"); + +class CODE39 extends Barcode { + constructor(data, options){ + data = data.toUpperCase(); + + // Calculate mod43 checksum if enabled + if(options.mod43){ + data += getCharacter(mod43checksum(data)); + } + + super(data, options); + } + + encode(){ + // First character is always a * + var result = getEncoding("*"); + + // Take every character and add the binary representation to the result + for(let i = 0; i < this.data.length; i++){ + result += getEncoding(this.data[i]) + "0"; + } + + // Last character is always a * + result += getEncoding("*"); + + return { + data: result, + text: this.text + }; + } + + valid(){ + return /^[0-9A-Z\-\.\ \$\/\+\%]+$/.test(this.data); + } +} + + + + + + +// All characters. The position in the array is the (checksum) value +var characters = [ + "0", "1", "2", "3", + "4", "5", "6", "7", + "8", "9", "A", "B", + "C", "D", "E", "F", + "G", "H", "I", "J", + "K", "L", "M", "N", + "O", "P", "Q", "R", + "S", "T", "U", "V", + "W", "X", "Y", "Z", + "-", ".", " ", "$", + "/", "+", "%", "*" +]; + +// The decimal representation of the characters, is converted to the +// corresponding binary with the getEncoding function +var encodings = [ + 20957, 29783, 23639, 30485, + 20951, 29813, 23669, 20855, + 29789, 23645, 29975, 23831, + 30533, 22295, 30149, 24005, + 21623, 29981, 23837, 22301, + 30023, 23879, 30545, 22343, + 30161, 24017, 21959, 30065, + 23921, 22385, 29015, 18263, + 29141, 17879, 29045, 18293, + 17783, 29021, 18269, 17477, + 17489, 17681, 20753, 35770 +]; + +// Get the binary representation of a character by converting the encodings +// from decimal to binary +function getEncoding(character){ + return getBinary(characterValue(character)); +} + +function getBinary(characterValue){ + return encodings[characterValue].toString(2); +} + +function getCharacter(characterValue){ + return characters[characterValue]; +} + +function characterValue(character){ + return characters.indexOf(character); +} + +function mod43checksum(data){ + var checksum = 0; + for(let i = 0; i < data.length; i++){ + checksum += characterValue(data[i]); + } + + checksum = checksum % 43; + return checksum; +} + +module.exports = CODE39; diff --git a/apps/cards/encode.js b/apps/cards/encode.js new file mode 100644 index 000000000..7cb1fcc87 --- /dev/null +++ b/apps/cards/encode.js @@ -0,0 +1,67 @@ +/* + * JS source adapted from https://github.com/lindell/JsBarcode + * + * The MIT License (MIT) + * + * Copyright (c) 2016 Johan Lindell (johan@lindell.me) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +const BINARIES = { + 'L': [ // The L (left) type of encoding + '0001101', '0011001', '0010011', '0111101', '0100011', + '0110001', '0101111', '0111011', '0110111', '0001011' + ], + 'G': [ // The G type of encoding + '0100111', '0110011', '0011011', '0100001', '0011101', + '0111001', '0000101', '0010001', '0001001', '0010111' + ], + 'R': [ // The R (right) type of encoding + '1110010', '1100110', '1101100', '1000010', '1011100', + '1001110', '1010000', '1000100', '1001000', '1110100' + ], + 'O': [ // The O (odd) encoding for UPC-E + '0001101', '0011001', '0010011', '0111101', '0100011', + '0110001', '0101111', '0111011', '0110111', '0001011' + ], + 'E': [ // The E (even) encoding for UPC-E + '0100111', '0110011', '0011011', '0100001', '0011101', + '0111001', '0000101', '0010001', '0001001', '0010111' + ] +}; + +// Encode data string +const encode = (data, structure, separator) => { + let encoded = data + .split('') + .map((val, idx) => BINARIES[structure[idx]]) + .map((val, idx) => val ? val[data[idx]] : ''); + + if (separator) { + const last = data.length - 1; + encoded = encoded.map((val, idx) => ( + idx < last ? val + separator : val + )); + } + + return encoded.join(''); +}; + +module.exports = encode; \ No newline at end of file diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json new file mode 100644 index 000000000..c8d19a375 --- /dev/null +++ b/apps/cards/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "cards", + "name": "Cards", + "version": "0.05", + "description": "Display loyalty cards", + "icon": "app.png", + "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], + "tags": "cards", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"cards.app.js","url":"app.js"}, + {"name":"cards.settings.js","url":"settings.js"}, + {"name":"cards.Barcode.js","url":"Barcode.js"}, + {"name":"cards.qrcode.js","url":"qrcode.js"}, + {"name":"cards.codabar.js","url":"codabar.js"}, + {"name":"cards.code39.js","url":"code39.js"}, + {"name":"cards.EAN.js","url":"EAN.js"}, + {"name":"cards.EAN8.js","url":"EAN8.js"}, + {"name":"cards.EAN13.js","url":"EAN13.js"}, + {"name":"cards.UPC.js","url":"UPC.js"}, + {"name":"cards.UPCE.js","url":"UPCE.js"}, + {"name":"cards.encode.js","url":"encode.js"}, + {"name":"cards.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"cards.settings.json"}] +} diff --git a/apps/cards/qrcode.js b/apps/cards/qrcode.js new file mode 100644 index 000000000..ff79d7bee --- /dev/null +++ b/apps/cards/qrcode.js @@ -0,0 +1,705 @@ +/* + * C source adapted from https://github.com/ricmoo/QRCode + * + * The MIT License (MIT) + * + * This library is written and maintained by Richard Moore. + * Major parts were derived from Project Nayuki's library. + * + * Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode) + * Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library) + * + * 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. + */ + +var c = E.compiledC(` +// int get_qr(int, int) + +typedef signed char __int8_t; +typedef unsigned char __uint8_t; +typedef signed short int __int16_t; +typedef unsigned short int __uint16_t; +typedef signed int __int32_t; +typedef unsigned int __uint32_t; + +typedef __int8_t int8_t; +typedef __int16_t int16_t; +typedef __int32_t int32_t; +typedef __uint8_t uint8_t; +typedef __uint16_t uint16_t; +typedef __uint32_t uint32_t; + +typedef struct QRCode { + uint8_t version; + uint8_t size; + uint8_t ecc; + uint8_t mode; + uint8_t mask; + uint8_t *modules; +} QRCode; +uint16_t qrcode_getBufferSize(uint8_t version); + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data); +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length); + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y); + +static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = { + { 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, + { 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, + { 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, + { 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, +}; + +static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = { + { 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, + { 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, + { 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, + { 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, +}; + +static const uint16_t NUM_RAW_DATA_MODULES[40] = { + 208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523, + 7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587, + 19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648 +}; +static int max(int a, int b) { + if (a > b) { return a; } + return b; +} + +static int abs(int value) { + if (value < 0) { return -value; } + return value; +} + +static void *memset(void *s, int c, int n) { + char *arr = (char *)s; + for (int i = 0; i= '0' && c <= '9') { return (c - '0'); } + if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); } + switch (c) { + case ' ': return 36; + case '$': return 37; + case '%': return 38; + case '*': return 39; + case '+': return 40; + case '-': return 41; + case '.': return 42; + case '/': return 43; + case ':': return 44; + } + + return -1; +} + +static bool isAlphanumeric(const char *text, uint16_t length) { + while (length != 0) { + if (getAlphanumeric(text[--length]) == -1) { return false; } + } + return true; +} +static bool isNumeric(const char *text, uint16_t length) { + while (length != 0) { + char c = text[--length]; + if (c < '0' || c > '9') { return false; } + } + return true; +} +static char getModeBits(uint8_t version, uint8_t mode) { + unsigned int modeInfo = 0x7bbb80a; + if (version > 9) { modeInfo >>= 9; } + + if (version > 26) { modeInfo >>= 9; } + char result = 8 + ((modeInfo >> (3 * mode)) & 0x07); + if (result == 15) { result = 16; } + + return result; +} + +typedef struct BitBucket { + uint32_t bitOffsetOrWidth; + uint16_t capacityBytes; + uint8_t *data; +} BitBucket; +static uint16_t bb_getGridSizeBytes(uint8_t size) { + return (((size * size) + 7) / 8); +} + +static uint16_t bb_getBufferSizeBytes(uint32_t bits) { + return ((bits + 7) / 8); +} + +static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) { + bitBuffer->bitOffsetOrWidth = 0; + bitBuffer->capacityBytes = capacityBytes; + bitBuffer->data = data; + + memset(data, 0, bitBuffer->capacityBytes); +} + +static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) { + bitGrid->bitOffsetOrWidth = size; + bitGrid->capacityBytes = bb_getGridSizeBytes(size); + bitGrid->data = data; + + memset(data, 0, bitGrid->capacityBytes); +} + +static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) { + uint32_t offset = bitBuffer->bitOffsetOrWidth; + for (int8_t i = length - 1; i >= 0; i--, offset++) { + bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7)); + } + bitBuffer->bitOffsetOrWidth = offset; +} + +static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + if (on) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0); + if (on ^ invert) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) { + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + for (uint8_t x = 0; x < size; x++) { + if (bb_getBit(isFunction, x, y)) { continue; } + + bool invert = 0; + switch (mask) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + } + bb_invertBit(modules, x, y, invert); + } + } +} + +static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) { + bb_setBit(modules, x, y, on); + bb_setBit(isFunction, x, y, true); +} +static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + uint8_t size = modules->bitOffsetOrWidth; + + for (int8_t i = -4; i <= 4; i++) { + for (int8_t j = -4; j <= 4; j++) { + uint8_t dist = max(abs(i), abs(j)); + int16_t xx = x + j, yy = y + i; + if (0 <= xx && xx < size && 0 <= yy && yy < size) { + setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4); + } + } + } +} +static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + for (int8_t i = -2; i <= 2; i++) { + for (int8_t j = -2; j <= 2; j++) { + setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1); + } + } +} + +static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) { + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t data = ecc << 3 | mask; + uint32_t rem = data; + for (int i = 0; i < 10; i++) { + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + } + + data = data << 10 | rem; + data ^= 0x5412; + for (uint8_t i = 0; i <= 5; i++) { + setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0); + setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0); + setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0); + + for (int8_t i = 9; i < 15; i++) { + setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0); + } + for (int8_t i = 0; i <= 7; i++) { + setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0); + } + + for (int8_t i = 8; i < 15; i++) { + setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, size - 8, true); +} +static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) { + + int8_t size = modules->bitOffsetOrWidth; + if (version < 7) { return; } + uint32_t rem = version; + for (uint8_t i = 0; i < 12; i++) { + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + } + + uint32_t data = version << 12 | rem; + for (uint8_t i = 0; i < 18; i++) { + bool bit = ((data >> i) & 1) != 0; + uint8_t a = size - 11 + i % 3, b = i / 3; + setFunctionModule(modules, isFunction, a, b, bit); + setFunctionModule(modules, isFunction, b, a, bit); + } +} + +static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) { + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t i = 0; i < size; i++) { + setFunctionModule(modules, isFunction, 6, i, i % 2 == 0); + setFunctionModule(modules, isFunction, i, 6, i % 2 == 0); + } + drawFinderPattern(modules, isFunction, 3, 3); + drawFinderPattern(modules, isFunction, size - 4, 3); + drawFinderPattern(modules, isFunction, 3, size - 4); + + if (version > 1) { + + uint8_t alignCount = version / 7 + 2; + uint8_t step; + if (version != 32) { + step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; + } else { + step = 26; + } + + uint8_t alignPositionIndex = alignCount - 1; + uint8_t alignPosition[alignCount]; + + alignPosition[0] = 6; + + uint8_t l_size = version * 4 + 17; + for (uint8_t i = 0, pos = l_size - 7; i < alignCount - 1; i++, pos -= step) { + alignPosition[alignPositionIndex--] = pos; + } + + for (uint8_t i = 0; i < alignCount; i++) { + for (uint8_t j = 0; j < alignCount; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) { + continue; + } else { + drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]); + } + } + } + } + drawFormatBits(modules, isFunction, ecc, 0); + drawVersion(modules, isFunction, version); +} +static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) { + + uint32_t bitLength = codewords->bitOffsetOrWidth; + uint8_t *data = codewords->data; + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t i = 0; + for (int16_t right = size - 1; right >= 1; right -= 2) { + if (right == 6) { right = 5; } + + for (uint8_t vert = 0; vert < size; vert++) { + for (int j = 0; j < 2; j++) { + uint8_t x = right - j; + bool upwards = ((right & 2) == 0) ^ (x < 6); + uint8_t y = upwards ? size - 1 - vert : vert; + if (!bb_getBit(isFunction, x, y) && i < bitLength) { + bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0); + i++; + } + } + } + } +} +static uint32_t getPenaltyScore(BitBucket *modules) { + uint32_t result = 0; + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + + bool colorX = bb_getBit(modules, 0, y); + for (uint8_t x = 1, runX = 1; x < size; x++) { + bool cx = bb_getBit(modules, x, y); + if (cx != colorX) { + colorX = cx; + runX = 1; + + } else { + runX++; + if (runX == 5) { + result += 3; + } else if (runX > 5) { + result++; + } + } + } + } + for (uint8_t x = 0; x < size; x++) { + bool colorY = bb_getBit(modules, x, 0); + for (uint8_t y = 1, runY = 1; y < size; y++) { + bool cy = bb_getBit(modules, x, y); + if (cy != colorY) { + colorY = cy; + runY = 1; + } else { + runY++; + if (runY == 5) { + result += 3; + } else if (runY > 5) { + result++; + } + } + } + } + + uint16_t black = 0; + for (uint8_t y = 0; y < size; y++) { + uint16_t bitsRow = 0, bitsCol = 0; + for (uint8_t x = 0; x < size; x++) { + bool color = bb_getBit(modules, x, y); + if (x > 0 && y > 0) { + bool colorUL = bb_getBit(modules, x - 1, y - 1); + bool colorUR = bb_getBit(modules, x, y - 1); + bool colorL = bb_getBit(modules, x - 1, y); + if (color == colorUL && color == colorUR && color == colorL) { + result += 3; + } + } + bitsRow = ((bitsRow << 1) & 0x7FF) | color; + bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x); + if (x >= 10) { + if (bitsRow == 0x05D || bitsRow == 0x5D0) { + result += 40; + } + if (bitsCol == 0x05D || bitsCol == 0x5D0) { + result += 40; + } + } + if (color) { black++; } + } + } + uint16_t total = size * size; + for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) { + result += 10; + } + + return result; +} + +static uint8_t rs_multiply(uint8_t x, uint8_t y) { + uint16_t z = 0; + for (int8_t i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + return z; +} + +static void rs_init(uint8_t degree, uint8_t *coeff) { + memset(coeff, 0, degree); + coeff[degree - 1] = 1; + uint16_t root = 1; + for (uint8_t i = 0; i < degree; i++) { + + for (uint8_t j = 0; j < degree; j++) { + coeff[j] = rs_multiply(coeff[j], root); + if (j + 1 < degree) { + coeff[j] ^= coeff[j + 1]; + } + } + root = (root << 1) ^ ((root >> 7) * 0x11D); + } +} + +static void rs_getRemainder(uint8_t degree, uint8_t *coeff, const uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) { + for (uint8_t i = 0; i < length; i++) { + uint8_t factor = data[i] ^ result[0]; + for (uint8_t j = 1; j < degree; j++) { + result[(j - 1) * stride] = result[j * stride]; + } + result[(degree - 1) * stride] = 0; + + for (uint8_t j = 0; j < degree; j++) { + result[j * stride] ^= rs_multiply(coeff[j], factor); + } + } +} +static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) { + int8_t mode = 2; + + if (isNumeric((char*)text, length)) { + mode = 0; + bb_appendBits(dataCodewords, 1 << 0, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 0)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 10 + ((char)(text[i]) - '0'); + accumCount++; + if (accumCount == 3) { + bb_appendBits(dataCodewords, accumData, 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1); + } + + } else if (isAlphanumeric((char*)text, length)) { + mode = 1; + bb_appendBits(dataCodewords, 1 << 1, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 1)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 45 + getAlphanumeric((char)(text[i])); + accumCount++; + if (accumCount == 2) { + bb_appendBits(dataCodewords, accumData, 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, 6); + } + + } else { + bb_appendBits(dataCodewords, 1 << 2, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 2)); + for (uint16_t i = 0; i < length; i++) { + bb_appendBits(dataCodewords, (char)(text[i]), 8); + } + } + + return mode; +} + +static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) { + uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1]; + uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1]; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint8_t blockEccLen = totalEcc / numBlocks; + uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks; + uint8_t shortBlockLen = moduleCount / 8 / numBlocks; + + uint8_t shortDataBlockLen = shortBlockLen - blockEccLen; + + uint8_t result[data->capacityBytes]; + memset(result, 0, sizeof(result)); + + uint8_t coeff[blockEccLen]; + rs_init(blockEccLen, coeff); + + uint16_t offset = 0; + uint8_t *dataBytes = data->data; + + for (uint8_t i = 0; i < shortDataBlockLen; i++) { + uint16_t index = i; + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + if (blockNum == numShortBlocks) { stride++; } + + index += stride; + } + } + { + uint16_t index = shortDataBlockLen * (numShortBlocks + 1); + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + + if (blockNum == 0) { stride++; } + index += stride; + } + } + + uint8_t blockSize = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + if (blockNum == numShortBlocks) { blockSize++; } + + rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks); + dataBytes += blockSize; + } + + memcpy(data->data, result, data->capacityBytes); + data->bitOffsetOrWidth = moduleCount; +} + +static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0); + +uint16_t qrcode_getBufferSize(uint8_t version) { + return bb_getGridSizeBytes(4 * version + 17); +} +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) { + uint8_t size = version * 4 + 17; + qrcode->version = version; + qrcode->size = size; + qrcode->ecc = ecc; + qrcode->modules = modules; + + uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1]; + struct BitBucket codewords; + uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)]; + bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes)); + int8_t mode = encodeDataCodewords(&codewords, data, length, version); + + if (mode < 0) { return -1; } + qrcode->mode = mode; + uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth; + if (padding > 4) { padding = 4; } + bb_appendBits(&codewords, 0, padding); + bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8); + for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) { + bb_appendBits(&codewords, padByte, 8); + } + + BitBucket modulesGrid; + bb_initGrid(&modulesGrid, modules, size); + + BitBucket isFunctionGrid; + uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)]; + bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size); + drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits); + performErrorCorrection(version, eccFormatBits, &codewords); + drawCodewords(&modulesGrid, &isFunctionGrid, &codewords); + uint8_t mask = 0; + int32_t minPenalty = (2147483647); + for (uint8_t i = 0; i < 8; i++) { + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i); + applyMask(&modulesGrid, &isFunctionGrid, i); + int penalty = getPenaltyScore(&modulesGrid); + if (penalty < minPenalty) { + mask = i; + minPenalty = penalty; + } + applyMask(&modulesGrid, &isFunctionGrid, i); + } + + qrcode->mask = mask; + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask); + applyMask(&modulesGrid, &isFunctionGrid, mask); + + return 0; +} + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) { + return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data)); +} + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) { + if (x >= qrcode->size || y >= qrcode->size) { + return false; + } + + uint32_t offset = y * qrcode->size + x; + return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +int get_qr (char *string, uint8_t *qrcodeBitmap) { + // The structure to manage the QR code + QRCode qrcode; + + // Allocate a chunk of memory to store the QR code + uint8_t qrcodeBytes[qrcode_getBufferSize(3)]; + + qrcode_initText(&qrcode, qrcodeBytes, 3, 0, string); + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + qrcodeBitmap[x + y * qrcode.size] = qrcode_getModule(&qrcode, x, y); + } + } + return qrcode.size; +} +`); + +function getBinaryQR (value) { + var qrcodeBitmap = new Uint8Array(850); + var flatValue = Uint8Array(E.toArrayBuffer(E.toFlatString(value ,0))); + var valueAddr = E.getAddressOf(flatValue, true); + var qrAddr = E.getAddressOf(qrcodeBitmap, true); + if (valueAddr == 0 || qrAddr == 0) { + console.log ("Failed to get flat arrays.."); + //return; + } + var qrsize = c.get_qr(valueAddr, qrAddr); + return { data: qrcodeBitmap, size: qrsize }; +} + +module.exports = getBinaryQR; diff --git a/apps/cards/screenshot_cards_barcode.png b/apps/cards/screenshot_cards_barcode.png new file mode 100644 index 000000000..e57e9765a Binary files /dev/null and b/apps/cards/screenshot_cards_barcode.png differ diff --git a/apps/cards/screenshot_cards_card1.png b/apps/cards/screenshot_cards_card1.png new file mode 100644 index 000000000..1c8c9514c Binary files /dev/null and b/apps/cards/screenshot_cards_card1.png differ diff --git a/apps/cards/screenshot_cards_card2.png b/apps/cards/screenshot_cards_card2.png new file mode 100644 index 000000000..15ee1cac4 Binary files /dev/null and b/apps/cards/screenshot_cards_card2.png differ diff --git a/apps/cards/screenshot_cards_overview.png b/apps/cards/screenshot_cards_overview.png new file mode 100644 index 000000000..0a933ad0a Binary files /dev/null and b/apps/cards/screenshot_cards_overview.png differ diff --git a/apps/cards/screenshot_cards_qrcode.png b/apps/cards/screenshot_cards_qrcode.png new file mode 100644 index 000000000..5bace3e6e Binary files /dev/null and b/apps/cards/screenshot_cards_qrcode.png differ diff --git a/apps/cards/settings.js b/apps/cards/settings.js new file mode 100644 index 000000000..451b02204 --- /dev/null +++ b/apps/cards/settings.js @@ -0,0 +1,26 @@ +(function(back) { + var settings = require("Storage").readJSON("cards.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("cards.settings.json", settings); + } + var mainmenu = { + "" : { "title" : "Cards" }, + "< Back" : back, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, + /*LANG*/"Full Brightness" : { + value : !!settings.fullBrightness, + onchange: v => { + settings.fullBrightness = v; + updateSettings(); + } + } + }; + E.showMenu(mainmenu); +}) diff --git a/apps/cassioWatch/ChangeLog b/apps/cassioWatch/ChangeLog new file mode 100644 index 000000000..e0abc576c --- /dev/null +++ b/apps/cassioWatch/ChangeLog @@ -0,0 +1,14 @@ +0.0: Main App. +0.1: Performance Fixes. +0.2: Correct Screen Clear and Draw. +0.3: Minor Fixes. +0.4: Clear Old Time on Screen Before Draw new Time +0.5: Update Date and Time to use Native date Funcions +0.6: Add Settings Page +0.7: Update Rocket Sequences Scope to not use memory all time +0.8: Update Some Variable Scopes to not use memory until need +0.9: Remove ESLint spaces +0.10: Show daily steps, heartrate and the temperature if weather information is available. +0.11: Tell clock widgets to hide. +0.12: Swipe down to see widgets, step counter now just uses getHealthStatus +0.13: Report latest HRM rather than HRM 10 minutes ago (fix #2395) \ No newline at end of file diff --git a/apps/cassioWatch/README.md b/apps/cassioWatch/README.md new file mode 100644 index 000000000..6c13cdcac --- /dev/null +++ b/apps/cassioWatch/README.md @@ -0,0 +1,12 @@ +# cassioWatch + +![Screenshot](screens/screen_night.png) ![Screenshot](screens/screen_day.png) + +Clock with Space Cassio Watch Style. + +It displays current temperature,day,steps,battery.heartbeat and weather. + + +**To-do**: + +* Align and change size of some elements diff --git a/apps/cassioWatch/app.js b/apps/cassioWatch/app.js new file mode 100644 index 000000000..68c8a3ceb --- /dev/null +++ b/apps/cassioWatch/app.js @@ -0,0 +1,156 @@ +const storage = require('Storage'); + +require("Font6x12").add(Graphics); +require("Font8x12").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); + +function bigThenSmall(big, small, x, y) { + g.setFont("7x11Numeric7Seg", 2); + g.drawString(big, x, y); + x += g.stringWidth(big); + g.setFont("8x12"); + g.drawString(small, x, y); +} + +function getBackgroundImage() { + return require("heatshrink").decompress(atob("2GwwkGIf4AfgMRkUiiIHCiMRiAMDAwYCCBAYVDAHMv/4ACkBIBAgPxBgM/BYXyAoICBCowA5gRADKQUDKAYMCmYCBiBXBCo4A5J4MxiMSKQUf+YBBBgSiBgc/kBXBBAMyCoK2CK/btCiUhfAJLCkBkDiMQgBXDCoUvNAJX+AAU/+MB/8wAQIAC+cQK5hoDgIEBBIQFEAYIPHBIgBBAQQIDBwZXSKIMxgJaBgEjmZYCmBXLgLBBkkAgUhiMxBIM0iMSCoMRkZECkQJEichBINDiETAgISBiQTDK6MvJAXzVIQrBBYMCK5E/K4kwGIJXFgdAMgQQBiYiCDgU0HQSlCgMikIEBEAMTDYJXQ+UikYDBj6nCAAMTWoJ6BK4oVEK4c0oQ+BK4MjAgMDJoJXHNYJXHBwa0BohcDY4QAKgJQE+LzBNwJVBkQMEkBXBCoyvFJAVAKISaBiMiHQRIDkVBoSyCK5CvBAgavNDAJAC+cQn5DCgSpBl4MDgBXBgCsBCoYoMLAKREgIKDBJIdKK5oA/AH4A/AH4A/ADUBIH4APiAFEi1mAGUADrkRKwUGK2ZXes1gK2xXfD8A3/K/4AWgxX/ACtga2AwIHLkAgCwvJw6RcDgIABK+w4cK/I4dsEGP5BXtSAQ6BV/5XSG4RX/K6Y3fK+42CK/5XTGwcGK/5XSVwY5cK+o1DAAayYsAhDsCv4K7BTBK4YeYK7CyFVzJXFFIpXtVwYiYK/rmZKYYDDELJXXG4YiaK/Y0aKgQAEK+gkdKt5XGKzqv5GTpX6ETlgK4xWrKTyxKVthXmAGRX/K/5X/AH5X/K/4gBAH4A/AFz/uAH4A/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/AHNggEGHfEAgAEHKyQXVK0qTCAggbUK+6SDAApzXK/5BRDYZX3KxBBSYqxXngyvaV25XEd4ZCSsAcBAoRZ2dQZXBLwgaQCIYeCAGirCS4YGCDSJXCC6ZaodYICBZzSw4S4I+XDgSv4K4rzCK/47RAQTMaWHI9YV3TscV3aVagByBK3SwCSqyt8AAQ+XK/4A/AH4A/AH4A3gAA/AH4AuZbdggwc3ADpX/K/5XxsEAgA+XK/o8BgBX/K64/WK/4/XK/5X/K/5XvgBX/K64cYHrw4CSTFggCuXK4oDCEQJXYDS6ScDgg4CPKyRCAAZX0HAgBDK+LlYK4oeBAwZ9aK+lgAoQGBgyvzDIIDBK66sCG4JXYCwIBDK7ADCK+xZCHwJXzGoQ8BK7DpBAAaSXSgRXZO4okCK+IaXV4oABEILSWSYjRCHSo3BDSxXEAAIcBAISvyKawcIAYIGCK/4cUH4YlaHS0AHgI1XOg5YBPrY6WHgRXfAGRXDHzBX8VoJX/K68ADjRX6sBX/K/5X/K8wdcK/UAG7B0iKzZYbK/BWDAH4A/hWpzWhIf4ASgOpzIAB0EAhhH/AB8ZzGJ1WazMA4pH/AB+pxOZxOpzVMqA2ugUzmcgD7cKVYOqzGqpnRFw8ykchK8kviEBmQFBgMiFocSCAcSkUQAgMikRsHhWqxOq0Ut4mqBw0DC4IxBD4wpBHAQMCA4cCGJIAFj8hDIQuBkMTCwU/AYQJBiUxFoPxiIVDK4kyxUz4cxl+KK5MfDQXyD4UCmMSmAEBAQQHDgMTmIxHAAqpBmaqCFwMDEYZRBgEjCQQBB+USK5E/ns/0Uzwc6K48ykYkCK4IfCc4I4CK4QHEBAYAMiICBmYuDmQEBh8iAgRXCLISvJO4MqwcklEiK5CADV4oaBV4oHEK6Eve4JNCbwRfCiMTFoMDkMRSAJXCD49azWp0UqzWayJXIQwcAO4cCkMCFIJOCA4XxK6KPBkR6DTwYyBAwYPEAggfFzORpWK1OZyAOHJ4QfERAUSEgQxIIIgAr1URWIOZzOgGtwAhgMZzWq1OaIv4ASKgOqzTkvAEmq1WgFtQA==")); +} + +function getRocketSequences() { + return { + 1: require("heatshrink").decompress(atob("qFGwkCkQAiiEBEkUgKQhPhE8ogCE8YhCiQoEE7pKEPIgncTQ4neEwpQCPoh1eJYYwCJ7QmHKAh1hZIpOjPAUBJ0ZQCTzEhExZ1lPAZ1kKDQmOJ65O2E65OPOy5O2E64mPOyxO/J2wnPJyx2QJ35O/J2khE0p2POq52PEy4nOiQnlOrEhiSfMJrEggQnLJzB1CPBQmZkInMEzBQDPBImbPBR1ZEoRMCZYImhgQgEE0BzFKAgmaDwLDFKAbqdYQwHBOrcgDgLBFJrsiiRNGYbpLBY4Ymhd4omkkUhE0pQEEwUBJjrHBd4QmCdzoiBDwYrCPLyZHF4QnagQeCE8UgJwYniJwgnIOzwfFO0wJCJzMQE4gyFEzR2FBQombkInDQI4AakAnBTYS+ZE5BMDE0LEES7YnLE0R3FAEQA=")), + 2: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYon/AA0gEAQniEwIhCAgYndEIjqBE8CaGKogmgKAp1fKAgncExBQBBQR1gKAp7BJ0IndExR4CE0idaOpYnbExqeYJxxPYEx0BJ0x2XExx2XJ20QE6xONJi5OPGwJOlBwLFkLoLFlBwJOkOwJOlE4JOkTjBOOE/52Pdi5OPEy7FnE5wmXE5xOZT5gmYEoMiiB1lgR4KTLAkDPBJ1WIAYDDKA4mWJwchDwYEDTjQiDJQh4GYLAhHFosSJy6OCTIxaEEywbBKYwjEEzMgUQxQFBogAURwZOGOjTKJdTYnOEryfHE0JQEfIpQgYQMAgJLeAgrtfTI4ndgSaFE4h0bdQkSZQpOfEAgIBO0AnEdrh2FJAb1EdbInEBIpObOwhOEEzYnFXzZ2HE4QlhE4QlDFMKcDYooniO0QnDT0YnCE0ciA")), + 3: require("heatshrink").decompress(atob("qFGwkEogAjiMUEkVAKYgnhPYolgOQIniOYZ4FOcLqBE8CaGKojpgKAomhEYUQE7gmHKAIxCE0QkCPYR1gZIgnZExR4CJ0idmE7ZONYzImNgEUJ0p3YJRh2ZJJwnXOpQhBdkpaETsMEGQhOhE7jFLUYpOfTzgmKE4hOiE4hOigEUJ0rvCEywnPEqx2OTjBOOE7ImOTsqeZE5zFYoJOmT5kBJzEAih4LdK5mBAQInKOqoYDEgR4JEypHDEYbxJOq5ABdgZ7CEzZOEJQgnGihOYEIzJFTionCKYxWGEy9ADAYnGUIYmWog/EdBFAEy7KIKAwnjKwLqWE5pMeT48CVQpQfgMjKEtEiAnfEQJQCgJSCTcB6FJzkEdYcUE8FAdQghDOzonKTjh2EZAidcDoInHJzodBOwx/BE8JxcOwsAOwQmhJgSXDObwnFEwUUO0LFGE8aeiE4YmiokQE0tE")), + 4: require("heatshrink").decompress(atob("qFGwkCkQAjiMSEkRTFE/4AGkMAgQCBE8MgEIYEDE7whDdQIngTQxVEE0ChFTjxQFE7jnFKAgxCOsBQFZgJ1gE7wmKPAROkTrTEHGAwnYiBHJFAaeXOoyXBEQZPac5AsFgJOhAoh2XJwwnFKoROdE4J9GJzwnIiQmVkInPAC0QE5AJFE64mHY5DFdE4SBEYr5JDJ0hKDJ0jCZJxoACgInmKLAmOTq5OOEy5OPTsxOYE5wmXO5wlYkAnMOqshiRNCgR4LOC8CkJCCEzxHDAgYnJOqpAEDoZ4HEyodDEQpQHdCsQOwwFHEyzoCPYzJGEy0gEwaZGA4acVEQSjHKAomXkQYEYAwlZeRKYDE8gjCYa7zJEwcCkImfKAb4FAD0hdTh4LgRSBOcR0CJz0gYYrrgN4QnEYrxOEE4bEeiAnGF4J2idL6VDE8ohBE0gnFE0J0BE4QGBiROgdIQABgJ2hJoTtjYgZSEE8ScgE4omikUQTcQADA=")), + 5: require("heatshrink").decompress(atob("qFGwkCkQAikMAgIliKYonhiAnjkEATIIniEwIhCAgYndEIhQFYUZVEE0BQFOr5QEeQQmiKAL1DOr5QEE7ROCDgZVEAoInZDwchFQQoDPAJOdEQYrBdrZFDOYwncEJDsDVIpOXgJxEE4pObEAgGFgJOaE48BaIhOZJ5ZObY5ROcE441CE6xOGPAwtCJzpGCJ0hHDkI1DJzwoEJzInLFg52dUo5O/J35OzE54mWOx4mXJxx1XE54mXkUhExkSJzCfMOrAlBPBiZXgQDBAQQmgJgh4JOqoYEFYwmaDoZzEFgh1YDgkiiAFEKAroXJJAGFiQmVkCNDTIz5EJy57HKAomXkQYEJoqaYeRadEJrAnJEQUAgJPiAoYmeT4cCkAnBE0BKCJkT1EkDCeJYYiDOkLDFFL5wBE4guCPDhEBEwQiDY70CkInDiQnCJzkhOwhKDdzp2Idb4nEE0B0Bdo4niE0J0CeYhOhgESUYYnidsgnEE0KeCE0gnDE0ciA")), + 6: require("heatshrink").decompress(atob("qFGwkCkQA/ABEgKQZPhEwgABEsAoGJkBxBE8JKEAowAbJIhQEgLDiPooAdKA4ncTZAndSwhQEFoInaJQkSKAwlZdgwnfSgYADE4h1ZDwInlcggnIOzAdCE8i7EY5J3XDgYhGd4pOZEI52bSYwGCOAJ2bYIodEOzZOFFAjFcEwwAIE6xOHABBO/J34ndEyx2PJ00BJ00SJ0p1XE54mXOxxO/J5wmYgQnMOrB2BPBgkWiJ1CPBbBYAYR4KiTAXRwIrFTjgZDJYZ4IEyoiEIwrDcEJJQFOqwiBDARxFFwgmXkAYDEogsBF4QmXEQJ7GUYYkBEzDKJAgYmdEQbKFEzonEKYgngJwgmfZggmjKQghgiBRGkBzeTgUikJRgc47LDErTnDEAkQJzkCJwYnEJzonEJIaddOwhJEJzgdBE4hYEJzieJADgnEE0KUCXzoAGkJLEiB2hOgQDBT0TsDT0YmlE4YmjkQ=")), + 7: require("heatshrink").decompress(atob("qFGwkCkQAhkIpBiQlhkBSEJ8InlEIIoFE7whEE8pQFE7giBJQoneI4MCTYhQDE7YdCYYondEQYnEPwZ1bE5BQCJzonHkR2ZEAkBE4pNBE7zHFYrYhFUgonaXAQeEEwruZEYcgiROHJ7AfDAwxOeAAURiAmHE65HIOzwmOJ35OPE6xOPO35O/J35O/J1gnPEyx2PEy5OOOq5OnE5xOYO5omZgJQMJrQnLiQnagR4JOq5nCDgZ1fEYRLDE5DoZkUQNoZ4GOrJKGAoomXOw7lCAwYmYDgJSEAAUBA4QDBJzB6FOQrDXJwTJFdLjJKE9jDYZRAmkKAwmhKAgmiKAYmBkApdJIgjCKYIncOQYvJYTovGE84lagR2DE4xOakBOEgJXFOjYnEJAbtdOwggEkAmbDgInDE0B0BE4QgcE5AkiXYbpCOLonGYo4nhPMYnCUEgnBY0kiA==")), + 8: require("heatshrink").decompress(atob("qFGwkCkQA/ABBSEJ8MgE4kBEsBPFE7xMCOIJ3hOYgFEE7rCGE70gE4pQBiAndYQwjBUohOZD4ZQFE7YkBE5AICYbZ2GE7sggJRCAA8iYzZOITroALE7EhExh4CAC0QExpPXOponZExx2XJ24nWdh52XdhzF/Yu5O/J35O0E55OXOx5O/J2omXE5x1XO54mYgQnMJrR4LOrciiAmiJgR4KEzIjDPBAlYiAiEeI51YkEBE4J5CD4KceTQQcBJgRQFdTZDCJIjDcNIqhGdTQmCkByFTTInDKgoAEE7ZEEJwhPdE1R1FE0InEE0R3DEwTGcDwomEE7hKFPYqafE8ROCE5DJbE5B/IEqh2ED4gnCJrMCJwgnEiB2bE4qeFEzUggQmIBQLEaEQImHLIImaE4YfcOw4lEFMLECS7onJO8wmkE4QljAAIA==")), + }; +} + +let rocketSequence = 1; +let settings = storage.readJSON("cassioWatch.settings.json", true) || {}; +let rocketSpeed = settings.rocketSpeed || 700; +delete settings; + +// schedule a draw for the next minute +let rocketInterval; +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function clearIntervals() { + if (rocketInterval) clearInterval(rocketInterval); + rocketInterval = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.clearRect(80, 57, 170, 96); + g.setColor(0, 255, 255); + g.drawRect(80, 57, 170, 96); + g.fillRect(80, 57, 170, 96); + g.setColor(0, 0, 0); + g.drawString(require("locale").time(new Date(), 1), 70, 60); + g.setFont("8x12", 2); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 130); + g.setFont("8x12"); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 126); + g.setFont("8x12", 2); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 78, 137); +} + +function drawBattery() { + bigThenSmall(E.getBattery(), "%", 135, 21); +} + +function drawRocket() { + let Rocket = getRocketSequences(); + g.clearRect(5, 62, 63, 115); + g.setColor(0, 255, 255); + g.drawRect(5, 62, 63, 115); + g.fillRect(5, 62, 63, 115); + g.drawImage(Rocket[rocketSequence], 5, 65, { scale: 0.7 }); + g.setColor(0, 0, 0); + rocketSequence = rocketSequence + 1; + if(rocketSequence > 8) rocketSequence = 1; +} + +function getTemperature(){ + try { + var weatherJson = storage.readJSON('weather.json'); + var weather = weatherJson.weather; + return Math.round(weather.temp-273.15); + } catch(ex) { + print(ex) + return "?" + } +} + +function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; +} + + +function draw() { + queueDraw(); + + g.clear(1); + g.setColor(0, 255, 255); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + let background = getBackgroundImage(); + g.drawImage(background, 0, 0, { scale: 1 }); + g.setColor(0, 0, 0); + g.setFont("6x12"); + g.drawString("Launching Process", 30, 20); + g.setFont("8x12"); + g.drawString("ACTIVATE", 40, 35); + + g.setFontAlign(0,-1); + g.setFont("8x12", 2); + g.drawString(getTemperature(), 155, 132); + g.drawString(Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm), 109, 98); + g.drawString(getSteps(), 158, 98); + + g.setFontAlign(-1,-1); + drawClock(); + drawRocket(); + drawBattery(); +} + +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + clearIntervals(); + } +}); + + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); + if (!locked) { + rocketInterval = setInterval(drawRocket, rocketSpeed); + } +}); + +Bangle.setUI("clock"); + +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe +g.clear(1); +draw(); diff --git a/apps/cassioWatch/app.png b/apps/cassioWatch/app.png new file mode 100644 index 000000000..3f9bbb36e Binary files /dev/null and b/apps/cassioWatch/app.png differ diff --git a/apps/cassioWatch/icon.js b/apps/cassioWatch/icon.js new file mode 100644 index 000000000..4e4428f88 --- /dev/null +++ b/apps/cassioWatch/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lkswkGswAHtGIxGGBhATJAAYXNCYoWOFIwWNChQWKBYWYCxIqTxGJFgwnDnACBkUiCwuYFQo9ECYIAClAsJxIUElAUDwQWEyxAHBwITBmczmUiCwprHCgMjmUhiIYBA4JCGIAeCwQUBCYIXBiUyFggVCFQsziMjmdCkcxiZbBiJCEFQZUBmND93uolEochFgpWEIAUUCgIACp00iZVBzIVEAoJABmUeConu8cRiNEoMZNwIrCzEiiUxpwVF901IwNN6JuBtGJlAVBkchCg3umkSiMNCoIAEQIJWFkgCB8UYinUjIVFxEhKwszDAUU7tRCg2hilTH4xVB6kRzAUGBQIVECYVEorEDAAeBptBiUenw7BCYQACieCCosd6MYwUVBwNUAQYvBeYOJCYVoxVeoK5BAAq0C8MjiM4LALFCilNCAQpCN4lBmUoFQQVCxTlBiMieI3uqUoagIVDRAeCkclCgvlkciFYdmsxwDlESmTdE8lRmSCECosiFgMhqgUDkZABBwWYCoNpIIYXBmchiKLBkYqBNggVBNwIsECwMikQUBlEiBgWJCoRCEEQITDNIJrEIAQABZYOYnIWBFoQUGIAZCCTYYWCAAQUExIUDFggNDAA5AEFg4AIIAhvGEYU4ChgWHAAoUIWYpdFFRIWHChxEICZoWFFBIA==")) diff --git a/apps/cassioWatch/metadata.json b/apps/cassioWatch/metadata.json new file mode 100644 index 000000000..5ec12e751 --- /dev/null +++ b/apps/cassioWatch/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "cassioWatch", + "name": "Cassio Watch", + "description": "Animated Clock with Space Cassio Watch Style", + "screenshots": [{ "url": "screens/screen_night.png" },{ "url": "screens/screen_day.png" }], + "icon": "app.png", + "version": "0.13", + "type": "clock", + "tags": "clock,weather,cassio,retro", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + { "name": "cassioWatch.app.js", "url": "app.js" }, + { "name": "cassioWatch.settings.js","url": "settings.js" }, + { "name": "cassioWatch.img", "url": "icon.js", "evaluate": true } + ], + "data": [ + { "name": "cassioWatch.settings.json" } + ] +} diff --git a/apps/cassioWatch/screens/screen_day.png b/apps/cassioWatch/screens/screen_day.png new file mode 100644 index 000000000..ba150b4f7 Binary files /dev/null and b/apps/cassioWatch/screens/screen_day.png differ diff --git a/apps/cassioWatch/screens/screen_night.png b/apps/cassioWatch/screens/screen_night.png new file mode 100644 index 000000000..4055e0943 Binary files /dev/null and b/apps/cassioWatch/screens/screen_night.png differ diff --git a/apps/cassioWatch/settings.js b/apps/cassioWatch/settings.js new file mode 100644 index 000000000..b07c6c58f --- /dev/null +++ b/apps/cassioWatch/settings.js @@ -0,0 +1,24 @@ +(function(back) { + var FILE = "cassioWatch.settings.json"; + var settings = Object.assign({ + rocketSpeed: 700, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + + E.showMenu({ + "" : { "title" : "Cassio Watch" }, + "< Back" : () => back(), + 'Rocket Speed': { + value: 0|settings.rocketSpeed, + min: 100, max: 60000, + onchange: v => { + settings.rocketSpeed = v; + writeSettings(); + } + }, + }); + }) \ No newline at end of file diff --git a/apps/chance/README.md b/apps/chance/README.md new file mode 100644 index 000000000..3f8e15ed5 --- /dev/null +++ b/apps/chance/README.md @@ -0,0 +1,11 @@ +# Chance + +![](chance-coin.png) + +* Toss a coin just touch the screen +* If you click in the dice button change to roll mode + +![](chance-dice.png) + +* Roll the dice just touch the screen +* If you click in the coin button change to toss mode diff --git a/apps/chance/chance-coin.png b/apps/chance/chance-coin.png new file mode 100644 index 000000000..dd1a581eb Binary files /dev/null and b/apps/chance/chance-coin.png differ diff --git a/apps/chance/chance-dice.png b/apps/chance/chance-dice.png new file mode 100644 index 000000000..45d12992f Binary files /dev/null and b/apps/chance/chance-dice.png differ diff --git a/apps/chance/chance.app.js b/apps/chance/chance.app.js new file mode 100644 index 000000000..d6a0d1a40 --- /dev/null +++ b/apps/chance/chance.app.js @@ -0,0 +1,328 @@ +/* +Chance +by Michael Sillas +*/ + + +var volinit = true; +var aleatorio; +var chance=false; +var chanceforma='moneda'; +var ang = 0; +var puntosale = 1; + +function inipinta(){ +g.clear(); + + +//color de fondo de default +g.setBgColor('#2c04ac'); + + +//Pinta rec de fondo +g.setColor('#2c04ac').fillRect(0, 0, g.getHeight(), g.getWidth()); +} + + +Bangle.on('touch', function(zone,e) { + var cordenadas= Object.values(e); +if(chanceforma=='moneda'){ + + if(chance==false){ + + + + if(cordenadas[0] > 85 && cordenadas[1] > 134) + { + + chanceforma='dado'; + chanceproc(); + } + + else + { + + drawvolado(); + } + } + } + else if(chanceforma=='dado'){ + if(chance==false){ + if(cordenadas[0] < 88 && cordenadas[1] > 134) + { + + volinit=true; + chanceforma='moneda'; + chanceproc(); + } + else + { + drawavdado(); + } + + } + } + }); + + +function getImage(x){ + +if (x==1) { + return require("heatshrink").decompress(atob("qFQwkBiIA/AH4A/AH4A/AGcAAAQllFMQmHFDwmJFDkQE5cBE0ooaExonYJxyhYDpIxGKCocGBZQnVNZgoFJzJCIE7wONE7AOYDBq4KE7CRDax4nUQzgn/E/7VTfxYn/OwgniEwaeIGgInlLLJ2NE7AmEDhANFPaYYLEowpTIBYmKE6AVFOwgmLE54VLBYwzCGIR1UJxLvHiB1YBYgJBfRJ1WJwYiCE6Z1LBgg6EE6DfHLZInVExgNDEIYnROpieHE6QmMTw4nROponYOowPME6YmOE65OFCRQncCBzvSJx4nWJyA5EE6zYRE6omMPA4nNJyI6HE8DaFE45sGOyLbHPgwnaKAoAHYy4oPE7YoLE5DGPFJiyKE6rvHE6I/LE6YAOE84dHDQKEKE6S6JE7zXTE87kFE+omTPBZHDBwROUAH4A/AH4Ar")); + } + + else if(x==2) { + return require("heatshrink").decompress(atob("qFQwkBiIA/AD0QNAZlhSQ5MiFETkLJ0oobExrMZExpQZJ5xQ/KFInPKDB4/KD4nYKBwnYKBwnbKhgnYiBCCE8akOE/7yPE0wnaExjvaE/52cgIn/E84mNE7DFOYzAmOdzJQOY0wnXEZhaCYyxLNE7CbOBwROjgKeXdR52mE7AoNEYKeXFBsBYzDINGgR2WE5YiCE8iEEE84mXO5oNBE8QjDE8hQCE7KSBE8qgNE88QE80RAYYnjADLIIgIJCE8gEDKTh7oE5JQdE5RQcE+R4cE/4AOiAn/OzTwaJxgnaExgn/Ox7IZE84mOE65OPE/4mPE/x2QE/wmQgIcKA")); + + } + + else if(x==3) { + return require("heatshrink").decompress(atob("t1uwkEogA/AH4A/AH4A/AC1AgAAZGSwxaHTJnbABUEGug3PGs6nNNlA3NCQ8BiIAZGyJsEgMimc//4AbmciiDdNBocCGboAFkBuMBgcvGsQABN4ajLgQ1k//xUpQ2DNkpuEGw41CgI1m//wUpIJCh42nUoY2Jj42n/42MbU7cLGwU/G1EgG2swGxY1oGxNAG1nwGxUBG342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G342/G3420gA2pmA2Hog25n42okA21iA2Ll42oFgQ1FGwcPGs/zGxkCG0/xGxNABQQ2nmA2Nj7apgg2KUsyjDGw7cDgEvGsfziCjJGwsBn42iGoY2JUoYABkY0fmQ1EUZBuFOAURADg0ENhRuHAEo2KNww1vN1LZKG9Q1OU8w0QHMRpRAH4A/AH4A/AH4ADA==")); + } +} + + +function rotar(){ + ang += 0.157; + g.clearRect(0, 0, g.getHeight(), 139); + g.drawImage(getImage(3),87,77, {rotate : ang}); + +} + +//Volado Letras (Toss) +function toss(){ + + if(chance==false) + { + g.setColor('#06f77b'); + } + + else + + { + g.setColor('#D8D8D8'); + } + + g.setFontAlign(0,0); // center font + g.setFont("Vector",26); // bitmap font, 8x magnified + g.drawString("Toss",44,160); +} + + + +//Roll dado Letras (Roll) +function roll(){ + + if(chance==false) + { + g.setColor('#06f77b'); + } + + else + + { + g.setColor('#D8D8D8'); + } + + g.setFontAlign(0,0); // center font + g.setFont("Vector",26); // bitmap font, 8x magnified + g.drawString("Roll",135,160); +} + + + +function getRandomIntInclusive(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive +} + + +function drawvolado() +{ + + + if(volinit==true){ //si es inicial el volado + aleatorio = 1; + g.drawImage(getImage(1),49,30); + + } + + else{ + chance=true; + toss(); + setInterval(function () { + + if(aleatorio==1){ + g.setColor('#D4AF37').fillCircle(g.getWidth()/2,73,50); + aleatorio=2; + } + else if(aleatorio==2){ + g.setColor('#c0c0c0').fillCircle(g.getWidth()/2,73,50); + aleatorio=1; + } +}, 500, aleatorio); + + + setTimeout(function () { + clearInterval(); + aleatorio = getRandomIntInclusive(1,2); + + if(aleatorio==1){ + + g.setColor('#c0c0c0'); + g.fillCircle(g.getWidth()/2,73,50); + + } + else if(aleatorio==2){ + + + g.setColor('#D4AF37'); + g.fillCircle(g.getWidth()/2,73,50); + } + + + g.drawImage(getImage(aleatorio),49,30);// Expected output: 1 or 2 + chance=false; + toss(); +}, 2500); + + + } + volinit = false; +} + +function drawavdado() +{ + chance=true; + roll(); + setInterval(rotar,100); + + setTimeout(function () { + clearInterval(); + + puntosale = getRandomIntInclusive(1,6); + + + g.clearRect(0, 0, g.getHeight(), 139); + g.drawImage(getImage(3),30,24); + + switch (puntosale) { + case 1: + g.setColor('#000000').fillCircle(86,77,9); + break; + case 2: + g.setColor('#000000').fillCircle(68,63,9); + g.setColor('#000000').fillCircle(104,98,9); + break; + case 3: + g.setColor('#000000').fillCircle(65,55,9); + g.setColor('#000000').fillCircle(86,77,9); + g.setColor('#000000').fillCircle(108,100,9); + break; + case 4: + g.setColor('#000000').fillCircle(65,55,9); + g.setColor('#000000').fillCircle(107,55,9); + g.setColor('#000000').fillCircle(65,100,9); + g.setColor('#000000').fillCircle(107,100,9); + break; + case 5: + g.setColor('#000000').fillCircle(65,55,9); + g.setColor('#000000').fillCircle(107,55,9); + g.setColor('#000000').fillCircle(86,77,9); + g.setColor('#000000').fillCircle(65,100,9); + g.setColor('#000000').fillCircle(107,100,9); + break; + case 6: + g.setColor('#000000').fillCircle(65,55,9); + g.setColor('#000000').fillCircle(65,77,9); + g.setColor('#000000').fillCircle(65,100,9); + g.setColor('#000000').fillCircle(107,55,9); + g.setColor('#000000').fillCircle(107,77,9); + g.setColor('#000000').fillCircle(107,100,9); + break; + } + + chance=false; + roll(); +}, 2000); +} + +//##################### Inicia Volado + + + +function chanceproc() +{ + + + if(chanceforma=='moneda'){ + + + inipinta(); + + //Pinta rec de boton + g.setColor('#06f77b').fillRect(g.getWidth()/2, 143, g.getWidth(), g.getWidth()); + + + //Circulo concentrico externo moneda + g.setColor('#000000').fillCircle(g.getWidth()/2,73,55); + + //Circulo concentrico interno moneda + g.setColor('#c0c0c0').fillCircle(g.getWidth()/2,73,50); + + + toss(); + + //####### Inicio dibuja dado boton + g.setColor('#000000'); + g.fillRect(117, 145,147, 173); + + g.setColor('#FFFFFF'); + g.fillRect(119, 147,145, 171); + + g.setColor('#000000'); + g.fillCircle(132,159,4); + //####### Fin dibuja dado boton + + + drawvolado(); + + }//##### fin volado + + else if(chanceforma=='dado'){ + + inipinta(); + + //Pinta rec de boton + g.setColor('#06f77b').fillRect(0, 143, g.getWidth()/2, g.getWidth()); + + + roll(); + + //####### Inicio dibuja moneda boton + + //Circulo icono externo moneda + g.setColor('#000000').fillCircle(43,159,15); + + //Circulo icono interno moneda + g.setColor('#c0c0c0').fillCircle(43,159,12); + + //####### Fin dibuja moneda boton + + g.setFont("Vector",17); g.setColor('#000000'); g.drawString('2c',45,160); + + //Uno + + g.drawImage(getImage(3),30,24); + + + g.setColor('#000000').fillCircle(86,77,9); + + + } + + +} + +chanceproc(); diff --git a/apps/chance/chance.img.js b/apps/chance/chance.img.js new file mode 100644 index 000000000..019fac9c2 --- /dev/null +++ b/apps/chance/chance.img.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEogAfFIgmQoBDHDJoWCgMRAAQYOCwczAAcyiAJBLZkSCwgACDBYuBCxAxDJJBEGAAsxGBAuBiYOBOgRJIGAwuDKwQ1HmQwGFwY8CAAUDDAsgC4wnCXwYwIEgJIEEwQhBCQQ/CMI4XFkZdCPIQdCBIJIJLwIICgQODmAdEJAgXDCYgXMNwJICQo5HKMAgkISgQhIC4YkHR4swBwZRBC4QkHSgJpCmUQZAIZBC5byCe4xwBSAYXHdwgDBkEjmQeDC5CMBNAQPBkZfEC5JACV4QcBO4qCBC450BIoQXBAgIvNIogdCSIIXNIogXOBAQHBIoZHJX4gICIwIuEB4SPFkEEC4SuBOoTEHX4MTX4UQC4VAFQQXId4wXELQUQLwpiEiUieIMAC4YrCHwMikMRAAMSMQZfCOwZgEHoZBFJILkEC4i0BBAIXIPohGCJAinBgJFBkURTQZ8EC4gwBEwwAHIwqRFABSNEJAo/FFxxICaw5dLSQxFJFxBJDDBBFBFxIYDgLnCAAMhFoIWLMQYABYQIECCxoxDAAgVOAH4ABA")) diff --git a/apps/chance/chance.png b/apps/chance/chance.png new file mode 100644 index 000000000..2e9edeef2 Binary files /dev/null and b/apps/chance/chance.png differ diff --git a/apps/chance/metadata.json b/apps/chance/metadata.json new file mode 100644 index 000000000..fd55e9d05 --- /dev/null +++ b/apps/chance/metadata.json @@ -0,0 +1,17 @@ +{ "id": "chance", + "name": "Chance", + "shortName":"Chance", + "version":"0.01", + "description": "Toss a coin or Roll the dice, chose your chance with this app.", + "icon": "chance.png", + "type":"app", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"chance.app.js","url":"chance.app.js"}, + {"name":"chance.img","url":"chance.img.js","evaluate":true} + ], + "screenshots": [{"url":"chance-coin.png"},{"url":"chance-dice.png"}] +} diff --git a/apps/chargent/ChangeLog b/apps/chargent/ChangeLog new file mode 100644 index 000000000..349618315 --- /dev/null +++ b/apps/chargent/ChangeLog @@ -0,0 +1,7 @@ +0.01: First version +0.02: Support BangleJS2 +0.03: Added threshold +0.04: Added notification +0.05: Fixed boot +0.06: Allow tap to silence notification/buzzing +0.07: Fix notification-tap silencing and notification length diff --git a/apps/chargent/README.md b/apps/chargent/README.md new file mode 100644 index 000000000..5fd486217 --- /dev/null +++ b/apps/chargent/README.md @@ -0,0 +1,17 @@ +# Charge Gently + +Charging Li-ion batteries to their full capacity has a significant impact on their lifespan. If possible, it is good practice to charge more often, but only to a certain lower capacity. + +The first stage of charging Li-ion ends at ~80% capacity when the charge voltage reaches its peak*. When that happens, the watch will buzz twice every 30s to remind you to disconnect the watch. + +This app has no UI and no configuration. To disable the app, you have to uninstall it. + +Tap the charged notification to prevent buzzing for this charging session. + +New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage. + +Side notes +- Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100. +- This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise. + +\* according to https://batteryuniversity.com/article/bu-409-charging-lithium-ion assuming similar characteristics and readouts from pin `D30` approximate charge voltage diff --git a/apps/chargent/boot.js b/apps/chargent/boot.js new file mode 100644 index 000000000..42c384711 --- /dev/null +++ b/apps/chargent/boot.js @@ -0,0 +1,45 @@ +(() => { + const pin = process.env.HWVERSION === 2 ? D3 : D30; + + var id; + function gent(charging) { + if (charging) { + if (!id) { + var max = 0; + var cnt = 0; + var sum = 0; + var lim = (require('Storage').readJSON('chargent.json', true) || {}).limit || 0; + id = setInterval(() => { + var val = analogRead(pin); + if (max < val) { + max = val; + cnt = 1; + sum = val; + } else { + cnt++; + sum += val; + } + if (10 < cnt || (lim && lim <= max)) { // 10 * 30s == 5 min // TODO ? customizable + if (!lim) { + lim = sum / cnt; + require('Storage').writeJSON('chargent.json', {limit: lim}); + } + const onHide = () => { if(id) id = clearInterval(id) }; + require('notify').show({id: 'chargent', title: 'Charged', onHide }); + // TODO ? customizable + Bangle.buzz(500); + setTimeout(() => Bangle.buzz(500), 1000); + } + }, 3e4); + } + } else { + if (id) { + id = clearInterval(id); + require('notify').hide({id: 'chargent'}); + } + } + } + + Bangle.on('charging', gent); + if (Bangle.isCharging()) gent(true); +})(); diff --git a/apps/chargent/icon.png b/apps/chargent/icon.png new file mode 100644 index 000000000..dce0014cb Binary files /dev/null and b/apps/chargent/icon.png differ diff --git a/apps/chargent/metadata.json b/apps/chargent/metadata.json new file mode 100644 index 000000000..75366ff59 --- /dev/null +++ b/apps/chargent/metadata.json @@ -0,0 +1,17 @@ +{ "id": "chargent", + "name": "Charge Gently", + "version": "0.07", + "description": "When charging, reminds you to disconnect the watch to prolong battery life.", + "icon": "icon.png", + "type": "bootloader", + "tags": "battery", + "supports": ["BANGLEJS", "BANGLEJS2"], + "dependencies": {"notify": "type"}, + "readme": "README.md", + "storage": [ + {"name": "chargent.boot.js", "url": "boot.js"} + ], + "data": [ + {"name": "chargent.json"} + ] +} diff --git a/apps/chargerot/ChangeLog b/apps/chargerot/ChangeLog new file mode 100644 index 000000000..07029aebd --- /dev/null +++ b/apps/chargerot/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Handle missing settings (e.g. first-install) diff --git a/apps/chargerot/README.md b/apps/chargerot/README.md new file mode 100644 index 000000000..764a5ffda --- /dev/null +++ b/apps/chargerot/README.md @@ -0,0 +1,10 @@ +# Charge LCD rotation + +This simple app is for handling all types of charging cradles i.e.: +- [Official Bangle.js 2 dock](https://shop.espruino.com/banglejs2-dock) +- [Many more you can 3d print](https://www.thingiverse.com/search?q=banglejs+dock&page=1&type=things&sort=relevant) + +## Setup +In app settings set desired rotation. +App will swap screen rotation when charged and return to default one (you can change this in settings app) when undocked. + diff --git a/apps/chargerot/boot.js b/apps/chargerot/boot.js new file mode 100644 index 000000000..2daeb3d50 --- /dev/null +++ b/apps/chargerot/boot.js @@ -0,0 +1,14 @@ +(() => { + const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate; + const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate; + if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear(); + Bangle.on('charging', (charging) => { + if (charging) { + g.setRotation(chargingRotation&3,chargingRotation>>2).clear(); + Bangle.showClock(); + } else { + g.setRotation(defaultRotation&3,defaultRotation>>2).clear(); + Bangle.showClock(); + } + }); +})(); diff --git a/apps/chargerot/icon.png b/apps/chargerot/icon.png new file mode 100644 index 000000000..3347098e5 Binary files /dev/null and b/apps/chargerot/icon.png differ diff --git a/apps/chargerot/metadata.json b/apps/chargerot/metadata.json new file mode 100644 index 000000000..8174836be --- /dev/null +++ b/apps/chargerot/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "chargerot", + "name": "Charge LCD rotation", + "version": "0.02", + "description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.", + "icon": "icon.png", + "tags": "battery", + "readme": "README.md", + "type": "bootloader", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"chargerot.boot.js","url":"boot.js"}, + {"name":"chargerot.settings.js","url":"settings.js"} + ], + "data":[{"name":"chargerot.settings.json"}] +} diff --git a/apps/chargerot/settings.js b/apps/chargerot/settings.js new file mode 100644 index 000000000..eaaa488f1 --- /dev/null +++ b/apps/chargerot/settings.js @@ -0,0 +1,28 @@ +(function(back) { + var rotNames = [/*LANG*/"No",/*LANG*/"Rotate CW",/*LANG*/"Left Handed",/*LANG*/"Rotate CCW",/*LANG*/"Mirror"]; + var FILE = "chargerot.settings.json"; + var appSettings = Object.assign({ + rotate: 0, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, appSettings); + } + + + E.showMenu({ + "" : { "title" : "Charging rotation" }, + "< Back" : () => back(), + 'Rotate': { + value: 0|appSettings.rotate, + min: 0, + max: rotNames.length-1, + format: v=> rotNames[v], + onchange: v => { + appSettings.rotate = 0 | v; + writeSettings(); + } + }, + }); + // If(true) big(); +}) \ No newline at end of file diff --git a/apps/chess/ChangeLog b/apps/chess/ChangeLog new file mode 100644 index 000000000..1fb70549c --- /dev/null +++ b/apps/chess/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Bugfixes +0.03: Use Bangle.setBacklight() +0.04: Add option to buzz after computer move +0.05: Minor code improvements diff --git a/apps/chess/app-icon.js b/apps/chess/app-icon.js new file mode 100644 index 000000000..2aa34d5ae --- /dev/null +++ b/apps/chess/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AEnPAEAv/wAAcF6XWAAYdFBQgLLF/4v/F/4vTFKoLGF/4v/F/4v/F4QpWBQov/F7UslgKBAYIABEgIDDF/4v/F4es1gvTdKoLBFYYAHF/4vTmQvuvQwJFxAvbAAOIxIAFFxIvzFKYLG6CMNF8GsF92BdpwvfqwvtRwgvQAC+IxIAJF5QAYF93OwEyRwqSPACwsJGEov/AH4A/AH4AwA=")) diff --git a/apps/chess/app.js b/apps/chess/app.js new file mode 100644 index 000000000..19802083d --- /dev/null +++ b/apps/chess/app.js @@ -0,0 +1,309 @@ +// Using p4wn chess engine: https://p4wn.sourceforge.net/ | https://github.com/douglasbagnall/p4wn +const engine = require("chessengine"); + +Bangle.loadWidgets(); // load before first appRect call + +const FIELD_WIDTH = Bangle.appRect.w/8; +const FIELD_HEIGHT = Bangle.appRect.h/8; +const SETTINGS_FILE = "chess.json"; +const ICON_SIZE=45; +const get_icon_bishop = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA=")); +const get_icon_pawn = () => require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA=")); +const get_icon_king = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA==")); +const get_icon_queen = () => require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA=")); +const get_icon_rook = () => require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA=")); +const get_icon_knight = () => require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA")); + +const settings = Object.assign({ + state: engine.P4_INITIAL_BOARD, + computer_level: 0, // default to "stupid" which is the fastest + buzz: false, // Buzz when computer move is done +}, require("Storage").readJSON(SETTINGS_FILE,1) || {}); + +const ovr = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,2,{msb:true}); +const curfield = [4*FIELD_WIDTH, 6*FIELD_HEIGHT]; // e2 +const startfield = Array(2); +let piece_sel = 0; +let showmenu = false; +let finished = false; + +const writeSettings = () => { + settings.state = engine.p4_state2fen(state); + require('Storage').writeJSON(SETTINGS_FILE, settings); +}; + +const generateBgImage = () => { + let buf = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,1,{msb:true}); + for(let idxrow=0; idxrow<8; idxrow++) { + for(let idxcol=0; idxcol<8; idxcol++) { + const bgCol = idxrow % 2 != idxcol % 2 ? 0 : 1; + const x = idxcol*FIELD_WIDTH; + const y = idxrow*FIELD_HEIGHT; + buf.setColor(bgCol).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT}); + } + } + return {width:buf.getWidth(), height:buf.getHeight(), + buffer:buf.buffer + }; +}; + +const idx2Pos = (idxcol, idxrow) => { + "ram" + return 2*(1+8+1) + (7-idxrow)*(1+8+1) + idxcol + 1; +}; + +const drawPiece = (buf, x, y, piece) => { + let icon; + + switch(piece & ~0x1) { + case engine.P4_PAWN: + icon = get_icon_pawn(); + break; + case engine.P4_BISHOP: + icon = get_icon_bishop(); + break; + case engine.P4_KING: + icon = get_icon_king(); + break; + case engine.P4_QUEEN: + icon = get_icon_queen(); + break; + case engine.P4_ROOK: + icon = get_icon_rook(); + break; + case engine.P4_KNIGHT: + icon = get_icon_knight(); + break; + } + + if (icon) { + const scale = FIELD_HEIGHT/ICON_SIZE; + buf.drawImage(icon, x+(FIELD_WIDTH-(ICON_SIZE*scale))/2, y, {scale: scale}); + } + return buf; +}; + +const drawBoard = () => { + //console.log("Free: " + process.memory().free); + + g.setBgColor("#555").setColor("#aaa").drawImage(bgImage, Bangle.appRect.x, Bangle.appRect.y); + for(let idxrow=0; idxrow<8; idxrow++) { + for(let idxcol=0; idxcol<8; idxcol++) { + const x = idxcol*FIELD_WIDTH+Bangle.appRect.x; + const y = idxrow*FIELD_HEIGHT+Bangle.appRect.y; + + const pos = idx2Pos(idxcol, idxrow); + const field = state.board[pos]; + + if (field) { + const fgCol = field & 0x1 ? "#000" : "#fff"; + drawPiece(g.setBgColor(fgCol), x, y, field); + } + } + } +}; + +const roundX = (x) => { + return Math.round(x/FIELD_WIDTH)*FIELD_WIDTH; +}; + +const roundY = (y) => { + return Math.round(y/FIELD_HEIGHT)*FIELD_HEIGHT; +}; + +const drawSelectedField = () => { + ovr.clear(); + if (!showmenu && !finished) { + if (startfield[0] !== undefined && startfield[1] !== undefined) { + // remove piece from startfield + const x = startfield[0]; + const y = startfield[1]; + ovr.setColor(2).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT}); + } + + const x = roundX(curfield[0]); + const y = roundY(curfield[1]); + ovr.setColor(piece_sel ? 1 : 2) + .drawRect({x:x+1, y:y, w:FIELD_WIDTH-2, h:FIELD_HEIGHT}) + .drawRect({x:x+2, y:y+1, w:FIELD_WIDTH-4, h:FIELD_HEIGHT-2}) + .drawRect({x:x+3, y:y+2, w:FIELD_WIDTH-6, h:FIELD_HEIGHT-4}); + if (piece_sel) { + drawPiece(ovr.setBgColor(1), x, y, piece_sel); + ovr.setBgColor(0); // back to transparent + } + } + Bangle.setLCDOverlay({width:ovr.getWidth(), height:ovr.getHeight(), + bpp:2, transparent:0, + palette:new Uint16Array([0, g.toColor("#F00"), g.toColor("#0F0"), 0]), + buffer:ovr.buffer + },Bangle.appRect.x,Bangle.appRect.y); +}; + +const isInside = (rect, e) => { + return e.x>=rect.x && e.x=rect.y && e.y<=rect.y+rect.h; +}; + +const showAlert = (msg, cb) => { + showmenu = true; + drawSelectedField(); + E.showAlert(msg).then(function() { + showmenu = false; + drawBoard(); + drawSelectedField(); + if (cb) { + cb(); + } + }); +}; + +const move = (from,to,cbok) => { + const res = state.move(from, to); + //console.log(res); + if (!res.ok) { + showAlert("Illegal move"); + } else { + if (res.flags & engine.P4_MOVE_FLAG_MATE) { + finished = true; + showAlert("Checkmate or stalemate", cbok); + } else if (res.flags & engine.P4_MOVE_FLAG_CHECK) { + showAlert("A king is in check", cbok); + } else if (res.flags & engine.P4_MOVE_FLAG_DRAW) { + showAlert("A draw is available", cbok); + } else if (cbok) { + cbok(); + } + } + return res; +}; + +const showMessage = (msg) => { + g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10).flip(); +}; + +// Run + +g.reset(); +const bgImage = generateBgImage(); +let state = engine.p4_fen2state(settings.state); +drawBoard(); +drawSelectedField(); +Bangle.drawWidgets(); + +// drag selected field +Bangle.on('drag', (ev) => { + if (showmenu) return; + const newx = curfield[0]+ev.dx; + const newy = curfield[1]+ev.dy; + if (newx >= 0 && newx <= 7*FIELD_WIDTH) { + curfield[0] = newx; + } + if (newy >= 0 && newy <= 7*FIELD_HEIGHT) { + curfield[1] = newy; + } + drawSelectedField(); +}); + +// touch to start/stop moving a piece +Bangle.on('touch', (button, xy) => { + if (isInside(Bangle.appRect, xy) && !showmenu) { + if (piece_sel === 0) { + startfield[0] = roundX(curfield[0]); + startfield[1] = roundY(curfield[1]); + const startpos = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT); + piece_sel = state.board[startpos]; + if (piece_sel === 0) { + startfield[0] = startfield[1] = undefined; + // nothing here, do nothing + return; + } + } else { // piece_sel === 0 + const colTo = roundX(curfield[0]); + const rowTo = roundY(curfield[1]); + if (startfield[0] !== colTo || startfield[1] !== rowTo) { + showMessage(/*LANG*/"Moving.."); + const posFrom = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT); + const posTo = idx2Pos(colTo/FIELD_WIDTH, rowTo/FIELD_HEIGHT); + const cb = () => { + // human move ok, update + drawBoard(); + drawSelectedField(); + if (!finished) { + // do computer move + Bangle.setBacklight(false); // this can take some time, turn off to save power + showMessage(/*LANG*/"Calculating.."); + const compMove = state.findmove(settings.computer_level+1); + const result = move(compMove[0], compMove[1]); + if (result.ok) { + writeSettings(); + } + Bangle.setLCDPower(true); + Bangle.setLocked(false); + Bangle.setBacklight(true); + if (settings.buzz) { + Bangle.buzz(500); + } + if (!showmenu) { + showAlert(result.string); + } + } + }; + move(posFrom, posTo,cb); + } // piece_sel === 0 + startfield[0] = startfield[1] = undefined; + piece_sel = 0; + } + drawSelectedField(); + } +}); + +// show menu on button +setWatch(() => { + if (showmenu) { + return; + } + showmenu = true; + piece_sel = 0; + startfield[0] = startfield[1] = undefined; + drawSelectedField(); + + const closeMenu = () => { + showmenu = false; + E.showMenu(); + drawBoard(); + drawSelectedField(); + }; + + E.showMenu({ + "" : { title : /*LANG*/"Chess settings" }, + "< Back" : () => closeMenu(), + /*LANG*/"Exit" : () => load(), + /*LANG*/"New Game" : () => { + finished = false; + state = engine.p4_fen2state(engine.P4_INITIAL_BOARD); + writeSettings(); + closeMenu(); + }, + /*LANG*/"Undo Move" : () => { + state.jump_to_moveno(-2); + writeSettings(); + closeMenu(); + }, + /*LANG*/'Level': { + value: settings.computer_level, + min: 0, max: 4, + format: v => [/*LANG*/'stupid', /*LANG*/'middling', /*LANG*/'default', /*LANG*/'slow', /*LANG*/'slowest'][v], + onchange: v => { + settings.computer_level = v; + writeSettings(); + } + }, + /*LANG*/'Buzz on next turn': { + value: !!settings.buzz, + onchange: v => { + settings.buzz = v; + writeSettings(); + } + }, + }); +}, BTN, { repeat: true, edge: "falling" }); diff --git a/apps/chess/app.png b/apps/chess/app.png new file mode 100644 index 000000000..9db78e27d Binary files /dev/null and b/apps/chess/app.png differ diff --git a/apps/chess/engine.js b/apps/chess/engine.js new file mode 100644 index 000000000..c30e2587d --- /dev/null +++ b/apps/chess/engine.js @@ -0,0 +1,1615 @@ +/* p4wn, AKA 5k chess - by Douglas Bagnall + * + * This code is in the public domain, or as close to it as various + * laws allow. No warranty; no restrictions. + * + * lives at http://p4wn.sf.net/ + */ + +/*Compatibility tricks: + * backwards for old MSIEs (to 5.5) + * sideways for seed command-line javascript.*/ +var p4_log; +if (this.imports !== undefined && + this.printerr !== undefined){//seed or gjs + p4_log = function(){ + var args = Array.prototype.slice.call(arguments); + this.printerr(args.join(', ')); + }; +} +else if (this.console === undefined){//MSIE + p4_log = function(){}; +} +else { + p4_log = function(){console.log.apply(console, arguments);}; +} + +/*MSIE Date.now backport */ +if (Date.now === undefined) + Date.now = function(){return (new Date).getTime();}; + +/* The pieces are stored as numbers between 2 and 13, inclusive. + * Empty squares are stored as 0, and off-board squares as 16. + * There is some bitwise logic to it: + * piece & 1 -> colour (white: 0, black: 1) + * piece & 2 -> single move piece (including pawn) + * if (piece & 2) == 0: + * piece & 4 -> row and column moves + * piece & 8 -> diagonal moves + */ +var P4_PAWN = 2, P4_ROOK = 4, P4_KNIGHT = 6, P4_BISHOP = 8, P4_QUEEN = 12, P4_KING = 10; +var P4_EDGE = 16; + +/* in order, even indices: , pawn, rook, knight, bishop, king, queen. Only the + * even indices are used.*/ +var P4_MOVES = [[], [], + [], [], + [1,10,-1,-10], [], + [21,19,12,8,-21,-19,-12,-8], [], + [11,9,-11,-9], [], + [1,10,11,9,-1,-10,-11,-9], [], + [1,10,11,9,-1,-10,-11,-9], [] + ]; + +/*P4_VALUES defines the relative value of various pieces. + * + * It follows the 1,3,3,5,9 pattern you learn as a kid, multiplied by + * 20 to give sub-pawn resolution to other factors, with bishops given + * a wee boost over knights. + */ +var P4_VALUES=[0, 0, //Piece values + 20, 20, //pawns + 100, 100, //rooks + 60, 60, //knights + 61, 61, //bishops + 8000, 8000,//kings + 180, 180, //queens + 0]; + +/* A score greater than P4_WIN indicates a king has been taken. It is + * less than the value of a king, in case someone finds a way to, say, + * sacrifice two queens in order to checkmate. + */ +var P4_KING_VALUE = P4_VALUES[10]; +var P4_WIN = P4_KING_VALUE >> 1; + +/* every move, a winning score decreases by this much */ +var P4_WIN_DECAY = 300; +var P4_WIN_NOW = P4_KING_VALUE - 250; + +/* P4_{MAX,MIN}_SCORE should be beyond any possible evaluated score */ + +var P4_MAX_SCORE = 9999; // extremes of evaluation range +var P4_MIN_SCORE = -P4_MAX_SCORE; + +/*initialised in p4_initialise_state */ +var P4_CENTRALISING_WEIGHTS; +var P4_BASE_PAWN_WEIGHTS; +var P4_KNIGHT_WEIGHTS; + +/*P4_DEBUG turns on debugging features */ +var P4_DEBUG = 0; +var P4_INITIAL_BOARD = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"; + +/*use javascript typed arrays rather than plain arrays + * (faster in some browsers, unsupported in others, possibly slower elsewhere) */ +var P4_USE_TYPED_ARRAYS = this.Int32Array !== undefined; + +var P4_PIECE_LUT = { /*for FEN, PGN interpretation */ + P: 2, + p: 3, + R: 4, + r: 5, + N: 6, + n: 7, + B: 8, + b: 9, + K: 10, + k: 11, + Q: 12, + q: 13 +}; + +var P4_ENCODE_LUT = ' PPRRNNBBKKQQ'; + + +function p4_alphabeta_treeclimber(state, count, colour, score, s, e, alpha, beta){ + var move = p4_make_move(state, s, e, P4_QUEEN); + var i; + var ncolour = 1 - colour; + var movelist = p4_parse(state, colour, move.ep, -score); + var movecount = movelist.length; + if(count){ + //branch nodes + var t; + for(i = 0; i < movecount; i++){ + var mv = movelist[i]; + var mscore = mv[0]; + var ms = mv[1]; + var me = mv[2]; + if (mscore > P4_WIN){ //we won! Don't look further. + alpha = P4_KING_VALUE; + break; + } + t = -p4_alphabeta_treeclimber(state, count - 1, ncolour, mscore, ms, me, + -beta, -alpha); + if (t > alpha){ + alpha = t; + } + if (alpha >= beta){ + break; + } + } + if (alpha < -P4_WIN_NOW && ! p4_check_check(state, colour)){ + /* Whatever we do, we lose the king. + * But if it is not check then this is stalemate, and the + * score doesn't apply. + */ + alpha = state.stalemate_scores[colour]; + } + if (alpha < -P4_WIN){ + /*make distant checkmate seem less bad */ + alpha += P4_WIN_DECAY; + } + } + else{ + //leaf nodes + while(beta > alpha && --movecount != -1){ + if(movelist[movecount][0] > alpha){ + alpha = movelist[movecount][0]; + } + } + } + p4_unmake_move(state, move); + return alpha; +} + + +/* p4_prepare() works out weightings for assessing various moves, + * favouring centralising moves early, for example. + * + * It is called before each tree search, not for each parse(), so it + * is OK for it to be a little bit slow. But that also means it drifts + * out of sync with the real board state, especially on deep searches. + */ + +function p4_prepare(state){ + var i, j, x, y, a; + var pieces = state.pieces = [[], []]; + /*convert state.moveno half move count to move cycle count */ + var moveno = state.moveno >> 1; + var board = state.board; + + /* high earliness_weight indicates a low move number. The formula + * should work above moveno == 50, but this is javascript. + */ + var earliness_weight = (moveno > 50) ? 0 : parseInt(6 * Math.exp(moveno * -0.07)); + var king_should_hide = moveno < 12; + var early = moveno < 5; + /* find the pieces, kings, and weigh material*/ + var kings = [0, 0]; + var material = [0, 0]; + var best_pieces = [0, 0]; + for(i = 20; i < 100; i++){ + a = board[i]; + var piece = a & 14; + var colour = a & 1; + if(piece){ + pieces[colour].push([a, i]); + if (piece == P4_KING){ + kings[colour] = i; + } + else{ + material[colour] += P4_VALUES[piece]; + best_pieces[colour] = Math.max(best_pieces[colour], P4_VALUES[piece]); + } + } + } + + /*does a draw seem likely soon?*/ + var draw_likely = (state.draw_timeout > 90 || state.current_repetitions >= 2); + if (draw_likely) + p4_log("draw likely", state.current_repetitions, state.draw_timeout); + state.values = [[], []]; + var qvalue = P4_VALUES[P4_QUEEN]; /*used as ballast in various ratios*/ + var material_sum = material[0] + material[1] + 2 * qvalue; + var wmul = 2 * (material[1] + qvalue) / material_sum; + var bmul = 2 * (material[0] + qvalue) / material_sum; + var multipliers = [wmul, bmul]; + var emptiness = 4 * P4_QUEEN / material_sum; + state.stalemate_scores = [parseInt(0.5 + (wmul - 1) * 2 * qvalue), + parseInt(0.5 + (bmul - 1) * 2 * qvalue)]; + //p4_log("value multipliers (W, B):", wmul, bmul, + // "stalemate scores", state.stalemate_scores); + for (i = 0; i < P4_VALUES.length; i++){ + var v = P4_VALUES[i]; + if (v < P4_WIN){//i.e., not king + state.values[0][i] = parseInt(v * wmul + 0.5); + state.values[1][i] = parseInt(v * bmul + 0.5); + } + else { + state.values[0][i] = v; + state.values[1][i] = v; + } + } + /*used for pruning quiescence search */ + state.best_pieces = [parseInt(best_pieces[0] * wmul + 0.5), + parseInt(best_pieces[1] * bmul + 0.5)]; + + var kx = [kings[0] % 10, kings[1] % 10]; + var ky = [parseInt(kings[0] / 10), parseInt(kings[1] / 10)]; + + /* find the frontmost pawns in each file */ + var pawn_cols = [[], []]; + for (y = 3; y < 9; y++){ + for (x = 1; x < 9; x++){ + i = y * 10 + x; + a = board[i]; + if ((a & 14) != P4_PAWN) + continue; + if ((a & 1) == 0){ + pawn_cols[0][x] = y; + } + else if (pawn_cols[1][x] === undefined){ + pawn_cols[1][x] = y; + } + } + } + var target_king = (moveno >= 20 || material_sum < 5 * qvalue); + var weights = state.weights; + + for (y = 2; y < 10; y++){ + for (x = 1; x < 9; x++){ + i = y * 10 + x; + var early_centre = P4_CENTRALISING_WEIGHTS[i] * earliness_weight; + var plateau = P4_KNIGHT_WEIGHTS[i]; + for (var c = 0; c < 2; c++){ + var dx = Math.abs(kx[1 - c] - x); + var dy = Math.abs(ky[1 - c] - y); + var our_dx = Math.abs(kx[c] - x); + var our_dy = Math.abs(ky[c] - y); + + var d = Math.max(Math.sqrt(dx * dx + dy * dy), 1) + 1; + var mul = multipliers[c]; /*(mul < 1) <==> we're winning*/ + var mul3 = mul * mul * mul; + var at_home = y == 2 + c * 7; + var pawn_home = y == 3 + c * 5; + var row4 = y == 5 + c; + var promotion_row = y == 9 - c * 7; + var get_out = (early && at_home) * -5; + + var knight = parseInt(early_centre * 0.3) + 2 * plateau + get_out; + var rook = parseInt(early_centre * 0.3); + var bishop = parseInt(early_centre * 0.6) + plateau + get_out; + if (at_home){ + rook += (x == 4 || x == 5) * (earliness_weight + ! target_king); + rook += (x == 1 || x == 8) * (moveno > 10 && moveno < 20) * -3; + rook += (x == 2 || x == 7) * (moveno > 10 && moveno < 20) * -1; + } + + /*Queen wants to stay home early, then jump right in*/ + /*keep kings back on home row for a while*/ + var queen = parseInt(plateau * 0.5 + early_centre * (0.5 - early)); + var king = (king_should_hide && at_home) * 2 * earliness_weight; + + /*empty board means pawn advancement is more urgent*/ + var get_on_with_it = Math.max(emptiness * 2, 1); + var pawn = get_on_with_it * P4_BASE_PAWN_WEIGHTS[c ? 119 - i : i]; + if (early){ + /* Early pawn weights are slightly randomised, so each game is different. + */ + if (y >= 4 && y <= 7){ + var boost = 1 + 3 * (y == 5 || y == 6); + pawn += parseInt((boost + p4_random_int(state, 4)) * 0.1 * + early_centre); + } + if (x == 4 || x == 5){ + //discourage middle pawns from waiting at home + pawn -= 3 * pawn_home; + pawn += 3 * row4; + } + } + /*pawn promotion row is weighted as a queen minus a pawn.*/ + if (promotion_row) + pawn += state.values[c][P4_QUEEN] - state.values[c][P4_PAWN]; + + /*pawns in front of a castled king should stay there*/ + pawn += 4 * (y == 3 && ky[c] == 2 && Math.abs(our_dx) < 2 && + kx[c] != 5 && x != 4 && x != 5); + /*passed pawns (having no opposing pawn in front) are encouraged. */ + var cols = pawn_cols[1 - c]; + if (cols[x] == undefined || + (c == 0 && cols[x] < y) || + (c == 1 && cols[x] > y)) + pawn += 2; + + /* After a while, start going for opposite king. Just + * attract pieces into the area so they can mill about in + * the area, waiting for an opportunity. + * + * As prepare is only called at the beginning of each tree + * search, the king could wander out of the targetted area + * in deep searches. But that's OK. Heuristics are + * heuristics. + */ + if (target_king){ + knight += 2 * parseInt(8 * mul / d); + rook += 2 * ((dx < 2) + (dy < 2)); + bishop += 3 * (Math.abs((dx - dy)) < 2); + queen += 2 * parseInt(8 / d) + (dx * dy == 0) + (dx - dy == 0); + /* The losing king wants to stay in the middle, while + the winning king goes in for the kill.*/ + var king_centre_wt = 8 * emptiness * P4_CENTRALISING_WEIGHTS[i]; + king += parseInt(150 * emptiness / (mul3 * d) + king_centre_wt * mul3); + } + weights[P4_PAWN + c][i] = pawn; + weights[P4_KNIGHT + c][i] = knight; + weights[P4_ROOK + c][i] = rook; + weights[P4_BISHOP + c][i] = bishop; + weights[P4_QUEEN + c][i] = queen; + weights[P4_KING + c][i] = king; + + if (draw_likely && mul < 1){ + /*The winning side wants to avoid draw, so adds jitter to its weights.*/ + var range = 3 / mul3; + for (j = 2 + c; j < 14; j += 2){ + weights[j][i] += p4_random_int(state, range); + } + } + } + } + } + state.prepared = true; +} + +function p4_maybe_prepare(state){ + if (! state.prepared) + p4_prepare(state); +} + + +function p4_parse(state, colour, ep, score) { + var board = state.board; + var s, e; //start and end position + var E, a; //E=piece at end place, a= piece moving + var i, j; + var other_colour = 1 - colour; + var dir = (10 - 20 * colour); //dir= 10 for white, -10 for black + var movelist = []; + var captures = []; + var weight; + var pieces = state.pieces[colour]; + var castle_flags = (state.castles >> (colour * 2)) & 3; + var values = state.values[other_colour]; + var all_weights = state.weights; + for (j = pieces.length - 1; j >= 0; j--){ + s = pieces[j][1]; // board position + a = board[s]; //piece number + var weight_lut = all_weights[a]; + weight = score - weight_lut[s]; + a &= 14; + if(a > 2){ //non-pawns + var moves = P4_MOVES[a]; + if(a & 2){ + for(i = 0; i < 8; i++){ + e = s + moves[i]; + E = board[e]; + if(!E){ + movelist.push([weight + values[E] + weight_lut[e], s, e]); + } + else if((E&17)==other_colour){ + captures.push([weight + values[E] + weight_lut[e] + all_weights[E][e], s, e]); + } + } + if(a == P4_KING && castle_flags){ + if((castle_flags & 1) && + (board[s - 1] + board[s - 2] + board[s - 3] == 0) && + p4_check_castling(board, s - 2, other_colour, dir, -1)){//Q side + movelist.push([weight + 12, s, s - 2]); //no analysis, just encouragement + } + if((castle_flags & 2) && (board[s + 1] + board[s + 2] == 0) && + p4_check_castling(board, s, other_colour, dir, 1)){//K side + movelist.push([weight + 13, s, s + 2]); + } + } + } + else{//rook, bishop, queen + var mlen = moves.length; + for(i=0;i= 0; i--){ + var mv = movelist[i]; + var score = mv[0]; + s = mv[1]; + e = mv[2]; + if(! board[e]){ + var x = scores[s]; + x.score = Math.max(x.score, score); + } + } + /* moving out of a threat is worth considering, especially + * if it is a pawn and you are not.*/ + for(i = threats.length - 1; i >= 0; i--){ + var mv = threats[i]; + var x = scores[mv[2]]; + if (x !== undefined){ + var S = board[mv[1]]; + var r = (1 + x.piece > 3 + S < 4) * 0.01; + if (x.threatened < r) + x.threatened = r; + } + } + var pieces2 = []; + for (i = 20; i < 100; i++){ + p = scores[i]; + if (p !== undefined){ + p.score += p.threatened * our_values[p.piece]; + pieces2.push(p); + } + } + pieces2.sort(function(a, b){return a.score - b.score;}); + for (i = 0; i < pieces2.length; i++){ + p = pieces2[i]; + pieces[i] = [p.piece, p.pos]; + } + } +} + +function p4_findmove(state, level, colour, ep){ + p4_prepare(state); + p4_optimise_piece_list(state); + var board = state.board; + + // Changed for espruino compatibility + if (colour === undefined) { + colour = state.to_play; + } + if (ep === undefined) { + ep = state.enpassant; + } + var movelist = p4_parse(state, colour, ep, 0); + var alpha = P4_MIN_SCORE; + var mv, t, i; + var bs = 0; + var be = 0; + + if (level <= 0){ + for (i = 0; i < movelist.length; i++){ + mv = movelist[i]; + if(movelist[i][0] > alpha){ + alpha = mv[0]; + bs = mv[1]; + be = mv[2]; + } + } + return [bs, be, alpha]; + } + + for(i = 0; i < movelist.length; i++){ + mv = movelist[i]; + var mscore = mv[0]; + var ms = mv[1]; + var me = mv[2]; + if (mscore > P4_WIN){ + p4_log("XXX taking king! it should never come to this"); + alpha = P4_KING_VALUE; + bs = ms; + be = me; + break; + } + t = -state.treeclimber(state, level - 1, 1 - colour, mscore, ms, me, + P4_MIN_SCORE, -alpha); + if (t > alpha){ + alpha = t; + bs = ms; + be = me; + } + } + if (alpha < -P4_WIN_NOW && ! p4_check_check(state, colour)){ + alpha = state.stalemate_scores[colour]; + } + return [bs, be, alpha]; +} + +/*p4_make_move changes the state and returns an object containing + * everything necesary to undo the change. + * + * p4_unmake_move uses the p4_make_move return value to restore the + * previous state. + */ + +function p4_make_move(state, s, e, promotion){ + var board = state.board; + var S = board[s]; + var E = board[e]; + board[e] = S; + board[s] = 0; + var piece = S & 14; + var moved_colour = S & 1; + var end_piece = S; /* can differ from S in queening*/ + //now some stuff to handle queening, castling + var rs = 0, re, rook; + var ep_taken = 0, ep_position; + var ep = 0; + if(piece == P4_PAWN){ + if((60 - e) * (60 - e) > 900){ + /*got to end; replace the pawn on board and in pieces cache.*/ + promotion |= moved_colour; + board[e] = promotion; + end_piece = promotion; + } + else if (((s ^ e) & 1) && E == 0){ + /*this is a diagonal move, but the end spot is empty, so we surmise enpassant */ + ep_position = e - 10 + 20 * moved_colour; + ep_taken = board[ep_position]; + board[ep_position] = 0; + } + else if ((s - e) * (s - e) == 400){ + /*delta is 20 --> two row jump at start*/ + ep = (s + e) >> 1; + } + } + else if (piece == P4_KING && ((s - e) * (s - e) == 4)){ //castling - move rook too + rs = s - 4 + (s < e) * 7; + re = (s + e) >> 1; //avg of s,e=rook's spot + rook = moved_colour + P4_ROOK; + board[rs] = 0; + board[re] = rook; + //piece_locations.push([rook, re]); + } + + var old_castle_state = state.castles; + if (old_castle_state){ + var mask = 0; + var shift = moved_colour * 2; + var side = moved_colour * 70; + var s2 = s - side; + var e2 = e + side; + //wipe both our sides if king moves + if (s2 == 25) + mask |= 3 << shift; + //wipe one side on any move from rook points + else if (s2 == 21) + mask |= 1 << shift; + else if (s2 == 28) + mask |= 2 << shift; + //or on any move *to* opposition corners + if (e2 == 91) + mask |= 4 >> shift; + else if (e2 == 98) + mask |= 8 >> shift; + state.castles &= ~mask; + } + + var old_pieces = state.pieces.concat(); + var our_pieces = old_pieces[moved_colour]; + var dest = state.pieces[moved_colour] = []; + for (var i = 0; i < our_pieces.length; i++){ + var x = our_pieces[i]; + var pp = x[0]; + var ps = x[1]; + if (ps != s && ps != rs){ + dest.push(x); + } + } + dest.push([end_piece, e]); + if (rook) + dest.push([rook, re]); + + if (E || ep_taken){ + var their_pieces = old_pieces[1 - moved_colour]; + dest = state.pieces[1 - moved_colour] = []; + var gone = ep_taken ? ep_position : e; + for (i = 0; i < their_pieces.length; i++){ + var x = their_pieces[i]; + if (x[1] != gone){ + dest.push(x); + } + } + } + + return { + /*some of these (e.g. rook) could be recalculated during + * unmake, possibly more cheaply. */ + s: s, + e: e, + S: S, + E: E, + ep: ep, + castles: old_castle_state, + rs: rs, + re: re, + rook: rook, + ep_position: ep_position, + ep_taken: ep_taken, + pieces: old_pieces + }; +} + +function p4_unmake_move(state, move){ + var board = state.board; + if (move.ep_position){ + board[move.ep_position] = move.ep_taken; + } + board[move.s] = move.S; + board[move.e] = move.E; + //move.piece_locations.length--; + if(move.rs){ + board[move.rs] = move.rook; + board[move.re] = 0; + //move.piece_locations.length--; + } + state.pieces = move.pieces; + state.castles = move.castles; +} + + +function p4_insufficient_material(state){ + var knights = false; + var bishops = undefined; + var i; + var board = state.board; + for(i = 20; i < 100; i++){ + var piece = board[i] & 14; + if(piece == 0 || piece == P4_KING){ + continue; + } + if (piece == P4_KNIGHT){ + /* only allow one knight of either colour, never with a bishop */ + if (knights || bishops !== undefined){ + return false; + } + knights = true; + } + else if (piece == P4_BISHOP){ + /*any number of bishops, but on only one colour square */ + var x = i & 1; + var y = parseInt(i / 10) & 1; + var parity = x ^ y; + if (knights){ + return false; + } + else if (bishops === undefined){ + bishops = parity; + } + else if (bishops != parity){ + return false; + } + } + else { + return false; + } + } + return true; +} + +/* p4_move(state, s, e, promotion) + * s, e are start and end positions + * + * promotion is the desired pawn promotion if the move gets a pawn to the other + * end. + * + * return value contains bitwise flags +*/ + +var P4_MOVE_FLAG_OK = 1; +var P4_MOVE_FLAG_CHECK = 2; +var P4_MOVE_FLAG_MATE = 4; +var P4_MOVE_FLAG_CAPTURE = 8; +var P4_MOVE_FLAG_CASTLE_KING = 16; +var P4_MOVE_FLAG_CASTLE_QUEEN = 32; +var P4_MOVE_FLAG_DRAW = 64; + +var P4_MOVE_ILLEGAL = 0; +var P4_MOVE_MISSED_MATE = P4_MOVE_FLAG_CHECK | P4_MOVE_FLAG_MATE; +var P4_MOVE_CHECKMATE = P4_MOVE_FLAG_OK | P4_MOVE_FLAG_CHECK | P4_MOVE_FLAG_MATE; +var P4_MOVE_STALEMATE = P4_MOVE_FLAG_OK | P4_MOVE_FLAG_MATE; + +function p4_move(state, s, e, promotion){ + var board = state.board; + var colour = state.to_play; + var other_colour = 1 - colour; + if (s != parseInt(s)){ + if (e === undefined){ + var mv = p4_interpret_movestring(state, s); + s = mv[0]; + e = mv[1]; + if (s == 0) + return {flags: P4_MOVE_ILLEGAL, ok: false}; + promotion = mv[2]; + } + else {/*assume two point strings: 'e2', 'e4'*/ + s = p4_destringify_point(s); + e = p4_destringify_point(e); + } + } + if (promotion === undefined) + promotion = P4_QUEEN; + var E=board[e]; + var S=board[s]; + + /*See if this move is even slightly legal, disregarding check. + */ + var i; + var legal = false; + p4_maybe_prepare(state); + var moves = p4_parse(state, colour, state.enpassant, 0); + for (i = 0; i < moves.length; i++){ + if (e == moves[i][2] && s == moves[i][1]){ + legal = true; + break; + } + } + if (! legal) { + return {flags: P4_MOVE_ILLEGAL, ok: false}; + } + + /*Try the move, and see what the response is.*/ + var changes = p4_make_move(state, s, e, promotion); + + /*is it check? */ + if (p4_check_check(state, colour)){ + p4_unmake_move(state, changes); + p4_log('in check', changes); + return {flags: P4_MOVE_ILLEGAL, ok: false, string: "in check!"}; + } + /*The move is known to be legal. We won't be undoing it.*/ + + var flags = P4_MOVE_FLAG_OK; + + state.enpassant = changes.ep; + state.history.push([s, e, promotion]); + + /*draw timeout: 50 moves without pawn move or capture is a draw */ + if (changes.E || changes.ep_position){ + state.draw_timeout = 0; + flags |= P4_MOVE_FLAG_CAPTURE; + } + else if ((S & 14) == P4_PAWN){ + state.draw_timeout = 0; + } + else{ + state.draw_timeout++; + } + if (changes.rs){ + flags |= (s > e) ? P4_MOVE_FLAG_CASTLE_QUEEN : P4_MOVE_FLAG_CASTLE_KING; + } + var shortfen = p4_state2fen(state, true); + var repetitions = (state.position_counts[shortfen] || 0) + 1; + state.position_counts[shortfen] = repetitions; + state.current_repetitions = repetitions; + if (state.draw_timeout > 100 || repetitions >= 3 || + p4_insufficient_material(state)){ + flags |= P4_MOVE_FLAG_DRAW; + } + state.moveno++; + state.to_play = other_colour; + + if (p4_check_check(state, other_colour)){ + flags |= P4_MOVE_FLAG_CHECK; + } + /* check for (stale|check)mate, by seeing if there is a move for + * the other side that doesn't result in check. (In other words, + * reduce the pseudo-legal-move list down to a legal-move list, + * and check it isn't empty). + * + * We don't need to p4_prepare because other colour pieces can't + * have moved (just disappeared) since previous call. Also, + * setting the promotion piece is unnecessary, because all + * promotions block check equally well. + */ + var is_mate = true; + var replies = p4_parse(state, other_colour, changes.ep, 0); + for (i = 0; i < replies.length; i++){ + var m = replies[i]; + var change2 = p4_make_move(state, m[1], m[2], P4_QUEEN); + var check = p4_check_check(state, other_colour); + p4_unmake_move(state, change2); + if (!check){ + is_mate = false; + break; + } + } + if (is_mate) + flags |= P4_MOVE_FLAG_MATE; + + var movestring = p4_move2string(state, s, e, S, promotion, flags, moves); + p4_log("successful move", s, e, movestring, flags); + state.prepared = false; + return { + flags: flags, + string: movestring, + ok: true + }; +} + + +function p4_move2string(state, s, e, S, promotion, flags, moves){ + var piece = S & 14; + var src, dest; + var mv, i; + var capture = flags & P4_MOVE_FLAG_CAPTURE; + + src = p4_stringify_point(s); + dest = p4_stringify_point(e); + if (piece == P4_PAWN){ + if (capture){ + mv = src.charAt(0) + 'x' + dest; + } + else + mv = dest; + if (e > 90 || e < 30){ //end row, queening + if (promotion === undefined) + promotion = P4_QUEEN; + mv += '=' + P4_ENCODE_LUT.charAt(promotion); + } + } + else if (piece == P4_KING && (s-e) * (s-e) == 4) { + if (e < s) + mv = 'O-O-O'; + else + mv = 'O-O'; + } + else { + var row_qualifier = ''; + var col_qualifier = ''; + var pstr = P4_ENCODE_LUT.charAt(S); + var sx = s % 10; + var sy = parseInt(s / 10); + + /* find any other pseudo-legal moves that would put the same + * piece in the same place, for which we'd need + * disambiguation. */ + var co_landers = []; + for (i = 0; i < moves.length; i++){ + var m = moves[i]; + if (e == m[2] && s != m[1] && state.board[m[1]] == S){ + co_landers.push(m[1]); + } + } + if (co_landers.length){ + for (i = 0; i < co_landers.length; i++){ + var c = co_landers[i]; + var cx = c % 10; + var cy = parseInt(c / 10); + if (cx == sx)/*same column, so qualify by row*/ + row_qualifier = src.charAt(1); + if (cy == sy) + col_qualifier = src.charAt(0); + } + if (row_qualifier == '' && col_qualifier == ''){ + /*no co-landers on the same rank or file, so one or the other will do. + * By convention, use the column (a-h) */ + col_qualifier = src.charAt(0); + } + } + mv = pstr + col_qualifier + row_qualifier + (capture ? 'x' : '') + dest; + } + if (flags & P4_MOVE_FLAG_CHECK){ + if (flags & P4_MOVE_FLAG_MATE) + mv += '#'; + else + mv += '+'; + } + else if (flags & P4_MOVE_FLAG_MATE) + mv += ' stalemate'; + return mv; +} + + +function p4_jump_to_moveno(state, moveno){ + p4_log('jumping to move', moveno); + if (moveno === undefined || moveno > state.moveno) + moveno = state.moveno; + else if (moveno < 0){ + moveno = state.moveno + moveno; + } + var state2 = p4_fen2state(state.beginning); + var i = 0; + while (state2.moveno < moveno){ + var m = state.history[i++]; + p4_move(state2, m[0], m[1], m[2]); + } + /* copy the replayed state across, not all that deeply, but + * enough to cover, eg, held references to board. */ + var attr, dest; + for (attr in state2){ + var src = state2[attr]; + if (attr instanceof Array){ + dest = state[attr]; + dest.length = 0; + for (i = 0; i < src.length; i++){ + dest[i] = src[i]; + } + } + else { + state[attr] = src; + } + } + state.prepared = false; +} + + +/* write a standard FEN notation + * http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + * */ +function p4_state2fen(state, reduced){ + var piece_lut = ' PpRrNnBbKkQq'; + var board = state.board; + var fen = ''; + //fen does Y axis backwards, X axis forwards */ + for (var y = 9; y > 1; y--){ + var count = 0; + for (var x = 1; x < 9; x++){ + var piece = board[y * 10 + x]; + if (piece == 0) + count++; + else{ + if (count) + fen += count.toString(); + fen += piece_lut.charAt(piece); + count = 0; + } + } + if (count) + fen += count; + if (y > 2) + fen += '/'; + } + /*white or black */ + fen += ' ' + 'wb'.charAt(state.to_play) + ' '; + /*castling */ + if (state.castles){ + var lut = [2, 'K', 1, 'Q', 8, 'k', 4, 'q']; + for (var i = 0; i < 8; i += 2){ + if (state.castles & lut[i]){ + fen += lut[i + 1]; + } + } + } + else + fen += '-'; + /*enpassant */ + if (state.enpassant !== 0){ + fen += ' ' + p4_stringify_point(state.enpassant); + } + else + fen += ' -'; + if (reduced){ + /*if the 'reduced' flag is set, the move number and draw + *timeout are not added. This form is used to detect draws by + *3-fold repetition.*/ + return fen; + } + fen += ' ' + state.draw_timeout + ' '; + fen += (state.moveno >> 1) + 1; + return fen; +} + +function p4_stringify_point(p){ + var letters = " abcdefgh"; + var x = p % 10; + var y = (p - x) / 10 - 1; + return letters.charAt(x) + y; +} + +function p4_destringify_point(p){ + var x = parseInt(p.charAt(0), 19) - 9; //a-h <-> 10-18, base 19 + var y = parseInt(p.charAt(1)) + 1; + if (y >= 2 && y < 10 && x >= 1 && x < 9) + return y * 10 + x; + return undefined; +} + +/* read a standard FEN notation + * http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + * */ +function p4_fen2state(fen, state){ + if (state === undefined) + state = p4_initialise_state(); + var board = state.board; + var fenbits = fen.split(' '); + var fen_board = fenbits[0]; + var fen_toplay = fenbits[1]; + var fen_castles = fenbits[2]; + var fen_enpassant = fenbits[3]; + var fen_timeout = fenbits[4]; + var fen_moveno = fenbits[5]; + if (fen_timeout === undefined) + fen_timeout = 0; + //fen does Y axis backwards, X axis forwards */ + var y = 90; + var x = 1; + var i, c; + for (var j = 0; j < fen_board.length; j++){ + c = fen_board.charAt(j); + if (c == '/'){ + x = 1; + y -= 10; + if (y < 20) + break; + continue; + } + var piece = P4_PIECE_LUT[c]; + if (piece && x < 9){ + board[y + x] = piece; + x++; + } + else { + var end = Math.min(x + parseInt(c), 9); + for (; x < end; x++){ + board[y + x] = 0; + } + } + } + state.to_play = (fen_toplay.toLowerCase() == 'b') ? 1 : 0; + state.castles = 0; + /* Sometimes we meet bad FEN that says it can castle when it can't. */ + var wk = board[25] == P4_KING; + var bk = board[95] == P4_KING + 1; + var castle_lut = { + k: 8 * (bk && board[98] == P4_ROOK + 1), + q: 4 * (bk && board[91] == P4_ROOK + 1), + K: 2 * (wk && board[28] == P4_ROOK), + Q: 1 * (wk && board[21] == P4_ROOK) + }; + for (i = 0; i < fen_castles.length; i++){ + c = fen_castles.charAt(i); + var castle = castle_lut[c]; + if (castle !== undefined){ + state.castles |= castle; + if (castle == 0){ + console.log("FEN claims castle state " + fen_castles + + " but pieces are not in place for " + c); + } + } + } + + state.enpassant = (fen_enpassant != '-') ? p4_destringify_point(fen_enpassant) : 0; + state.draw_timeout = parseInt(fen_timeout); + if (fen_moveno === undefined){ + /*have a guess based on entropy and pieces remaining*/ + var pieces = 0; + var mix = 0; + var p, q, r; + for (y = 20; y < 100; y+=10){ + for (x = 1; x < 9; x++){ + p = board[y + x] & 15; + pieces += (!!p); + if (x < 8){ + q = board[y + x + 1]; + mix += (!q) != (!p); + } + if (y < 90){ + q = board[y + x + 10]; + mix += (!q) != (!p); + } + } + } + fen_moveno = Math.max(1, parseInt((32 - pieces) * 1.3 + (4 - fen_castles.length) * 1.5 + ((mix - 16) / 5))); + //p4_log("pieces", pieces, "mix", mix, "estimate", fen_moveno); + } + state.moveno = 2 * (parseInt(fen_moveno) - 1) + state.to_play; + state.history = []; + state.beginning = fen; + state.prepared = false; + state.position_counts = {}; + /* Wrap external functions as methods. */ + state.move = function(s, e, promotion){ + return p4_move(this, s, e, promotion); + }; + state.findmove = function(level){ + return p4_findmove(this, level); + }; + state.jump_to_moveno = function(moveno){ + return p4_jump_to_moveno(this, moveno); + }; + return state; +} + +/* +Weights would all fit within an Int8Array *except* for the last row +for pawns, which is close to the queen value (180, max is 127). + +Int8Array seems slightly quicker in Chromium 18, no different in +Firefox 12. + +Int16Array is no faster, perhaps slower than Int32Array. + +Int32Array is marginally slower than plain arrays with Firefox 12, but +significantly quicker with Chromium. + */ + +var P4_ZEROS = []; +function p4_zero_array(){ + if (P4_USE_TYPED_ARRAYS) + return new Int32Array(120); + if (P4_ZEROS.length == 0){ + for(var i = 0; i < 120; i++){ + P4_ZEROS[i] = 0; + } + } + return P4_ZEROS.slice(); +} + +/* p4_initialise_state() creates the board and initialises weight + * arrays etc. Some of this is really only needs to be done once. + */ + +function p4_initialise_state(){ + var board = p4_zero_array(); + P4_CENTRALISING_WEIGHTS = p4_zero_array(); + P4_BASE_PAWN_WEIGHTS = p4_zero_array(); + P4_KNIGHT_WEIGHTS = p4_zero_array(); + for(var i = 0; i < 120; i++){ + var y = parseInt(i / 10); + var x = i % 10; + var dx = Math.abs(x - 4.5); + var dy = Math.abs(y - 5.5); + P4_CENTRALISING_WEIGHTS[i] = parseInt(6 - Math.pow((dx * dx + dy * dy) * 1.5, 0.6)); + //knights have a flat topped centre (bishops too, but less so). + P4_KNIGHT_WEIGHTS[i] = parseInt(((dx < 2) + (dy < 2) * 1.5) + + (dx < 3) + (dy < 3)) - 2; + P4_BASE_PAWN_WEIGHTS[i] = parseInt('000012347000'.charAt(y)); + if (y > 9 || y < 2 || x < 1 || x > 8) + board[i] = 16; + } + var weights = []; + for (i = 0; i < 14; i++){ + weights[i] = p4_zero_array(); + } + var state = { + board: board, + weights: weights, + history: [], + treeclimber: p4_alphabeta_treeclimber + }; + p4_random_seed(state, P4_DEBUG ? 1 : Date.now()); + return state; +} + +function p4_new_game(){ + return p4_fen2state(P4_INITIAL_BOARD); +} + +/*convert an arbitrary movestring into a pair of integers offsets into + * the board. The string might be in any of these forms: + * + * "d2-d4" "d2d4" "d4" -- moving a pawn + * + * "b1-c3" "b1c3" "Nc3" "N1c3" "Nbc3" "Nb1c3" -- moving a knight + * + * "b1xc3" "b1xc3" "Nxc3" -- moving a knight, also happens to capture. + * + * "O-O" "O-O-O" -- special cases for castling ("e1-c1", etc, also work) + * + * Note that for the "Nc3" (pgn) format, some knowledge of the board + * is necessary, so the state parameter is required. If it is + * undefined, the other forms will still work. + */ + +function p4_interpret_movestring(state, str){ + /* Ignore any irrelevant characters, then tokenise. + * + */ + var FAIL = [0, 0]; + var algebraic_re = /^\s*([RNBQK]?[a-h]?[1-8]?)[ :x-]*([a-h][1-8]?)(=[RNBQ])?[!?+#e.p]*\s*$/; + var castle_re = /^\s*([O0o]-[O0o](-[O0o])?)\s*$/; + var position_re = /^[a-h][1-8]$/; + + var m = algebraic_re.exec(str); + if (m == null){ + /*check for castling notation (O-O, O-O-O) */ + m = castle_re.exec(str); + if (m){ + s = 25 + state.to_play * 70; + if (m[2])/*queenside*/ + e = s - 2; + else + e = s + 2; + } + else + return FAIL; + } + var src = m[1]; + var dest = m[2]; + var queen = m[3]; + var s, e, q; + var moves, i; + if (src == '' || src == undefined){ + /* a single coordinate pawn move */ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, 'P' + dest.charAt(0)); + } + else if (/^[RNBQK]/.test(src)){ + /*pgn format*/ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, src); + } + else if (position_re.test(src) && position_re.test(dest)){ + s = p4_destringify_point(src); + e = p4_destringify_point(dest); + } + else if (/^[a-h]$/.test(src)){ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, 'P' + src); + } + if (s == 0) + return FAIL; + + if (queen){ + /* the chosen queen piece */ + q = P4_PIECE_LUT[queen.charAt(1)]; + } + return [s, e, q]; +} + + +function p4_find_source_point(state, e, str){ + var colour = state.to_play; + var piece = P4_PIECE_LUT[str.charAt(0)]; + piece |= colour; + var s, i; + + var row, column; + /* can be specified as Na, Na3, N3, and who knows, N3a? */ + for (i = 1; i < str.length; i++){ + var c = str.charAt(i); + if (/[a-h]/.test(c)){ + column = str.charCodeAt(i) - 96; + } + else if (/[1-8]/.test(c)){ + /*row goes 2 - 9 */ + row = 1 + parseInt(c); + } + } + var possibilities = []; + p4_prepare(state); + var moves = p4_parse(state, colour, + state.enpassant, 0); + for (i = 0; i < moves.length; i++){ + var mv = moves[i]; + if (e == mv[2]){ + s = mv[1]; + if (state.board[s] == piece && + (column === undefined || column == s % 10) && + (row === undefined || row == parseInt(s / 10)) + ){ + var change = p4_make_move(state, s, e, P4_QUEEN); + if (! p4_check_check(state, colour)) + possibilities.push(s); + p4_unmake_move(state, change); + } + } + } + p4_log("finding", str, "that goes to", e, "got", possibilities); + + if (possibilities.length == 0){ + return 0; + } + else if (possibilities.length > 1){ + p4_log("p4_find_source_point seems to have failed", + state, e, str, + possibilities); + } + return possibilities[0]; +} + + +/*random number generator based on + * http://burtleburtle.net/bob/rand/smallprng.html + */ +function p4_random_seed(state, seed){ + seed &= 0xffffffff; + state.rng = (P4_USE_TYPED_ARRAYS) ? new Uint32Array(4) : []; + state.rng[0] = 0xf1ea5eed; + state.rng[1] = seed; + state.rng[2] = seed; + state.rng[3] = seed; + for (var i = 0; i < 20; i++) + p4_random31(state); +} + +function p4_random31(state){ + var rng = state.rng; + var b = rng[1]; + var c = rng[2]; + /* These shifts amount to rotates. + * Note the three-fold right shift '>>>', meaning an unsigned shift. + * The 0xffffffff masks are needed to keep javascript to 32bit. (supposing + * untyped arrays). + */ + var e = rng[0] - ((b << 27) | (b >>> 5)); + rng[0] = b ^ ((c << 17) | (c >>> 15)); + rng[1] = (c + rng[3]) & 0xffffffff; + rng[2] = (rng[3] + e) & 0xffffffff; + rng[3] = (e + rng[0]) & 0xffffffff; + return rng[3] & 0x7fffffff; +} + +function p4_random_int(state, top){ + /* uniform integer in range [0 <= n < top), supposing top < 2 ** 31 + * + * This method is slightly (probably pointlessly) more accurate + * than converting to 0-1 float, multiplying and truncating, and + * considerably more accurate than a simple modulus. + * Obviously it is a bit slower. + */ + /* mask becomes one less than the next highest power of 2 */ + var mask = top; + mask--; + mask |= mask >>> 1; + mask |= mask >>> 2; + mask |= mask >>> 4; + mask |= mask >>> 8; + mask |= mask >>> 16; + var r; + do{ + r = p4_random31(state) & mask; + } while (r >= top); + return r; +} + +// Added for espruino +exports.p4_new_game = p4_new_game; +exports.p4_fen2state = p4_fen2state; +exports.p4_state2fen = p4_state2fen; +exports.p4_random_int = p4_random_int; +exports.P4_INITIAL_BOARD = P4_INITIAL_BOARD; +exports.P4_PAWN = P4_PAWN; +exports.P4_ROOK = P4_ROOK; +exports.P4_KNIGHT = P4_KNIGHT; +exports.P4_BISHOP = P4_BISHOP; +exports.P4_QUEEN = P4_QUEEN; +exports.P4_KING = P4_KING; +exports.P4_MOVE_FLAG_OK = P4_MOVE_FLAG_OK; +exports.P4_MOVE_FLAG_CHECK = P4_MOVE_FLAG_CHECK; +exports.P4_MOVE_FLAG_MATE = P4_MOVE_FLAG_MATE; +exports.P4_MOVE_FLAG_CAPTURE = P4_MOVE_FLAG_CAPTURE; +exports.P4_MOVE_FLAG_CASTLE_KING = P4_MOVE_FLAG_CASTLE_KING; +exports.P4_MOVE_FLAG_CASTLE_QUEEN = P4_MOVE_FLAG_CASTLE_QUEEN; +exports.P4_MOVE_FLAG_DRAW = P4_MOVE_FLAG_DRAW; +exports.P4_MOVE_ILLEGAL = P4_MOVE_ILLEGAL; +exports.P4_MOVE_MISSED_MATE = P4_MOVE_MISSED_MATE; +exports.P4_MOVE_CHECKMATE = P4_MOVE_CHECKMATE; +exports.P4_MOVE_STALEMATE = P4_MOVE_STALEMATE; diff --git a/apps/chess/metadata.json b/apps/chess/metadata.json new file mode 100644 index 000000000..4f810886b --- /dev/null +++ b/apps/chess/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "chess", + "name": "Chess", + "shortName": "Chess", + "version": "0.05", + "description": "Chess game based on the [p4wn engine](https://p4wn.sourceforge.net/). Drag on the touchscreen to move the green cursor onto a piece, select it with a single touch and drag the now red cursor around. Release the piece with another touch to finish the move. The button opens a menu.", + "icon": "app.png", + "tags": "game", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"chess.app.js","url":"app.js"}, + {"name":"chessengine","url":"engine.js"}, + {"name":"chess.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"chess.json"}], + "screenshots": [ {"url":"screenshot.png"} ] +} diff --git a/apps/chess/screenshot.png b/apps/chess/screenshot.png new file mode 100644 index 000000000..11d96163b Binary files /dev/null and b/apps/chess/screenshot.png differ diff --git a/apps/chimer/ChangeLog b/apps/chimer/ChangeLog new file mode 100644 index 000000000..7af6c18ea --- /dev/null +++ b/apps/chimer/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial Creation +0.02: Fixed some sleep bugs. Added a sleep mode toggle +0.03: Reduce busy-loop and code +0.04: Separate buzz-time and sleep-time +0.05: Minor code improvements diff --git a/apps/chimer/README.MD b/apps/chimer/README.MD new file mode 100644 index 000000000..a78c677f2 --- /dev/null +++ b/apps/chimer/README.MD @@ -0,0 +1,11 @@ +# Chimer - For the BangleJS + +A fork of [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime) that adds extra features such as: + +- Buzz or beep on every 60, 30 or 15 minutes. +- Repeat Chime up to 3 times +- Set hours to disable chime + +Setting the hours you don't want your watch to chime for is done by setting the hour you want it to stop, and the hour you want it to start. + +Hours range from 0 - 23. diff --git a/apps/chimer/icon.txt b/apps/chimer/icon.txt new file mode 100644 index 000000000..cc969bc81 --- /dev/null +++ b/apps/chimer/icon.txt @@ -0,0 +1,2 @@ + +widget.png: "https://icons8.com/icon/114436/alarm" \ No newline at end of file diff --git a/apps/chimer/metadata.json b/apps/chimer/metadata.json new file mode 100644 index 000000000..9d2d0a698 --- /dev/null +++ b/apps/chimer/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "chimer", + "name": "Chimer", + "version": "0.05", + "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Repeat Chime up to 3 times \n - Set hours to disable chime", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.MD", + "storage": [ + { "name": "chimer.wid.js", "url": "widget.js" }, + { "name": "chimer.settings.js", "url": "settings.js" } + ], + "data": [{ "name": "chimer.json" }] +} diff --git a/apps/chimer/settings.js b/apps/chimer/settings.js new file mode 100644 index 000000000..1cfb980f4 --- /dev/null +++ b/apps/chimer/settings.js @@ -0,0 +1,94 @@ +/** + * @param {function} back Use back() to return to settings menu + */ + +(function (back) { + // default to buzzing + var FILE = "chimer.json"; + var settings = {}; + const chimes = ["Off", "Buzz", "Beep", "Both"]; + const frequency = ["60 min", "30 min", "15 min", "1 min"]; + + var showMainMenu = () => { + E.showMenu({ + "": { title: "Chimer" }, + "< Back": () => back(), + "Chime Type": { + value: settings.type, + min: 0, + max: 2, // both is just silly + format: (v) => chimes[v], + onchange: (v) => { + settings.type = v; + writeSettings(settings); + }, + }, + Frequency: { + value: settings.freq, + min: 0, + max: 2, + format: (v) => frequency[v], + onchange: (v) => { + settings.freq = v; + writeSettings(settings); + }, + }, + Repetition: { + value: settings.repeat, + min: 1, + max: 5, + format: (v) => v, + onchange: (v) => { + settings.repeat = v; + writeSettings(settings); + }, + }, + "Sleep Mode": { + value: !!settings.sleep, + onchange: (v) => { + settings.sleep = v; + writeSettings(settings); + }, + }, + "Sleep Start": { + value: settings.start, + min: 0, + max: 23, + format: (v) => v, + onchange: (v) => { + settings.start = v; + writeSettings(settings); + }, + }, + "Sleep End": { + value: settings.end, + min: 0, + max: 23, + format: (v) => v, + onchange: (v) => { + settings.end = v; + writeSettings(settings); + }, + }, + }); + }; + + var readSettings = () => { + var settings = require("Storage").readJSON(FILE, 1) || { + type: 1, + freq: 0, + repeat: 1, + sleep: true, + start: 6, + end: 22, + }; + return settings; + }; + + var writeSettings = (settings) => { + require("Storage").writeJSON(FILE, settings); + }; + + settings = readSettings(); + showMainMenu(); +}) diff --git a/apps/chimer/widget.js b/apps/chimer/widget.js new file mode 100644 index 000000000..d27ecd78f --- /dev/null +++ b/apps/chimer/widget.js @@ -0,0 +1,96 @@ +(function () { + // 0: off, 1: buzz, 2: beep, 3: both + var FILE = "chimer.json"; + + var readSettings = () => { + var settings = require("Storage").readJSON(FILE, 1) || { + type: 1, + freq: 0, + repeat: 1, + sleep: true, + start: 6, + end: 22, + }; + return settings; + }; + + var settings = readSettings(); + + function chime() { + let count = settings.repeat; + + const chime1 = () => { + let p; + if (settings.type === 1) { + p = Bangle.buzz(100); + } else if (settings.type === 2) { + p = Bangle.beep(); + } else { + return; + } + if (--count > 0) + p.then(() => setTimeout(chime1, 150)); + }; + + chime1(); + } + + function queueNextCheckMins(mins) { + const now = new Date(), + m = now.getMinutes(), + s = now.getSeconds(), + ms = now.getMilliseconds(); + + const mLeft = mins - (m + mins * 2) % mins, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + + setTimeout(check, msLeft); + } + + let lastHour = new Date().getHours(); + let lastMinute = new Date().getMinutes(); // don't chime when (re)loaded at a whole hour + function check() { + const now = new Date(), + h = now.getHours(), + m = now.getMinutes(); + if (settings.sleep && ( + h > settings.end || + (h >= settings.end && m !== 0) || + h < settings.start) + ) { + queueNextCheckMins(60); + return; + } + switch (settings.freq) { + case 1: + if (m !== lastMinute && m % 30 === 0) + chime(); + lastHour = h; + lastMinute = m; + queueNextCheckMins(30); + break; + case 2: + if (m !== lastMinute && m % 15 === 0) + chime(); + lastHour = h; + lastMinute = m; + queueNextCheckMins(15); + break; + case 3: + // unreachable - not available in settings + if (m !== lastMinute) chime(); + lastHour = h; + lastMinute = m; + queueNextCheckMins(1); + break; + default: + if (h !== lastHour && m === 0) chime(); + lastHour = h; + queueNextCheckMins(60); + break; + } + } + + check(); +})(); diff --git a/apps/chimer/widget.png b/apps/chimer/widget.png new file mode 100644 index 000000000..14edf4150 Binary files /dev/null and b/apps/chimer/widget.png differ diff --git a/apps/choozi/ChangeLog b/apps/choozi/ChangeLog index 03f7ef832..35adc7430 100644 --- a/apps/choozi/ChangeLog +++ b/apps/choozi/ChangeLog @@ -1,3 +1,9 @@ 0.01: New App! 0.02: Support Bangle.js 2 0.03: Fix bug for Bangle.js 2 where g.flip was not being called. +0.04: Combine code for both apps + Better colors for Bangle.js 2 + Fix selection animation for Bangle.js 2 + New icon + Slightly wider arc segments for better visibility + Extract arc drawing code in library diff --git a/apps/choozi/README.md b/apps/choozi/README.md index f1e4255bc..ccaa97a27 100644 --- a/apps/choozi/README.md +++ b/apps/choozi/README.md @@ -11,16 +11,21 @@ the players seated in a circle, set the number of segments equal to the number of players, ensure that each person knows which colour represents them, and then choose a segment. After a short animation, the chosen segment will fill the screen. -You can use Choozi to randomly select an element from any set with 2 to 13 members, +You can use Choozi to randomly select an element from any set with 2 to 15 members, as long as you can define a bijection between members of the set and coloured segments on the Bangle.js display. -## Controls +## Controls Bangle 1 BTN1: increase the number of segments BTN2: choose a segment at random BTN3: decrease the number of segments +## Controls Bangle 2 + +Swipe up/down: increase/decrease the number of segments +BTN1 or tap: choose a segment at random + ## Creator James Stanley diff --git a/apps/choozi/app-icon.js b/apps/choozi/app-icon.js index 51b3bead3..560286098 100644 --- a/apps/choozi/app-icon.js +++ b/apps/choozi/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) +require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4=")) \ No newline at end of file diff --git a/apps/choozi/app.js b/apps/choozi/app.js index 1a5b2f17e..b9f53bc89 100644 --- a/apps/choozi/app.js +++ b/apps/choozi/app.js @@ -4,15 +4,16 @@ * * James Stanley 2021 */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; +const GU = require("graphics_utils"); +var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff']; +var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0']; var stepAngle = 0.18; // radians - resolution of polygon var gapAngle = 0.035; // radians - gap between segments -var perimMin = 110; // px - min. radius of perimeter -var perimMax = 120; // px - max. radius of perimeter +var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter +var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter -var segmentMax = 106; // px - max radius of filled-in segment +var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment var segmentStep = 5; // px - step size of segment fill animation var circleStep = 4; // px - step size of circle fill animation @@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec var animStartSteps = 300; // how many steps before it can start slowing? var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate var ballSize = 3; // px - ball radius -var ballTrack = 100; // px - radius of ball path +var ballTrack = perimMin - ballSize*2; // px - radius of ball path -var centreX = 120; // px - centre of screen -var centreY = 120; // px - centre of screen +var centreX = g.getWidth()*0.5; // px - centre of screen +var centreY = g.getWidth()*0.5; // px - centre of screen var fontSize = 50; // px @@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle var defaultN = 3; // default value for N var minN = 2; -var maxN = colours.length; var N; var arclen; @@ -51,42 +51,14 @@ function shuffle (array) { } } -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - // draw the arc segments around the perimeter function drawPerimeter() { + g.setBgColor('#000000'); g.clear(); for (var i = 0; i < N; i++) { g.setColor(colours[i%colours.length]); var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); + GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle); } } @@ -131,6 +103,7 @@ function animateChoice(target) { g.fillCircle(x, y, ballSize); oldx=x; oldy=y; + if (process.env.HWVERSION == 2) g.flip(); } } @@ -141,11 +114,15 @@ function choose() { var maxAngle = minAngle + arclen; animateChoice((minAngle+maxAngle)/2); g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) + for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){ + GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle); + if (process.env.HWVERSION == 2) g.flip(); + } + GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle); + for (var r = 1; r < segmentMax; r += circleStep){ g.fillCircle(centreX,centreY,r); + if (process.env.HWVERSION == 2) g.flip(); + } g.fillCircle(centreX,centreY,segmentMax); } @@ -171,38 +148,47 @@ function setN(n) { drawPerimeter(); } -// save N to choozi.txt +// save N to choozi.save function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); + var savedN = read(); + if (savedN != N) require("Storage").write("choozi.save","" + N); } -// load N from choozi.txt +function read(){ + var n = require("Storage").read("choozi.save"); + if (n !== undefined) return parseInt(n); + return defaultN; +} + +// load N from choozi.save function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); + setN(read()); } -shuffle(colours); // is this really best? -Bangle.setLCDMode("direct"); -Bangle.setLCDTimeout(0); // keep screen on +if (process.env.HWVERSION == 1){ + colours=colours.concat(colours2); + shuffle(colours); +} else { + shuffle(colours); + shuffle(colours2); + colours=colours.concat(colours2); +} + +var maxN = colours.length; +if (process.env.HWVERSION == 1){ + Bangle.setLCDMode("direct"); + Bangle.setLCDTimeout(0); // keep screen on +} readN(); drawN(); -setWatch(() => { - setN(N+1); - drawN(); -}, BTN1, {repeat:true}); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN2, {repeat:true}); - -setWatch(() => { - setN(N-1); - drawN(); -}, BTN3, {repeat:true}); +Bangle.setUI("updown", (v)=>{ + if (!v){ + writeN(); + drawPerimeter(); + choose(); + } else { + setN(N-v); + drawN(); + } +}); diff --git a/apps/choozi/app.png b/apps/choozi/app.png index 99c9fa07a..50f09f164 100644 Binary files a/apps/choozi/app.png and b/apps/choozi/app.png differ diff --git a/apps/choozi/appb2.js b/apps/choozi/appb2.js deleted file mode 100644 index 5f217f638..000000000 --- a/apps/choozi/appb2.js +++ /dev/null @@ -1,207 +0,0 @@ -/* Choozi - Choose people or things at random using Bangle.js. - * Inspired by the "Chwazi" Android app - * - * James Stanley 2021 - */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; - -var stepAngle = 0.18; // radians - resolution of polygon -var gapAngle = 0.035; // radians - gap between segments -var perimMin = 80; // px - min. radius of perimeter -var perimMax = 87; // px - max. radius of perimeter - -var segmentMax = 70; // px - max radius of filled-in segment -var segmentStep = 5; // px - step size of segment fill animation -var circleStep = 4; // px - step size of circle fill animation - -// rolling ball animation: -var maxSpeed = 0.08; // rad/sec -var minSpeed = 0.001; // rad/sec -var animStartSteps = 300; // how many steps before it can start slowing? -var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate -var ballSize = 3; // px - ball radius -var ballTrack = 75; // px - radius of ball path - -var centreX = 88; // px - centre of screen -var centreY = 88; // px - centre of screen - -var fontSize = 50; // px - -var radians = 2*Math.PI; // radians per circle - -var defaultN = 3; // default value for N -var minN = 2; -var maxN = colours.length; -var N; -var arclen; - -// https://www.frankmitchell.org/2015/01/fisher-yates/ -function shuffle (array) { - var i = 0 - , j = 0 - , temp = null; - - for (i = array.length - 1; i > 0; i -= 1) { - j = Math.floor(Math.random() * (i + 1)); - temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } -} - -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - -// draw the arc segments around the perimeter -function drawPerimeter() { - g.clear(); - for (var i = 0; i < N; i++) { - g.setColor(colours[i%colours.length]); - var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); - } -} - -// animate a ball rolling around and settling at "target" radians -function animateChoice(target) { - var angle = 0; - var speed = 0; - var oldx = -10; - var oldy = -10; - var decelFromAngle = -1; - var allowDecel = false; - for (var i = 0; true; i++) { - angle = angle + speed; - if (angle > radians) angle -= radians; - if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) { - speed = speed + accel; - if (speed > maxSpeed) { - speed = maxSpeed; - /* when we reach max speed, we know how long it takes - * to accelerate, and therefore how long to decelerate, so - * we can work out what angle to start decelerating from */ - if (decelFromAngle < 0) { - decelFromAngle = target-angle; - while (decelFromAngle < 0) decelFromAngle += radians; - while (decelFromAngle > radians) decelFromAngle -= radians; - } - } - } else { - if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true; - if (allowDecel) speed = speed - accel; - if (speed < minSpeed) speed = minSpeed; - if (speed == minSpeed && angle < target && angle+speed >= target) return; - } - - var r = i/2; - if (r > ballTrack) r = ballTrack; - var x = centreX+Math.cos(angle)*r; - var y = centreY+Math.sin(angle)*r; - g.setColor('#000000'); - g.fillCircle(oldx,oldy,ballSize+1); - g.setColor('#ffffff'); - g.fillCircle(x, y, ballSize); - oldx=x; - oldy=y; - g.flip(); - } -} - -// choose a winning segment and animate its selection -function choose() { - var chosen = Math.floor(Math.random()*N); - var minAngle = (chosen/N)*radians; - var maxAngle = minAngle + arclen; - animateChoice((minAngle+maxAngle)/2); - g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) - g.fillCircle(centreX,centreY,r); - g.fillCircle(centreX,centreY,segmentMax); -} - -// draw the current value of N in the middle of the screen, with -// up/down arrows -function drawN() { - g.setColor(g.theme.fg); - g.setFont("Vector",fontSize); - g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2); - if (N < maxN) - g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]); - if (N > minN) - g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]); -} - -// update number of segments, with min/max limit, "arclen" update, -// and screen reset -function setN(n) { - N = n; - if (N < minN) N = minN; - if (N > maxN) N = maxN; - arclen = radians/N - gapAngle; - drawPerimeter(); -} - -// save N to choozi.txt -function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); -} - -// load N from choozi.txt -function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); -} - -shuffle(colours); // is this really best? -Bangle.setLCDTimeout(0); // keep screen on -readN(); -drawN(); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN1, {repeat:true}); - -Bangle.on('touch', function(zone,e) { - if(e.x>+88){ - setN(N-1); - drawN(); - }else{ - setN(N+1); - drawN(); - } -}); diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png index 104024958..ee422ed10 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot1.png and b/apps/choozi/bangle1-choozi-screenshot1.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png index f3b6868bf..20edf4c78 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot2.png and b/apps/choozi/bangle1-choozi-screenshot2.png differ diff --git a/apps/choozi/metadata.json b/apps/choozi/metadata.json index 79af76fa2..c42abe079 100644 --- a/apps/choozi/metadata.json +++ b/apps/choozi/metadata.json @@ -1,7 +1,7 @@ { "id": "choozi", "name": "Choozi", - "version": "0.03", + "version": "0.04", "description": "Choose people or things at random using Bangle.js.", "icon": "app.png", "tags": "tool", @@ -10,8 +10,10 @@ "allow_emulator": true, "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "storage": [ - {"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]}, - {"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]}, + {"name":"choozi.app.js","url":"app.js"}, {"name":"choozi.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"choozi.save"} ] } diff --git a/apps/chronlog/ChangeLog b/apps/chronlog/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/chronlog/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/chronlog/README.md b/apps/chronlog/README.md new file mode 100644 index 000000000..7c542cf73 --- /dev/null +++ b/apps/chronlog/README.md @@ -0,0 +1,53 @@ +# Chrono Logger + +Record times active on a task, course, work or anything really. + +**Disclaimer:** No one is responsible for any loss of data you recorded with this app. If you run into problems please report as advised under **Requests** below. + +With time on your side and a little help from your friends - you'll surely triumph over Lavos in the end! + +![dump](dump.png) ![dump1](dump1.png) ![dump2](dump2.png) ![dump3](dump3.png) ![dump4](dump4.png) ![dump5](dump5.png) ![dump6](dump6.png) + + +## Usage + +Click the large green button to log the start of your activity. Click the now red button again to log that you stopped. + +## Features + +- Saves to file on every toggling of the active state. + - csv file contents looks like: + ``` + 1,Start,2024-03-02T15:18:09 GMT+0200 + 2,Note,Critical hit! + 3,Stop,2024-03-02T15:19:17 GMT+0200 + ``` +- Add annotations to the log. +- Create and switch between multiple logs. +- Sync log files to an Android device through Gadgetbridge (Needs pending code changes to Gadgetbridge). +- App state is restored when you start the app again. + +## Controls + +- Large button to toggle active state. +- Menu icon to access additional functionality. +- Hardware button exits menus, closes the app on the main screen. + +## TODO and notes + +- Delete individual tasks/logs through the app? +- Reset everything through the app? +- Scan for chronlog storage files that somehow no longer have tasks associated with it? +- Complete the Gadgetbridge side of things for sync. +- Sync to iOS? +- Inspect log files through the app, similarly to Recorder app? +- Changes to Android file system permissions makes it not always trivial to access the synced files. + + +## Requests + +Tag @thyttan in an issue to https://gitbub.com/espruino/BangleApps/issues to report problems or suggestions. + +## Creator + +[thyttan](https://github.com/thyttan) diff --git a/apps/chronlog/app-icon.js b/apps/chronlog/app-icon.js new file mode 100644 index 000000000..dc25e4b5b --- /dev/null +++ b/apps/chronlog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///gElq3X0ELJf4AiitAAYMBqgKEgNVrgEBmtVCAQABgtVr/Agf1qtQEQlpq6QB6tpEgkVywLDywLEq2uyoLB6wEBBZAECBYda32lBYIECBZ9W3wjDAgILPquWqoACAgILEtILDAgKOEAAyQCRwIAGSAUVBY6ECBZYGD7WnAoYLF9WrBYupAoWq1QECtQLBtWdBYt21QLC1LfBBYVfA4ILBlWq1f9rWVv/q1WoBYMKCgOvTYP6AoOgBYMCAoIAFwCQCBY6nDGAIAEFwQkIEQZVCBQZRCAAcGBYeQBYoYDCwwYECw5KC0gKIAH4APA=")) diff --git a/apps/chronlog/app.js b/apps/chronlog/app.js new file mode 100644 index 000000000..827ca14e1 --- /dev/null +++ b/apps/chronlog/app.js @@ -0,0 +1,376 @@ +// TODO: +// - Add more /*LANG*/ tags for translations. +// - Check if there are chronlog storage files that should be added to tasks. + +{ + const storage = require("Storage"); + let appData = storage.readJSON("chronlog.json", true) || { + currentTask : "default", + tasks : { + default: { + file : "chronlog_default.csv", // Existing default task log file + state : "stopped", + lineNumber : 0, + lastLine : "", + lastSyncedLine : "", + }, + // Add more tasks as needed + }, + }; + let currentTask = appData.currentTask; + let tasks = appData.tasks; + delete appData; + + let themeColors = g.theme; + + let logEntry; // Avoid previous lint warning + + // Function to draw the Start/Stop button with play and pause icons + let drawButton = ()=>{ + var btnWidth = g.getWidth() - 40; + var btnHeight = 50; + var btnX = 20; + var btnY = (g.getHeight() - btnHeight) / 2; + var cornerRadius = 25; + + var isStopped = tasks[currentTask].state === "stopped"; + g.setColor(isStopped ? "#0F0" : "#F00"); // Set color to green when stopped and red when started + + // Draw rounded corners of the button + g.fillCircle(btnX + cornerRadius, btnY + cornerRadius, cornerRadius); + g.fillCircle(btnX + btnWidth - cornerRadius, btnY + cornerRadius, cornerRadius); + g.fillCircle(btnX + cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius); + g.fillCircle(btnX + btnWidth - cornerRadius, btnY + btnHeight - cornerRadius, cornerRadius); + + // Draw rectangles to fill in the button + g.fillRect(btnX + cornerRadius, btnY, btnX + btnWidth - cornerRadius, btnY + btnHeight); + g.fillRect(btnX, btnY + cornerRadius, btnX + btnWidth, btnY + btnHeight - cornerRadius); + + g.setColor(themeColors.bg); // Set icon color to contrast against the button's color + + // Center the icon within the button + var iconX = btnX + btnWidth / 2; + var iconY = btnY + btnHeight / 2; + + if (isStopped) { + // Draw play icon + var playSize = 10; // Side length of the play triangle + var offset = playSize / Math.sqrt(3) - 3; + g.fillPoly([ + iconX - playSize, iconY - playSize + offset, + iconX - playSize, iconY + playSize + offset, + iconX + playSize * 2 / Math.sqrt(3), iconY + offset + ]); + } else { + // Draw pause icon + var barWidth = 5; // Width of pause bars + var barHeight = btnHeight / 2; // Height of pause bars + var barSpacing = 5; // Spacing between pause bars + g.fillRect(iconX - barSpacing / 2 - barWidth, iconY - barHeight / 2, iconX - barSpacing / 2, iconY + barHeight / 2); + g.fillRect(iconX + barSpacing / 2, iconY - barHeight / 2, iconX + barSpacing / 2 + barWidth, iconY + barHeight / 2); + } + }; + + let drawHamburgerMenu = ()=>{ + var x = g.getWidth() / 2; // Center the hamburger menu horizontally + var y = (7/8)*g.getHeight(); // Position it near the bottom + var lineLength = 18; // Length of the hamburger lines + var spacing = 6; // Space between the lines + + g.setColor(themeColors.fg); // Set color to foreground color for the icon + // Draw three horizontal lines + for (var i = -1; i <= 1; i++) { + g.fillRect(x - lineLength/2, y + i * spacing - 1, x + lineLength/2, y + i * spacing + 1); + } + }; + + // Function to draw the task name centered between the widget field and the start/stop button + let drawTaskName = ()=>{ + g.setFont("Vector", 20); // Set a smaller font for the task name display + + // Calculate position to center the task name horizontally + var x = (g.getWidth()) / 2; + + // Calculate position to center the task name vertically between the widget field and the start/stop button + var y = g.getHeight()/4; // Center vertically + + g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color + g.drawString(currentTask, x, y); // Draw the task name centered on the screen + }; + + // Function to draw the last log entry of the current task + let drawLastLogEntry = ()=>{ + g.setFont("Vector", 10); // Set a smaller font for the task name display + + // Calculate position to center the log entry horizontally + var x = (g.getWidth()) / 2; + + // Calculate position to place the log entry properly between the start/stop button and hamburger menu + var btnBottomY = (g.getHeight() + 50) / 2; // Y-coordinate of the bottom of the start/stop button + var menuBtnYTop = g.getHeight() * (5 / 6); // Y-coordinate of the top of the hamburger menu button + var y = btnBottomY + (menuBtnYTop - btnBottomY) / 2 + 2; // Center vertically between button and menu + + g.setColor(themeColors.fg).setFontAlign(0,0); // Set text color to foreground color + g.drawString(g.wrapString(tasks[currentTask].lastLine, 150).join("\n"), x, y); + }; + + /* + // Helper function to read the last log entry from the current task's log file + let updateLastLogEntry = ()=>{ + var filename = tasks[currentTask].file; + var file = require("Storage").open(filename, "r"); + var lastLine = ""; + var line; + while ((line = file.readLine()) !== undefined) { + lastLine = line; // Keep reading until the last line + } + tasks[currentTask].lastLine = lastLine; + }; + */ + + // Main UI drawing function + let drawMainMenu = ()=>{ + g.clear(); + Bangle.drawWidgets(); // Draw any active widgets + g.setColor(themeColors.bg); // Set color to theme's background color + g.fillRect(Bangle.appRect); // Fill the app area with the background color + + drawTaskName(); // Draw the centered task name + drawLastLogEntry(); // Draw the last log entry of the current task + drawButton(); // Draw the Start/Stop toggle button + drawHamburgerMenu(); // Draw the hamburger menu button icon + + //g.flip(); // Send graphics to the display + }; + + // Function to toggle the active state + let toggleChronlog = ()=>{ + var dateObj = new Date(); + var dateObjStrSplit = dateObj.toString().split(" "); + var currentTime = dateObj.getFullYear().toString() + "-" + (dateObj.getMonth()<10?"0":"") + dateObj.getMonth().toString() + "-" + (dateObj.getDate()<10?"0":"") + dateObj.getDate().toString() + "T" + (dateObj.getHours()<10?"0":"") + dateObj.getHours().toString() + ":" + (dateObj.getMinutes()<10?"0":"") + dateObj.getMinutes().toString() + ":" + (dateObj.getSeconds()<10?"0":"") + dateObj.getSeconds().toString() + " " + dateObjStrSplit[dateObjStrSplit.length-1]; + + tasks[currentTask].lineNumber = Number(tasks[currentTask].lineNumber) + 1; + logEntry = tasks[currentTask].lineNumber + (tasks[currentTask].state === "stopped" ? ",Start," : ",Stop,") + currentTime + "\n"; + var filename = tasks[currentTask].file; + + // Open the appropriate file and append the log entry + var file = require("Storage").open(filename, "a"); + file.write(logEntry); + tasks[currentTask].lastLine = logEntry; + + // Toggle the state and update the button text + tasks[currentTask].state = tasks[currentTask].state === "stopped" ? "started" : "stopped"; + drawMainMenu(); // Redraw the main UI + }; + + // Define the touch handler function for the main menu + let handleMainMenuTouch = (button, xy)=>{ + var btnTopY = (g.getHeight() - 50) / 2; + var btnBottomY = btnTopY + 50; + var menuBtnYTop = (7/8)*g.getHeight() - 15; + var menuBtnYBottom = (7/8)*g.getHeight() + 15; + var menuBtnXLeft = (g.getWidth() / 2) - 15; + var menuBtnXRight = (g.getWidth() / 2) + 15; + + // Detect if the touch is within the toggle button area + if (xy.x >= 20 && xy.x <= (g.getWidth() - 20) && xy.y > btnTopY && xy.y < btnBottomY) { + toggleChronlog(); + } + // Detect if the touch is within the hamburger menu button area + else if (xy.x >= menuBtnXLeft && xy.x <= menuBtnXRight && xy.y >= menuBtnYTop && xy.y <= menuBtnYBottom) { + showMenu(); + } + }; + + // Function to attach the touch event listener + let setMainUI = ()=>{ + Bangle.setUI({ + mode: "custom", + back: load, + touch: handleMainMenuTouch + }); + }; + + let saveAppState = ()=>{ + let appData = { + currentTask : currentTask, + tasks : tasks, + }; + require("Storage").writeJSON("chronlog.json", appData); + }; + // Set up a listener for the 'kill' event + E.on('kill', saveAppState); + + // Function to switch to a selected task + let switchTask = (taskName)=>{ + currentTask = taskName; // Update the current task + + // Reinitialize the UI elements + setMainUI(); + drawMainMenu(); // Redraw UI to reflect the task change and the button state + }; + + // Function to create a new task + let createNewTask = ()=>{ + // Prompt the user to input the task's name + require("textinput").input({ + text: "" // Default empty text for new task + }).then(result => { + var taskName = result; // Store the result from text input + if (taskName) { + if (tasks.hasOwnProperty(taskName)) { + // Task already exists, handle this case as needed + E.showAlert(/*LANG*/"Task already exists", "Error").then(drawMainMenu); + } else { + // Create a new task log file for the new task + var filename = "chronlog_" + taskName.replace(/\W+/g, "_") + ".csv"; + tasks[taskName] = { + file : filename, + state : "stopped", + lineNumber : 0, + lastLine : "", + lastSyncedLine : "", + }; + + currentTask = taskName; + + setMainUI(); + drawMainMenu(); // Redraw UI with the new task + } + } else { + setMainUI(); + drawMainMenu(); // User cancelled, redraw main menu + } + }).catch(e => { + console.log("Text input error", e); + setMainUI(); + drawMainMenu(); // In case of error also redraw main menu + }); + }; + + // Function to display the list of tasks for selection + let chooseTask = ()=>{ + // Construct the tasks menu from the tasks object + var taskMenu = { + "": { "title": /*LANG*/"Choose Task", + "back" : function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Cancel task selection + } + } + }; + for (var taskName in tasks) { + if (!tasks.hasOwnProperty(taskName)) continue; + taskMenu[taskName] = (function(name) { + return function() { + switchTask(name); + }; + })(taskName); + } + + // Add a menu option for creating a new task + taskMenu[/*LANG*/"Create New Task"] = createNewTask; + + E.showMenu(taskMenu); // Display the task selection + }; + + // Function to annotate the current or last work session + let annotateTask = ()=>{ + + // Prompt the user to input the annotation text + require("textinput").input({ + text: "" // Default empty text for annotation + }).then(result => { + var annotationText = result.trim(); + if (annotationText) { + // Append annotation to the last or current log entry + tasks[currentTask].lineNumber ++; + var annotatedEntry = tasks[currentTask].lineNumber + /*LANG*/",Note," + annotationText + "\n"; + var filename = tasks[currentTask].file; + var file = require("Storage").open(filename, "a"); + file.write(annotatedEntry); + tasks[currentTask].lastLine = annotatedEntry; + setMainUI(); + drawMainMenu(); // Redraw UI after adding the annotation + } else { + // User cancelled, so we do nothing and just redraw the main menu + setMainUI(); + drawMainMenu(); + } + }).catch(e => { + console.log("Annotation input error", e); + setMainUI(); + drawMainMenu(); // In case of error also redraw main menu + }); + }; + + let syncToAndroid = (taskName, isFullSync)=>{ + let mode = "a"; + if (isFullSync) mode = "w"; + let lastSyncedLine = tasks[taskName].lastSyncedLine || 0; + let taskNameValidFileName = taskName.replace(" ","_"); // FIXME: Should use something similar to replaceAll using a regular expression to catch all illegal characters. + + let storageFile = require("Storage").open("chronlog_"+taskNameValidFileName+".csv", "r"); + let contents = storageFile.readLine(); + let lineNumber = contents ? contents.slice(0, contents.indexOf(",")) : 0; + let shouldSyncLine = ()=>{return (contents && (isFullSync || (Number(lineNumber)>Number(lastSyncedLine))));}; + let doSyncLine = (mde)=>{Bluetooth.println(JSON.stringify({t:"file", n:"chronlog_"+taskNameValidFileName+".csv", c:contents, m:mde}));}; + + if (shouldSyncLine()) doSyncLine(mode); + contents = storageFile.readLine(); + while (contents) { + lineNumber = contents.slice(0, contents.indexOf(",")); // Could theoretically do with `lineNumber++`, but this is more robust in case numbering in file ended up irregular. + if (shouldSyncLine()) doSyncLine("a"); + contents = storageFile.readLine(); + } + tasks[taskName].lastSyncedLine = lineNumber; + }; + + // Function to display the list of tasks for selection + let syncTasks = ()=>{ + let isToDoFullSync = false; + // Construct the tasks menu from the tasks object + var syncMenu = { + "": { "title": /*LANG*/"Sync Tasks", + "back" : function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Cancel task selection + } + } + }; + syncMenu[/*LANG*/"Full Resyncs"] = { + value: !!isToDoFullSync, // !! converts undefined to false + onchange: ()=>{ + isToDoFullSync = !isToDoFullSync + }, + } + for (var taskName in tasks) { + if (!tasks.hasOwnProperty(taskName)) continue; + syncMenu[taskName] = (function(name) { + return function() {syncToAndroid(name,isToDoFullSync);}; + })(taskName); + } + + E.showMenu(syncMenu); // Display the task selection + }; + + let showMenu = ()=>{ + var menu = { + "": { "title": /*LANG*/"Menu", + "back": function() { + setMainUI(); // Reattach when the menu is closed + drawMainMenu(); // Redraw the main UI when closing the menu + }, + }, + /*LANG*/"Annotate": annotateTask, // Now calls the real annotation function + /*LANG*/"Change Task": chooseTask, // Opens the task selection screen + /*LANG*/"Sync to Android": syncTasks, + }; + E.showMenu(menu); + }; + + Bangle.loadWidgets(); + drawMainMenu(); // Draw the main UI when the app starts + // When the application starts, attach the touch event listener + setMainUI(); +} diff --git a/apps/chronlog/app.png b/apps/chronlog/app.png new file mode 100644 index 000000000..c21a147ea Binary files /dev/null and b/apps/chronlog/app.png differ diff --git a/apps/chronlog/dump.png b/apps/chronlog/dump.png new file mode 100644 index 000000000..0c40b190e Binary files /dev/null and b/apps/chronlog/dump.png differ diff --git a/apps/chronlog/dump1.png b/apps/chronlog/dump1.png new file mode 100644 index 000000000..04f625f04 Binary files /dev/null and b/apps/chronlog/dump1.png differ diff --git a/apps/chronlog/dump2.png b/apps/chronlog/dump2.png new file mode 100644 index 000000000..be0791659 Binary files /dev/null and b/apps/chronlog/dump2.png differ diff --git a/apps/chronlog/dump3.png b/apps/chronlog/dump3.png new file mode 100644 index 000000000..eeeba525f Binary files /dev/null and b/apps/chronlog/dump3.png differ diff --git a/apps/chronlog/dump4.png b/apps/chronlog/dump4.png new file mode 100644 index 000000000..b1bd51669 Binary files /dev/null and b/apps/chronlog/dump4.png differ diff --git a/apps/chronlog/dump5.png b/apps/chronlog/dump5.png new file mode 100644 index 000000000..debf919cc Binary files /dev/null and b/apps/chronlog/dump5.png differ diff --git a/apps/chronlog/dump6.png b/apps/chronlog/dump6.png new file mode 100644 index 000000000..29a06b68f Binary files /dev/null and b/apps/chronlog/dump6.png differ diff --git a/apps/chronlog/metadata.json b/apps/chronlog/metadata.json new file mode 100644 index 000000000..50a9166bf --- /dev/null +++ b/apps/chronlog/metadata.json @@ -0,0 +1,14 @@ +{ "id": "chronlog", + "name": "Chrono Logger", + "version":"0.01", + "description": "Record time active on a task, course, work or anything really.", + "icon": "app.png", + "tags": "logging,record,work,tasks", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots" : [ { "url":"dump.png"}, { "url":"dump1.png" }, { "url":"dump2.png" }, { "url":"dump3.png" }, { "url":"dump4.png" }, { "url":"dump5.png" }, { "url":"dump6.png" } ], + "storage": [ + {"name":"chronlog.app.js","url":"app.js"}, + {"name":"chronlog.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/chrono/ChangeLog b/apps/chrono/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/chrono/ChangeLog +++ b/apps/chrono/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/chrono/chrono.js b/apps/chrono/chrono.js index cd50b8a22..5b9e6eda9 100644 --- a/apps/chrono/chrono.js +++ b/apps/chrono/chrono.js @@ -69,5 +69,5 @@ Bangle.on('touch', function (button) { started = !started; }); -var interval = setInterval(countDown, 1000); +setInterval(countDown, 1000); drawInterface(); \ No newline at end of file diff --git a/apps/chrono/metadata.json b/apps/chrono/metadata.json index 59fc1dbeb..aa8a68b20 100644 --- a/apps/chrono/metadata.json +++ b/apps/chrono/metadata.json @@ -2,7 +2,7 @@ "id": "chrono", "name": "Chrono", "shortName": "Chrono", - "version": "0.01", + "version": "0.02", "description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.", "icon": "chrono.png", "tags": "tool", diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog index ed230b737..08a9ac828 100644 --- a/apps/chronowid/ChangeLog +++ b/apps/chronowid/ChangeLog @@ -4,3 +4,4 @@ 0.04: Change to 7 segment font, move to top widget bar Better auto-update behaviour, less RAM used 0.05: Fix error running app on new firmwares (fix #1140) +0.06: Use default Bangle formatter for booleans diff --git a/apps/chronowid/app.js b/apps/chronowid/app.js index ab363ed17..b0ee7625a 100644 --- a/apps/chronowid/app.js +++ b/apps/chronowid/app.js @@ -79,7 +79,6 @@ function showMenu() { }, 'Timer on': { value: settingsChronowid.started, - format: v => v ? "On" : "Off", onchange: v => { settingsChronowid.started = v; updateSettings(); diff --git a/apps/chronowid/metadata.json b/apps/chronowid/metadata.json index 7cb32709f..69a5d3a2e 100644 --- a/apps/chronowid/metadata.json +++ b/apps/chronowid/metadata.json @@ -2,7 +2,7 @@ "id": "chronowid", "name": "Chrono Widget", "shortName": "Chrono Widget", - "version": "0.05", + "version": "0.06", "description": "Chronometer (timer) which runs as widget.", "icon": "app.png", "tags": "tool,widget", diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index c3e7918e7..4e10bfe49 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -24,3 +24,22 @@ Improve performance, reduce memory usage Small optical adjustments 0.12: Allow configuration of update interval +0.13: Load step goal from Bangle health app as fallback + Memory optimizations +0.14: Support to show big weather info +0.15: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16 +0.16: Fix const error + Use widget_utils if available +0.17: Load circles from clkinfo +0.18: Improved clkinfo handling and using it for the weather circle +0.19: Remove old code and fixing clkinfo handling (fix HRM and other items that change) + Remove settings for what is displayed and instead allow circles to be changed by swiping +0.20: Add much faster circle rendering (250ms -> 40ms) + Add fast load capability +0.21: Remade all icons without a palette for dark theme + Now re-adds widgets if they were hidden when fast-loading +0.22: Fixed crash if item has no image and cutting long overflowing text +0.23: Setting circles colours per clkinfo and not position +0.24: Using suggested color from clock_info if set as default and available +0.25: Use clock_info module as an app +0.26: Minor code improvements diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index aa429d5ec..1dc5be61b 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -5,28 +5,41 @@ A clock with three or four circles for different data at the bottom in a probabl By default the time, date and day of week is shown. It can show the following information (this can be configured): + * Steps * Steps distance * Heart rate (automatically updates when screen is on and unlocked) * Battery (including charging status and battery low warning) - * Weather (requires [weather app](https://banglejs.com/apps/#weather)) + * Weather (requires [OWM weather provider](https://banglejs.com/apps/?id=owmweather)) * Humidity or wind speed as circle progress * Temperature inside circle * Condition as icon below circle - * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) - * Temperature, air pressure or altitude from internal pressure sensor + * Big weather icon next to clock + * Altitude from internal pressure sensor + * Active alarms (if `Alarm` app installed) + * Sunrise or sunset (if `Sunrise Clockinfo` app installed) +To change what is shown: -The color of each circle can be configured. The following colors are available: +* Unlock the watch +* Tap on the circle to change (a border is drawn around it) +* Swipe up/down to change the gauge within the given group +* Swipe left/right to change the group (eg. between standard Bangle.js and Alarms/etc) + +Data is provided by ['Clock Info'](http://www.espruino.com/Bangle.js+Clock+Info) +so any apps that implement this feature can add extra information to be displayed. + +The color of each circle can be configured from `Settings -> Apps -> Circles Clock`. The following colors are available: * Basic colors (red, green, blue, yellow, magenta, cyan, black, white) * Color depending on value (green -> red, red -> green) - ## Screenshots ![Screenshot dark theme](screenshot-dark.png) ![Screenshot light theme](screenshot-light.png) ![Screenshot dark theme with four circles](screenshot-dark-4.png) ![Screenshot light theme with four circles](screenshot-light-4.png) +![Screenshot light theme with big weather enabled](screenshot-light-with-big-weather.png) + ## Ideas * Show compass heading @@ -35,4 +48,5 @@ The color of each circle can be configured. The following colors are available: Marco ([myxor](https://github.com/myxor)) ## Icons -Icons taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 +Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from +[icons8](https://icons8.com/icon/set/weather/small--static--black) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 48e3a1a1a..9be308bb3 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,10 +1,3 @@ -const locale = require("locale"); -const storage = require("Storage"); -const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); - -const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"); -const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"); - Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) { // Actual height 39 (40 - 2) this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16)); @@ -17,48 +10,34 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) { return this; }; -const SETTINGS_FILE = "circlesclock.json"; +{ +let locale = require("locale"); +let storage = require("Storage"); + +let SETTINGS_FILE = "circlesclock.json"; let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} ); -// Load step goal from pedometer widget as fallback -if (settings.stepGoal == undefined) { - const d = storage.readJSON("wpedom.json", true) || {}; - settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; -} - -/* - * Read location from myLocation app - */ -function getLocation() { - return storage.readJSON("mylocation.json", 1) || undefined; -} -let location = getLocation(); +let drawTimeout; const showWidgets = settings.showWidgets || false; const circleCount = settings.circleCount || 3; +const showBigWeather = settings.showBigWeather || false; -let hrtValue; -let now = Math.round(new Date().getTime() / 1000); - +//let now = Math.round(new Date().getTime() / 1000); // layout values: -const colorFg = g.theme.dark ? '#fff' : '#000'; -const colorBg = g.theme.dark ? '#000' : '#fff'; -const colorGrey = '#808080'; -const colorRed = '#ff0000'; -const colorGreen = '#008000'; -const colorBlue = '#0000ff'; -const colorYellow = '#ffff00'; -const widgetOffset = showWidgets ? 24 : 0; -const dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date -const h = g.getHeight() - widgetOffset; -const w = g.getWidth(); -const hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset; -const h1 = Math.round(1 * h / 5 - hOffset); -const h2 = Math.round(3 * h / 5 - hOffset); -const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position +let colorFg = g.theme.dark ? '#fff' : '#000'; +let colorBg = g.theme.dark ? '#000' : '#fff'; +let widgetOffset = showWidgets ? 24 : 0; +let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date +let h = g.getHeight() - widgetOffset; +let w = g.getWidth(); +let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset; +let h1 = Math.round(1 * h / 5 - hOffset); +let h2 = Math.round(3 * h / 5 - hOffset); +let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle middle y position /* * circle x positions @@ -72,492 +51,155 @@ const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position * | (1) (2) (3) (4) | * => circles start at 1,3,5,7 / 8 */ -const parts = circleCount * 2; -const circlePosX = [ +let parts = circleCount * 2; +let circlePosX = [ Math.round(1 * w / parts), // circle1 Math.round(3 * w / parts), // circle2 Math.round(5 * w / parts), // circle3 Math.round(7 * w / parts), // circle4 ]; -const radiusOuter = circleCount == 3 ? 25 : 20; -const radiusInner = circleCount == 3 ? 20 : 15; -const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; -const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; -const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; -const iconOffset = circleCount == 3 ? 6 : 8; -const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; +let radiusOuter = circleCount == 3 ? 25 : 20; +let radiusBorder = radiusOuter+3; // absolute border of circles +let radiusInner = circleCount == 3 ? 20 : 15; +let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; +let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; +let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; +let iconOffset = circleCount == 3 ? 6 : 8; -function hideWidgets() { - /* - * we are not drawing the widgets as we are taking over the whole screen - * so we will blank out the draw() functions of each widget and change the - * area to the top bar doesn't get cleared. - */ - if (WIDGETS && typeof WIDGETS === "object") { - for (let wd of WIDGETS) { - wd.draw = () => {}; - wd.area = ""; - } - } -} -function draw() { - g.clear(true); - if (!showWidgets) { - hideWidgets(); - } else { - Bangle.drawWidgets(); - } +let draw = function() { + let R = Bangle.appRect; + g.reset().clearRect(R.x,R.y, R.x2, h3-(radiusBorder+1)); g.setColor(colorBg); g.fillRect(0, widgetOffset, w, h2 + 22); // time g.setFontRobotoRegular50NumericOnly(); - g.setFontAlign(0, -1); g.setColor(colorFg); - g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6); - now = Math.round(new Date().getTime() / 1000); + if (!showBigWeather) { + g.setFontAlign(0, -1); + g.drawString(locale.time(new Date(), 1), w / 2, h1 + 6); + } + else { + g.setFontAlign(-1, -1); + g.drawString(locale.time(new Date(), 1), 2, h1 + 6); + } + //now = Math.round(new Date().getTime() / 1000); // date & dow g.setFontRobotoRegular21(); - g.setFontAlign(0, 0); - g.drawString(locale.date(new Date()), w / 2, h2); - g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset); - - // draw the circles a little bit delayed so we decrease the blocking time - setTimeout(function() { - drawCircle(1); - }, 1); - setTimeout(function() { - drawCircle(2); - }, 1); - setTimeout(function() { - drawCircle(3); - }, 1); - setTimeout(function() { - if (circleCount >= 4) drawCircle(4); - }, 1); -} - -function drawCircle(index) { - let type = settings['circle' + index]; - if (!type) type = defaultCircleTypes[index - 1]; - const w = getCircleXPosition(type); - - switch (type) { - case "steps": - drawSteps(w); - break; - case "stepsDist": - drawStepsDistance(w); - break; - case "hr": - drawHeartRate(w); - break; - case "battery": - drawBattery(w); - break; - case "weather": - drawWeather(w); - break; - case "sunprogress": - case "sunProgress": - drawSunProgress(w); - break; - case "temperature": - drawTemperature(w); - break; - case "pressure": - drawPressure(w); - break; - case "altitude": - drawAltitude(w); - break; - case "empty": - // we draw nothing here - return; - } -} - -// serves as cache for quicker lookup of circle positions -let circlePositionsCache = []; -/* - * Looks in the following order if a circle with the given type is somewhere visible/configured - * 1. circlePositionsCache - * 2. settings - * 3. defaultCircleTypes - * - * In case 2 and 3 the circlePositionsCache will be updated - */ -function getCirclePosition(type) { - if (circlePositionsCache[type] >= 0) { - return circlePositionsCache[type]; - } - for (let i = 1; i <= circleCount; i++) { - const setting = settings['circle' + i]; - if (setting == type) { - circlePositionsCache[type] = i - 1; - return i - 1; - } - } - for (let i = 0; i < defaultCircleTypes.length; i++) { - if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) { - circlePositionsCache[type] = i; - return i; - } - } - return undefined; -} - -function getCircleXPosition(type) { - const circlePos = getCirclePosition(type); - if (circlePos != undefined) { - return circlePosX[circlePos]; - } - return undefined; -} - -function isCircleEnabled(type) { - return getCirclePosition(type) != undefined; -} - -function getCircleColor(type) { - const pos = getCirclePosition(type); - const color = settings["circle" + (pos + 1) + "color"]; - if (color && color != "") return color; -} - -function getCircleIconColor(type, color, percent) { - const pos = getCirclePosition(type); - const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; - if (colorizeIcon) { - return getGradientColor(color, percent); + if (!showBigWeather) { + g.setFontAlign(0, 0); + g.drawString(locale.date(new Date()), w / 2, h2); + g.drawString(locale.dow(new Date()), w / 2, h2 + dowOffset); } else { - return ""; + g.setFontAlign(-1, 0); + g.drawString(locale.date(new Date()), 2, h2); + g.drawString(locale.dow(new Date()), 2, h2 + dowOffset, 1); } + + // weather + if (showBigWeather) { + let weather = getWeather(); + let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; + g.setFontAlign(1, 0); + if (tempString) g.drawString(tempString, w, h2); + + let code = weather ? weather.code : -1; + let icon = getWeatherIconByCode(code, true); + if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75}); + } + + queueDraw(); } -function getGradientColor(color, percent) { +let getCircleColor = function(item, data, clkmenu) { + let colorKey = clkmenu.name; + if(!clkmenu.dynamic) colorKey += "/"+item.name; + colorKey += "_color"; + let color = settings[colorKey]; + //use default color only if no other color is set + if(data.color && !color) return data.color; + if (color && color != "") return color; + return g.theme.fg; +} + +let getGradientColor = function(color, percent) { if (isNaN(percent)) percent = 0; if (percent > 1) percent = 1; - const colorList = [ - '#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000' + let colorList = [ + '#00ff00', '#80ff00', '#ffff00', '#ff8000', '#ff0000' ]; if (color == "fg") { color = colorFg; } if (color == "green-red") { - const colorIndex = Math.round(colorList.length * percent); + let colorIndex = Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00"; + //return g.blendColor('#00ff00', '#ff0000', percent); //mostly dithering } if (color == "red-green") { - const colorIndex = colorList.length - Math.round(colorList.length * percent); + let colorIndex = colorList.length - Math.round(colorList.length * percent); return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; + //return g.blendColor('#ff0000', '#00ff00', percent); + } + colorList = [ + '#0000ff', '#8800ff', '#ff00ff', '#ff0088', '#ff0000' + ]; + if (color == "blue-red") { + let colorIndex = Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length) - 1] || "#0000ff"; + //return g.blendColor('#0000ff', '#ff0000', percent); + } + if (color == "red-blue") { + let colorIndex = colorList.length - Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; + //return g.blendColor('#ff0000', '#0000ff', percent); } return color; } -function getImage(graphic, color) { - if (!color || color == "") { - return graphic; +let getCircleIconColor = function(index, color, percent) { + let colorizeIcon = settings["circle" + index + "colorizeIcon"] == true; + if (colorizeIcon) { + return getGradientColor(color, percent); } else { - return { - width: 16, - height: 16, - bpp: 1, - transparent: 0, - buffer: E.toArrayBuffer(graphic), - palette: new Uint16Array([colorBg, g.toColor(color)]) - }; + return g.theme.fg; } } -function drawSteps(w) { - if (!w) w = getCircleXPosition("steps"); - const steps = getSteps(); - - drawCircleBackground(w); - - const color = getCircleColor("steps"); - - let percent; - const stepGoal = settings.stepGoal; - if (stepGoal > 0) { - percent = steps / stepGoal; - if (stepGoal < steps) percent = 1; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, shortValue(steps)); - - g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawStepsDistance(w) { - if (!w) w = getCircleXPosition("stepsDistance"); - const steps = getSteps(); - const stepDistance = settings.stepLength; - const stepsDistance = Math.round(steps * stepDistance); - - drawCircleBackground(w); - - const color = getCircleColor("stepsDistance"); - - let percent; - const stepDistanceGoal = settings.stepDistanceGoal; - if (stepDistanceGoal > 0) { - percent = stepsDistance / stepDistanceGoal; - if (stepDistanceGoal < stepsDistance) percent = 1; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, shortValue(stepsDistance)); - - g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawHeartRate(w) { - if (!w) w = getCircleXPosition("hr"); - - const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); - - drawCircleBackground(w); - - const color = getCircleColor("hr"); - - let percent; - if (hrtValue != undefined) { - const minHR = settings.minHR; - const maxHR = settings.maxHR; - percent = (hrtValue - minHR) / (maxHR - minHR); - if (isNaN(percent)) percent = 0; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - - g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawBattery(w) { - if (!w) w = getCircleXPosition("battery"); - const battery = E.getBattery(); - - const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); - - drawCircleBackground(w); - - let color = getCircleColor("battery"); - - let percent; - if (battery > 0) { - percent = battery / 100; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (Bangle.isCharging()) { - color = colorGreen; - } else { - if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { - color = colorRed; - } - } - writeCircleText(w, battery + '%'); - - g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawWeather(w) { - if (!w) w = getCircleXPosition("weather"); - const weather = getWeather(); - const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; - const code = weather ? weather.code : -1; - - drawCircleBackground(w); - - const color = getCircleColor("weather"); - let percent; - const data = settings.weatherCircleData; - switch (data) { - case "humidity": - const humidity = weather ? weather.hum : undefined; - if (humidity >= 0) { - percent = humidity / 100; - drawGauge(w, h3, percent, color); - } - break; - case "wind": - if (weather) { - const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - if (wind[1] >= 0) { - if (wind[2] == "kmh") { - wind[1] = windAsBeaufort(wind[1]); - } - // wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - percent = wind[1] / 12; - drawGauge(w, h3, percent, color); - } - } - break; - case "empty": - break; - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, tempString ? tempString : "?"); - - if (code > 0) { - const icon = getWeatherIconByCode(code); - if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - } else { - g.drawString("?", w, h3 + radiusOuter); - } -} - - -function drawSunProgress(w) { - if (!w) w = getCircleXPosition("sunprogress"); - const percent = getSunProgress(); - - // sunset icons: - const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); - const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); - - drawCircleBackground(w); - - const color = getCircleColor("sunprogress"); - - drawGauge(w, h3, percent, color); - - drawInnerCircleAndTriangle(w); - - let icon = sunSetDown; - let text = "?"; - const times = getSunData(); - if (times != undefined) { - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); - if (!isDay()) { - // night - if (now > sunRise) { - // after sunRise - const upcomingSunRise = sunRise + 60 * 60 * 24; - text = formatSeconds(upcomingSunRise - now); - } else { - text = formatSeconds(sunRise - now); - } - icon = sunSetUp; - } else { - // day, approx sunrise tomorrow: - text = formatSeconds(sunSet - now); - icon = sunSetDown; - } - } - - writeCircleText(w, text); - - g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawTemperature(w) { - if (!w) w = getCircleXPosition("temperature"); - - getPressureValue("temperature").then((temperature) => { - drawCircleBackground(w); - - const color = getCircleColor("temperature"); - - let percent; - if (temperature) { - const min = -40; - const max = 85; - percent = (temperature - min) / (max - min); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (temperature) - writeCircleText(w, locale.temp(temperature)); - - g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - -function drawPressure(w) { - if (!w) w = getCircleXPosition("pressure"); - - getPressureValue("pressure").then((pressure) => { - drawCircleBackground(w); - - const color = getCircleColor("pressure"); - - let percent; - if (pressure && pressure > 0) { - const minPressure = 950; - const maxPressure = 1050; - percent = (pressure - minPressure) / (maxPressure - minPressure); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (pressure) - writeCircleText(w, Math.round(pressure)); - - g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - -function drawAltitude(w) { - if (!w) w = getCircleXPosition("altitude"); - - getPressureValue("altitude").then((altitude) => { - drawCircleBackground(w); - - const color = getCircleColor("altitude"); - - let percent; - if (altitude) { - const min = 0; - const max = 10000; - percent = (altitude - min) / (max - min); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (altitude) - writeCircleText(w, locale.distance(Math.round(altitude))); - - g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - /* - * wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - */ -function windAsBeaufort(windInKmh) { - const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118]; - let l = 0; - while (l < beaufort.length && beaufort[l] < windInKmh) { - l++; - } - return l; +let drawEmpty = function(img, w, color) { + drawGauge(w, h3, 0, color); + drawInnerCircleAndTriangle(w); + writeCircleText(w, "?"); + if(img) + g.setColor(getGradientColor(color, 0)) + .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); +} +*/ + +let drawCircle = function(index, item, data, clkmenu) { + var w = circlePosX[index-1]; + drawCircleBackground(w); + const color = getCircleColor(item, data, clkmenu); + //drawEmpty(info? info.img : null, w, color); + var img = data.img; + var percent = 1; //fill up if no range + var txt = ""+data.text; + if (txt.endsWith(" bpm")) txt=txt.slice(0,-4); // hack for heart rate - remove the 'bpm' text + if(item.hasRange) percent = (data.v-data.min) / (data.max-data.min); + if(data.short) txt = data.short; + //long text can overflow and we do not draw there anymore.. + if(txt.length>6) txt = txt.slice(0,5)+"\n"+txt.slice(5,10) + drawGauge(w, h3, percent, color); + drawInnerCircleAndTriangle(w); + writeCircleText(w, txt); + if(!img) return; //or get it from the clkinfo? + g.setColor(getCircleIconColor(index, color, percent)) + .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); } @@ -565,19 +207,21 @@ function windAsBeaufort(windInKmh) { * Choose weather icon to display based on weather conditition code * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 */ -function getWeatherIconByCode(code) { - const codeGroup = Math.round(code / 100); +let getWeatherIconByCode = function(code, big) { + let codeGroup = Math.round(code / 100); + if (big == undefined) big = false; // weather icons: - const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); - const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); - const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); - const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); - const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); - const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); - const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); - const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); - const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); + let weatherCloudy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); + let weatherSunny = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAP//AAAP//AA//8AAA//8AD//wAAD//wAP//AAAP//AAAA8AAA8AAAAAB4AAHgAAAAAHgAAeAAAAAAfAAD4AAAAAA8AAPAAAAAAD4AB8AAAAAAH4AfgAAAAAA/4H/AAAAAAH///+AAAAAA+//98AAAAAHw//D4AAAAA+AfgHwAAAAHwA8APgAAAA+ADwAfAAAAHwAPAA+AAAAeAA8AB4AAAAwADwADAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); + let weatherMoon = big ? atob("QECBAAAGAAAADwAAAA+AAAAPAAAAD8AAAA8AAAAP4AAADwAAAAfwDwD/8AAAB/gPAP/wAAAH+A8A//AAAAf8DwD/8AAAB/4AAA8AAAAHvgAADwAAAAeeAAAPAAAAB54AAA8AAAAHjwAAAAAAAA+PDgAAAAAADw8PgAAAAAAfDw/AAAAAAB4PD+AAAAAAPg8D8AAAAAB8HwH4AAAAAfg+APwAAPAH8H4Af/AA///g/gA//AD//8H4AB/+AH//AfAAH/8Af/wD4AAIH4A/gAPAAAAHwB/AB8AAAAPgD/AfgAAAAeAH//+AAAAB4AP//4AAAADwAP//AAAAAPAAH/8AAAAA8AAAAAAAAADwAAAAAAAAAPAHAAAAAAAA8A/gAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); + let weatherPartlyCloudy = big ? atob("QECBAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAcADwAOAAAAB4APAB4AAAAHwA8APgAAAAPgH4B8AAAAAfD/8PgAAAAA+//98AAAAAB////gQAAAAD/gf8DgAAAAH4AfgfAAAAA+AA/B+AAAADwAP8D8AAAAfAB/gH/wAAB4APwAP/wAAHgB+AAf/gAA8AHwAB//AP/wA+AACB+A//AHwAAAB8D/8AeAAAAD4P/wB4AAAAHgAPAfgAAAAeAAeH8AAAAA8AB5/wAAAADwAH3/AAAAAPAAP/AAAAAA8AA/wAAAAADwBB+AAAAAAPAOD4AAAAAB8B4HAAAAAAH4PgMAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); + let weatherRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+ADwAPAAH4PgAPAA8AAHx8AA8ADwAAPngADwAPAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAA8PDw8AD/AADw8PDwAP8AAPDw8PAA94AA8PDw8AHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/8PDw8PD/AH/w8PDw8P4AP/Dw8PDw/AAH8PDw8PDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ADwAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); + let weatherPartlyRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAPAAH4PgAAAA8AAHx8AAAADwAAPngAAAAPAAAeeAAAAA8AAB7wAAAADwAAD/AAAAAPAAAP8AAAAA8AAA/wAAAPDwAAD/AAAA8PAAAP8AAADw8AAA94AAAPDwAAHngAAA8PAAAefAAADw8AAD4+AAAPDwAAfB+AAA8PAAH4D///Dw8P//AH//8PDw//4AP//w8PD//AAH//Dw8P/gAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); + let weatherSnowy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAA8AH4PgAAAADwAHx8AAAAAPAAPngAAAAA8AAeeAAPAA//AB7wAA8AD/8AD/AADwAP/wAP8AAPAA//AA/wAP/wAPAAD/AA//AA8AAP8AD/8ADwAA94AP/wAPAAHngADwAAAAAefAAPAAAAAD4+AA8AAAAAfB+ADwAAAAH4D/8AAPAP//AH/wAA8A//4AP/AADwD//AAH8AAPAP/gAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); + let weatherFoggy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AA///wAA8AAAD///AAHgAAAP//8AAeAAAA///wAD4AAAAAAAAAPAAAAAAAAAB8AAAAAAAAAfgAAAAAAAAH/AAAAA///w/+AAAAD///D98AAAAP//8PD4AAAA///wgHwAAAAAAAAAPgAAAAAAAAAfAAAAAAAAAA+AAAAAAAAAB4AAD///DwADAAAP//8PAAAAAA///w8AAAAAD///DwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); + let weatherStormy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAA/wAB7wAAAAH+AAD/AAAAAf4AAP8AAAAD/AAA/wAAAAP4AAD/AAAAB/gAAP8AAAAH8AAA94AAAA/wAAHngAAAD+AAAefAAAAfwAAD4+AAAB/AAAfB+AAAP4AAH4D///g//w//AH//+H/+D/4AP//wf/wf/AAH//D//D/gAAAAAAD4AAAAAAAAAfAAAAAAAAAB8AAAAAAAAAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAAeAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); + let unknown = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/+AAAAAAAB//+AAAAAAAP//8AAAAAAB/gf4AAAAAAPwAPwAAAAAB+AAfgAAAAAPwAA+AAAAAA+B+B8AAAAAHwf+D4AAAAAfD/8HgAAAAB4P/4eAAAAAPh8Ph8AAAAA+HgfDwAAAAD/8A8PAAAAAP/wDw8AAAAA//APDwAAAAB/4A8PAAAAAAAAHw8AAAAAAAB+HwAAAAAAAfweAAAAAAAH+B4AAAAAAA/wPgAAAAAAH8B8AAAAAAA/APgAAAAAAD4B+AAAAAAAfAfwAAAAAAB4H+AAAAAAAPh/gAAAAAAA8H8AAAAAAADw/AAAAAAAAPDwAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA8PAAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : undefined; switch (codeGroup) { case 2: @@ -606,7 +250,9 @@ function getWeatherIconByCode(code) { case 8: switch (code) { case 800: - return isDay() ? weatherSunny : weatherMoon; + var hr = (new Date()).getHours(); + var isDay = (hr>6) && (hr<=18); // fixme we don't want to include ALL of suncalc just to choose one icon + return isDay ? weatherSunny : weatherMoon; case 801: return weatherPartlyCloudy; case 802: @@ -614,84 +260,24 @@ function getWeatherIconByCode(code) { default: return weatherCloudy; } - default: - return undefined; - } -} - - -function isDay() { - const times = getSunData(); - if (times == undefined) return true; - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); - - return (now > sunRise && now < sunSet); -} - -function formatSeconds(s) { - if (s > 60 * 60) { // hours - return Math.round(s / (60 * 60)) + "h"; - } - if (s > 60) { // minutes - return Math.round(s / 60) + "m"; - } - return "<1m"; -} - -function getSunData() { - if (location != undefined && location.lat != undefined) { - // get today's sunlight times for lat/lon - return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined; - } - return undefined; -} - -/* - * Calculated progress of the sun between sunrise and sunset in percent - * - * Taken from rebble app and modified - */ -function getSunProgress() { - const times = getSunData(); - if (times == undefined) return 0; - const sunRise = Math.round(times.sunrise.getTime() / 1000); - const sunSet = Math.round(times.sunset.getTime() / 1000); - - if (isDay()) { - // during day - const dayLength = sunSet - sunRise; - if (now > sunRise) { - return (now - sunRise) / dayLength; - } else { - return (sunRise - now) / dayLength; - } - } else { - // during night - if (now < sunRise) { - const prevSunSet = sunSet - 60 * 60 * 24; - return 1 - (sunRise - now) / (sunRise - prevSunSet); - } else { - const upcomingSunRise = sunRise + 60 * 60 * 24; - return (upcomingSunRise - now) / (upcomingSunRise - sunSet); - } + default: + return unknown; } } /* * Draws the background and the grey circle */ -function drawCircleBackground(w) { - g.clearRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); +let drawCircleBackground = function(w) { // Draw rectangle background: g.setColor(colorBg); - g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.fillRect(w - radiusBorder, h3 - radiusBorder, w + radiusBorder, g.getHeight()-1); // Draw grey background circle: - g.setColor(colorGrey); + g.setColor('#808080'); // grey g.fillCircle(w, h3, radiusOuter); } -function drawInnerCircleAndTriangle(w) { +let drawInnerCircleAndTriangle = function(w) { // Draw inner circle g.setColor(colorBg); g.fillCircle(w, h3, radiusInner); @@ -699,38 +285,39 @@ function drawInnerCircleAndTriangle(w) { g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); } -function radians(a) { - return a * Math.PI / 180; -} - /* * This draws the actual gauge consisting out of lots of little filled circles */ -function drawGauge(cx, cy, percent, color) { - const offset = 15; - const end = 360 - offset; - const radius = radiusInner + (circleCount == 3 ? 3 : 2); - const size = radiusOuter - radiusInner - 2; +let drawGauge = function(cx, cy, percent, color) { + let offset = 15; + let end = 360 - offset; + let radius = radiusOuter+1; if (percent <= 0) return; // no gauge needed if (percent > 1) percent = 1; - const startRotation = -offset; - const endRotation = startRotation - ((end - offset) * percent); + let startRotation = -offset; + let endRotation = startRotation - ((end - offset) * percent); color = getGradientColor(color, percent); g.setColor(color); - - for (let i = startRotation; i > endRotation - size; i -= size) { - x = cx + radius * Math.sin(radians(i)); - y = cy + radius * Math.cos(radians(i)); - g.fillCircle(x, y, size); - } + // convert to radians + startRotation *= Math.PI / 180; + let amt = Math.PI / 10; + endRotation = (endRotation * Math.PI / 180) - amt; + // all we need to draw is an arc, because we'll fill the center + let poly = [cx,cy]; + for (let r = startRotation; r > endRotation; r -= amt) + poly.push( + cx + radius * Math.sin(r), + cy + radius * Math.cos(r) + ); + g.fillPoly(poly); } -function writeCircleText(w, content) { +let writeCircleText = function(w, content) { if (content == undefined) return; - const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; + let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; g.setFont(font); g.setFontAlign(0, 0); @@ -738,118 +325,57 @@ function writeCircleText(w, content) { g.drawString(content, w, h3); } -function shortValue(v) { - if (isNaN(v)) return '-'; - if (v <= 999) return v; - if (v >= 1000 && v < 10000) { - v = Math.floor(v / 100) * 100; - return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - } - if (v >= 10000) { - v = Math.floor(v / 1000) * 1000; - return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - } -} - -function getSteps() { - if (Bangle.getHealthStatus) { - return Bangle.getHealthStatus("day").steps; - } - if (WIDGETS && WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom.getSteps(); - } - return 0; -} - -function getWeather() { - const jsonWeather = storage.readJSON('weather.json'); +let getWeather=function() { + let jsonWeather = storage.readJSON('weather.json'); return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; } -function enableHRMSensor() { - Bangle.setHRMPower(1, "circleclock"); - if (hrtValue == undefined) { - hrtValue = '...'; - drawHeartRate(); +g.clear(1); // clear the whole screen + +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app (allowing for 'fast load') + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + clockInfoMenu.forEach(c => c.remove()); + delete Graphics.prototype.setFontRobotoRegular50NumericOnly; + delete Graphics.prototype.setFontRobotoRegular21; + if (!showWidgets) require("widget_utils").show(); } -} +}); -let pressureLocked = false; -let pressureCache; - -function getPressureValue(type) { - return new Promise((resolve) => { - if (Bangle.getPressure) { - if (!pressureLocked) { - pressureLocked = true; - if (pressureCache && pressureCache[type]) { - resolve(pressureCache[type]); - } - Bangle.getPressure().then(function(d) { - pressureLocked = false; - if (d) { - pressureCache = d; - if (d[type]) { - resolve(d[type]); - } - } - }).catch(() => {}); - } else { - if (pressureCache && pressureCache[type]) { - resolve(pressureCache[type]); - } - } - } +let clockInfoDraw = (itm, info, options) => { + //print("Draw",itm.name,options); + let clkmenu = clockInfoItems[options.menuA]; + drawCircle(options.circlePosition, itm, info, clkmenu); + if (options.focus) g.reset().drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1) +}; +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = []; +for(var i=0;i= (settings.confidence)) { - hrtValue = hrm.bpm; - if (Bangle.isLCDOn()) { - drawHeartRate(); - } - } - // Let us wait before we overwrite "good" HRM values: - if (Bangle.isLCDOn()) { - if (timerHrm) clearTimeout(timerHrm); - timerHrm = setTimeout(() => { - hrtValue = '...'; - drawHeartRate(); - }, settings.hrmValidity * 1000); - } - } -}); - -Bangle.on('charging', function(charging) { - if (isCircleEnabled("battery")) drawBattery(); -}); - -if (isCircleEnabled("hr")) { - enableHRMSensor(); + }, queueMillis - (Date.now() % queueMillis)); } -Bangle.setUI("clock"); -Bangle.loadWidgets(); - -// schedule a draw for the next minute -setTimeout(function() { - // draw in interval - setInterval(draw, settings.updateInterval * 1000); -}, 60000 - (Date.now() % 60000)); - draw(); +} diff --git a/apps/circlesclock/default.json b/apps/circlesclock/default.json index ea00dc347..c343a8131 100644 --- a/apps/circlesclock/default.json +++ b/apps/circlesclock/default.json @@ -1,26 +1,20 @@ { - "minHR": 40, - "maxHR": 200, - "confidence": 0, - "stepGoal": 10000, - "stepDistanceGoal": 8000, - "stepLength": 0.8, "batteryWarn": 30, "showWidgets": false, "weatherCircleData": "humidity", "circleCount": 3, - "circle1": "hr", - "circle2": "steps", - "circle3": "battery", - "circle4": "weather", - "circle1color": "green-red", - "circle2color": "#0000ff", - "circle3color": "red-green", - "circle4color": "#ffff00", + "Bangle/Battery_color":"red-green", + "Bangle/Steps_color":"#0000ff", + "Bangle/HRM_color":"green-red", + "Bangle/Altitude_color":"#00ff00", + "Weather/humidity_color":"#00ffff", + "Weather/wind_color":"fg", + "Weather/temperature_color":"blue-red", + "Alarms_color":"#00ff00", "circle1colorizeIcon": true, "circle2colorizeIcon": true, "circle3colorizeIcon": true, "circle4colorizeIcon": false, - "hrmValidity": 60, - "updateInterval": 60 + "updateInterval": 60, + "showBigWeather": false } diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index c35d99334..713b237ac 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,14 +1,14 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.12", + "version": "0.26", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "icon": "app.png", "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], "type": "clock", - "tags": "clock", + "tags": "clock,clkinfo", "supports" : ["BANGLEJS2"], - "allow_emulator":true, + "dependencies" : { "clock_info":"module" }, "readme": "README.md", "storage": [ {"name":"circlesclock.app.js","url":"app.js"}, diff --git a/apps/circlesclock/screenshot-light-with-big-weather.png b/apps/circlesclock/screenshot-light-with-big-weather.png new file mode 100644 index 000000000..d1d569247 Binary files /dev/null and b/apps/circlesclock/screenshot-light-with-big-weather.png differ diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index fb23f8d5e..0a92f5a5a 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -1,6 +1,7 @@ (function(back) { const SETTINGS_FILE = "circlesclock.json"; const storage = require('Storage'); + const clock_info = require("clock_info"); let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} @@ -11,15 +12,10 @@ storage.write(SETTINGS_FILE, settings); } - const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"]; - const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"]; - - const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", - "#00ffff", "#fff", "#000", "green-red", "red-green", "fg"]; - const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", - "cyan", "white", "black", "green->red", "red->green", "foreground"]; - - const weatherData = ["empty", "humidity", "wind"]; + const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", + "#00ffff", "#fff", "#000", "green-red", "red-green", "blue-red", "red-blue", "fg"]; + const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", + "cyan", "white", "black", "green->red", "red->green", "blue->red", "red->blue", "foreground"]; function showMainMenu() { let menu ={ @@ -32,33 +28,10 @@ step: 1, onchange: x => save('circleCount', x), }, - /*LANG*/'circle 1': ()=>showCircleMenu(1), - /*LANG*/'circle 2': ()=>showCircleMenu(2), - /*LANG*/'circle 3': ()=>showCircleMenu(3), - /*LANG*/'circle 4': ()=>showCircleMenu(4), - /*LANG*/'heartrate': ()=>showHRMenu(), - /*LANG*/'steps': ()=>showStepMenu(), - /*LANG*/'battery warn': { - value: settings.batteryWarn, - min: 10, - max : 100, - step: 10, - format: x => { - return x + '%'; - }, - onchange: x => save('batteryWarn', x), - }, /*LANG*/'show widgets': { value: !!settings.showWidgets, - format: () => (settings.showWidgets ? 'Yes' : 'No'), onchange: x => save('showWidgets', x), }, - /*LANG*/'weather data': { - value: weatherData.indexOf(settings.weatherCircleData), - min: 0, max: 2, - format: v => weatherData[v], - onchange: x => save('weatherCircleData', weatherData[x]), - }, /*LANG*/'update interval': { value: settings.updateInterval, min: 0, @@ -68,125 +41,55 @@ return x + 's'; }, onchange: x => save('updateInterval', x), + }, + /*LANG*/'show big weather': { + value: !!settings.showBigWeather, + onchange: x => save('showBigWeather', x), + }, + /*LANG*/'colorize icons': ()=>showCircleMenus() + }; + clock_info.load().forEach(e=>{ + if(e.dynamic) { + const colorKey = e.name + "_color"; + menu[e.name+/*LANG*/' color'] = { + value: valuesColors.indexOf(settings[colorKey]) || 0, + min: 0, max: valuesColors.length - 1, + format: v => namesColors[v], + onchange: x => save(colorKey, valuesColors[x]), + }; + } else { + let values = e.items.map(i=>e.name+"/"+i.name); + let names = e.name=="Bangle" ? e.items.map(i=>i.name) : values; + values.forEach((v,i)=>{ + const colorKey = v + "_color"; + menu[names[i]+/*LANG*/' color'] = { + value: valuesColors.indexOf(settings[colorKey]) || 0, + min: 0, max: valuesColors.length - 1, + format: v => namesColors[v], + onchange: x => save(colorKey, valuesColors[x]), + }; + }); } - }; + }) E.showMenu(menu); } - function showHRMenu() { - let menu = { - '': { 'title': /*LANG*/'Heartrate' }, - /*LANG*/'< Back': ()=>showMainMenu(), - /*LANG*/'minimum': { - value: settings.minHR, - min: 0, - max : 250, - step: 5, - format: x => { - return x + " bpm"; - }, - onchange: x => save('minHR', x), - }, - /*LANG*/'maximum': { - value: settings.maxHR, - min: 20, - max : 250, - step: 5, - format: x => { - return x + " bpm"; - }, - onchange: x => save('maxHR', x), - }, - /*LANG*/'min. confidence': { - value: settings.confidence, - min: 0, - max : 100, - step: 10, - format: x => { - return x + "%"; - }, - onchange: x => save('confidence', x), - }, - /*LANG*/'valid period': { - value: settings.hrmValidity, - min: 10, - max : 1800, - step: 10, - format: x => { - return x + "s"; - }, - onchange: x => save('hrmValidity', x), - }, - }; - E.showMenu(menu); - } - - function showStepMenu() { - let menu = { - '': { 'title': /*LANG*/'Steps' }, - /*LANG*/'< Back': ()=>showMainMenu(), - /*LANG*/'goal': { - value: settings.stepGoal, - min: 1000, - max : 50000, - step: 500, - format: x => { - return x; - }, - onchange: x => save('stepGoal', x), - }, - /*LANG*/'distance goal': { - value: settings.stepDistanceGoal, - min: 1000, - max : 50000, - step: 500, - format: x => { - return x; - }, - onchange: x => save('stepDistanceGoal', x), - }, - /*LANG*/'step length': { - value: settings.stepLength, - min: 0.1, - max : 1.5, - step: 0.01, - format: x => { - return x; - }, - onchange: x => save('stepLength', x), - } - }; - E.showMenu(menu); - } - function showCircleMenu(circleId) { - const circleName = "circle" + circleId; - const colorKey = circleName + "color"; - const colorizeIconKey = circleName + "colorizeIcon"; - - const menu = { - '': { 'title': /*LANG*/'Circle ' + circleId }, - /*LANG*/'< Back': ()=>showMainMenu(), - /*LANG*/'data': { - value: valuesCircleTypes.indexOf(settings[circleName]), - min: 0, max: valuesCircleTypes.length - 1, - format: v => namesCircleTypes[v], - onchange: x => save(circleName, valuesCircleTypes[x]), - }, - /*LANG*/'color': { - value: valuesColors.indexOf(settings[colorKey]) || 0, - min: 0, max: valuesColors.length - 1, - format: v => namesColors[v], - onchange: x => save(colorKey, valuesColors[x]), - }, - /*LANG*/'colorize icon': { + function showCircleMenus() { + const menu = { + '': { 'title': /*LANG*/'Colorize icons'}, + /*LANG*/'< Back': ()=>showMainMenu(), + }; + for(var circleId=1; circleId<=4; ++circleId) { + const circleName = "circle" + circleId; + //const colorKey = circleName + "color"; + const colorizeIconKey = circleName + "colorizeIcon"; + menu[/*LANG*/'circle ' + circleId] = { value: settings[colorizeIconKey] || false, - format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'), onchange: x => save(colorizeIconKey, x), - }, - }; + }; + } E.showMenu(menu); } - showMainMenu(); -}); +}) diff --git a/apps/clickms/ChangeLog b/apps/clickms/ChangeLog index 5560f00bc..7727f3cc4 100644 --- a/apps/clickms/ChangeLog +++ b/apps/clickms/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Minor code improvements diff --git a/apps/clickms/click-master.js b/apps/clickms/click-master.js index 55027e733..a5b3d1a7e 100644 --- a/apps/clickms/click-master.js +++ b/apps/clickms/click-master.js @@ -19,7 +19,7 @@ function drawPlayers() { g.clear(1); g.setFont("6x8",2); var l = [{name:ME,cnt:mycounter}]; - for (p of players) l.push(p); + for (const p of players) l.push(p); l.sort((a,b)=>a.cnt-b.cnt); var y=0; l.forEach(player=>{ diff --git a/apps/clickms/metadata.json b/apps/clickms/metadata.json index baa8c9563..c07f83dcd 100644 --- a/apps/clickms/metadata.json +++ b/apps/clickms/metadata.json @@ -1,7 +1,7 @@ { "id": "clickms", "name": "Click Master", - "version": "0.01", + "version": "0.02", "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", "icon": "click-master.png", "tags": "game", diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog index f4d146d5f..7e0b4fd69 100644 --- a/apps/cliclockJS2Enhanced/ChangeLog +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -1,3 +1,4 @@ 0.01: Submitted to App Loader 0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p 0.03: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) +0.04: Minor code improvements diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js index b6172b497..b99ed2542 100644 --- a/apps/cliclockJS2Enhanced/app.js +++ b/apps/cliclockJS2Enhanced/app.js @@ -148,7 +148,7 @@ g.clear(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); -var click = setInterval(updateTime, 1000); +/*var click =*/ setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ drawAll(); // why do we redraw here?? diff --git a/apps/cliclockJS2Enhanced/metadata.json b/apps/cliclockJS2Enhanced/metadata.json index f428650a7..dcbf5da63 100644 --- a/apps/cliclockJS2Enhanced/metadata.json +++ b/apps/cliclockJS2Enhanced/metadata.json @@ -2,7 +2,7 @@ "id": "cliclockJS2Enhanced", "name": "Commandline-Clock JS2 Enhanced", "shortName": "CLI-Clock JS2", - "version": "0.03", + "version": "0.04", "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!", "icon": "app.png", "screenshots": [{"url":"screengrab.png"}], diff --git a/apps/clicompleteclk/ChangeLog b/apps/clicompleteclk/ChangeLog index 50c84593e..436549f8a 100644 --- a/apps/clicompleteclk/ChangeLog +++ b/apps/clicompleteclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New clock! 0.02: Load steps from Health Tracking app (if installed) 0.03: ... +0.04: Minor code improvements diff --git a/apps/clicompleteclk/app.js b/apps/clicompleteclk/app.js index a39b37e58..7472907e1 100644 --- a/apps/clicompleteclk/app.js +++ b/apps/clicompleteclk/app.js @@ -17,8 +17,8 @@ const textColorRed = g.theme.dark ? "#FF0000" : "#FF0000"; let hrtValue; let hrtValueIsOld = false; -let localTempValue; -let weatherTempString; +//let localTempValue; +//let weatherTempString; let lastHeartRateRowIndex; let lastStepsRowIndex; let i = 2; @@ -114,7 +114,7 @@ function drawWeather() { const currentWeather = weatherJson.weather; const weatherTempValue = locale.temp(currentWeather.temp-273.15); - weatherTempString = weatherTempValue; + //weatherTempString = weatherTempValue; writeLineTopic("WTHR", i); writeLine(currentWeather.txt,i); i++; diff --git a/apps/clicompleteclk/metadata.json b/apps/clicompleteclk/metadata.json index 8753c3c37..4d4f7e5ef 100644 --- a/apps/clicompleteclk/metadata.json +++ b/apps/clicompleteclk/metadata.json @@ -2,7 +2,7 @@ "id": "clicompleteclk", "name": "CLI complete clock", "shortName":"CLI cmplt clock", - "version":"0.03", + "version": "0.04", "description": "Command line styled clock with lots of information", "icon": "app.png", "allow_emulator": true, diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js index 2df20ed3e..f062b98b1 100644 --- a/apps/clicompleteclk/settings.js +++ b/apps/clicompleteclk/settings.js @@ -9,7 +9,6 @@ '': { 'title': 'CLI complete clk' }, 'Show battery': { value: "battery" in settings ? settings.battery : false, - format: () => (settings.battery ? 'Yes' : 'No'), onchange: () => { settings.battery = !settings.battery; save('battery', settings.battery); @@ -27,7 +26,6 @@ }, 'Show weather': { value: "weather" in settings ? settings.weather : false, - format: () => (settings.weather ? 'Yes' : 'No'), onchange: () => { settings.weather = !settings.weather; save('weather', settings.weather); @@ -35,7 +33,6 @@ }, 'Show steps': { value: "steps" in settings ? settings.steps : false, - format: () => (settings.steps ? 'Yes' : 'No'), onchange: () => { settings.steps = !settings.steps; save('steps', settings.steps); @@ -43,7 +40,6 @@ }, 'Show heartrate': { value: "heartrate" in settings ? settings.heartrate : false, - format: () => (settings.heartrate ? 'Yes' : 'No'), onchange: () => { settings.heartrate = !settings.heartrate; save('heartrate', settings.heartrate); @@ -51,4 +47,4 @@ }, '< Back': back, }); -}); +}) diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog index 68249b622..83bd2eb39 100644 --- a/apps/cliock/ChangeLog +++ b/apps/cliock/ChangeLog @@ -8,3 +8,4 @@ 0.14: Fix BTN1 (fix #853) Add light/dark theme support 0.15: Load widgets after Bangle.setUI to ensure widgets know if they're on a clock or not (fix #970) +0.16: Minor code improvements diff --git a/apps/cliock/app.js b/apps/cliock/app.js index d9271bf15..c1b3a3106 100644 --- a/apps/cliock/app.js +++ b/apps/cliock/app.js @@ -186,7 +186,7 @@ g.clear(); Bangle.on('lcdPower',function(on) { if (on) drawAll(); }); -var click = setInterval(updateTime, 1000); +/*var click =*/ setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ if (btn<0) changeInfoMode(); diff --git a/apps/cliock/metadata.json b/apps/cliock/metadata.json index 2df48892e..ff50e3869 100644 --- a/apps/cliock/metadata.json +++ b/apps/cliock/metadata.json @@ -2,7 +2,7 @@ "id": "cliock", "name": "Commandline-Clock", "shortName": "CLI-Clock", - "version": "0.15", + "version": "0.16", "description": "Simple CLI-Styled Clock", "icon": "app.png", "screenshots": [{"url":"screenshot_cli.png"}], diff --git a/apps/clkinfocal/ChangeLog b/apps/clkinfocal/ChangeLog new file mode 100644 index 000000000..ccb73b648 --- /dev/null +++ b/apps/clkinfocal/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: added settings options to change date format +0.03: Remove un-needed font requirement, now outputs transparent image +0.04: Fix image after 0.03 regression +0.05: Remove duplicated day in calendar when format setting is 'dd MMM' \ No newline at end of file diff --git a/apps/clkinfocal/app.png b/apps/clkinfocal/app.png new file mode 100644 index 000000000..ed79cd884 Binary files /dev/null and b/apps/clkinfocal/app.png differ diff --git a/apps/clkinfocal/clkinfo.js b/apps/clkinfocal/clkinfo.js new file mode 100644 index 000000000..dc93ddd0e --- /dev/null +++ b/apps/clkinfocal/clkinfo.js @@ -0,0 +1,46 @@ +(function() { + var settings = require("Storage").readJSON("clkinfocal.json",1)||{}; + settings.fmt = settings.fmt||"DDD"; + + var getDateString = function(dt) { + switch(settings.fmt) { + case "dd MMM": + return require("locale").month(dt,1).toUpperCase(); + case "DDD dd": + return require("locale").dow(dt,1).toUpperCase() + ' ' + dt.getDate(); + default: // DDD + return require("locale").dow(dt,1).toUpperCase(); + } + }; + + return { + name: "Bangle", + items: [ + { name : "Date", + get : () => { + let d = new Date(); + let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.transparent = 0; + g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0); + g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17); + return { + text : getDateString(d), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 86400000); + }, 86400000 - (Date.now() % 86400000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + } + ] + }; +}) diff --git a/apps/clkinfocal/metadata.json b/apps/clkinfocal/metadata.json new file mode 100644 index 000000000..71dc811e9 --- /dev/null +++ b/apps/clkinfocal/metadata.json @@ -0,0 +1,15 @@ +{ "id": "clkinfocal", + "name": "Calendar Clockinfo", + "version":"0.05", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday. There is also a settings menu to select the format of the text", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo,calendar", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"}, + {"name":"clkinfocal.settings.js","url":"settings.js"} + ], + "data": [{"name":"clkinfocal.json"}] +} diff --git a/apps/clkinfocal/screenshot.png b/apps/clkinfocal/screenshot.png new file mode 100644 index 000000000..bb054e3a4 Binary files /dev/null and b/apps/clkinfocal/screenshot.png differ diff --git a/apps/clkinfocal/settings.js b/apps/clkinfocal/settings.js new file mode 100644 index 000000000..508de5ddc --- /dev/null +++ b/apps/clkinfocal/settings.js @@ -0,0 +1,37 @@ +(function(back) { + const SETTINGS_FILE = "clkinfocal.json"; + + // initialize with default settings... + let s = {'fmt': 0}; + + // and overwrite them with any saved values + // this way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + const saved = settings || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + function save() { + settings = s; + storage.write(SETTINGS_FILE, settings); + } + + var date_options = ["DDD","DDD dd","dd MMM"]; + + E.showMenu({ + '': { 'title': 'Cal Clkinfo' }, + '< Back': back, + 'Format': { + value: 0 | date_options.indexOf(s.fmt), + min: 0, max: 2, + format: v => date_options[v], + onchange: v => { + s.fmt = date_options[v]; + save(); + }, + } + }); + +}) diff --git a/apps/clkinfoclk/ChangeLog b/apps/clkinfoclk/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/clkinfoclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/clkinfoclk/app.png b/apps/clkinfoclk/app.png new file mode 100644 index 000000000..cf057046b Binary files /dev/null and b/apps/clkinfoclk/app.png differ diff --git a/apps/clkinfoclk/clkinfo.js b/apps/clkinfoclk/clkinfo.js new file mode 100644 index 000000000..f98edb5e7 --- /dev/null +++ b/apps/clkinfoclk/clkinfo.js @@ -0,0 +1,27 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "Clock", + get : () => { + return { + text : require("locale").time(new Date(),1), + img : atob("FhaBAAAAAAPwAD/wA8DwHADgYMGDAwMMDAxgMBmAwGYDAZgOBmAcGYA4YwBjDAAMGABgcAOA8DwA/8AA/AAAAAA=") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 60000); + }, 60000 - (Date.now() % 60000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + } + ] + }; +}) diff --git a/apps/clkinfoclk/metadata.json b/apps/clkinfoclk/metadata.json new file mode 100644 index 000000000..8d676d0e0 --- /dev/null +++ b/apps/clkinfoclk/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfoclk", + "name": "Clockinfo Clock", + "version":"0.01", + "description": "This displays a clock *inside* a ClockInfo. This can be really handy for the [Clock Info Widget](https://banglejs.com/apps/?id=widclkinfo) where you might want the option to show a clock in the top bar of a non-clock app.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfoclk/screenshot.png b/apps/clkinfoclk/screenshot.png new file mode 100644 index 000000000..00f4c0c9a Binary files /dev/null and b/apps/clkinfoclk/screenshot.png differ diff --git a/apps/clkinfofw/ChangeLog b/apps/clkinfofw/ChangeLog new file mode 100644 index 000000000..10810802b --- /dev/null +++ b/apps/clkinfofw/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Update clock_info to avoid a redraw and image allocation diff --git a/apps/clkinfofw/app.png b/apps/clkinfofw/app.png new file mode 100644 index 000000000..c6575b73b Binary files /dev/null and b/apps/clkinfofw/app.png differ diff --git a/apps/clkinfofw/clkinfo.js b/apps/clkinfofw/clkinfo.js new file mode 100644 index 000000000..2b3cb32ba --- /dev/null +++ b/apps/clkinfofw/clkinfo.js @@ -0,0 +1,17 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "FW", + get : () => { + return { + text : process.env.VERSION, + img : atob("GBjC////AADve773VWmmmmlVVW22nnlVVbLL445VVwAAAADVWAAAAAAlrAAAAAA6sAAAAAAOWAAAAAAlrAD//wA6sANVVcAOWANVVcAlrANVVcA6rANVVcA6WANVVcAlsANVVcAOrAD//wA6WAAAAAAlsAAAAAAOrAAAAAA6WAAAAAAlVwAAAADVVbLL445VVW22nnlVVWmmmmlV") + }; + }, + show : function() {}, + hide : function() {} + } + ] + }; +}) diff --git a/apps/clkinfofw/metadata.json b/apps/clkinfofw/metadata.json new file mode 100644 index 000000000..720a5baa5 --- /dev/null +++ b/apps/clkinfofw/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfofw", + "name": "Firmware Clockinfo", + "version":"0.02", + "description": "For clocks that display 'clockinfo', this displays the firmware version string", + "icon": "app.png", + "type": "clkinfo", + "screenshots": [{"url":"screenshot.png"}], + "tags": "clkinfo,firmware", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfofw.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfofw/screenshot.png b/apps/clkinfofw/screenshot.png new file mode 100644 index 000000000..da185bd2e Binary files /dev/null and b/apps/clkinfofw/screenshot.png differ diff --git a/apps/clkinfogps/ChangeLog b/apps/clkinfogps/ChangeLog new file mode 100644 index 000000000..11bb8526f --- /dev/null +++ b/apps/clkinfogps/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version +0.02: turned off debugging diff --git a/apps/clkinfogps/README.md b/apps/clkinfogps/README.md new file mode 100644 index 000000000..18e470436 --- /dev/null +++ b/apps/clkinfogps/README.md @@ -0,0 +1,64 @@ +# GPS Clock Info + +![](app.png) + +A clock info that displays the Ordanance Survey (OS) grid reference + +- At the start of the walk update the GPS with using one of the AGPS apps. This will + significantly reduce the time to the first fix. +- I suggest installing the GPS power widget so that you can be assured + when the GPS is draining power. +- The primary use is for walking where a GPS fix that is 2 minutes old is + perfectly fine for providing an OS map grid reference. +- Saves power by only turning the GPS on for the time it takes to get a fix. +- In a static test from 100% charge the battery fell to 20% over 48 hours. +- It then switches the GPS off for 90 seconds before trying to get the next fix +- Displays the GPS time and number of satelites while waiting for a fix. +- The fix is invalidated after 4 minutes and will display 00:00:00 0 + or the gps time while the gps is powered on and searching for a fix. +- If the display is shows solid 00:00:00 0 then tap the clkinfo to restart it + as it will have timed out after 4 minutes +- It is unlikley that this style of gps clock info will work well with the Recorder + app as that would hold the GPS power permantly on all the time during the + recording. +- The design is intended to be minimal so if you want to complicate matters + please fork the app. + + +## Screenshots + + +![](screenshot0.png) + +- Above: The GPS is powered on and waiting for a fix. +- The GPS widget shows yellow indicating powered on +- The time from the GPS chip is displayed with the satellite count +- The time from the GPS chip is incrementing approximately every second +- Note the time on the GPS is in UTC and not the current timezone + + +![](screenshot1.png) + +- Above: The GPS has a fix +- The OS grid reference has been calculated +- The GPS has been turned off for 90 seconds +- The GPS widget is grey showing the GPS is off +- You will not see the GPS widget turn green as the GPS is turned off after a fix + + +![](screenshot2.png) + +- Above: The GPS has been powered on after 90 seconds and is waiting for a fix + + +![](screenshot3.png) + +- Above: The GPS has not had a fix for 4 minutes and the cycle has stopped +- The time from the GPS is 00:00:00 0, indicating that the GPS not on +- The GPS widget is grey showing the GPS is off +- Tap the clock_info to restart the GPS clock info + + + + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/clkinfogps/app.png b/apps/clkinfogps/app.png new file mode 100644 index 000000000..ab4aa46e5 Binary files /dev/null and b/apps/clkinfogps/app.png differ diff --git a/apps/clkinfogps/clkinfo.js b/apps/clkinfogps/clkinfo.js new file mode 100644 index 000000000..740e05eda --- /dev/null +++ b/apps/clkinfogps/clkinfo.js @@ -0,0 +1,127 @@ +(function () { + var timeout; + var last_fix; + var fixTs; + var geo = require("geotools"); + + var debug = function(o) { + //console.log(o); + }; + + var resetLastFix = function() { + last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + course: 0, + satellites: 0 + }; + }; + + var formatTime = function(now) { + try { + var fd = now.toUTCString().split(" "); + return fd[4]; + } catch (e) { + return "00:00:00"; + } + }; + + var clearTimer = function() { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + debug("timer cleared"); + } + }; + + var queueGPSon = function() { + clearTimer(); + // power on the GPS again in 90 seconds + timeout = setTimeout(function() { + timeout = undefined; + Bangle.setGPSPower(1,"clkinfo"); + }, 90000); + debug("gps on queued"); + }; + + var onGPS = function(fix) { + debug(fix); + last_fix.time = fix.time; + + // we got a fix + if (fix.fix) { + last_fix = fix; + fixTs = Math.round(getTime()); + // cancel the timeout, if not already timed out + clearTimer(); + // power off the GPS + Bangle.setGPSPower(0,"clkinfo"); + queueGPSon(); + } + + // if our last fix was more than 4 minutes ago, reset the fix to show gps time + satelites + if (Math.round(getTime()) - fixTs > 240) { + resetLastFix(); + fixTs = Math.round(getTime()); + // cancel the timeout and power off the gps, tap required to restart + clearTimer(); + Bangle.setGPSPower(0,"clkinfo"); + } + + info.items[0].emit("redraw"); + }; + + var img = function() { + return atob("GBgBAAAAAAAAABgAAb2ABzzgB37gD37wHn54AAAADEwIPn58Pn58Pv58Pn58FmA4AAAAHn54D37wD37gBzzAAb2AABgAAAAAAAAA"); + }; + + var gpsText = function() { + if (last_fix === undefined) + return ''; + + // show gps time and satelite count + if (!last_fix.fix) + return formatTime(last_fix.time) + ' ' + last_fix.satellites; + + return geo.gpsToOSMapRef(last_fix); + }; + + var info = { + name: "GPS", + items: [ + { + name: "gridref", + get: function () { return ({ + img: img(), + text: gpsText() + }); }, + run : function() { + debug("run"); + // if the timer is already runnuing reset it, we can get multiple run calls by tapping + clearTimer(); + Bangle.setGPSPower(1,"clkinfo"); + }, + show: function () { + debug("show"); + resetLastFix(); + fixTs = Math.round(getTime()); + Bangle.on("GPS",onGPS); + this.run(); + }, + hide: function() { + debug("hide"); + clearTimer(); + Bangle.setGPSPower(0,"clkinfo"); + Bangle.removeListener("GPS", onGPS); + resetLastFix(); + } + } + ] + }; + + return info; +}) diff --git a/apps/clkinfogps/geotools.js b/apps/clkinfogps/geotools.js new file mode 100644 index 000000000..2f25c8453 --- /dev/null +++ b/apps/clkinfogps/geotools.js @@ -0,0 +1,128 @@ +/** + * + * A module of Geo functions for use with gps fixes + * + * let geo = require("geotools"); + * let os = geo.gpsToOSGrid(fix); + * let ref = geo.gpsToOSMapRef(fix); + * + */ + +Number.prototype.toRad = function() { return this*Math.PI/180; }; +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* Ordnance Survey Grid Reference functions (c) Chris Veness 2005-2014 */ +/* - www.movable-type.co.uk/scripts/gridref.js */ +/* - www.movable-type.co.uk/scripts/latlon-gridref.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +function OsGridRef(easting, northing) { + this.easting = 0|easting; + this.northing = 0|northing; +} +OsGridRef.latLongToOsGrid = function(point) { + var lat = point.lat.toRad(); + var lon = point.lon.toRad(); + + var a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes + var F0 = 0.9996012717; // NatGrid scale factor on central meridian + var lat0 = (49).toRad(), lon0 = (-2).toRad(); // NatGrid true origin is 49�N,2�W + var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres + var e2 = 1 - (b*b)/(a*a); // eccentricity squared + var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n; + + var cosLat = Math.cos(lat), sinLat = Math.sin(lat); + var nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat); // transverse radius of curvature + var rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5); // meridional radius of curvature + var eta2 = nu/rho-1; + + var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0); + var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0); + var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0)); + var Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0)); + var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc + + var cos3lat = cosLat*cosLat*cosLat; + var cos5lat = cos3lat*cosLat*cosLat; + var tan2lat = Math.tan(lat)*Math.tan(lat); + var tan4lat = tan2lat*tan2lat; + + var I = M + N0; + var II = (nu/2)*sinLat*cosLat; + var III = (nu/24)*sinLat*cos3lat*(5-tan2lat+9*eta2); + var IIIA = (nu/720)*sinLat*cos5lat*(61-58*tan2lat+tan4lat); + var IV = nu*cosLat; + var V = (nu/6)*cos3lat*(nu/rho-tan2lat); + var VI = (nu/120) * cos5lat * (5 - 18*tan2lat + tan4lat + 14*eta2 - 58*tan2lat*eta2); + + var dLon = lon-lon0; + var dLon2 = dLon*dLon, dLon3 = dLon2*dLon, dLon4 = dLon3*dLon, dLon5 = dLon4*dLon, dLon6 = dLon5*dLon; + + var N = I + II*dLon2 + III*dLon4 + IIIA*dLon6; + var E = E0 + IV*dLon + V*dLon3 + VI*dLon5; + + return new OsGridRef(E, N); +}; + +/* + * converts northing, easting to standard OS grid reference. + * + * [digits=10] - precision (10 digits = metres) + * to_map_ref(8, 651409, 313177); => 'TG 5140 1317' + * to_map_ref(0, 651409, 313177); => '651409,313177' + * + */ +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}'`); + + let e = easting; + let n = northing; + + // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7 + if (digits == 0) { + const format = { useGrouping: false, minimumIntegerDigits: 6, maximumFractionDigits: 3 }; + const ePad = e.toLocaleString('en', format); + const nPad = n.toLocaleString('en', format); + return `${ePad},${nPad}`; + } + + // get the 100km-grid indices + const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000); + + // translate those into numeric equivalents of the grid letters + let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5); + let l2 = (19 - n100km) * 5 % 25 + e100km % 5; + + // compensate for skipped 'I' and build grid letter-pairs + if (l1 > 7) l1++; + if (l2 > 7) l2++; + const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0)); + + // strip 100km-grid indices from easting & northing, and reduce precision + e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2)); + n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2)); + + // pad eastings & northings with leading zeros + e = e.toString().padStart(digits/2, '0'); + n = n.toString().padStart(digits/2, '0'); + + return `${letterPair} ${e} ${n}`; +} + +/** + * + * Module exports section, example code below + * + * let geo = require("geotools"); + * let os = geo.gpsToOSGrid(fix); + * let ref = geo.gpsToOSMapRef(fix); + */ + +// get easting and northings +exports.gpsToOSGrid = function(gps_fix) { + return OsGridRef.latLongToOsGrid(gps_fix); +} + +// string with an OS Map grid reference +exports.gpsToOSMapRef = function(gps_fix) { + let os = OsGridRef.latLongToOsGrid(gps_fix); + return to_map_ref(6, os.easting, os.northing); +} diff --git a/apps/clkinfogps/metadata.json b/apps/clkinfogps/metadata.json new file mode 100644 index 000000000..72651495f --- /dev/null +++ b/apps/clkinfogps/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "clkinfogps", + "name": "GPS Grid Ref Clockinfo", + "version":"0.02", + "description": "Clockinfo that displays the GPS OS Grid Reference", + "icon": "app.png", + "screenshots": [{"url":"screenshot1.png"}], + "type": "clkinfo", + "tags": "clkinfo,gps", + "supports" : ["BANGLEJS2"], + "readme":"README.md", + "storage": [ + {"name":"geotools","url":"geotools.js"}, + {"name":"gps.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfogps/screenshot0.png b/apps/clkinfogps/screenshot0.png new file mode 100644 index 000000000..9429c369d Binary files /dev/null and b/apps/clkinfogps/screenshot0.png differ diff --git a/apps/clkinfogps/screenshot1.png b/apps/clkinfogps/screenshot1.png new file mode 100644 index 000000000..9e1d019ba Binary files /dev/null and b/apps/clkinfogps/screenshot1.png differ diff --git a/apps/clkinfogps/screenshot2.png b/apps/clkinfogps/screenshot2.png new file mode 100644 index 000000000..7bb5bee36 Binary files /dev/null and b/apps/clkinfogps/screenshot2.png differ diff --git a/apps/clkinfogps/screenshot3.png b/apps/clkinfogps/screenshot3.png new file mode 100644 index 000000000..6f0bfffc4 Binary files /dev/null and b/apps/clkinfogps/screenshot3.png differ diff --git a/apps/clkinfogpsspeed/ChangeLog b/apps/clkinfogpsspeed/ChangeLog new file mode 100644 index 000000000..78ba28f3b --- /dev/null +++ b/apps/clkinfogpsspeed/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock Info! diff --git a/apps/clkinfogpsspeed/clkinfo.js b/apps/clkinfogpsspeed/clkinfo.js new file mode 100644 index 000000000..4875f2687 --- /dev/null +++ b/apps/clkinfogpsspeed/clkinfo.js @@ -0,0 +1,27 @@ +(function() { + var speed; + function gpsHandler(e) { + speed = e.speed; + ci.items[0].emit('redraw'); + } + var ci = { + name: "GPS", + items: [ + { name : "Speed", + get : function() { return { text : isFinite(speed) ? require("locale").speed(speed) : "--", + v : 0, min : isFinite(speed) ? speed : 0, max : 150, + img : atob("GBiBAAAAAAAAAAAAAAAAAAD/AAHDgAMYwAbDYAwAMAoA0BgDmBgfGB4ceBgYGBgAGBoAWAwAMAwAMAf/4AP/wAAAAAAAAAAAAAAAAA==") }}, + show : function() { + Bangle.setGPSPower(1, "clkinfogpsspeed"); + Bangle.on("GPS", gpsHandler); + }, + hide : function() { + Bangle.removeListener("GPS", gpsHandler); + Bangle.setGPSPower(0, "clkinfogpsspeed"); + } + // run : function() {} optional (called when tapped) + } + ] + }; + return ci; +}) diff --git a/apps/clkinfogpsspeed/icon.png b/apps/clkinfogpsspeed/icon.png new file mode 100644 index 000000000..10186f13f Binary files /dev/null and b/apps/clkinfogpsspeed/icon.png differ diff --git a/apps/clkinfogpsspeed/metadata.json b/apps/clkinfogpsspeed/metadata.json new file mode 100644 index 000000000..7fceeb7b8 --- /dev/null +++ b/apps/clkinfogpsspeed/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfogpsspeed", + "name": "GPS Speed Clockinfo", + "shortName":"GPS Speed", + "version":"0.01", + "description": "A Clockinfo that displays your current speed according to the GPS", + "icon": "icon.png", + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfogpsspeed.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfom/ChangeLog b/apps/clkinfom/ChangeLog new file mode 100644 index 000000000..7f837e50e --- /dev/null +++ b/apps/clkinfom/ChangeLog @@ -0,0 +1 @@ +0.01: First version diff --git a/apps/clkinfom/README.md b/apps/clkinfom/README.md new file mode 100644 index 000000000..baa241fc7 --- /dev/null +++ b/apps/clkinfom/README.md @@ -0,0 +1,11 @@ +# RAM Clock Info + +![](app.png) + +A clock info that displays the % memory used + +## Screenshots + +![](screenshot.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/clkinfom/app.png b/apps/clkinfom/app.png new file mode 100644 index 000000000..aea4bcbcd Binary files /dev/null and b/apps/clkinfom/app.png differ diff --git a/apps/clkinfom/clkinfo.js b/apps/clkinfom/clkinfo.js new file mode 100644 index 000000000..f01649c4c --- /dev/null +++ b/apps/clkinfom/clkinfo.js @@ -0,0 +1,61 @@ +(function () { + var timeout; + + var debug = function(o) { + //console.log(o); + }; + + var clearTimer = function() { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + debug("timer cleared"); + } + }; + + var queueRedraw = function() { + clearTimer(); + timeout = setTimeout(function() { + timeout = undefined; + queueRedraw(); + }, 60000); + info.items[0].emit("redraw"); + debug("queued"); + }; + + var img = function() { + return atob("GBgBAAAAAAAAAAAAB//gD//wH//4HgB4HAA4HAA4HAA4HDw4HDw4HDw4HDw4HAA4HAA4HAA4HgB4H//4D//wB//gAAAAAAAAAAAA"); + }; + + var text = function() { + var val = process.memory(false); + return '' + Math.round(val.usage*100 / val.total) + '%'; + }; + + var info = { + name: "Bangle", + items: [ + { + name: "ram", + get: function () { return ({ + img: img(), + text: text() + }); }, + run : function() { + debug("run"); + queueRedraw(); + }, + show: function () { + debug("show"); + this.run(); + }, + hide: function() { + debug("hide"); + clearTimer(); + } + } + ] + }; + + return info; +}) diff --git a/apps/clkinfom/metadata.json b/apps/clkinfom/metadata.json new file mode 100644 index 000000000..36ab221cd --- /dev/null +++ b/apps/clkinfom/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "clkinfom", + "name": "RAM Clock Info", + "version":"0.01", + "description": "Clockinfo that displays % used memory", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "readme":"README.md", + "storage": [ + {"name":"ram.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfom/screenshot.png b/apps/clkinfom/screenshot.png new file mode 100644 index 000000000..4ae613edd Binary files /dev/null and b/apps/clkinfom/screenshot.png differ diff --git a/apps/clkinfomag/ChangeLog b/apps/clkinfomag/ChangeLog new file mode 100644 index 000000000..4d19c5998 --- /dev/null +++ b/apps/clkinfomag/ChangeLog @@ -0,0 +1,2 @@ +0.01: New Widget! +0.02: Ensure that the generated image is transparent (2v18+) \ No newline at end of file diff --git a/apps/clkinfomag/clkinfo.js b/apps/clkinfomag/clkinfo.js new file mode 100644 index 000000000..f841bb490 --- /dev/null +++ b/apps/clkinfomag/clkinfo.js @@ -0,0 +1,49 @@ +(function() { + var heading, cnt; + function magHandler(m) { + var h = m.heading; + if (isNaN(heading) || isNaN(h)) + heading = h; + else { + // Average + if (Math.abs(heading-h)>180) { + if (h<180 && heading>180) h+=360; + if (h>180 && heading<180) h-=360; + } + heading = heading*0.8 + h*0.2; + if (heading<0) heading+=360; + if (heading>=360) heading-=360; + } + // only draw 1 in 2 to try and save some power! + if (!(1&cnt++)) ci.items[0].emit('redraw'); + } + var ci = { + name: "Bangle", + items: [ + { name : "Compass", + get : function() { + var g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + if (isNaN(heading)) + g.drawLine(8,12,16,12); + else + g.fillPoly(g.transformVertices([0,-10,4,10,-4,10],{x:12,y:12,rotate:-heading/57})); + g.transparent=0; // only works on 2v18+, ignored otherwise (makes image background transparent) + return { text : isNaN(heading)?"--":Math.round(heading), + v : 0|heading, min : 0, max : 360, + img : g.asImage("string") }}, + show : function() { + Bangle.setCompassPower(1,"clkinfomag"); + Bangle.on('mag',magHandler); + cnt=0; + heading = undefined; + }, + hide : function() { + Bangle.removeListener('mag', magHandler); + Bangle.setCompassPower(0,"clkinfomag"); + }, + run : function() { Bangle.resetCompass(); } + } + ] + }; + return ci; +}) diff --git a/apps/clkinfomag/icon.png b/apps/clkinfomag/icon.png new file mode 100644 index 000000000..aaa9c7027 Binary files /dev/null and b/apps/clkinfomag/icon.png differ diff --git a/apps/clkinfomag/metadata.json b/apps/clkinfomag/metadata.json new file mode 100644 index 000000000..464bba778 --- /dev/null +++ b/apps/clkinfomag/metadata.json @@ -0,0 +1,12 @@ +{ "id": "clkinfomag", + "name": "Compass Clockinfo", + "version":"0.02", + "description": "Extra information to add to clock screens. When selected, displays the compass heading and an arrow pointing North", + "icon": "icon.png", + "type": "clkinfo", + "tags": "clkinfo,compass,mag,magnetometer", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfomag.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfomsg/ChangeLog b/apps/clkinfomsg/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/clkinfomsg/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/clkinfomsg/README.md b/apps/clkinfomsg/README.md new file mode 100644 index 000000000..90baa3d70 --- /dev/null +++ b/apps/clkinfomsg/README.md @@ -0,0 +1,18 @@ +# Messages Clockinfo + +A simple messages counter for clockinfo enabled watchfaces + +## Usage + +You can choose between read and unread counter. +Tap to go to messages UI. + +## Todo / Known Issues + +* GB triggers for message read on phone are not handled +* Icons are not consistent +* Maybe use messageicons for handling icon from last notification + +## Attributions + +All icons used in this app are from [icons8](https://icons8.com). \ No newline at end of file diff --git a/apps/clkinfomsg/app.png b/apps/clkinfomsg/app.png new file mode 100644 index 000000000..81a968a40 Binary files /dev/null and b/apps/clkinfomsg/app.png differ diff --git a/apps/clkinfomsg/clkinfo.js b/apps/clkinfomsg/clkinfo.js new file mode 100644 index 000000000..bbb950f3f --- /dev/null +++ b/apps/clkinfomsg/clkinfo.js @@ -0,0 +1,84 @@ +(function() { + + var unreadImg = function() { + return atob("GBiBAAAAAAAAAAAAAB//+D///D///D///D///D///D///D5mfD5mfD///D///D///D///D///D///B//+APgAAOAAAOAAAAAAAAAAA=="); + } + var allImg = function() { + return atob("GBiBAAAAAAAAAAB+AAD/AAPDwA8A8B4AeDgAHDgAHDwAPD8A/D/D/D/n/D///D///D///D///D///D///B//+AAAAAAAAAAAAAAAAA=="); + } + + var debug = function(o) { + //console.log(o); + } + var msgUnread; + var msgAll; + var msgs = require("messages"); + + var getAllMSGs = function() { + if (msgAll === undefined) { + debug("msgAll is undefined"); + msgAll = msgs.getMessages().filter(m => !['call', 'map', 'music'].includes(m.id)).length; + } + return msgAll; + } + + + var getUnreadMGS = function() { + if (msgUnread === undefined) { + debug("msgUnread is undefined"); + msgUnread = msgs.getMessages().filter(m => m.new && !['call', 'map', 'music'].includes(m.id)).length; + } + return msgUnread; + } + + var msgCounter = function(type, msg) { + var msgsNow = msgs.getMessages(msg); + msgUnread = msgsNow.filter(m => m.new && !['call', 'map', 'music'].includes(m.id)).length; + msgAll = msgsNow.filter(m => !['call', 'map', 'music'].includes(m.id)).length; + //TODO find nicer way to redraw current shown CI counter + info.items[0].emit("redraw"); + info.items[1].emit("redraw"); + } + + var info = { + name: "Messages", + img: unreadImg(), + items: [ + { name : "Unread", + get : () => { + return { + text : getUnreadMGS(), + img : unreadImg() + }; + }, + show : function() { + Bangle.on("message", msgCounter); + }, + hide : function() { + Bangle.removeListener("message", msgCounter); + }, + run : () => { + require("messages").openGUI(); + } + }, + { name : "All", + get : () => { + return { + text : getAllMSGs(), + img : allImg() + }; + }, + show : function() { + Bangle.on("message", msgCounter); + }, + hide : function() { + Bangle.removeListener("message", msgCounter); + }, + run : () => { + require("messages").openGUI(); + } + } + ] + }; + return info; +}) diff --git a/apps/clkinfomsg/metadata.json b/apps/clkinfomsg/metadata.json new file mode 100644 index 000000000..e675b69f7 --- /dev/null +++ b/apps/clkinfomsg/metadata.json @@ -0,0 +1,15 @@ +{ "id": "clkinfomsg", + "name": "Messages Clockinfo", + "version":"0.01", + "description": "For clocks that display 'clockinfo', this displays the messages count", + "icon": "app.png", + "type": "clkinfo", + "screenshots": [{"url":"screenshot.png"}], + "readme":"README.md", + "tags": "clkinfo", + "supports" : ["BANGLEJS2"], + "dependencies" : { "messages":"app" }, + "storage": [ + {"name":"clkinfomsg.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfomsg/screenshot.png b/apps/clkinfomsg/screenshot.png new file mode 100644 index 000000000..e46415ad5 Binary files /dev/null and b/apps/clkinfomsg/screenshot.png differ diff --git a/apps/clkinfosec/ChangeLog b/apps/clkinfosec/ChangeLog new file mode 100644 index 000000000..11fefd24b --- /dev/null +++ b/apps/clkinfosec/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Remove 's' after seconds (on some clocks this looks like '5') \ No newline at end of file diff --git a/apps/clkinfosec/app.png b/apps/clkinfosec/app.png new file mode 100644 index 000000000..ed79cd884 Binary files /dev/null and b/apps/clkinfosec/app.png differ diff --git a/apps/clkinfosec/clkinfo.js b/apps/clkinfosec/clkinfo.js new file mode 100644 index 000000000..d407228db --- /dev/null +++ b/apps/clkinfosec/clkinfo.js @@ -0,0 +1,33 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "Seconds", + get : () => { + let d = new Date(), s = d.getSeconds(), sr = s*Math.PI/30, + x = 11+9*Math.sin(sr), y = 11-9*Math.cos(sr), + g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.transparent = 0; + g.drawImage(atob("GBgBAP4AA/+ABwHAHABwGAAwMAAYYAAMYAAMwAAGwAAGwAAGwAAGwAAGwAAGwAAGYAAMYAAMMAAYGAAwHABwBwHAA/+AAP4AAAAA")); + g.drawLine(11,11,x,y).drawLine(12,11,x+1,y).drawLine(11,12,x,y+1).drawLine(12,12,x+1,y+1); + return { + text : s.toString().padStart(2,0), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 1000); + }, 1000 - (Date.now() % 1000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + } + ] + }; +}) \ No newline at end of file diff --git a/apps/clkinfosec/metadata.json b/apps/clkinfosec/metadata.json new file mode 100644 index 000000000..aaf96e9bb --- /dev/null +++ b/apps/clkinfosec/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfosec", + "name": "Seconds Clockinfo", + "version":"0.02", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the time in seconds (many clocks only display minutes)", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clkinfo", + "tags": "clkinfo,seconds,time", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfosec.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfosec/screenshot.png b/apps/clkinfosec/screenshot.png new file mode 100644 index 000000000..bb054e3a4 Binary files /dev/null and b/apps/clkinfosec/screenshot.png differ diff --git a/apps/clkinfostopw/ChangeLog b/apps/clkinfostopw/ChangeLog new file mode 100644 index 000000000..20742562f --- /dev/null +++ b/apps/clkinfostopw/ChangeLog @@ -0,0 +1,3 @@ +0.01: New clkinfo! +0.02: Added format option, reduced battery usage +0.03: Hardcode colon-format, show milliseconds for the first minute diff --git a/apps/clkinfostopw/README.md b/apps/clkinfostopw/README.md new file mode 100644 index 000000000..e92eee3ef --- /dev/null +++ b/apps/clkinfostopw/README.md @@ -0,0 +1,15 @@ +# StopW + +A simple stopwatch widget + +## Usage + +Tap to start, tap again to pause. Tap again to restart + +## Requests + +[Contact Rob](https://www.github.com/bobrippling) for features/bugs + +# TypeScript + +This app is written in TypeScript, see [typescript/README.md](/typescript/README.md) for more info diff --git a/apps/clkinfostopw/app.png b/apps/clkinfostopw/app.png new file mode 100644 index 000000000..fb1d74b5c Binary files /dev/null and b/apps/clkinfostopw/app.png differ diff --git a/apps/clkinfostopw/clkinfo.js b/apps/clkinfostopw/clkinfo.js new file mode 100644 index 000000000..8b7a6a9ad --- /dev/null +++ b/apps/clkinfostopw/clkinfo.js @@ -0,0 +1,77 @@ +(function () { + var durationOnPause = "---"; + var redrawInterval; + var startTime; + var showMillis = true; + var milliTime = 60; + var unqueueRedraw = function () { + if (redrawInterval) + clearInterval(redrawInterval); + redrawInterval = undefined; + }; + var queueRedraw = function () { + var _this = this; + unqueueRedraw(); + redrawInterval = setInterval(function () { + if (startTime) { + if (showMillis && Date.now() - startTime > milliTime * 1000) { + showMillis = false; + changeInterval(redrawInterval, 1000); + } + } + else { + unqueueRedraw(); + } + _this.emit('redraw'); + }, 100); + }; + var pad2 = function (s) { return ('0' + s.toFixed(0)).slice(-2); }; + var duration = function (start) { + var seconds = (Date.now() - start) / 1000; + if (seconds < milliTime) + return seconds.toFixed(1); + var mins = seconds / 60; + seconds %= 60; + if (mins < 60) + return "".concat(mins.toFixed(0), ":").concat(pad2(seconds)); + var hours = mins / 60; + mins %= 60; + return "".concat(hours.toFixed(0), ":").concat(pad2(mins), ":").concat(pad2(seconds)); + }; + var img = function () { return atob("GBiBAAAAAAB+AAB+AAAAAAB+AAH/sAOB8AcA4A4YcAwYMBgYGBgYGBg8GBg8GBgYGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="); }; + return { + name: "timer", + img: img(), + items: [ + { + name: "stopw", + get: function () { return ({ + text: startTime + ? duration(startTime) + : durationOnPause, + img: img(), + }); }, + show: function () { + if (startTime) { + queueRedraw.call(this); + } + else { + this.emit('redraw'); + } + }, + hide: unqueueRedraw, + run: function () { + if (startTime) { + durationOnPause = duration(startTime); + startTime = undefined; + } + else { + queueRedraw.call(this); + showMillis = true; + startTime = Date.now(); + } + } + } + ] + }; +}) diff --git a/apps/clkinfostopw/clkinfo.ts b/apps/clkinfostopw/clkinfo.ts new file mode 100644 index 000000000..78794205e --- /dev/null +++ b/apps/clkinfostopw/clkinfo.ts @@ -0,0 +1,83 @@ +(() => { + let durationOnPause = "---"; + let redrawInterval: IntervalId | undefined; + let startTime: number | undefined; + let showMillis = true; + const milliTime = 60; + + const unqueueRedraw = () => { + if (redrawInterval) clearInterval(redrawInterval); + redrawInterval = undefined; + }; + + const queueRedraw = function(this: ClockInfo.MenuItem) { + unqueueRedraw(); + redrawInterval = setInterval(() => { + if (startTime) { + if (showMillis && Date.now() - startTime > milliTime * 1000) { + showMillis = false; + changeInterval(redrawInterval!, 1000); + } + } else { + unqueueRedraw(); + } + this.emit('redraw') + }, 100); + }; + + const pad2 = (s: number) => ('0' + s.toFixed(0)).slice(-2); + + const duration = (start: number) => { + let seconds = (Date.now() - start) / 1000; + + if (seconds < milliTime) + return seconds.toFixed(1); + + let mins = seconds / 60; + seconds %= 60; + + if (mins < 60) + return `${mins.toFixed(0)}:${pad2(seconds)}`; + + let hours = mins / 60; + mins %= 60; + + return `${hours.toFixed(0)}:${pad2(mins)}:${pad2(seconds)}`; + }; + + const img = () => atob("GBiBAAAAAAB+AAB+AAAAAAB+AAH/sAOB8AcA4A4YcAwYMBgYGBgYGBg8GBg8GBgYGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="); + + return { + name: "timer", + img: img(), + items: [ + { + name: "stopw", + get: () => ({ + text: startTime + ? duration(startTime) + : durationOnPause, + img: img(), + }), + show: function(this: ClockInfo.MenuItem) { + if(startTime){ // only queue if active + queueRedraw.call(this); + }else{ + this.emit('redraw') + } + }, + hide: unqueueRedraw, + run: function() { // tapped + if (startTime) { + durationOnPause = duration(startTime); + startTime = undefined; // this also unqueues the redraw + } else { + queueRedraw.call(this); + showMillis = true; + startTime = Date.now(); + } + } + } + ] + }; +}) satisfies ClockInfoFunc // FIXME: semi-colon added automatically when Typescript generates Javascript file? diff --git a/apps/clkinfostopw/metadata.json b/apps/clkinfostopw/metadata.json new file mode 100644 index 000000000..f33f61dbb --- /dev/null +++ b/apps/clkinfostopw/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "clkinfostopw", + "name": "Stop Watch Clockinfo", + "version":"0.03", + "description": "A simple stopwatch, shown via clockinfo", + "icon": "app.png", + "type": "clkinfo", + "tags": "clkinfo,timer", + "supports" : ["BANGLEJS2"], + "readme":"README.md", + "allow_emulator": true, + "storage": [ + {"name":"stopw.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfosunrise/ChangeLog b/apps/clkinfosunrise/ChangeLog new file mode 100644 index 000000000..a89e38715 --- /dev/null +++ b/apps/clkinfosunrise/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps + Add a 'time' clockinfo that also displays a percentage of day left +0.03: Change 3rd mode to show the time to next sunrise/sunset time (not actual time) +0.04: Minor code improvements +0.05: Minor code improvements diff --git a/apps/clkinfosunrise/app.png b/apps/clkinfosunrise/app.png new file mode 100644 index 000000000..a1d53946d Binary files /dev/null and b/apps/clkinfosunrise/app.png differ diff --git a/apps/clkinfosunrise/clkinfo.js b/apps/clkinfosunrise/clkinfo.js new file mode 100644 index 000000000..69df208f4 --- /dev/null +++ b/apps/clkinfosunrise/clkinfo.js @@ -0,0 +1,77 @@ +(function() { + // get today's sunlight times for lat/lon + var sunrise, sunset, date; + var SunCalc = require("suncalc"); // from modules folder + const locale = require("locale"); + + function calculate() { + var location = require("Storage").readJSON("mylocation.json",1)||{}; + location.lat = location.lat||51.5072; + location.lon = location.lon||0.1276; // London + date = new Date(Date.now()); + var times = SunCalc.getTimes(date, location.lat, location.lon); + sunrise = times.sunrise; + sunset = times.sunset; + /* do we want to re-calculate this every day? Or we just assume + that 'show' will get called once a day? */ + } + + function show() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 60000); + }, 60000 - (Date.now() % 60000)); + } + function hide() { + clearInterval(this.interval); + this.interval = undefined; + } + + return { + name: "Bangle", + items: [ + { name : "Sunrise", + get : () => { calculate(); + return { text : locale.time(sunrise,1), + img : atob("GBiBAAAAAAAAAAAAAAAYAAA8AAB+AAD/AAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }}, + show : show, hide : hide + }, { name : "Sunset", + get : () => { calculate(); + return { text : locale.time(sunset,1), + img : atob("GBiBAAAAAAAAAAAAAAB+AAA8AAAYAAAYAAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }}, + show : show, hide : hide + }, { name : "Sunrise/set", // Time in day (uses v/min/max to show percentage through day) + hasRange : true, + get : () => { + calculate(); + let day = true; + let d = date.getTime(); + let dayLength = sunset.getTime()-sunrise.getTime(); + let timePast; + let timeTotal; + if (d < sunrise.getTime()) { + day = false; // early morning + timePast = sunrise.getTime()-d; + timeTotal = 86400000-dayLength; + } else if (d > sunset.getTime()) { + day = false; // evening + timePast = d-sunset.getTime(); + timeTotal = 86400000-dayLength; + } else { // day! + timePast = d-sunrise.getTime(); + timeTotal = dayLength; + } + let v = Math.round(100 * timePast / timeTotal); + let minutesTo = (timeTotal-timePast)/60000; + return { text : (minutesTo>90) ? (Math.round(minutesTo/60)+"h") : (Math.round(minutesTo)+"m"), + v : v, min : 0, max : 100, + img : day ? atob("GBiBAAAYAAAYAAAYAAgAEBwAOAx+MAD/AAH/gAP/wAf/4Af/4Of/5+f/5wf/4Af/4AP/wAH/gAD/AAx+MBwAOAgAEAAYAAAYAAAYAA==") : atob("GBiBAAfwAA/8AAP/AAH/gAD/wAB/wAB/4AA/8AA/8AA/8AAf8AAf8AAf8AAf8AA/8AA/8AA/4AB/4AB/wAD/wAH/gAf/AA/8AAfwAA==") + } + }, + show : show, hide : hide + } + ] + }; +}) diff --git a/apps/clkinfosunrise/metadata.json b/apps/clkinfosunrise/metadata.json new file mode 100644 index 000000000..723f1f0a5 --- /dev/null +++ b/apps/clkinfosunrise/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfosunrise", + "name": "Sunrise Clockinfo", + "version": "0.05", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays sunrise and sunset based on the location from the 'My Location' app", + "icon": "app.png", + "type": "clkinfo", + "tags": "clkinfo,sunrise", + "dependencies": {"mylocation":"app"}, + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"sunrise.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkshortcuts/ChangeLog b/apps/clkshortcuts/ChangeLog new file mode 100644 index 000000000..759f68777 --- /dev/null +++ b/apps/clkshortcuts/ChangeLog @@ -0,0 +1 @@ +0.01: New app! \ No newline at end of file diff --git a/apps/clkshortcuts/README.md b/apps/clkshortcuts/README.md new file mode 100644 index 000000000..0d4b72318 --- /dev/null +++ b/apps/clkshortcuts/README.md @@ -0,0 +1,10 @@ +# Shortcuts + +An app that allows you to create custom ClockInfos that act as shortcuts to your favourite apps. + +## Create a Shortcut +After installing the app, you can open the app to add and manage existing shortcuts. If no shortcuts exist, the app will display a ``[+] Shortcuts`` button as a ClockInfo on your Clock (see screenshots), which when clicked will quickly launch into the app so you can add new shortcuts. + +When adding a shortcut, first select the app you wish to use, then select the icon you wish to register to the shortcut. If a keyboard is installed, you'll get the option to rename the shortcut before saving. + +Once created, your shortcut will appear on any Clocks with ClockInfo support, and tapping the shortcut will launch the chosen app \ No newline at end of file diff --git a/apps/clkshortcuts/add_shortcuts_screenshot.png b/apps/clkshortcuts/add_shortcuts_screenshot.png new file mode 100644 index 000000000..ea7c6273b Binary files /dev/null and b/apps/clkshortcuts/add_shortcuts_screenshot.png differ diff --git a/apps/clkshortcuts/app-icon.js b/apps/clkshortcuts/app-icon.js new file mode 100644 index 000000000..99f51a2e8 --- /dev/null +++ b/apps/clkshortcuts/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UB/4ACBIM889VAHmqAAwKCrQLH0oLCuBoFhoLCtJ1HtILBtYLH9ILBtALHlILQlRFCwALSnWwgECBY8O1gLJgeoBaojLHZfqAQILIFwMDBY8CFwMO2QLGhwuBlWuBY0K4ED1aDHBYJUBlQLGnRUChQLIKgJHHBYJUBZY8KgZUBBZHCKgILHh2CBZMC0fABZC+CBZBSBC5JUB14MDcY2q1YLIDAILGtY4EAAXpBYNpBY9pBYNauAKFhulBYWqAAwLCqoLHBQQA6A=")) \ No newline at end of file diff --git a/apps/clkshortcuts/app.js b/apps/clkshortcuts/app.js new file mode 100644 index 000000000..197ca70cf --- /dev/null +++ b/apps/clkshortcuts/app.js @@ -0,0 +1,224 @@ +var storage = require("Storage"); +var keyboard = "textinput"; +try { + keyboard = require(keyboard); +} catch (e) { + keyboard = null; +} + +var icons = [ + {name:"agenda", img :"GBiBAAAAAAA8AAB+AA/n8B/n+BgAGBgAGBn/mBn/mBgAGBgAGBn/mBn/+BgD/BgHDhn+Bhn8MxgMIxgMIx/8Ew/+BgAHDgAD/AAA8A=="}, + {name:"alarm", img:"GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAOBwAcA4A4YcAwYMBgYGBgYGBgYGBgYGBgeGBgHGAwBMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="}, + {name:"mail", img:"GBiBAAAAAAAAAAAAAAAAAB//+D///DAADDgAHDwAPDcA7DPDzDDnDDA8DDAYDDAADDAADDAADDAADD///B//+AAAAAAAAAAAAAAAAA=="}, + {name:"android", img: "GBiBAAAAAAEAgAD/AAD/AAHDgAGBgAMkwAMAwAP/wBv/2BsA2BsA2BsA2BsA2BsA2BsA2Bv/2AP/wADnAADnAADnAADnAADnAAAAAA=="}, + {name:"add", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgYGBgYGBgYGBgYGBn/mBn/mBgYGBgYGBgYGBgYGBgAGBgAGB//+A//8AAAAAAAAAAAAA=="}, + {name:"bangle", img:"GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="}, + {name:"bike", img:"GBiBAAAAAAAAAAAAAAAD+AAD/AADjAABjAfhnAfjyAMDwAcGwB4O+H8M/GGZ5sD7c8fzM8fjM8DDA2GBhn8B/h4AeAAAAAAAAAAAAA=="}, + {name:"map", img:"GBiBAAAAAAAAAAAAAADgGAf8+B//+BjHGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjDGBjjGB//+B8/4BgHAAAAAAAAAAAAAA=="}, + {name:"play", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBjAGBjwGBj8GBjeGBjHmBjB2BjB2BjHmBjeGBj8GBjwGBjAGBgAGB//+A//8AAAAAAAAAAAAA=="}, + {name:"fast forward", img:"GBiBAAAAAAAAAAAAAH///v///8AAA8YYA8eeA8f/g8b7w8Y488YYO8YYO8Y488b7w8f/g8eeA8YYA8AAA////3///gAAAAAAAAAAAA=="}, + {name:"rewind", img:"GBiBAAAAAAAAAAAAAH///v///8AAA8AYY8B548H/48PfY88cY9wYY9wYY88cY8PfY8H/48B548AYY8AAA////3///gAAAAAAAAAAAA=="}, + {name:"timer", img:"GBiBAAAAAAB+AAB+AAAAMAB+OAH/nAOByAcA4A4YcAwYMBgYGBgYGBgYGBgYGBgAGBgAGAwAMA4AcAcA4AOBwAH/gAB+AAAAAAAAAA=="}, + {name:"connected", img:"GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBngGBn4GBgcGBgOGBnHGBnzGBgxmBgZmBmZmBmZmBgAGBgAGB//+A//8AAAAAAAAAAAAA=="}, + {name:"lock", img:"GBiBAAAAAAA8AAD/AAHDgAGBgAMAwAMAwAMAwAf/4A//8AwAMAwAMAwAMAwYMAw8MAw8MAwYMAwAMAwAMAwAMA//8Af/4AAAAAAAAA=="}, + {name:"battery", img:"GBiBAAAAAAAAAAB+AAB+AAHngAPnwAMAwAMAwAMIwAMIwAMYwAM4wAM+wAN8wAMcwAMYwAMQwAMQwAMAwAMAwAP/wAH/gAAAAAAAAA=="}, + {name:"game", img:"GBiBAAAAAAAAAAAAAAA8AAB+AABmAABmAAB+AAA8AAAYAAAYAAAYAAMYAA//8B//+BgAGBgAGBgAGBgAGB//+A//8AAAAAAAAAAAAA=="}, + {name:"dice", img:"GBiBAAAAAB//8D//+HAAPGMDHmeHnmeHnmMDHmAAHmMDHmeHnmeHnmMDHmAAHmMDHmeHnmeHnmMDHnAAPn///j///h///g///AAAAA=="}, + {name:"gear", img:"GBiBAAAAAAAAAAA8AAB+AABmAA3nsA/D8B8A+Dg8HBx+OAznMAzDMAzDMAznMBx+ODg8HB8A+A/D8A3nsABmAAB+AAA8AAAAAAAAAA=="}, + {name:"wrench", img:"GBiBAAAAAAAAAAAAAAAHgAAfwAA7gAAzEABjOABj+ABh+ABgGADgMAHAcAOP4AcfgA44AB9wADHgADHAADGAAB8AAA4AAAAAAAAAAA=="}, + {name:"calendar", img:"FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"}, + {name:"power", img:"GBiBAAAAAAAAAAB+AAH/gAeBwA4YcAwYMBjbGBnbmDGZjDMYzDMYzDMAzDMAzDGBjBnDmBj/GAw8MA4AcAeB4AH/gAB+AAAAAAAAAA=="}, + {name:"terminal", img:"GBiBAAAAAAAAAAAAAA//8B//+B//+B//+B//+BgAGBgAGBgAGBmAGBjAGBhgGBhgGBjAGBmPmBgAGBgAGB//+A//8AAAAAAAAAAAAA=="}, + {name:"camera", img:"GBiBAAAAAAAAAAD/AAH/gAMAwD8A/H8A/mA8BmD/BmHDhmGBhmMAxmMAxmMAxmMAxmGBhmHDhmD/BmA8BmAABn///j///AAAAAAAAA=="}, + {name:"phone", img:"GBiBAAAAAAAAAAOAAA/AABzgADBgADBgADBgABjgABjAABzAAAxgAA5wAAc58AMf+AGHHADgDABwDAA8GAAfGAAH8AAA4AAAAAAAAA=="}, + {name:"two prong plug", img:"GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA=="}, + {name:"steps", img:"GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA=="}, + {name:"graph", img:"GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA=="}, + {name:"hills", img:"GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA=="}, + {name:"sun", img:"GBiBAAAYAAAYAAAYAAgAEBwAOAx+MAD/AAHDgAMAwAcA4AYAYOYAZ+YAZwYAYAcA4AMAwAHDgAD/AAx+MBwAOAgAEAAYAAAYAAAYAA=="}, + {name:"home", img:"GBiBAAAAAAAAAAAAAAH/gAP/wAdg4A5wYA44MBwf+DgP/BgAGBgAGBgAGBnnmBnnmBnnmBnnmBngGBngGB//+B//+AAAAAAAAAAAAA=="}, + {name:"bell", img:"GBiBAAAAAAAAAAAfgAB/2ADw+AHAMAOAGAcAGD4ADHgADDgADBwADA4AHAcAGAOAOAHAcAPg4ANxwAM5gAP/AAHvAAAHAAACAAAAAA=="}, + {name:"bin", img:"GBiBAAAAAAAAAAB+AB//+B//+AwAMAwAMAxmMAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYANmwAMAwAMAwAP/wAH/gAAAAAAAAA=="}, +]; + +let storedApps; +var showMainMenu = () => { + storedApps = storage.readJSON("clkshortcuts.json", 1) || {}; + + var mainMenu = { + "": { + title: "Shortcuts", + }, + "< Back": () => { + load(); + }, + "New": () => { + // Select the app + getSelectedApp().then((app) => { + getSelectedIcon().then((icon) => { + promptForRename(app.name).then((name) => { + E.showMessage("Saving..."); + storedApps[app.src] = { + name: name, src: app.src, icon: icon + }; + storage.writeJSON("clkshortcuts.json", storedApps); + showMainMenu(); + }).catch(() => { + E.showMessage("Saving..."); + storedApps[app.src] = { + name: app.name, src: app.src, icon: icon + }; + storage.writeJSON("clkshortcuts.json", storedApps); + showMainMenu(); + } ); + }).catch(() => {showMainMenu();}); + }).catch(() => {showMainMenu();}); + }, + }; + getStoredAppsArray(storedApps).forEach((app) => { + mainMenu[app.name] = { + onchange: () => { + showEditMenu(app).then((dirty) => { + if (dirty) { + E.showMessage("Saving..."); + storage.writeJSON("clkshortcuts.json", storedApps); + } + showMainMenu(); + }); + }, + format: v=>"\0" + atob(app.icon) + }; + }); + E.showMenu(mainMenu); +}; + +var showEditMenu = (app) => { + return new Promise((resolve, reject) => { + var editMenu = { + "": { + title: "Edit " + app.name, + }, + "< Back": () => { + resolve(false); + }, + "Name":{ + onchange: () => { + promptForRename(app.name).then((name) => { + storedApps[app.src].name = name; + resolve(true); + }).catch(); + }, + format: v=>app.name.substring(0, 7) + }, + "Icon": { + onchange: () => { + getSelectedIcon().then((icon) => { + storedApps[app.src].icon = icon; + resolve(true); + }).catch(() => resolve(false)); + }, + format: v=>"\0" + atob(app.icon) + }, + "Delete": { + onchange: () => { + delete storedApps[app.src] + resolve(true); + }, + format: v=>"\0" + atob("GBiBAAAAAAAAAAB+AB//+B//+AwAMAwAMAxmMAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYANmwAMAwAMAwAP/wAH/gAAAAAAAAA==") + } + }; + E.showMenu(editMenu); + }); +}; + +var promptForRename = (name) => { + return new Promise((resolve, reject) => { + if (!keyboard) { reject("No textinput is available"); } + else { + return require("textinput").input({text:name}).then((result) => resolve(result)); + } + }); +}; + +var getStoredAppsArray = (apps) => { + var appList = Object.keys(apps); + var storedAppArray = []; + for (var i = 0; i < appList.length; i++) { + var app = "" + appList[i]; + storedAppArray.push( + apps[app] + ); + } + return storedAppArray; +}; + +var getSelectedIcon = () => { + return new Promise((resolve, reject) => { + var iconMenu = { + "": { + title: "Select Icon", + }, + "< Back": () => { + reject("The user cancelled the operation"); + }, + }; + + icons.forEach((icon) => { + iconMenu["\0" + atob(icon.img) + " " + icon.name] = () => { + resolve(icon.img); + }; + }); + + E.showMenu(iconMenu); + }); +}; + +var getAppList = () => { + var appList = storage + .list(/\.info$/) + .map((appInfoFileName) => { + var appInfo = storage.readJSON(appInfoFileName, 1); + return ( + appInfo && { + name: appInfo.name, + sortorder: appInfo.sortorder, + src: appInfo.src, + } + ); + }) + .filter((app) => app && !!app.src); + appList.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + + return appList; +}; + +var getSelectedApp = () => { + return new Promise((resolve, reject) => { + E.showMessage("Loading apps..."); + var selectAppMenu = { + "": { + title: "Select App", + }, + "< Back": () => { + reject("The user cancelled the operation"); + }, + }; + + var appList = getAppList(); + appList.forEach((app) => { + selectAppMenu[app.name] = () => { + resolve(app); + }; + }); + + E.showMenu(selectAppMenu); + }); +}; + +showMainMenu(); \ No newline at end of file diff --git a/apps/clkshortcuts/app.png b/apps/clkshortcuts/app.png new file mode 100644 index 000000000..66e28fde7 Binary files /dev/null and b/apps/clkshortcuts/app.png differ diff --git a/apps/clkshortcuts/clkinfo.js b/apps/clkshortcuts/clkinfo.js new file mode 100644 index 000000000..842ae2747 --- /dev/null +++ b/apps/clkshortcuts/clkinfo.js @@ -0,0 +1,45 @@ +(function() { + var storage = require("Storage"); + var storedApps = storage.readJSON("clkshortcuts.json", 1) || {}; + var items = []; + if (Object.keys(storedApps).length !== 0) { + for (var key in storedApps) { + var source = { + name: storedApps[key].name, + img: storedApps[key].icon, + src: storedApps[key].src, + get : function() { + return { + text : this.name, + img : atob(this.img) + } + }, + run: function() { load(this.src);}, + show : function() {}, + hide : function() {}, + } + items.push(source); + } + } + else { + var source = { + name: "Shortcuts", + img: "GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgYGBgYGBgYGBgYGBn/mBn/mBgYGBgYGBgYGBgYGBgAGBgAGB//+A//8AAAAAAAAAAAAA==", + src: "clkshortcuts.app.js", + get : function() { + return { + text : this.name, + img : atob(this.img) + } + }, + run: function() { load(this.src);}, + show : function() {}, + hide : function() {}, + }; + items = [source]; + } + return { + name: "Shortcuts", + items: items + }; +}) \ No newline at end of file diff --git a/apps/clkshortcuts/example_shortcuts_screenshot.png b/apps/clkshortcuts/example_shortcuts_screenshot.png new file mode 100644 index 000000000..b60a57f57 Binary files /dev/null and b/apps/clkshortcuts/example_shortcuts_screenshot.png differ diff --git a/apps/clkshortcuts/metadata.json b/apps/clkshortcuts/metadata.json new file mode 100644 index 000000000..3b58bcb48 --- /dev/null +++ b/apps/clkshortcuts/metadata.json @@ -0,0 +1,17 @@ +{ "id": "clkshortcuts", + "name": "Shortcuts", + "shortName": "Shortcuts", + "version": "0.01", + "description": "Add shortcuts to launch your favourite apps straight from the Clock", + "icon": "app.png", + "screenshots": [{"url":"add_shortcuts_screenshot.png"}, {"url":"example_shortcuts_screenshot.png"}], + "tags": "clkinfo,clockinfo", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "clkshortcuts.app.js", "url": "app.js" }, + { "name": "clkshortcuts.img", "url": "app-icon.js", "evaluate": true }, + {"name":"clkshortcuts.clkinfo.js","url":"clkinfo.js"} + ], + "data": [{"name":"clkshortcuts.json"}] +} \ No newline at end of file diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog new file mode 100644 index 000000000..cf7da2fa1 --- /dev/null +++ b/apps/clock_info/ChangeLog @@ -0,0 +1,16 @@ +0.01: Moved from modules/clock_info.js +0.02: Fix settings page +0.03: Reported image for battery now reflects charge level +0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps + eg. when a clockinfo is selected, swipes won't affect swipe-down widgets +0.05: Reported image for battery is now transparent (2v18+) +0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing +0.07: Developer tweak: clkinfo load errors are emitted +0.08: Pass options to show(), hide() and run(), and add focus() and blur() item methods +0.09: Save clkinfo settings on kill and remove +0.10: Fix focus bug when changing focus between two clock infos +0.11: Prepend swipe listener if possible +0.12: Add drawFilledImage to allow drawing icons with a separately coloured middle +0.13: Cache loaded ClockInfos so if we have clockInfoWidget and a clock, we don't load them twice (saves ~300ms) +0.14: Check for .clkinfocache and use that if exists (from boot 0.64) +0.15: Fix error when displaying a category with only one clockinfo (fix #3728) diff --git a/apps/clock_info/README.md b/apps/clock_info/README.md new file mode 100644 index 000000000..220a0cf4d --- /dev/null +++ b/apps/clock_info/README.md @@ -0,0 +1,101 @@ +# Clock Info module + +Module that allows for loading of clock 'info' displays +that can be scrolled through on the clock face. + +## Usage + +In most clocks that use Clock Info, you can interact with it the following way: + +* Tap on an info menu to 'focus' it (this will highlight it in some way) +* Swipe up/down to change which info is displayed within the category +* Tap to activate (if supported), eg for a Stopwatch, Home Assistant, etc +* Swipe left/right to change between categories (Bangle.js/Agenda/etc) +* Tap outside the area of the Clock Info to 'defocus' it + +## Extensions + +By default Clock Info provides: + +* Battery +* Steps +* Heart Rate (HRM) +* Altitude + +But by installing other apps that are tagged with the type `clkinfo` you can +add extra features. For example [Sunrise Clockinfo](http://banglejs.com/apps/?id=clkinfosunrise) + +A full list is available at https://banglejs.com/apps/?c=clkinfo + +## Settings + +Available from `Settings -> Apps -> Clock Info` + +* `Defocus on Lock` - (default=on) when the watch screen auto-locks, defocus +and previously focussed Clock Infos +* `HRM` - (default=always) when does the HRM stay on? + * `Always` - When a HRM ClockInfo is shown, keep the HRM on + * `Tap` - When a HRM ClockInfo is shown, turn HRM on for 1 minute. Turn on again when tapped. +* `Max Altitude` - on clocks like [Circles Clock](https://banglejs.com/apps/?id=circlesclock) a + progress/percent indicator may be shown. The percentage for altitude will be how far towards + the Max Altitude you are. If you go higher than `Max Altitude` the correct altitude will still + be shown - the percent indicator will just read 100% + +## API (Software development) + +See http://www.espruino.com/Bangle.js+Clock+Info for details on using +this module inside your apps (or generating your own Clock Info +extensions). + +`load()` returns an array of menu objects, where each object contains a list of menu items: +* `name` : text to display and identify menu object (e.g. weather) +* `img` : a 24x24px image +* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date). This is only used by a few clocks, for example `circlesclock` +* `items` : menu items such as temperature, humidity, wind etc. + +Note that each item is an object with: + +* `item.name` : friendly name to identify an item (e.g. temperature) +* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage) +* `item.get` : function that returns an object: + +```JS +{ + 'text' // the text to display for this item + 'short' // optional: a shorter text to display for this item (at most 6 characters) + 'img' // optional: a 24x24px image to display for this item + 'color' // optional: a color string (like "#f00") to color the icon in compatible clocks + 'v' // (if hasRange==true) a numerical value + 'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage) +} +``` + +* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get'. Passed the clockinfo options (same as what's returned from `addInteractive`). +* `item.hide` : called when item should be hidden. Disables updates. Passed the clockinfo options. +* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show') +* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. +* `item.focus` : called when the item is focussed (the user has tapped on it). Passed the clockinfo options. +* `item.blur` : called when the item is unfocussed (the user has tapped elsewhere, the screen has locked, etc). Passed the clockinfo options. + +See the bottom of `lib.js` for example usage... + +example.clkinfo.js : + +```JS +(function() { + return { + name: "Bangle", + img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }), + items: [ + { name : "Item1", + get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100, + img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") + }), + show : () => {}, + hide : () => {} + // run : () => {} optional (called when tapped) + } + ] + }; +}) // must not have a semi-colon! +``` diff --git a/apps/clock_info/app-icon.js b/apps/clock_info/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/clock_info/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/clock_info/app.png b/apps/clock_info/app.png new file mode 100644 index 000000000..4013f353f Binary files /dev/null and b/apps/clock_info/app.png differ diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js new file mode 100644 index 000000000..cb6a19abb --- /dev/null +++ b/apps/clock_info/lib.js @@ -0,0 +1,445 @@ +/* See the README for more info... */ + +let storage = require("Storage"); +let stepGoal = undefined; +// Load step goal from health app and pedometer widget +let d = storage.readJSON("health.json", true) || {}; +stepGoal = d.stepGoal; +if (stepGoal == undefined) { + d = storage.readJSON("wpedom.json", true) || {}; + stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; +} + +/// How many times has addInteractive been called? +exports.loadCount = 0; +/// A list of all the instances returned by addInteractive +exports.clockInfos = []; +/// A list of loaded clockInfos +exports.clockInfoMenus = undefined; + +/// Load the settings, with defaults +exports.loadSettings = function() { + return Object.assign({ + hrmOn : 0, // 0(Always), 1(Tap) + defocusOnLock : true, + maxAltitude : 3000, + apps : {} + }, + require("Storage").readJSON("clock_info.json",1)||{} + ); +}; + +/// Load a list of ClockInfos - this does not cache and reloads each time +exports.load = function() { + if (exports.clockInfoMenus) + return exports.clockInfoMenus; + var settings = exports.loadSettings(); + delete settings.apps; // keep just the basic settings in memory + // info used for drawing... + var hrm = 0; + var alt = "--"; + // callbacks (needed for easy removal of listeners) + function batteryUpdateHandler() { bangleItems[0].emit("redraw"); } + function stepUpdateHandler() { bangleItems[1].emit("redraw"); } + function hrmUpdateHandler(e) { + if (e && e.confidence>60) hrm = Math.round(e.bpm); + bangleItems[2].emit("redraw"); + } + function altUpdateHandler() { + try { + Bangle.getPressure().then(data=>{ + if (!data) return; + alt = Math.round(data.altitude) + "m"; + bangleItems[3].emit("redraw"); + }); + } catch (e) { + print("Caught "+e+"\n in function altUpdateHandler in module clock_info"); + bangleItems[3].emit('redraw');} + } + // actual menu + var menu = [{ + name: "Bangle", + img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="), + items: [ + { name : "Battery", + hasRange : true, + get : () => { let v = E.getBattery(); + var img; + if (!Bangle.isCharging()) { + var s=24, g=Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.fillRect(0,6,s-3,18).clearRect(2,8,s-5,16).fillRect(s-2,10,s,15).fillRect(3,9,3+v*(s-9)/100,15); + g.transparent=0; // only works on 2v18+, ignored otherwise (makes image background transparent) + img = g.asImage("string"); + } else img=atob("GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA=="); + return { + text : v + "%", v : v, min:0, max:100, img : img + }; + }, + show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); }, + hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); }, + }, + { name : "Steps", + hasRange : true, + get : () => { let v = Bangle.getHealthStatus("day").steps; return { + text : v, v : v, min : 0, max : stepGoal, + img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==") + };}, + show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); }, + hide : function() { Bangle.removeListener("step", stepUpdateHandler); }, + }, + { name : "HRM", + hasRange : true, + get : () => { return { + text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200, + img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==") + };}, + run : function() { + Bangle.setHRMPower(1,"clkinfo"); + if (settings.hrmOn==1/*Tap*/) { + /* turn off after 1 minute. If Health HRM monitoring is + enabled we will still get HRM events every so often */ + this.timeout = setTimeout(function() { + this.timeout = undefined; + Bangle.setHRMPower(0,"clkinfo"); + }, 60000); + } + }, + show : function() { + Bangle.on("HRM", hrmUpdateHandler); + hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); + this.run(); // start HRM + }, + hide : function() { + Bangle.setHRMPower(0,"clkinfo"); + Bangle.removeListener("HRM", hrmUpdateHandler); + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + hrm = 0; + }, + } + ], + }]; + var bangleItems = menu[0].items; + + if (Bangle.getPressure){ // Altimeter may not exist + bangleItems.push({ name : "Altitude", + hasRange : true, + get : () => ({ + text : alt, v : parseInt(alt), + min : 0, max : settings.maxAltitude, + img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==") + }), + show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); }, + hide : function() { clearInterval(this.interval); delete this.interval; }, + }); + } + var clkInfoCache = require('Storage').read('.clkinfocache'); + if (clkInfoCache!==undefined) { + // note: code below is included in clkinfocache by bootupdate.js + // we use clkinfocache if it exists as it's faster + eval(clkInfoCache); + } else require("Storage").list(/clkinfo\.js$/).forEach(fn => { + // In case there exists already a menu object b with the same name as the next + // object a, we append the items. Otherwise we add the new object a to the list. + try{ + var a = eval(require("Storage").read(fn))(); + var b = menu.find(x => x.name === a.name); + if(b) b.items = b.items.concat(a.items); + else menu = menu.concat(a); + } catch(e){ + console.log("Could not load clock info "+E.toJS(fn)+": "+e); + } + }); + + // return it all! + exports.clockInfoMenus = menu; + return menu; +}; + + +/** Adds an interactive menu that could be used on a clock face by swiping. +Simply supply the menu data (from .load) and a function to draw the clock info. + +options = { + app : "str", // optional: app ID used when saving clock_info positions + // if defined, your app will remember its own positions, + // otherwise all apps share the same ones + x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info + draw : (itm, info, options) // draw function +} + +For example: + +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { + x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info + draw : (itm, info, options) => { + g.reset().clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); + if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); // show if focused + var midx = options.x+options.w/2; + if (info.img) g.drawImage(info.img, midx-12,options.y+4); + g.setFont("6x8:2").setFontAlign(0,1).drawString(info.text, midx,options.y+44); + } +}); +// then when clock 'unloads': +clockInfoMenu.remove(); +delete clockInfoMenu; + +Then if you need to unload the clock info so it no longer +uses memory or responds to swipes, you can call clockInfoMenu.remove() +and delete clockInfoMenu + +clockInfoMenu is the 'options' parameter, with the following added: + +* `index` : int - which instance number are we? Starts at 0 +* `menuA` : int - index in 'menu' of showing clockInfo item +* `menuB` : int - index in 'menu[menuA].items' of showing clockInfo item +* `remove` : function - remove this clockInfo item +* `redraw` : function - force a redraw +* `focus` : function - bool to show if menu is focused or not + +You can have more than one clock_info at once as well, for instance: + +let clockInfoDraw = (itm, info, options) => { + g.reset().setBgColor(options.bg).setColor(options.fg).clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); + if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1) + var midx = options.x+options.w/2; + if (info.img) g.drawImage(info.img, midx-12,options.y); + g.setFont("6x15").setFontAlign(0,1).drawString(info.text, midx,options.y+41); +}; +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg }); +let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { x:0, y:120, w:50, h:40, draw : clockInfoDraw, bg : bgColor, fg : g.theme.bg}); + +*/ +exports.addInteractive = function(menu, options) { + if (!menu.length || !menu[0].items.length) return; // no infos - can't load a clock_info + if ("function" == typeof options) options = {draw:options}; // backwards compatibility + options.index = 0|exports.loadCount; + exports.loadCount = options.index+1; + exports.clockInfos[options.index] = options; + options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined + const appName = (options.app||"default")+":"+options.index; + + // load the currently showing clock_infos + let settings = exports.loadSettings(); + if (settings.apps[appName]) { + let a = settings.apps[appName].a|0; + let b = settings.apps[appName].b|0; + if (menu[a] && menu[a].items[b]) { // all ok + options.menuA = a; + options.menuB = b; + } + } + const save = () => { + // save the currently showing clock_info + const settings = exports.loadSettings(); + settings.apps[appName] = {a:options.menuA, b:options.menuB}; + require("Storage").writeJSON("clock_info.json",settings); + }; + E.on("kill", save); + + if (options.menuA===undefined) options.menuA = 0; + if (options.menuB===undefined) options.menuB = Math.min(exports.loadCount, menu[options.menuA].items.length)-1; + function drawItem(itm) { + options.draw(itm, itm.get(), options); + } + function menuShowItem(itm) { + options.redrawHandler = ()=>drawItem(itm); + itm.on('redraw', options.redrawHandler); + itm.uses = (0|itm.uses)+1; + if (itm.uses==1) itm.show(options); + itm.emit("redraw"); + } + function menuHideItem(itm) { + itm.removeListener('redraw',options.redrawHandler); + delete options.redrawHandler; + itm.uses--; + if (!itm.uses) + itm.hide(options); + } + // handling for swipe between menu items + function swipeHandler(lr,ud){ + if (!options.focus) return; // ignore if we're not focussed + var oldMenuItem; + if (ud) { + if (menu[options.menuA].items.length==1) return; // 1 item - can't move + oldMenuItem = menu[options.menuA].items[options.menuB]; + options.menuB += ud; + if (options.menuB<0) options.menuB = menu[options.menuA].items.length-1; + if (options.menuB>=menu[options.menuA].items.length) options.menuB = 0; + } else if (lr) { + if (menu.length==1) return; // 1 item - can't move + oldMenuItem = menu[options.menuA].items[options.menuB]; + do { + options.menuA += lr; + if (options.menuA<0) options.menuA = menu.length-1; + if (options.menuA>=menu.length) options.menuA = 0; + options.menuB = 0; + //get the next one if the menu is empty + //can happen for dynamic ones (alarms, events) + //in the worst case we come back to 0 + } while(menu[options.menuA].items.length==0); + // When we change, ensure we don't display the same thing as another clockinfo if we can avoid it + while ((options.menuB < menu[options.menuA].items.length-1) && + exports.clockInfos.some(m => (m!=options) && m.menuA==options.menuA && m.menuB==options.menuB)) + options.menuB++; + } + if (oldMenuItem) { + menuHideItem(oldMenuItem); + oldMenuItem.removeAllListeners("draw"); + menuShowItem(menu[options.menuA].items[options.menuB]); + } + // On 2v18+ firmware we can stop other event handlers from being executed since we handled this + E.stopEventPropagation&&E.stopEventPropagation(); + } + if (Bangle.prependListener) {Bangle.prependListener("swipe",swipeHandler);} else {Bangle.on("swipe",swipeHandler);} + const blur = () => { + options.focus=false; + Bangle.CLKINFO_FOCUS--; + const itm = menu[options.menuA].items[options.menuB]; + let redraw = true; + if (itm.blur && itm.blur(options) === false) + redraw = false; + if (redraw) options.redraw(); + }; + const focus = () => { + let redraw = true; + Bangle.CLKINFO_FOCUS = (0 | Bangle.CLKINFO_FOCUS) + 1; + if (!options.focus) { + options.focus=true; + const itm = menu[options.menuA].items[options.menuB]; + if (itm.focus && itm.focus(options) === false) + redraw = false; + } + if (redraw) options.redraw(); + }; + let touchHandler, lockHandler; + if (options.x!==undefined && options.y!==undefined && options.w && options.h) { + touchHandler = function(_,e) { + if (e.x(options.x+options.w) || e.y>(options.y+options.h)) { + if (options.focus) + blur(); + return; // outside area + } + if (!options.focus) { + focus(); + } else if (menu[options.menuA].items[options.menuB].run) { + Bangle.buzz(100, 0.7); + menu[options.menuA].items[options.menuB].run(options); // allow tap on an item to run it (eg home assistant) + } + }; + Bangle.on("touch",touchHandler); + if (settings.defocusOnLock) { + lockHandler = function() { + if(options.focus) + blur(); + }; + Bangle.on("lock", lockHandler); + } + } + // draw the first item + menuShowItem(menu[options.menuA].items[options.menuB]); + // return an object with info that can be used to remove the info + options.remove = function() { + save(); + E.removeListener("kill", save); + Bangle.removeListener("swipe",swipeHandler); + if (touchHandler) Bangle.removeListener("touch",touchHandler); + if (lockHandler) Bangle.removeListener("lock", lockHandler); + Bangle.CLKINFO_FOCUS--; + menuHideItem(menu[options.menuA].items[options.menuB]); + exports.loadCount--; + delete exports.clockInfos[options.index]; + // If nothing loaded now, clear our list of loaded menus + if (exports.loadCount==0) + exports.clockInfoMenus = undefined; + }; + options.redraw = function() { + drawItem(menu[options.menuA].items[options.menuB]); + }; + options.setItem = function (menuA, menuB) { + if (!menu[menuA] || !menu[menuA].items[menuB] || (options.menuA == menuA && options.menuB == menuB)) { + // menuA or menuB did not exist or did not change + return false; + } + + const oldMenuItem = menu[options.menuA].items[options.menuB]; + if (oldMenuItem) { + menuHideItem(oldMenuItem); + oldMenuItem.removeAllListeners("draw"); + } + options.menuA = menuA; + options.menuB = menuB; + menuShowItem(menu[options.menuA].items[options.menuB]); + + return true; + }; + if (options.focus) focus(); + delete settings; // don't keep settings in RAM - save space + return options; +}; + +/* clockinfos usually return a 24x24 image. This draws that image but +recolors it such that it is transparent, with the middle of the image as background +and the image itself as foreground. options is passed to g.drawImage */ +exports.drawFilledImage = function(img,x,y,options) { + if (!img) return; + if (!g.floodFill/*2v18+*/) return g.drawImage(img,x,y,options); + let gfx = exports.imgGfx; + if (!gfx) { + gfx = exports.imgGfx = Graphics.createArrayBuffer(26, 26, 2, {msb:true}); + gfx.transparent = 3; + gfx.palette = new Uint16Array([g.theme.bg, g.theme.fg, g.toColor("#888"), g.toColor("#888")]); + } + /* img is (usually) a black and white transparent image. But we really would like the bits in + the middle of it to be white. So what we do is we draw a slightly bigger rectangle in white, + draw the image, and then flood-fill the rectangle back to the background color. floodFill + was only added in 2v18 so we have to check for it and fallback if not. */ + gfx.clear(1).setColor(1).drawImage(img, 1,1).floodFill(0,0,3); + var scale = (options && options.scale) || 1; + return g.drawImage(gfx, x-scale,y-scale,options); +}; + +/* clockinfos usually return a 24x24 image. This creates a 26x26 gfx of the image but +recolors it such that it is transparent, with the middle and border of the image as background +and the image itself as foreground. options is passed to g.drawImage */ +exports.drawBorderedImage = function(img,x,y,options) { + if (!img) return; + if (!g.floodFill/*2v18+*/) return g.drawImage(img,x,y,options); + let gfx = exports.imgGfxB; + if (!gfx) { + gfx = exports.imgGfxB = Graphics.createArrayBuffer(28, 28, 2, {msb:true}); + gfx.transparent = 3; + gfx.palette = new Uint16Array([g.theme.bg, g.theme.fg, g.theme.bg/*border*/, g.toColor("#888")]); + } + gfx.clear(1).setColor(2).drawImage(img, 1,1).drawImage(img, 3,1).drawImage(img, 1,3).drawImage(img, 3,3); // border + gfx.setColor(1).drawImage(img, 2,2); // main image + gfx.floodFill(27,27,3); // flood fill edge to transparent + var o = ((options && options.scale) || 1)*2; + return g.drawImage(gfx, x-o,y-o,options); +}; + +// Code for testing (plots all elements from first list) +/* +g.clear(); +var menu = exports.load(); // or require("clock_info").load() +var items = menu[0].items; +items.forEach((itm,i) => { + var y = i*24; + console.log("Starting", itm.name); + function draw() { + var info = itm.get(); + g.reset().setFont("6x8:2").setFontAlign(-1,0); + g.clearRect(0,y,g.getWidth(),y+23); + g.drawImage(info.img, 0,y); + g.drawString(info.text, 48,y+12); + } + itm.on('redraw', draw); // ensures we redraw when we need to + itm.show(); + draw(); +}); +*/ diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json new file mode 100644 index 000000000..9e9079c28 --- /dev/null +++ b/apps/clock_info/metadata.json @@ -0,0 +1,18 @@ +{ "id": "clock_info", + "name": "Clock Info Module", + "shortName": "Clock Info", + "version":"0.15", + "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", + "icon": "app.png", + "type": "module", + "tags": "clkinfo,clockinfo", + "supports" : ["BANGLEJS2"], + "provides_modules" : ["clock_info"], + "readme": "README.md", + "storage": [ + {"name":"clock_info","url":"lib.js"}, + {"name":"clock_info.settings.js","url":"settings.js"} + ], "data": [ + {"name":"clock_info.json","url":"lib.js"} + ] +} diff --git a/apps/clock_info/settings.js b/apps/clock_info/settings.js new file mode 100644 index 000000000..a86fae473 --- /dev/null +++ b/apps/clock_info/settings.js @@ -0,0 +1,30 @@ +(function(back) { + let settings = require("clock_info").loadSettings(); + + function save(key, value) { + settings[key] = value; + require('Storage').write("clock_info.json", settings); + } + + let menu ={ + '': { 'title': 'Clock Info' }, + /*LANG*/'< Back': back, + /*LANG*/'Defocus on Lock': { + value: !!settings.defocusOnLock, + onchange: x => save('defocusOnLock', x), + }, + /*LANG*/'HRM': { + value: settings.hrmOn, + min: 0, max: 1, step: 1, + format: v => ["Always","Tap"][v], + onchange: x => save('hrmOn', x), + }, + /*LANG*/'Max Altitude': { + value: settings.maxAltitude, + min: 500, max: 10000, step: 500, + format: v => v+"m", + onchange: x => save('maxAltitude', x), + } + }; + E.showMenu(menu); +}) diff --git a/apps/clockbg/ChangeLog b/apps/clockbg/ChangeLog new file mode 100644 index 000000000..a370befc0 --- /dev/null +++ b/apps/clockbg/ChangeLog @@ -0,0 +1,8 @@ +0.01: New App! +0.02: Moved settings into 'Settings->Apps' +0.03: Add 'Squares' option for random squares background +0.04: More options for different background colors + 'Plasma' generative background + Add a 'view' option in settings menu to view the current background +0.05: Random square+plasma speed improvements (~2x faster) +0.06: 25% speed improvement if Math.randInt exists (2v25 fw) \ No newline at end of file diff --git a/apps/clockbg/README.md b/apps/clockbg/README.md new file mode 100644 index 000000000..6573ae165 --- /dev/null +++ b/apps/clockbg/README.md @@ -0,0 +1,51 @@ +# Clock Backgrounds + +This app provides a library (`clockbg`) that can be used by clocks to +provide different backgrounds for them. + +## Usage + +By default the app provides just a red/green/blue background but it can easily be configured. + +You can either: + +* Go to [the Clock Backgrounds app](https://banglejs.com/apps/?id=clockbg) in the App Loader and use pre-made image backgrounds (or upload your own) +* Go to the `Backgrounds` app on the Bangle itself, and choose between: + * `Solid Color` - one color that never changes + * `Random Color` - a new color every time the clock starts + * `Image` - choose from a previously uploaded image + * `Squares` - a randomly generated pattern of squares in the selected color palette + * `Plasma` - a randomly generated 'plasma' pattern of squares in the selected color palette (random noise with a gaussian filter applied) + + +## Usage in code + +Just use the following to use this library within your code: + +```JS +// once at the start +let background = require("clockbg"); + +// to fill the whole area +background.fillRect(Bangle.appRect); + +// to fill just one part of the screen +background.fillRect(x1, y1, x2, y2); + +// if you ever need to reload to a new background (this could take ~100ms) +background.reload(); +``` + +You should also add `"dependencies" : { "clockbg":"module" },` to your app's metadata to +ensure that the clock background library is automatically loaded. + +## Features to be added + +A few features could be added that would really improve functionality: + +* When 'fast loading', 'random' backgrounds don't update at the moment (calling `.reload` can fix this now, but it slows things down) +* Support for >1 image to be uploaded (requires some image management in `interface.html`), and choose randomly between them +* Support for gradients (random colors) +* More types of auto-generated pattern (as long as they can be generated quickly or in the background) +* Storing 'clear' areas of uploaded images so clocks can easily position themselves +* Some backgrounds could update themselves in the background (eg a mandelbrot could calculate the one it should display next time while the watch is running) \ No newline at end of file diff --git a/apps/clockbg/app.png b/apps/clockbg/app.png new file mode 100644 index 000000000..21e39a4e5 Binary files /dev/null and b/apps/clockbg/app.png differ diff --git a/apps/clockbg/img/README.md b/apps/clockbg/img/README.md new file mode 100644 index 000000000..17846e6b8 --- /dev/null +++ b/apps/clockbg/img/README.md @@ -0,0 +1,19 @@ +Clock Images +============= + +If you want to add your own images ensure they're in the same style and then also list the image file in custom.html in the root directory. + +## Flags + +The flags come from https://icons8.com/icon/set/flags/color and are 480x480px + +If your flag is listed in https://icons8.com/icon/set/flags/color and you can't download it in the right size, please file an issue and we'll download it with our account. + + +## Other backgrounds + +Backgrounds prefixed `ai_` are generated by the AI [Bing Image Creator](https://www.bing.com/images/create) + + + + diff --git a/apps/clockbg/img/ai_eye.jpeg b/apps/clockbg/img/ai_eye.jpeg new file mode 100644 index 000000000..e41d40457 Binary files /dev/null and b/apps/clockbg/img/ai_eye.jpeg differ diff --git a/apps/clockbg/img/ai_flow.jpeg b/apps/clockbg/img/ai_flow.jpeg new file mode 100644 index 000000000..f30f89843 Binary files /dev/null and b/apps/clockbg/img/ai_flow.jpeg differ diff --git a/apps/clockbg/img/ai_hero.jpeg b/apps/clockbg/img/ai_hero.jpeg new file mode 100644 index 000000000..e0bad0895 Binary files /dev/null and b/apps/clockbg/img/ai_hero.jpeg differ diff --git a/apps/clockbg/img/ai_horse.jpeg b/apps/clockbg/img/ai_horse.jpeg new file mode 100644 index 000000000..3b7da61f1 Binary files /dev/null and b/apps/clockbg/img/ai_horse.jpeg differ diff --git a/apps/clockbg/img/ai_robot.jpeg b/apps/clockbg/img/ai_robot.jpeg new file mode 100644 index 000000000..a493a5c6a Binary files /dev/null and b/apps/clockbg/img/ai_robot.jpeg differ diff --git a/apps/clockbg/img/ai_rock.jpeg b/apps/clockbg/img/ai_rock.jpeg new file mode 100644 index 000000000..18cbe63b2 Binary files /dev/null and b/apps/clockbg/img/ai_rock.jpeg differ diff --git a/apps/clockbg/img/icons8-australia-480.png b/apps/clockbg/img/icons8-australia-480.png new file mode 100644 index 000000000..3bb1ac09b Binary files /dev/null and b/apps/clockbg/img/icons8-australia-480.png differ diff --git a/apps/clockbg/img/icons8-austria-480.png b/apps/clockbg/img/icons8-austria-480.png new file mode 100644 index 000000000..5431bbd7a Binary files /dev/null and b/apps/clockbg/img/icons8-austria-480.png differ diff --git a/apps/clockbg/img/icons8-belgium-480.png b/apps/clockbg/img/icons8-belgium-480.png new file mode 100644 index 000000000..f0c6485f9 Binary files /dev/null and b/apps/clockbg/img/icons8-belgium-480.png differ diff --git a/apps/clockbg/img/icons8-brazil-480.png b/apps/clockbg/img/icons8-brazil-480.png new file mode 100644 index 000000000..505af26ca Binary files /dev/null and b/apps/clockbg/img/icons8-brazil-480.png differ diff --git a/apps/clockbg/img/icons8-canada-480.png b/apps/clockbg/img/icons8-canada-480.png new file mode 100644 index 000000000..62181468f Binary files /dev/null and b/apps/clockbg/img/icons8-canada-480.png differ diff --git a/apps/clockbg/img/icons8-china-480.png b/apps/clockbg/img/icons8-china-480.png new file mode 100644 index 000000000..ba56ccc1e Binary files /dev/null and b/apps/clockbg/img/icons8-china-480.png differ diff --git a/apps/clockbg/img/icons8-denmark-480.png b/apps/clockbg/img/icons8-denmark-480.png new file mode 100644 index 000000000..716e15f98 Binary files /dev/null and b/apps/clockbg/img/icons8-denmark-480.png differ diff --git a/apps/clockbg/img/icons8-england-480.png b/apps/clockbg/img/icons8-england-480.png new file mode 100644 index 000000000..2343521f3 Binary files /dev/null and b/apps/clockbg/img/icons8-england-480.png differ diff --git a/apps/clockbg/img/icons8-flag-of-europe-480.png b/apps/clockbg/img/icons8-flag-of-europe-480.png new file mode 100644 index 000000000..616d5fe7a Binary files /dev/null and b/apps/clockbg/img/icons8-flag-of-europe-480.png differ diff --git a/apps/clockbg/img/icons8-france-480.png b/apps/clockbg/img/icons8-france-480.png new file mode 100644 index 000000000..09a24c8b1 Binary files /dev/null and b/apps/clockbg/img/icons8-france-480.png differ diff --git a/apps/clockbg/img/icons8-germany-480.png b/apps/clockbg/img/icons8-germany-480.png new file mode 100644 index 000000000..dd2b317bb Binary files /dev/null and b/apps/clockbg/img/icons8-germany-480.png differ diff --git a/apps/clockbg/img/icons8-great-britain-480.png b/apps/clockbg/img/icons8-great-britain-480.png new file mode 100644 index 000000000..f89add414 Binary files /dev/null and b/apps/clockbg/img/icons8-great-britain-480.png differ diff --git a/apps/clockbg/img/icons8-greece-480.png b/apps/clockbg/img/icons8-greece-480.png new file mode 100644 index 000000000..eb823b4bc Binary files /dev/null and b/apps/clockbg/img/icons8-greece-480.png differ diff --git a/apps/clockbg/img/icons8-hungary-480.png b/apps/clockbg/img/icons8-hungary-480.png new file mode 100644 index 000000000..4097e88d3 Binary files /dev/null and b/apps/clockbg/img/icons8-hungary-480.png differ diff --git a/apps/clockbg/img/icons8-italy-480.png b/apps/clockbg/img/icons8-italy-480.png new file mode 100644 index 000000000..82b1b710e Binary files /dev/null and b/apps/clockbg/img/icons8-italy-480.png differ diff --git a/apps/clockbg/img/icons8-lgbt-flag-480.png b/apps/clockbg/img/icons8-lgbt-flag-480.png new file mode 100644 index 000000000..2e8bbebb1 Binary files /dev/null and b/apps/clockbg/img/icons8-lgbt-flag-480.png differ diff --git a/apps/clockbg/img/icons8-netherlands-480.png b/apps/clockbg/img/icons8-netherlands-480.png new file mode 100644 index 000000000..4ea397e27 Binary files /dev/null and b/apps/clockbg/img/icons8-netherlands-480.png differ diff --git a/apps/clockbg/img/icons8-new-zealand-480.png b/apps/clockbg/img/icons8-new-zealand-480.png new file mode 100644 index 000000000..e21fdc574 Binary files /dev/null and b/apps/clockbg/img/icons8-new-zealand-480.png differ diff --git a/apps/clockbg/img/icons8-norway-480.png b/apps/clockbg/img/icons8-norway-480.png new file mode 100644 index 000000000..a57c0f7fb Binary files /dev/null and b/apps/clockbg/img/icons8-norway-480.png differ diff --git a/apps/clockbg/img/icons8-scotland-480.png b/apps/clockbg/img/icons8-scotland-480.png new file mode 100644 index 000000000..20f08cfbb Binary files /dev/null and b/apps/clockbg/img/icons8-scotland-480.png differ diff --git a/apps/clockbg/img/icons8-spain-480.png b/apps/clockbg/img/icons8-spain-480.png new file mode 100644 index 000000000..17fed3360 Binary files /dev/null and b/apps/clockbg/img/icons8-spain-480.png differ diff --git a/apps/clockbg/img/icons8-sweden-480.png b/apps/clockbg/img/icons8-sweden-480.png new file mode 100644 index 000000000..99299e93a Binary files /dev/null and b/apps/clockbg/img/icons8-sweden-480.png differ diff --git a/apps/clockbg/img/icons8-switzerland-480.png b/apps/clockbg/img/icons8-switzerland-480.png new file mode 100644 index 000000000..cde8c6ff0 Binary files /dev/null and b/apps/clockbg/img/icons8-switzerland-480.png differ diff --git a/apps/clockbg/img/icons8-ukraine-480.png b/apps/clockbg/img/icons8-ukraine-480.png new file mode 100644 index 000000000..718695b38 Binary files /dev/null and b/apps/clockbg/img/icons8-ukraine-480.png differ diff --git a/apps/clockbg/img/icons8-usa-480.png b/apps/clockbg/img/icons8-usa-480.png new file mode 100644 index 000000000..a7867f070 Binary files /dev/null and b/apps/clockbg/img/icons8-usa-480.png differ diff --git a/apps/clockbg/img/icons8-wales-480.png b/apps/clockbg/img/icons8-wales-480.png new file mode 100644 index 000000000..58233db34 Binary files /dev/null and b/apps/clockbg/img/icons8-wales-480.png differ diff --git a/apps/clockbg/interface.html b/apps/clockbg/interface.html new file mode 100644 index 000000000..c92600b1b --- /dev/null +++ b/apps/clockbg/interface.html @@ -0,0 +1,202 @@ + + + + + + +

Upload an image:

+
+

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

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

Click

+ + + + + + + + \ No newline at end of file diff --git a/apps/clockbg/lib.js b/apps/clockbg/lib.js new file mode 100644 index 000000000..256f2f372 --- /dev/null +++ b/apps/clockbg/lib.js @@ -0,0 +1,65 @@ +let settings; + +exports.reload = function() { + //let t = Date.now(); + 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") { // 32ms + settings.style = "image"; + let bpp = (settings.colors.length>4)?4:2; + let bg = Graphics.createArrayBuffer(11,11,bpp,{msb:true}); + let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256); + if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels + else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels + bg.buffer[bg.buffer.length-1]=Math.random()*256; // 11x11 isn't a multiple of 4 bytes - we need to set the last one! + bg.palette = new Uint16Array(1<g.toColor(c))); + settings.img = bg; + settings.imgOpt = {scale:16}; + delete settings.colors; + } else if (settings.style=="plasma") { // ~47ms + settings.style = "image"; + let bg = Graphics.createArrayBuffer(16,16,4,{msb:true}); + let u32 = new Uint32Array(bg.buffer); // faster to do 1/4 of the ops of E.mapInPlace(bg.buffer, bg.buffer, ()=>Math.random()*256); + if (Math.randInt) E.mapInPlace(u32, u32, Math.randInt); // random pixels + else E.mapInPlace(u32, u32, function(r,n){"ram";return r()*n}.bind(null,Math.random,0x100000000)); // random pixels + bg.filter([ // a gaussian filter to smooth out + 1, 4, 7, 4, 1, + 4,16,26,16, 4, + 7,26,41,26, 7, + 4,16,26,16, 4, + 1, 4, 7, 4, 1 + ], { w:5, h:5, div:120, offset:-800 }); + bg.palette = new Uint16Array(16); + bg.palette.set(settings.colors.map(c=>g.toColor(c))); + settings.img = bg; + settings.imgOpt = {scale:11}; + delete settings.colors; + } + //console.log("bg",Date.now()-t); +}; +exports.reload(); + +// 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..ba6fb6712 --- /dev/null +++ b/apps/clockbg/metadata.json @@ -0,0 +1,21 @@ +{ "id": "clockbg", + "name": "Clock Backgrounds", + "shortName":"Backgrounds", + "version": "0.06", + "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"},{"url":"screenshot3.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/screenshot3.png b/apps/clockbg/screenshot3.png new file mode 100644 index 000000000..c1aaae219 Binary files /dev/null and b/apps/clockbg/screenshot3.png differ diff --git a/apps/clockbg/settings.js b/apps/clockbg/settings.js new file mode 100644 index 000000000..22256478e --- /dev/null +++ b/apps/clockbg/settings.js @@ -0,0 +1,171 @@ +(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 (!["randomcolor","squares","plasma"].includes(settings.style)) + 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"], + ["#FF0","#F0F","#0FF"], + ["#00f","#0bf","#0f7","#3f0","#ff0","#f30","#f07","#b0f"], + ["#66f","#6df","#6fb","#8f6","#ff6","#f86","#f6b","#d6f"], + ["#007","#057","#073","#170","#770","#710","#703","#507"] + // 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() { + 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"], + ["#44f","#48f","#4df","#4fe","#4fa","#4f6","#7f4","#bf4","#ff4","#fb4","#f74","#f46","#f4a","#f4e","#d4f","#84f"], + ["#009","#039","#079","#098","#094","#091","#290","#590","#990","#950","#920","#901","#904","#908","#709","#309"], + ["#0FF","#0CC","#088","#044"], + ["#FFF","#FBB","#F66","#F44"], + ["#FFF","#BBB","#666","#000"], + ["#fff","#bbf","#77f","#33f"], + ["#fff","#bff","#7fe","#3fd"] + // Please add some more! 4 or 16 only! + ]; + var menu = {"":{title:/*LANG*/"Squares", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "squares"; + settings.colors = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + }, + /*LANG*/"Plasma" : function() { + var cols = [ // list of color palettes used as possible square colours - 16 entries + ["#00f","#05f","#0bf","#0fd","#0f7","#0f1","#3f0","#9f0","#ff0","#f90","#f30","#f01","#f07","#f0d","#b0f","#50f"], + ["#44f","#48f","#4df","#4fe","#4fa","#4f6","#7f4","#bf4","#ff4","#fb4","#f74","#f46","#f4a","#f4e","#d4f","#84f"], + ["#009","#039","#079","#098","#094","#091","#290","#590","#990","#950","#920","#901","#904","#908","#709","#309"], + ["#fff","#fef","#fdf","#fcf","#fbf","#fae","#f9e","#f8e","#f7e","#f6e","#f5d","#f4d","#f3d","#f2d","#f1d","#f0c"], + ["#fff","#eff","#dff","#cef","#bef","#adf","#9df","#8df","#7cf","#6cf","#5bf","#4bf","#3bf","#2af","#1af","#09f"], + ["#000","#010","#020","#130","#140","#250","#260","#270","#380","#390","#4a0","#4b0","#5c0","#5d0","#5e0","#6f0"] + // Please add some more! + ]; + var menu = {"":{title:/*LANG*/"Plasma", back:showModeMenu}}; + cols.forEach(col => { + menu[getColorsImage(col)] = () => { + settings.style = "plasma"; + settings.colors = col; + saveSettings(); + showMainMenu(); + }; + }); + E.showMenu(menu); + } + }); + } + + function showMainMenu() { + E.showMenu({ + "" : {title:/*LANG*/"Clock Background", back:back}, + /*LANG*/"Mode" : { + value : settings.style, + onchange : showModeMenu + }, + /*LANG*/"View" : () => { + Bangle.setUI({mode:"custom",touch:showMainMenu,btn:showMainMenu}); + require("clockbg").reload(); + require("clockbg").fillRect(Bangle.appRect); + } + }); + } + + /* Scripts for generating colors. Change the values in HSBtoRGB to generate different effects + + + a = new Array(16); + a.fill(0); + g.clear(); + w = Math.floor(g.getWidth()/a.length); + print(a.map((n,i)=>{ + var j = i/(a.length-1); // 0..1 + var c = E.HSBtoRGB(j,1,1,24); // rainbow + var c = E.HSBtoRGB(j,0.6,1,24); // faded rainbow + var c = E.HSBtoRGB(0.8, j,1,24); // purple->white + var c = E.HSBtoRGB(0.1, j,1,24); // blue->white + var c = E.HSBtoRGB(0.4, 1,j,24); // black->green + var col = c.toString(16).padStart(6,0).replace(/(.).(.).(.)./,"\"#$1$2$3\""); + g.setColor(eval(col)).fillRect(i*w,0, i*w+w-1,31); + return col; + }).join(",")) + + */ + + showMainMenu(); + }) \ No newline at end of file diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 8b40a87ac..f2365fab7 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -1,3 +1,11 @@ 0.01: Initial upload 0.02: Added scrollable calendar and swipe gestures 0.03: Configurable drag gestures +0.04: Use default Bangle formatter for booleans +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 +0.09: You can now back out of the calendar using the button +0.10: Fix linter warnings +0.11: Added option to show prior weeks on clock calendar 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 5e8c7f796..06436420a 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -1,7 +1,9 @@ +Bangle.setUI("clock"); Bangle.loadWidgets(); var s = Object.assign({ - CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + CAL_ROWS: 4, //total number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + CAL_ROWS_PRIOR: 0, //number of calendar rows.(weeks) that show above the current week 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 FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su @@ -23,15 +25,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; @@ -44,10 +56,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(); @@ -59,8 +71,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; @@ -89,7 +101,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; } @@ -155,7 +167,7 @@ function drawSeconds() { } function drawWatch() { - if (DEBUG) console.log("CALENDAR"); + if (DEBUG) console.log("DRAWWATCH"); monthOffset = 0; state = "watch"; var d = new Date(); @@ -167,7 +179,7 @@ function drawWatch() { const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6 const today = d.getDate(); var rD = new Date(d.getTime()); - rD.setDate(rD.getDate() - dow); + rD.setDate(rD.getDate() - dow - s.CAL_ROWS_PRIOR * 7); var rDate = rD.getDate(); g.setFontAlign(1, 1); for (var y = 1; y <= s.CAL_ROWS; y++) { @@ -176,7 +188,7 @@ function drawWatch() { bottomrightY = y * CELL_H + CAL_Y; g.setFont("Vector", 16); var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff'; - if (y == 1 && today == rDate) { + if (y == s.CAL_ROWS_PRIOR + 1 && today == rDate) { g.setColor('#0f0'); g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); g.setColor('#000'); @@ -196,6 +208,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() { @@ -210,8 +223,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(); @@ -228,6 +245,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) { @@ -275,7 +298,6 @@ function input(dir) { drawWatch(); } break; - } } @@ -307,4 +329,11 @@ NRF.on('disconnect', BTevent); dimSeconds = Bangle.isLocked(); drawWatch(); -Bangle.setUI("clock"); + +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 3998215d7..189d68597 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.03", + "version": "0.11", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index abedad99b..c3abe6f1c 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -1,7 +1,8 @@ (function (back) { var FILE = "clockcal.json"; - defaults={ - CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + const defaults={ + CAL_ROWS: 4, //total number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. + CAL_ROWS_PRIOR: 0, //number of calendar rows.(weeks) that show above the current week 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 FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su @@ -9,24 +10,23 @@ 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.?': { value: settings.BUZZ_ON_BT, - format: v => v ? "On" : "Off", onchange: v => { settings.BUZZ_ON_BT = v; writeSettings(); @@ -40,6 +40,14 @@ writeSettings(); } }, + '#Cal Rows Prior': { + value: settings.CAL_ROWS_PRIOR, + min: 0, max: 4, + onchange: v => { + settings.CAL_ROWS_PRIOR = v; + writeSettings(); + } + }, 'Clock mode': { value: settings.MODE24, format: v => v ? "24h" : "12h", @@ -59,7 +67,6 @@ }, 'Red Saturday?': { value: settings.REDSAT, - format: v => v ? "On" : "Off", onchange: v => { settings.REDSAT = v; writeSettings(); @@ -67,7 +74,6 @@ }, 'Red Sunday?': { value: settings.REDSUN, - format: v => v ? "On" : "Off", onchange: v => { settings.REDSUN = v; writeSettings(); @@ -96,7 +102,7 @@ value: actions.indexOf(settings.DRAGDOWN), format: v => actions[v], onchange: v => { - settings.DRGDOWN = actions[v]; + settings.DRAGDOWN = actions[v]; writeSettings(); } }, @@ -109,19 +115,12 @@ writeSettings(); } }, - 'Load deafauls?': { - value: 0, - min: 0, max: 1, - format: v => ["No", "Yes"][v], - onchange: v => { - if (v == 1) { - settings = defaults; - writeSettings(); - load(); - } - } - }, + 'Load defaults': () => { + settings = defaults; + writeSettings(); + load(); + } }; // Show the menu E.showMenu(menu); -}); +}) diff --git a/apps/clockswitch/README.md b/apps/clockswitch/README.md new file mode 100644 index 000000000..74fa82b2f --- /dev/null +++ b/apps/clockswitch/README.md @@ -0,0 +1,14 @@ +# Clock Switcher + +This switches the default clock. +The idea is that you can use this app in combination with e.g. the +[Pattern Launcher](?q=ptlaunch) as a quick toggle, instead of navigating through +the settings menu. + +## Usage + +Load the app to switch to your next installed clock. + +## Creator + +Richard de Boer (rigrig) diff --git a/apps/clockswitch/app.js b/apps/clockswitch/app.js new file mode 100644 index 000000000..db738eb56 --- /dev/null +++ b/apps/clockswitch/app.js @@ -0,0 +1,23 @@ +const storage = require('Storage'); +const clocks = storage.list(/\.info$/) + .map(app => { + const a=storage.readJSON(app, 1); + return (a && a.type == "clock") ? a : undefined; + }) + .filter(app => app) // filter out any undefined apps + .sort((a, b) => a.sortorder - b.sortorder) + .map(app => app.src); +if (clocks.length<1) { + E.showAlert(/*LANG*/"No clocks found!", "Clock Switcher") + .then(load); +} else if (clocks.length<2) { + E.showAlert(/*LANG*/"Nothing to do:\nOnly one clock installed!", "Clock Switcher") + .then(load); +} else { + let settings = storage.readJSON('setting.json',true)||{clock:null}; + const old = clocks.indexOf(settings.clock), + next = (old+1)%clocks.length; + settings.clock = clocks[next]; + storage.writeJSON('setting.json', settings); + setTimeout(load, 100); // storage.writeJSON needs some time to complete +} diff --git a/apps/clockswitch/icon.js b/apps/clockswitch/icon.js new file mode 100644 index 000000000..8a85e4da5 --- /dev/null +++ b/apps/clockswitch/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AClhCyoAYsIwuF4IwtF4Qxqw2GF4mG1YsqAAeF1eyAAIteFhAvHGLeGwouLR4IuEGDJcGwooBAweH6/X6wwGGKtbKownB640C1gGCAAQwZLgotDF4WG6wuFMZAuVw2yEgqLCABIuD1eGF6eGExYwLw4bCF1BuCDgWFdaGFRgwAJlb0HJogvPdQoAKq0AlYJG1YwDRr+sgEAL4wABwxgNF4ZeSqwLIMAYvNwpebAAOFSBgMCw7sQLxSQORwZLKLw4OLSBlbBgWyLznX2RfPLqBeM6/WcQYvZldbrYvN64jDF7rRNF7qPDGBqPLd6YxDGBTvQPpowQ1YvLGAeHF54wDlYMIwwvPwovQGAIuJ6+FdxSQF1YwRABKONF4mGF7aONAANbMDpeDRxRgFsOyFy+yP4gvLMAiRX6yNDwouMGDYuELxyRGwySS2QuUMAr0SdQguSGA+G1gtMLgguUGAQxFwuH1aWE2QsBwoQEFyzEHAB+FFzAwCMQoALFrRiRwwtefI5mCQwIslAH4A/AFw")) diff --git a/apps/clockswitch/icon.png b/apps/clockswitch/icon.png new file mode 100644 index 000000000..ac80cd84d Binary files /dev/null and b/apps/clockswitch/icon.png differ diff --git a/apps/clockswitch/metadata.json b/apps/clockswitch/metadata.json new file mode 100644 index 000000000..f13c4829e --- /dev/null +++ b/apps/clockswitch/metadata.json @@ -0,0 +1,14 @@ +{ "id": "clockswitch", + "name": "Clock Switcher", + "shortName":"Switch Clock", + "version":"0.01", + "description": "Switch to the next installed clock", + "icon": "icon.png", + "tags": "tool", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"clockswitch.app.js","url":"app.js"}, + {"name":"clockswitch.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/cogclock/15x32.png b/apps/cogclock/15x32.png new file mode 100644 index 000000000..0af326e71 Binary files /dev/null and b/apps/cogclock/15x32.png differ diff --git a/apps/cogclock/ChangeLog b/apps/cogclock/ChangeLog new file mode 100644 index 000000000..23885e519 --- /dev/null +++ b/apps/cogclock/ChangeLog @@ -0,0 +1,5 @@ +0.01: New clock +0.02: Use ClockFace library, add settings +0.03: Use ClockFace_menu.addSettingsFile +0.04: Hide widgets instead of not loading them at all +0.05: Support Bangle.js 2 diff --git a/apps/cogclock/app.js b/apps/cogclock/app.js new file mode 100644 index 000000000..30df613ae --- /dev/null +++ b/apps/cogclock/app.js @@ -0,0 +1,111 @@ +Graphics.prototype.setFont15x32N = function() { + this.setFontCustom(atob( + // 15x32.png, converted using http://ebfc.mattbrailsford.com/ + "/////oAAAAKAAAACgAAAAoAAAAKAAAACgf//AoEAAQKB//8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+/wAB/oEAAQKBAAECgf//AoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAC////AgAAAQIAAAH+/w///oEIAAKBCAACgQgAAoEIAAKBCAACgQg/AoEIIQKB+CECgAAhAoAAIQKAACECgAAhAoAAIQL//+H+/w/h/oEIIQKBCCECgQghAoEIIQKBCCECgQghAoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+///gAIAAIACAACAAgAAgAIAAIAD/+CAAAAggAAAIIAAACD/+//gAAoAAAAKAAAACgAAAAoAAAAL////+///h/oAAIQKAACECgAAhAoAAIQKAACECgfghAoEIIQKBCD8CgQgAAoEIAAKBCAACgQgAAoEIAAL/D//+/////oAAAAKAAAACgAAAAoAAAAKAAAACgfg/AoEIIQKBCD8CgQgAAoEIAAKBCAACgQgAAoEIAAL/D//+/wAAAIEAAACBAAAAgQAAAIEAAACBAAAAgQAAAIH///6AAAACgAAAAoAAAAKAAAACgAAAAoAAAAL////+/////oAAAAKAAAACgAAAAoAAAAKAAAACgfg/AoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+///h/oAAIQKAACECgAAhAoAAIQKAACECgfghAoEIIQKB+D8CgAAAAoAAAAKAAAACgAAAAoAAAAL////+" + ), "0".charCodeAt(0), 15, 32); +}; + +/** + * Add coordinates for nth tooth to vertices + * @param {array} poly Array to add points to + * @param {number} n Tooth number + */ +function addTooth(poly, n) { + const + tau = Math.PI*2, arc = tau/clock.teeth, + e = arc*clock.edge, p = arc*clock.point, s = (arc-(e+p))/2; // edge,point,slopes + const sin = Math.sin, cos = Math.cos, + x = clock.x, y = clock.y, + r2 = clock.r2, r3 = clock.r3; + let r = (n-1)*arc+e/2; // rads + poly.push(x+r2*sin(r), y-r2*cos(r)); + r += s; + poly.push(x+r3*sin(r), y-r3*cos(r)); + r += p; + poly.push(x+r3*sin(r), y-r3*cos(r)); + r += s; + poly.push(x+r2*sin(r), y-r2*cos(r)); +} + +/** + * @param {number} n Tooth number to fill (1-based) + * @param col Fill color + */ +function fillTooth(n, col) { + if (!n) return; // easiest to check here + let poly = []; + addTooth(poly, n); + g.setColor(col).fillPoly(poly) + .setColor(g.theme.fg2).drawPoly(poly); // fillPoly colored over the outline +} + +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + precision: 1, + settingsFile: "cogclock.settings.json", + init: function() { + this.r1 = (process.env.HWVERSION>1) ? 68 : 84; // inner radius + this.r3 = Math.min(Bangle.appRect.w/2, Bangle.appRect.h/2); // outer radius + this.r2 = (this.r1*3+this.r3*2)/5; + this.teeth = 12; + this.edge = 0.45; + this.point = 0.35; // as fraction of arc + this.x = Bangle.appRect.x+Bangle.appRect.w/2; + this.y = Bangle.appRect.y+Bangle.appRect.h/2; + }, + draw: function(d) { + const x = this.x, y = this.y; + g.setColor(g.theme.bg2).fillCircle(x, y, this.r2) // fill cog + .setColor(g.theme.bg).fillCircle(x, y, this.r1) // clear center + .setColor(g.theme.fg2).drawCircle(x, y, this.r1); // draw inner border + let poly = []; // complete teeth outline + for(let t = 1; t<=this.teeth; t++) { + fillTooth(t, g.theme.bg2); + addTooth(poly, t); + } + g.drawPoly(poly, true); // draw outer border + if (!this.showDate) { + // draw top/bottom rectangles (instead of year/date) + g.reset() + .fillRect(x-30, y-60, x+29, y-33).clearRect(x-28, y-58, x+27, y-33) + .fillRect(x-30, y+60, x+29, y+30).clearRect(x-28, y+58, x+27, y+30); + } + this.tooth = 0; + this.update(d, {s: 1, m: 1, h: 1, d: 1}); + }, + update: function(d, c) { + g.reset(); + const pad2 = num => (num<10 ? "0" : "")+num, + year = d.getFullYear(), + date = pad2(d.getDate())+pad2(d.getMonth()), + time = pad2(d.getHours())+pad2(d.getMinutes()), + tooth = Math.round(d.getSeconds()/60*this.teeth); + const x = this.x, y = this.y; + if (c.m) { + g.setFont("15x32N:2").setFontAlign(0, 0) // center middle + .drawString(time, x, y, true); + } + if (this.showDate) { + if (c.d) { + g.setFont("15x32N").setFontAlign(0, -1) // center top + .drawString(year, x, y+32, true) + .setFont("15x32N").setFontAlign(0, 1) // center bottom + .drawString(date, x, y-32, true); + } + } + + if (tooth!==this.tooth) { + if (tooth>this.tooth) { + for(let t = this.tooth; t<=tooth; t++) { // fill missing teeth + fillTooth(t, g.theme.fg2); + } + } else { + for(let t = this.tooth; t>tooth; t--) { // erase extraneous teeth + fillTooth(t, g.theme.bg2); + } + } + } + this.tooth = tooth; + } +}); +clock.start(); diff --git a/apps/cogclock/icon.js b/apps/cogclock/icon.js new file mode 100644 index 000000000..899cfc7c1 --- /dev/null +++ b/apps/cogclock/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/ACcikQXpCQUCC4MgBAgqMCQIXEAgQXNBwIDCAggXOABAXLHwQAHMYQXmmczI5oiCBwUjCwIABmQXDEgJ0KCwMwCwMDDAgyGLYoWBgAXBgAYBMZIXEkYWBC4YYBGAh7FFwgHCC4YEBPRIwCFwYXFGAaqHC56oIIwgXFJAbUJLwpgHI4qPDIwpIFR4wWDLwa6BAAQHDVIYYCC/gYCC453MPIR3HU5gADd5bXHC4rvJMAYAECwJeCd5MjGAjVDC4ZHGNARIFGAgNDFw5IJUogwFC4gwBDAhGBBghIFBQhhBbYguEPAweCDAgACCwZACNg5LFXQYsIC5QAFdg4XcCxJHNBwYTEC6A+BJYQEEC5YYBMYhbCCxo0GCaIXbAHgA=")) \ No newline at end of file diff --git a/apps/cogclock/icon.png b/apps/cogclock/icon.png new file mode 100644 index 000000000..8520fcf5d Binary files /dev/null and b/apps/cogclock/icon.png differ diff --git a/apps/cogclock/metadata.json b/apps/cogclock/metadata.json new file mode 100644 index 000000000..fee8982df --- /dev/null +++ b/apps/cogclock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "cogclock", + "name": "Cog Clock", + "version": "0.05", + "description": "A cross-shaped clock inside a cog", + "icon": "icon.png", + "screenshots": [{"url":"screenshot_b1.png"},{"url":"screenshot_b2.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"cogclock.app.js","url":"app.js"}, + {"name":"cogclock.settings.js","url":"settings.js"}, + {"name":"cogclock.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name": "cogclock.settings.json"} + ] +} diff --git a/apps/cogclock/screenshot_b1.png b/apps/cogclock/screenshot_b1.png new file mode 100644 index 000000000..f49709aef Binary files /dev/null and b/apps/cogclock/screenshot_b1.png differ diff --git a/apps/cogclock/screenshot_b2.png b/apps/cogclock/screenshot_b2.png new file mode 100644 index 000000000..3eddda615 Binary files /dev/null and b/apps/cogclock/screenshot_b2.png differ diff --git a/apps/cogclock/settings.js b/apps/cogclock/settings.js new file mode 100644 index 000000000..deae484c9 --- /dev/null +++ b/apps/cogclock/settings.js @@ -0,0 +1,10 @@ +(function(back) { + let menu = { + "": {"title": /*LANG*/"Cog Clock"}, + /*LANG*/"< Back": back, + }; + require("ClockFace_menu").addSettingsFile(menu, "cogclock.settings.json", [ + "showDate", "hideWidgets" + ]); + E.showMenu(menu); +}) diff --git a/apps/color_catalog/ChangeLog b/apps/color_catalog/ChangeLog new file mode 100644 index 000000000..d1d409730 --- /dev/null +++ b/apps/color_catalog/ChangeLog @@ -0,0 +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/Changelog b/apps/color_catalog/Changelog deleted file mode 100644 index b79d0c85b..000000000 --- a/apps/color_catalog/Changelog +++ /dev/null @@ -1 +0,0 @@ -0.01: 1st ver,RGB565 and RGB888 colors in a common UI/UX 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..e445e3175 100644 --- a/apps/color_catalog/metadata.json +++ b/apps/color_catalog/metadata.json @@ -2,10 +2,10 @@ "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", + "tags": "color,input,buttons,touch,ui", "supports": ["BANGLEJS"], "readme": "README.md", "storage": [ diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog new file mode 100644 index 000000000..e38a7c5a5 --- /dev/null +++ b/apps/colorful_clock/ChangeLog @@ -0,0 +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 afc6b321f..b58892311 100644 --- a/apps/colorful_clock/app.js +++ b/apps/colorful_clock/app.js @@ -3,6 +3,8 @@ let outerRadius = Math.min(CenterX,CenterY) * 0.9; + Bangle.setUI('clock'); + Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -118,7 +120,6 @@ let twoPi = 2*Math.PI; let Pi = Math.PI; - let halfPi = Math.PI/2; let sin = Math.sin, cos = Math.cos; @@ -241,7 +242,3 @@ refreshDisplay(); } }); - - Bangle.loadWidgets(); - - Bangle.setUI('clock'); diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json index 5b6dbe87e..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.03", + "version": "0.05", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", diff --git a/apps/colorwheel/app.js b/apps/colorwheel/app.js index 7874c3f54..e8367d329 100644 --- a/apps/colorwheel/app.js +++ b/apps/colorwheel/app.js @@ -64,13 +64,14 @@ switch (true) { case (Radius > outerRadius): Color = '#000000'; break; case (Radius < innerRadius): Color = '#FFFFFF'; break; - default: + default: { let Phi = Math.atan2(dy,dx) + halfPi; if (Phi < 0) { Phi += twoPi; } if (Phi > twoPi) { Phi -= twoPi; } let Index = Math.floor(12*Phi/twoPi); Color = ColorList[Index]; + } } g.setColor(1,1,1); g.fillCircle(CenterX,CenterY, innerRadius); diff --git a/apps/coloursdemo/ChangeLog b/apps/coloursdemo/ChangeLog new file mode 100644 index 000000000..d44ed23f0 --- /dev/null +++ b/apps/coloursdemo/ChangeLog @@ -0,0 +1 @@ +1.00: first release diff --git a/apps/coloursdemo/README.md b/apps/coloursdemo/README.md new file mode 100644 index 000000000..b0fdc6f6b --- /dev/null +++ b/apps/coloursdemo/README.md @@ -0,0 +1,22 @@ +# Colours Demo + +This is a simple app to demonstrate colours on a Bangle 2. + +The colours are "optimised" for the Bangle 2's 3-bit display. They only include values which use either the full, half or no primary RGB colour, which should reduce the artifacts due to dithering (the exception are light and dark grey). + +![](screenshot.png) + +Use this app for choosing colours for your own project, and copy the colour definitions from the source code. + + +## Use colours in other projects + +Copy-and-paste the colour constants to be used in your own app from `coloursdemo.app.js`. They are sandwiched between the "BEGIN" and "END" comments at the beginning of the file. + +With the constants available in your own code, you can for example set the foreground colour to yellow with: + + g.setColor(COLOUR_YELLOW); + +This works for any graphics call requiring a colour value (like `g.setBgColor()`). + + diff --git a/apps/coloursdemo/coloursdemo-icon.js b/apps/coloursdemo/coloursdemo-icon.js new file mode 100644 index 000000000..8c58b0b19 --- /dev/null +++ b/apps/coloursdemo/coloursdemo-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AC8sAAYqpmVdr2Irwvklkzq4qBx4ADxAvDM0EyxAABFwgABF4k5rsyGTksF5MzBwdjAAVdnIzCF69dF5FdEYUyF4YADGQSPVF5LwCRwIvHAAIvVllXF5DwCRwgAFNobwbxFeEISOIAAMzF6zwCsgqBDoMsmUzWQMzF5MyeC4lBEwM5nNAsgABGgMyX5JeWF4IsBFYYADnIvBHgJmBrouDBYIvZnIvHLwIABnBvCMwSOXeAQvImU4F4QADMwReXF5csFwwxDF7IlCYAqOEF44uYF5MzF5ReZR4LwBF4qOKnAvalgvBYAk6RxYvaeAs6EYK+lMAZOBlgtBAQS+jF4QoBSQQjBGRKOcF4YjCMgM4AAIyCBoaOcF4YwCAYIvCGQxeceAQvDGoIvFGQYveSAguJF8iOHAAYueF4iOqeAksRyz8CAAzwNR1RgDMQZeIADJ0JqwmCGQoFB0gAEq2A5wAG0ky54AFrowGFQVXAAIyGmVWF8VWF4QyGlmAF8QsDLYIyFFwovbGAIuDSoqOHF8CJCF4aOHF7q/CqyVEAoIuGF7hgEAAiOIF7xhDYgiOHF7oxDXwLyCRxAvfGAYAhF5QA/AH4AEA")) diff --git a/apps/coloursdemo/coloursdemo.app.js b/apps/coloursdemo/coloursdemo.app.js new file mode 100644 index 000000000..85066bd31 --- /dev/null +++ b/apps/coloursdemo/coloursdemo.app.js @@ -0,0 +1,128 @@ +/* + * Demonstrate colours + */ + + +// BEGIN colour constants +const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0) +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_WHITE = 0xffff; // same as: g.setColor(1, 1, 1) + +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 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_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1) +const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1) + +const COLOUR_LIGHT_RED = 0xfc10; // same as: g.setColor(1, 0.5, 0.5) +const COLOUR_LIGHT_GREEN = 0x87f0; // same as: g.setColor(0.5, 1, 0.5) +const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1) +const COLOUR_LIGHT_YELLOW = 0xfff0; // same as: g.setColor(1, 1, 0.5) +const COLOUR_LIGHT_MAGENTA = 0xfc1f; // same as: g.setColor(1, 0.5, 1) +const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) + +const COLOUR_DARK_RED = 0x8000; // same as: g.setColor(0.5, 0, 0) +const COLOUR_DARK_GREEN = 0x0400; // same as: g.setColor(0, 0.5, 0) +const COLOUR_DARK_BLUE = 0x0010; // same as: g.setColor(0, 0, 0.5) +const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +const COLOUR_DARK_MAGENTA = 0x8010; // same as: g.setColor(0.5, 0, 0.5) +const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) + +const COLOUR_PINK = 0xf810; // same as: g.setColor(1, 0, 0.5) +const COLOUR_LIMEGREEN = 0x87e0; // same as: g.setColor(0.5, 1, 0) +const COLOUR_ROYALBLUE = 0x041f; // same as: g.setColor(0, 0.5, 1) +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) +const COLOUR_INDIGO = 0x801f; // same as: g.setColor(0.5, 0, 1) +const COLOUR_TURQUOISE = 0x07f0; // same as: g.setColor(0, 1, 0.5) +// END colour constants + + +// array of colours to be demoed: +// [ colour value, label colour, label ] +const demo = [ + [ COLOUR_LIGHT_RED, COLOUR_BLACK, 'LIGHT RED' ], + [ COLOUR_RED, COLOUR_WHITE, 'RED' ], + [ COLOUR_DARK_RED, COLOUR_WHITE, 'DARK RED' ], + + [ COLOUR_LIGHT_YELLOW, COLOUR_BLACK, 'LIGHT YELLOW' ], + [ COLOUR_YELLOW, COLOUR_BLACK, 'YELLOW' ], + [ COLOUR_DARK_YELLOW, COLOUR_WHITE, 'DARK YELLOW' ], + + [ COLOUR_LIGHT_GREEN, COLOUR_BLACK, 'LIGHT GREEN' ], + [ COLOUR_GREEN, COLOUR_BLACK, 'GREEN' ], + [ COLOUR_DARK_GREEN, COLOUR_WHITE, 'DARK GREEN' ], + + [ COLOUR_LIGHT_CYAN, COLOUR_BLACK, 'LIGHT CYAN' ], + [ COLOUR_CYAN, COLOUR_BLACK, 'CYAN' ], + [ COLOUR_DARK_CYAN, COLOUR_WHITE, 'DARK CYAN' ], + + [ COLOUR_LIGHT_BLUE, COLOUR_BLACK, 'LIGHT BLUE' ], + [ COLOUR_BLUE, COLOUR_WHITE, 'BLUE' ], + [ COLOUR_DARK_BLUE, COLOUR_WHITE, 'DARK BLUE' ], + + [ COLOUR_LIGHT_MAGENTA, COLOUR_BLACK, 'LIGHT MAGENTA' ], + [ COLOUR_MAGENTA, COLOUR_WHITE, 'MAGENTA' ], + [ COLOUR_DARK_MAGENTA, COLOUR_WHITE, 'DARK MAGENTA' ], + + [ COLOUR_LIMEGREEN, COLOUR_BLACK, 'LIMEGREEN' ], + [ COLOUR_TURQUOISE, COLOUR_BLACK, 'TURQUOISE' ], + [ COLOUR_ROYALBLUE, COLOUR_WHITE, 'ROYALBLUE' ], + + [ COLOUR_ORANGE, COLOUR_BLACK, 'ORANGE' ], + [ COLOUR_PINK, COLOUR_WHITE, 'PINK' ], + [ COLOUR_INDIGO, COLOUR_WHITE, 'INDIGO' ], + + [ COLOUR_LIGHT_GREY, COLOUR_BLACK, 'LIGHT GREY' ], + [ COLOUR_GREY, COLOUR_BLACK, 'GREY' ], + [ COLOUR_DARK_GREY, COLOUR_WHITE, 'DARK GREY' ], + + [ COLOUR_WHITE, COLOUR_BLACK, 'WHITE' ], + [ COLOUR_BLACK, COLOUR_WHITE, 'BLACK' ], +]; + +const columns = 3; +const rows = 10; + + +// initialise +g.clear(true); +g.setFont('6x8').setFontAlign(-1, -1); + +// calc some values required to draw the grid +const colWidth = Math.floor(g.getWidth() / columns); +const rowHeight = Math.floor(g.getHeight() / rows); +const xStart = Math.floor((g.getWidth() - (columns * colWidth)) / 2); +var x = xStart; +var y = Math.floor((g.getHeight() - (rows * rowHeight)) / 2); + +// loop through the colours to be demoed +for (var idx in demo) { + var colour = demo[idx][0]; + var labelColour = demo[idx][1]; + var label = demo[idx][2]; + + // draw coloured box + g.setColor(colour).fillRect(x, y, x + colWidth - 1, y + rowHeight - 1); + + // label it + g.setColor(labelColour).drawString(g.wrapString(label, colWidth).join("\n"), x, y); + + x += colWidth; + if ((x + colWidth) >= g.getWidth()) { + x = xStart; + y += rowHeight; + } +} + +// there's an "unused" box left - cross it out +g.setColor(COLOUR_RED); +g.drawLine(x, y, x + colWidth - 1, y + rowHeight - 1); +g.drawLine(x, y + rowHeight - 1, x + colWidth - 1, y); + + +// exit on button press +setWatch(e => { Bangle.showClock(); }, BTN1); + diff --git a/apps/coloursdemo/coloursdemo.png b/apps/coloursdemo/coloursdemo.png new file mode 100644 index 000000000..305ccc9b8 Binary files /dev/null and b/apps/coloursdemo/coloursdemo.png differ diff --git a/apps/coloursdemo/metadata.json b/apps/coloursdemo/metadata.json new file mode 100644 index 000000000..8c34d7ea8 --- /dev/null +++ b/apps/coloursdemo/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "coloursdemo", + "name": "Colours Demo", + "shortName":"Colours Demo", + "version":"1.00", + "description": "Simple app to demonstrate colours", + "icon": "coloursdemo.png", + "screenshots": [{ "url": "screenshot.png" }], + "type": "app", + "tags": "tool", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name":"coloursdemo.app.js", "url":"coloursdemo.app.js" }, + { "name":"coloursdemo.img", "url":"coloursdemo-icon.js", "evaluate":true } + ] +} diff --git a/apps/coloursdemo/screenshot.png b/apps/coloursdemo/screenshot.png new file mode 100644 index 000000000..350011b3f Binary files /dev/null and b/apps/coloursdemo/screenshot.png differ diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog index d1adafc4c..1e7360018 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -4,3 +4,6 @@ 0.04: Fix for Bangle.js 2 and themes 0.05: Fix bearing not clearing correctly (visible in single or double digit bearings) 0.06: Add button for force compass calibration +0.07: Use 360-heading to output the correct heading value (fix #1866) +0.08: Added adjustment for Bangle.js magnetometer heading fix +0.09: use falling edge of button to reset compass (allows exit without compass reset) \ No newline at end of file diff --git a/apps/compass/compass-icon.js b/apps/compass/compass-icon.js index 6a09df608..63a12aeee 100644 --- a/apps/compass/compass-icon.js +++ b/apps/compass/compass-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AE8IxAAEwAWVDB4WIDBwWJAAIWOwcz///mc4DBhFDwYVBAAYYDJJAWJDAoXKCw//+YXJIwWPCQk/Aof4JBAuHC4v/GBBdHC4nzMIZGHCAIOBC4vz75hDJAgXCCgS9CC4fdAYQXGIwsyCAPyl//nvdVQoXFRofzkYXCCwJGBSIgXFQ4kymcykfdIwZgDC5XzkUyCwJGDC6FNCwPTC5i9FmQXCMgLZFC48zLgMilUv/vdkUjBII9BC6HSC55HD1WiklDNIgXIBok61QYBkSBFC5kqCwMjC6RGB1RcCR4gXIx4MC+Wqkfyl70BEQf4C4+DIwYqBC4XzGAc4C4sISAfz0QDCFgUzRwmAC4wQB+QTCC4f/AYJeCC4hIEPQi9FIwwXDbIzVHC4xICSIYXGRoRGFGAgqFXgouGC4iqDLo4XIJAQYHCwZGHGAgYBXQUzCwYuIDAwAHCxRJEAAxFJDBgWNDBAWPAH4AYA=")) +require("heatshrink").decompress(atob("mEwwZC/AB8G7dt2AQMtu2CIICBCBUbCItsCJIODAQeACA82BYO////+wUCCA0BDoN/CAIAB/YRB4ARFhu274QDAAPt23YNA4QF//9Nw9t24RG/4RGgZWDAApcBsCMFCA///ySFjdvCA+f/4RF7YRH8gCBUgqMFAAUkSQYRL5MnCJ36pM/CI0G7YiFyQRD/9t2ARI8gRBAwYRJ/1m6VNCJ1//VPCJvyCIJgECJVPCYIRNK4KNCCJf8CIKVFCIahECIIQFWZPkyamFCJNkdgwRGt4JBKwoAB7YREjYRB/IQGCINsCAQRBtoPHXIIRFgdt34RH+3bsARDgFt24RH23bCAgRB2wQG/oRHhu274RF9u27ARFgIaBWIiMB23ACIsAmyGBLgRWBCIIQGUgQLBAQieDAAqSBCIiMEAAwRFCBQABgwRB2AQMAH4AB")) \ No newline at end of file diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 4730111ac..b7c8ebb71 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -20,7 +20,7 @@ ag.setColor(1).fillCircle(AGM,AGM,AGM-1,AGM-1); ag.setColor(0).fillCircle(AGM,AGM,AGM-11,AGM-11); function arrow(r,c) { - r=r*Math.PI/180; + r=(360-r)*Math.PI/180; var p = Math.PI/2; ag.setColor(c).fillPoly([ AGM+AGH*Math.sin(r), AGM-AGH*Math.cos(r), @@ -68,7 +68,7 @@ g.clear(1); g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"RESET", g.getWidth()-5, g.getHeight()/2); setWatch(function() { Bangle.resetCompass(); -}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true}); +}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true, edge:"falling"}); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json index 3e3b37f72..9cc03d6c8 100644 --- a/apps/compass/metadata.json +++ b/apps/compass/metadata.json @@ -1,7 +1,7 @@ { "id": "compass", "name": "Compass", - "version": "0.06", + "version": "0.09", "description": "Simple compass that points North", "icon": "compass.png", "screenshots": [{"url":"screenshot_compass.png"}], diff --git a/apps/configurable_clock/ChangeLog b/apps/configurable_clock/ChangeLog new file mode 100644 index 000000000..59708756a --- /dev/null +++ b/apps/configurable_clock/ChangeLog @@ -0,0 +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 157d57741..4192954ae 100644 --- a/apps/configurable_clock/app.js +++ b/apps/configurable_clock/app.js @@ -5,6 +5,7 @@ let ScreenWidth = g.getWidth(), CenterX; let ScreenHeight = g.getHeight(), CenterY, outerRadius; + Bangle.setUI('clock'); Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -747,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; @@ -893,7 +893,7 @@ g.setFontAlign(-1,0); g.drawString('9', CenterX-outerRadius,CenterY); break; - case '1-12': + case '1-12': { let innerRadius = outerRadius * 0.9 - 10; let dark = g.theme.dark; @@ -941,6 +941,7 @@ g.drawString(i == 0 ? '12' : '' + i, x,y); } + } } let now = new Date(); @@ -1377,4 +1378,3 @@ } }); - Bangle.setUI('clock'); diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json index 28feae7e4..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.02", + "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/confthyttan/ChangeLog b/apps/confthyttan/ChangeLog new file mode 100644 index 000000000..77d527619 --- /dev/null +++ b/apps/confthyttan/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: update to my current preferences. +0.03: update app list +0.04: change app name "mysetup" -> "anotherconf" +0.05: remove apps that are not "core" to the experience. +0.06: change name "anotherconf" -> "confthyttan" diff --git a/apps/confthyttan/README.md b/apps/confthyttan/README.md new file mode 100644 index 000000000..e9b952c82 --- /dev/null +++ b/apps/confthyttan/README.md @@ -0,0 +1,31 @@ +# Thyttan's Default Config + +A different default set of apps and configurations. Brings many quality of life improvements. Opinionated based on the creators taste. Read more below before installing. + +## Usage + +Before installing do this: + +1. Backup your current setup (via the "More..." tab of the App Loader) so you can restore it later if you want. +2. Install this app (you'll be prompted about all data being removed from your Bangle) +3. Try it out, switch out apps to your favorites and tweak to your liking! + +## Features + +There will not be a trace of a "Thyttan's Default Config" app on your watch after installation. Only the apps it installed and the configurations. + +On the clock face: +- Swipe right on the screen to open the launcher (Desktop Launcher) - or press the hardware button. +- Swipe left to open a flashlight app. +- Swipe up to open the messages. +- Swipe down for quick access to music and podcast controls. + - (Do a subsequent left or right swipe to enter the listed apps) +- (Check out the "Quick Launch" app readme for more info) + +## Requests + +Add to the espruino/BangleApps issue tracker and mention @thyttan for bug reports and suggestions. + +## Creator + +thyttan diff --git a/apps/confthyttan/app.png b/apps/confthyttan/app.png new file mode 100644 index 000000000..24ff01b8a Binary files /dev/null and b/apps/confthyttan/app.png differ diff --git a/apps/confthyttan/autoreset.json b/apps/confthyttan/autoreset.json new file mode 100644 index 000000000..18e94a282 --- /dev/null +++ b/apps/confthyttan/autoreset.json @@ -0,0 +1 @@ +{"mode":0,"apps":[{"name":"Run+","src":"runplus.app.js","files":"runplus.info,runplus.app.js,runplus.img,runplus.settings.js,runplus_karvonen"}],"timeout":10} \ No newline at end of file diff --git a/apps/confthyttan/backswipe.json b/apps/confthyttan/backswipe.json new file mode 100644 index 000000000..aa66bd534 --- /dev/null +++ b/apps/confthyttan/backswipe.json @@ -0,0 +1 @@ +{"mode":0,"apps":[{"name":"Calculator","src":"calculator.app.js"},{"name":"SleepLog","src":"sleeplog.app.js"},{"name":"Messages","sortorder":-9,"src":"messagegui.app.js"},{"name":"Messages","sortorder":-9,"src":"messagegui.app.js","files":"messagegui.info,messagegui,messagegui.app.js,messagegui.new.js,messagegui.boot.js,messagegui.img"}],"standardNumSwipeHandlers":5,"standardNumDragHandlers":1} diff --git a/apps/confthyttan/dtlaunch.json b/apps/confthyttan/dtlaunch.json new file mode 100644 index 000000000..5417cca3b --- /dev/null +++ b/apps/confthyttan/dtlaunch.json @@ -0,0 +1 @@ +{"showClocks":true,"showLaunchers":true,"direct":false,"swipeExit":false,"timeOut":"15s"} \ No newline at end of file diff --git a/apps/confthyttan/edgeclk.settings.json b/apps/confthyttan/edgeclk.settings.json new file mode 100644 index 000000000..bdf875621 --- /dev/null +++ b/apps/confthyttan/edgeclk.settings.json @@ -0,0 +1 @@ +{"buzzOnCharge":true,"monthFirst":true,"twentyFourH":true,"showAmPm":false,"showSeconds":true,"showWeather":false,"stepGoal":10000,"stepBar":true,"weekBar":true,"mondayFirst":true,"dayBar":true,"redrawOnStep":false} diff --git a/apps/confthyttan/fastload.json b/apps/confthyttan/fastload.json new file mode 100644 index 000000000..beef5d0cc --- /dev/null +++ b/apps/confthyttan/fastload.json @@ -0,0 +1 @@ +{useAppHistory:true,disregardQuicklaunch:true,hideLoading:true} \ No newline at end of file diff --git a/apps/confthyttan/lightswitch.json b/apps/confthyttan/lightswitch.json new file mode 100644 index 000000000..818aa6b07 --- /dev/null +++ b/apps/confthyttan/lightswitch.json @@ -0,0 +1 @@ +{"colors":"011","image":"heart","touchOn":"always","oversize":7,"dragDelay":500,"minValue":0.01,"tapToLock":false,"unlockSide":"","tapSide":"","tapOn":"always","tOut":2000,"minFlash":0.2,"isOn":true,"value":0.9109} \ No newline at end of file diff --git a/apps/confthyttan/messages.settings.json b/apps/confthyttan/messages.settings.json new file mode 100644 index 000000000..88efa6082 --- /dev/null +++ b/apps/confthyttan/messages.settings.json @@ -0,0 +1 @@ +{vibrateTimeout:10,vibrate:":",vibrateCalls:":::",flash:false} \ No newline at end of file diff --git a/apps/confthyttan/metadata.json b/apps/confthyttan/metadata.json new file mode 100644 index 000000000..e662879a7 --- /dev/null +++ b/apps/confthyttan/metadata.json @@ -0,0 +1,68 @@ +{ "id": "confthyttan", + "name": "Thyttan's Default Config", + "version":"0.06", + "description": "A different default set of apps and configurations. Brings many quality of life improvements. Opinionated based on the creators taste. Read more below before installing.", + "icon": "app.png", + "type": "defaultconfig", + "tags": "system,configuration,config,anotherconfig,thyttan,defaultconfig", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "dependencies" : { + "sched":"app", + "kbmulti":"app", + "messageicons":"app", + "widmsggrid":"app", + "msgwakefup":"app", + "delaylock":"app", + "notify":"app", + "health":"app", + "widminbate":"app", + "podadrem":"app", + "spotrem":"app", + "android":"app", + "widanclk":"app", + "backswipe":"app", + "torch":"app", + "calculator":"app", + "widbt_notify":"app", + "smpltmr":"app", + "clkinfostopw":"app", + "runplus":"app", + "dtlaunch":"app", + "quicklaunch":"app", + "kineticscroll":"app", + "alarm":"app", + "recorder":"app", + "agenda":"app", + "edgeclk":"app", + "autoreset":"app", + "chargent":"app", + "setting":"app", + "fastload":"app", + "boot":"app", + "ateatimer":"app", + "drained":"app" + }, + "storage": [ + {"name":"backswipe.json", + "url":"backswipe.json"}, + {"name":"autoreset.json", + "url":"autoreset.json"}, + {"name":"dtlaunch.json", + "url":"dtlaunch.json"}, + {"name":"fastload.json", + "url":"fastload.json"}, + {"name":"quicklaunch.json", + "url":"quicklaunch.json"}, + {"name":"messages.settings.json", + "url":"messages.settings.json"}, + {"name":"widbt_notify.json", + "url":"widbt_notify.json"}, + {"name":"recorder.json", + "url":"recorder.json"}, + {"name":"edgeclk.settings.json", + "url":"edgeclk.settings.json"}, + {"name":"setting.json", + "url":"setting.json"} + ] +} diff --git a/apps/confthyttan/quicklaunch.json b/apps/confthyttan/quicklaunch.json new file mode 100644 index 000000000..4edbb083b --- /dev/null +++ b/apps/confthyttan/quicklaunch.json @@ -0,0 +1 @@ +{lapp:{name:"Show Launcher",sortorder:-12,src:"no source"},rapp:{name:"torch",type:"app",sortorder:-11,src:"torch.app.js"},uapp:{name:"Messages",sortorder:-9,src:"messagegui.app.js"},dapp:{name:"Extension",type:"app",sortorder:-11,src:"quicklaunch.app.js"},tapp:{name:""},dlapp:{name:"PA Remote",src:"podadrem.app.js"},drapp:{name:"Remote for Spotify",src:"spotrem.app.js"},duapp:{name:""},ddapp:{name:"Extension",type:"app",sortorder:-11,src:"quicklaunch.app.js"},dtapp:{name:""},ddlapp:{name:"Alarms",src:"alarm.app.js"},ddrapp:{name:"Run+",src:"runplus.app.js"},dduapp:{name:""},dddapp:{name:"Agenda",src:"agenda.app.js"},ddtapp:{name:""},rlapp:{name:""},rrapp:{name:""},ruapp:{name:""},rdapp:{name:""},rtapp:{name:""},trace:"dr"} diff --git a/apps/confthyttan/recorder.json b/apps/confthyttan/recorder.json new file mode 100644 index 000000000..f598b41d2 --- /dev/null +++ b/apps/confthyttan/recorder.json @@ -0,0 +1 @@ +{recording:false,period:10,record:["gps","hrm","steps"],file:"recorder.log0.csv"} \ No newline at end of file diff --git a/apps/confthyttan/setting.json b/apps/confthyttan/setting.json new file mode 100644 index 000000000..ece0c87ed --- /dev/null +++ b/apps/confthyttan/setting.json @@ -0,0 +1 @@ +{ble:true,blerepl:true,log:false,quiet:0,timeout:10,vibrate:true,beep:true,timezone:2,HID:false,clock:"edgeclk.app.js","12hour":false,firstDayOfWeek:1,brightness:0.5,options:{wakeOnBTN1:true,wakeOnBTN2:true,wakeOnBTN3:true,wakeOnFaceUp:false,wakeOnTouch:false,wakeOnTwist:false,twistThreshold:819.2,twistMaxY:-800,twistTimeout:1000,btnLoadTimeout:700},theme:{fg:65535,bg:0,fg2:65535,bg2:8,fgH:65535,bgH:31,dark:true},clockHasWidgets:true,launcher:"dtlaunch.app.js",touch:{x1:6,y1:14,x2:197,y2:178}} diff --git a/apps/confthyttan/widbt_notify.json b/apps/confthyttan/widbt_notify.json new file mode 100644 index 000000000..fbcd97f76 --- /dev/null +++ b/apps/confthyttan/widbt_notify.json @@ -0,0 +1 @@ +{showWidget:true,buzzOnConnect:false,buzzOnLoss:false,hideConnected:false,showMessage:false,nextBuzz:30000} \ No newline at end of file diff --git a/apps/contacts/ChangeLog b/apps/contacts/ChangeLog new file mode 100644 index 000000000..5d6abbb74 --- /dev/null +++ b/apps/contacts/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Minor code improvements +0.03: Minor code improvements +0.04: Allow calling contacts from the app, Refactoring +0.05: Nicer UI, Refactoring, new icon \ No newline at end of file diff --git a/apps/contacts/README.md b/apps/contacts/README.md new file mode 100644 index 000000000..53503d56b --- /dev/null +++ b/apps/contacts/README.md @@ -0,0 +1,39 @@ +# Contacts + +View, edit and call contacts on your bangle.js. Calling is done via the bluetooth connection to your android phone. + +## Contacts JSON file + +When the app is loaded from the app loader, a file named +`contacts.json` is loaded along with the javascript etc. The file +has the following contents: + +``` +[ + { + "name":"First Last", + "number":"123456789", + }, + { + "name": "James Bond", + "number":"555-007", + }, + ... +] +``` + +## Contacts Editor + +Clicking on the download icon of `Contents` in the app loader invokes +the contact editor. The editor downloads and displays the current +`contacts.json` file. Clicking the `Edit` button beside an entry +causes the entry to be deleted from the list and displayed in the edit +boxes. It can be restored - by clicking the `Add` button. + +# Icons + +Phone icon by Icons8 + +Delete Button icon by Icons8 + +Call List icon by Icons8 \ No newline at end of file diff --git a/apps/contacts/app-icon.js b/apps/contacts/app-icon.js new file mode 100644 index 000000000..0333b855a --- /dev/null +++ b/apps/contacts/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/ABcN23btoCG7ARFBw4CDCP4RegYRQjY1RgE2LMlsHY4RIsBfHLJBxICKxrDCJxHQCK59LBYYRBa59sgzXPCJwLDwACCgENCJewjbaBgEBCJkDLgIRIO4vABIQRN7ARKNYaSELJo4EPpgCHCAjjGYpwRNLIzFPCP4RnZwQRHd4YA/AA4A=")) \ No newline at end of file diff --git a/apps/contacts/app.png b/apps/contacts/app.png new file mode 100644 index 000000000..a1594e7a9 Binary files /dev/null and b/apps/contacts/app.png differ diff --git a/apps/contacts/contacts.app.js b/apps/contacts/contacts.app.js new file mode 100644 index 000000000..bf2f8a455 --- /dev/null +++ b/apps/contacts/contacts.app.js @@ -0,0 +1,135 @@ +var Layout = require("Layout"); + +var contacts = require('Storage').readJSON("contacts.json", true) || []; + +function writeContacts() { + require('Storage').writeJSON("contacts.json", contacts); +} + +function callNumber (number) { + E.showMessage('Calling ' + number + '...'); + setTimeout(() => mainMenu(), 2000); + 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 = { + "": { + "title": "Contacts", + }, + "< Back" : Bangle.load + }; + if (!contacts.length) { + menu['No Contacts'] = () => {}; + } + contacts.forEach((e, idx) => { + menu[e.name] = () => showContact(idx) + }) + menu["Add Contact"] = addContact; + g.clear(); + E.showMenu(menu); +} + +function showContact(idx) { + g.clear(); + var name = contacts[idx].name; + let longName = g.setFont("6x8:2").stringWidth(name) >= g.getWidth(); + var number = contacts[idx].number; + let longNumber = g.setFont("6x8:2").stringWidth(number) >= g.getWidth(); + (new Layout ({ + type:"v", + c: [ + {type: 'h', filly: 3, fillx:1, c: [ + {type:"btn", font:"6x8", pad:1, fillx:1, filly:1, label: "<- Back to list", cb: mainMenu}, + {type:"btn", pad:1, fillx:1, filly:3, src: require("heatshrink").decompress(atob("jUawYGDgVJkgQGBAOSBAsJkALBBIoaCDogaCAQYXBgIIFkmAC4IIFyVAgAIGGQUJHwo4FAo2QBwICDNAVAkgCEEAYUFEAQUFE34mRPwgmEcYgmDUg8AgjLGgAA==")), + cb: () => ( + E.showPrompt("Delete Contact '" + name + "'?", ) + .then((res) => { if (res) { deleteContact(idx) } else { mainMenu() } }) + ) + }, + ]}, + {type:"txt", font:longName ? "6x8" : "6x8:2", pad:1, fillx:2, filly:3, label: longName ? name.slice(0, name.length/2) + '\n' + name.slice(name.length/2) : name}, + {type:"txt", font: "6x8:2", pad:1, fillx:2, filly:3, label: longNumber ? number.slice(0, number.length/2) + '\n' + number.slice(number.length/2) : number}, + {type: 'h', filly: 3, fillx:1, c: [ + {type:"btn", pad:1, fillx:1, filly:3, src:atob("GBiBAAAAAAAAAAAAAB8AAB+AAB+AAB+AAB+AAA+AAA8AAA4AAAYAAAcAAAMAAAGAAAHB8ADz+AA/+AAf+AAH+AAA+AAAAAAAAAAAAA=="), cb: l => callNumber(number)}, + ]}, + ], + lazy:true + })).render(); +} + +function showNumpad() { + return new Promise((resolve, reject) => { + let number = '' + E.showMenu(); + function addDigit(digit) { + number += digit; + Bangle.buzz(20); + update(); + } + 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(); + }); +} + +function deleteContact(idx) { + contacts.splice(idx, 1); + writeContacts(); + mainMenu(); +} + +function addContact() { + require("textinput").input({text:""}) + .then(name => { + name = name.trim(); + if (name !== "") { + g.clear(); + showNumpad().then((number) => { + contacts.push({name: name, number: number}); + writeContacts(); + mainMenu(); + }) + } else { + E.showMessage("Invalid name"); + setTimeout(() => mainMenu(), 1000); + } + }); +} + +g.reset(); +Bangle.setUI(); +mainMenu(); diff --git a/apps/contacts/contacts.json b/apps/contacts/contacts.json new file mode 100644 index 000000000..40afa27dd --- /dev/null +++ b/apps/contacts/contacts.json @@ -0,0 +1,6 @@ +[ + { + "name":"EU emergency", + "number":"112" + } +] diff --git a/apps/contacts/interface.html b/apps/contacts/interface.html new file mode 100644 index 000000000..00dcd6655 --- /dev/null +++ b/apps/contacts/interface.html @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + +

Contacts

+
+
+ + + +
+
+
+ + + + + + + + + + +
NameNumber
+
+

Add a new contact

+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + + + + + + + diff --git a/apps/contacts/metadata.json b/apps/contacts/metadata.json new file mode 100644 index 000000000..384c2aa7d --- /dev/null +++ b/apps/contacts/metadata.json @@ -0,0 +1,19 @@ +{ "id": "contacts", + "name": "Contacts", + "version": "0.05", + "description": "View, edit and call contacts on device and from the App loader", + "icon": "app.png", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "interface": "interface.html", + "dependencies": {"textinput":"type"}, + "storage": [ + {"name":"contacts.app.js","url":"contacts.app.js"}, + {"name":"contacts.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"contacts.json","url":"contacts.json"} + ] +} diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog index d415a604d..488e07b29 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -7,3 +7,9 @@ 0.25: Fixed a bug that would let widgets change the color of the clock. 0.26: Time formatted to locale 0.27: Fixed the timing code, which sometimes did not update for one minute +0.28: More config options for cleaner look, enabled fast loading +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 +0.33: Messages would sometimes halt the clock. This should be fixed now. diff --git a/apps/contourclock/README.md b/apps/contourclock/README.md new file mode 100644 index 000000000..3341439da --- /dev/null +++ b/apps/contourclock/README.md @@ -0,0 +1,6 @@ +# New Features: +- Fast load! (only works if your launcher uses widgets) +- widgets, date and weekday are individually configurable +- you can hide widgets, date and weekday for a cleaner look when the watch is locked + +Contact me for bug reports or feature requests: ContourClock@gmx.de diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index d5c97edfa..95587369e 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -1,35 +1,100 @@ -var digits = []; -var drawTimeout; -var fontName=""; -var settings = require('Storage').readJSON("contourclock.json", true) || {}; -if (settings.fontIndex==undefined) { - settings.fontIndex=0; - require('Storage').writeJSON("myapp.json", settings); +{ + let drawTimeout; + let extrasTimer=0; + let settings = require('Storage').readJSON("contourclock.json", true) || {}; + if (settings.fontIndex == undefined) { + settings.fontIndex = 0; + settings.widgets = true; + settings.weekday = true; + settings.hideWhenLocked = false; + settings.tapToShow = false; + settings.twistToShow = false; + settings.date = true; + require('Storage').writeJSON("contourclock.json", settings); + } + require("FontTeletext10x18Ascii").add(Graphics); + let installedFonts = require('Storage').readJSON("contourclock-install.json") || {}; + // New install - check for unused font files. This should probably be handled by the installer instead + if (installedFonts.n > 0) { + settings.fontIndex = E.clip(settings.fontIndex, -installedFonts.n + 1, installedFonts.n - 1); + require('Storage').writeJSON("contourclock.json", settings); + for (let n = installedFonts.n;; n++) { + if (require("Storage").read("contourclock-" + n + ".json") == undefined) break; + require("Storage").erase("contourclock-" + n + ".json"); + } + require("Storage").erase("contourclock-install.json"); + } + let onLock = function(locked) {if (!locked) showExtras();}; + let showExtras = function() { //show extras for 5s + drawExtras(); + extrasTimer = 5000-60000-(Date.now()%60000); + if (extrasTimer<0) { //schedule next redraw early to hide extras + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 5000); + } + }; + let hideExtras = function() { + g.reset(); + g.clearRect(0, 138, g.getWidth() - 1, 176); + if (settings.widgets) require("widget_utils").hide(); + }; + let drawExtras = function() { //draw date, day of the week and widgets + let date = new Date(); + g.reset(); + g.clearRect(0, 138, g.getWidth() - 1, 176); + g.setFont("Teletext10x18Ascii").setFontAlign(0, 1); + if (settings.weekday) g.drawString(require("locale").dow(date).toUpperCase(), g.getWidth() / 2, g.getHeight() - 18); + if (settings.date) g.drawString(require('locale').date(date, 1), g.getWidth() / 2, g.getHeight()); + if (settings.widgets) require("widget_utils").show(); + }; + let draw = function() { + if (extrasTimer>0) { //schedule next draw early to remove extras + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, extrasTimer); + extrasTimer=0; + } else { + if (settings.hideWhenLocked) hideExtras(); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + } + g.reset(); + if (!settings.hideWhenLocked) drawExtras(); + require('contourclock').drawClock(settings.fontIndex); + }; + if (settings.hideWhenLocked) { + Bangle.on('lock', onLock); + if (settings.tapToShow) Bangle.on('tap', showExtras); + if (settings.twistToShow) Bangle.on('twist', showExtras); + } + Bangle.setUI({ + mode: "clock", + remove: function() { + if (settings.hideWhenLocked) { + Bangle.removeListener('lock', onLock); + if (settings.tapToShow) Bangle.removeListener('tap', showExtras); + if (settings.twistToShow) Bangle.removeListener('twist', showExtras); + } + if (drawTimeout) { + clearTimeout(drawTimeout); + drawTimeout = undefined; + } + if (settings.hideWhenLocked && settings.widgets) require("widget_utils").show(); + g.reset(); + g.clear(); + } + }); + g.clear(); + if (settings.widgets) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } + draw(); + if (!settings.hideWhenLocked || !Bangle.isLocked()) showExtras(); } - -function queueDraw() { - setTimeout(function() { - draw(); - queueDraw(); - }, 60000 - (Date.now() % 60000)); -} - -function draw() { - var date = new Date(); - // Draw day of the week - g.reset(); - g.setFont("Teletext10x18Ascii"); - g.clearRect(0,138,g.getWidth()-1,176); - g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18); - // Draw Date - g.setFontAlign(0,1).drawString(require('locale').date(new Date(),1),g.getWidth()/2,g.getHeight()); - require('contourclock').drawClock(settings.fontIndex); -} - -require("FontTeletext10x18Ascii").add(Graphics); -Bangle.setUI("clock"); -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -queueDraw(); -draw(); diff --git a/apps/contourclock/contourclock.settings.js b/apps/contourclock/contourclock.settings.js index a12538fc5..064167312 100644 --- a/apps/contourclock/contourclock.settings.js +++ b/apps/contourclock/contourclock.settings.js @@ -1,43 +1,78 @@ (function(back) { - Bangle.removeAllListeners('drag'); Bangle.setUI(""); var settings = require('Storage').readJSON('contourclock.json', true) || {}; if (settings.fontIndex==undefined) { - settings.fontIndex=0; - require('Storage').writeJSON("myapp.json", settings); + settings.fontIndex=0; + settings.widgets=true; + settings.weekday=true; + settings.date=true; + settings.hideWhenLocked=false; + settings.tapToShow=false; + settings.twistToShow=false; + require('Storage').writeJSON("contourclock.json", settings); } - savedIndex=settings.fontIndex; - saveListener = setWatch(function() { //save changes and return to settings menu - require('Storage').writeJSON('contourclock.json', settings); - Bangle.removeAllListeners('swipe'); - Bangle.removeAllListeners('lock'); - clearWatch(saveListener); - g.clear(); - back(); - }, BTN, { repeat:false, edge:'falling' }); - lockListener = Bangle.on('lock', function () { //discard changes and return to clock - settings.fontIndex=savedIndex; - require('Storage').writeJSON('contourclock.json', settings); - Bangle.removeAllListeners('swipe'); - Bangle.removeAllListeners('lock'); - clearWatch(saveListener); - g.clear(); - load(); - }); - swipeListener = Bangle.on('swipe', function (direction) { - var fontName = require('contourclock').drawClock(settings.fontIndex+direction); - if (fontName) { - settings.fontIndex+=direction; - g.clearRect(0,0,g.getWidth()-1,16); - g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0); - } else { - require('contourclock').drawClock(settings.fontIndex); - } - }); - g.reset(); - g.clear(); - g.setFont('6x8:2x2').setFontAlign(0,-1); - g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0); - g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36); - g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18); + function mainMenu() { + E.showMenu({ + "" : { "title" : "ContourClock" }, + "< Back" : () => back(), + 'Widgets': { + value: (settings.widgets !== undefined ? settings.widgets : true), + onchange : v => {settings.widgets=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Weekday': { + value: (settings.weekday !== undefined ? settings.weekday : true), + onchange : v => {settings.weekday=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Date': { + value: (settings.date !== undefined ? settings.date : true), + onchange : v => {settings.date=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Hide widgets, weekday and date when locked': { + value: (settings.hideWhenLocked !== undefined ? settings.hideWhenLocked : false), + onchange : v => {settings.hideWhenLocked=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Tap to show': { + value: (settings.tapToShow !== undefined ? settings.tapToShow : false), + onchange : v => {settings.tapToShow=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Twist to show': { + value: (settings.twistToShow !== undefined ? settings.twistToShow : false), + onchange : v => {settings.twistToShow=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'set Font': () => fontMenu() + }); + } + function fontMenu() { + Bangle.setUI(""); + savedIndex=settings.fontIndex; + saveListener = setWatch(function() { //save changes and return to settings menu + require('Storage').writeJSON('contourclock.json', settings); + Bangle.removeAllListeners('swipe'); + Bangle.removeAllListeners('lock'); + mainMenu(); + }, BTN, { repeat:false, edge:'falling' }); + lockListener = Bangle.on('lock', function () { //discard changes and return to clock + settings.fontIndex=savedIndex; + require('Storage').writeJSON('contourclock.json', settings); + Bangle.removeAllListeners('swipe'); + Bangle.removeAllListeners('lock'); + mainMenu(); + }); + swipeListener = Bangle.on('swipe', function (direction) { + var fontName = require('contourclock').drawClock(settings.fontIndex+direction); + if (fontName) { + settings.fontIndex+=direction; + g.clearRect(0,g.getHeight()-36,g.getWidth()-1,g.getHeight()-36+16); + g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,g.getHeight()-36); + } else { + require('contourclock').drawClock(settings.fontIndex); + } + }); + g.reset(); + g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1); + g.setFont('6x8:2x2').setFontAlign(0,-1); + g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,g.getHeight()-36); + g.drawString('Button to save',g.getWidth()/2,g.getHeight()-18); + } + mainMenu(); }) diff --git a/apps/contourclock/custom.html b/apps/contourclock/custom.html index 602182573..71cc788c7 100644 --- a/apps/contourclock/custom.html +++ b/apps/contourclock/custom.html @@ -14,60 +14,54 @@ -

   Select Fonts to upload:

+

   Select Fonts to upload:

- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-

diff --git a/apps/contourclock/font-Dosis.json b/apps/contourclock/font-Dosis.json deleted file mode 100644 index c206bab7e..000000000 --- a/apps/contourclock/font-Dosis.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name":"Dosis", - "size":"100", - "characters":[ - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf//////VVVVVVVVVVVVVVf////////1VVVVVVVVVVVX//AAAAAAD//VVVVVVVVVVf/AAAAAAAAA//VVVVVVVVV/wAAAAAAAAAAD/VVVVVVVX/AAAAAAAAAAAAD9VVVVVVX8AAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAAAP1VVVVXwAAAAAAAAAAAAAAA/VVVVXwAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAAD1V8AAAAAAAAP/AAAAAAAAA9V8AAAAAAAD///wAAAAAAAPVfAAAAAAAD/VX/wAAAAAAA9XwAAAAAAP1VVV/AAAAAAAPXwAAAAAAP1VVVV8AAAAAAD18AAAAAAPVVVVVXwAAAAAA9fAAAAAAD1VVVVVfAAAAAAD3wAAAAAD1VVVVVXwAAAAAA98AAAAAA9VVVVVVfAAAAAAPfAAAAAA9VVVVVVXwAAAAAD3wAAAAAPVVVVVVV8AAAAAA98AAAAAD1VVVVVVXwAAAAAP8AAAAAA9VVVVVVV8AAAAAD/AAAAAAPVVVVVVVfAAAAAA/wAAAAAD1VVVVVVXwAAAAAP8AAAAAA9VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVX/9VVfAAAAAA/wAAAAAPVVf//9VXwAAAAAP8AAAAAD1VfwAP1V8AAAAAD/AAAAAA9VfAAA9VfAAAAAA/wAAAAAPVXwAAD1XwAAAAAP8AAAAAD1V8AAA9V8AAAAAD/AAAAAA9V8AAAPVfAAAAAA/wAAAAAPVfAAAD1XwAAAAAP8AAAAAD1V8AAA9V8AAAAAD/AAAAAA9VfAAAPVfAAAAAA/wAAAAAPVXwAAD1XwAAAAAP8AAAAAD1V8AAD1V8AAAAAD/AAAAAA9VX8AD1VfAAAAAA/wAAAAAPVVf//1VXwAAAAAP8AAAAAD1VVf/1VV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAAPVVVVVVVfAAAAAA/wAAAAAD1VVVVVVXwAAAAAP8AAAAAA9VVVVVVV8AAAAAD/AAAAAAPVVVVVVVfAAAAAA98AAAAAD1VVVVVVfAAAAAAPfAAAAAA9VVVVVVXwAAAAAD3wAAAAAPVVVVVVV8AAAAAA98AAAAAA9VVVVVVfAAAAAAPfAAAAAAPVVVVVVfAAAAAAD3wAAAAAA9VVVVVXwAAAAAA98AAAAAAD1VVVVXwAAAAAA9fAAAAAAAPVVVVfwAAAAAAPV8AAAAAAA/VVVfwAAAAAAD1fAAAAAAAD////AAAAAAAA9XwAAAAAAAD///AAAAAAAA9VfAAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVV/AAAAAAAAAAAAAAAPVVVVVX8AAAAAAAAAAAAAAPVVVVVVXwAAAAAAAAAAAAA/VVVVVVVfwAAAAAAAAAAAD/VVVVVVVV/8AAAAAAAAAA/9VVVVVVVVV//wAAAAAAAP/1VVVVVVVVVVf//AAAAA//9VVVVVVVVVVVVV////////VVVVVVVVVVVVVVVX////9VVVVVVVVQ=="}, - {"width" : "36", "buffer":"VVVVVVVX/1VVVVVVVVV///1VVVVVVVf8AP/VVVVVVX/AAAP1VVVVVfwAAAA9VVVVX8AAAAA9VVVV/wAAAAA9VVVX8AAAAAAPVVV/AAAAAAAPVVf8AAAAAAAPVV/AAAAAAAAPVfwAAAAAAAAPV/AAAAAAAAAPXwAAAAAAAAAPfAAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAP8AAAAAAAAAAPfAAAwAAAAAAPfAAP8AAAAAAPX///fAAAAAAPV//1fAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAAPVVVVfAAAAAA9VVVVXwAAAAA9VVVVXwAAAAD9VVVVV/wAAA/1VVVVVf////9VVVVVVVf///VVVVVVVVVVVVVV"}, - {"width" : "65", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////1VVVVVVVVVVVVVVf///////1VVVVVVVVVVVV//AAAAAD//VVVVVVVVVVV/8AAAAAAAD/1VVVVVVVVV/wAAAAAAAAAP1VVVVVVVVfwAAAAAAAAAAD9VVVVVVVfwAAAAAAAAAAAD/VVVVVVX8AAAAAAAAAAAAA/VVVVVV8AAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAPVXwAAAAAAAP//AAAAAAAA9VfAAAAAAAP///wAAAAAAA9V8AAAAAAD9VVfwAAAAAAD1fAAAAAAA9VVVXwAAAAAAPV8AAAAAAPVVVVXwAAAAAA9XwAAAAAD1VVVVXwAAAAAA9fAAAAAAPVVVVVfAAAAAAD18AAAAAD1VVVVVfAAAAAAPXwAAAAAPVVVVVV8AAAAAA9fAAAAAA9VVVVVXwAAAAAD18AAAAAD1VVVVVfAAAAAAPXwAAAAAPVVVVVV8AAAAAA9fAAAAAA9VVVVVXwAAAAAD1fAAAAAPVVVVVVfAAAAAAPV8AAAAA9VVVVVXwAAAAAD1V8AAAAPVVVVVVfAAAAAAPVV8AAAP1VVVVVXwAAAAAA9VV////9VVVVVVfAAAAAAD1VV///9VVVVVVXwAAAAAAPVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVV/AAAAAAAAPVVVVVVVVVVVfwAAAAAAAD1VVVVVVVVVVXwAAAAAAAA9VVVVVVVVVVV8AAAAAAAAD1VVVVVVVVVV/AAAAAAAAA9VVVVVVVVVVfwAAAAAAAAPVVVVVVVVVVXwAAAAAAAAD1VVVVVVVVVX8AAAAAAAAA9VVVVVVVVVV/AAAAAAAAAPVVVVVVVVVVfAAAAAAAAAD1VVVVVVVVVfwAAAAAAAAA9VVVVVVVVVX8AAAAAAAAA/VVVVVVVVVV8AAAAAAAAAP1VVVVVVVVVfAAAAAAAAAD1VVVVVVVVVfwAAAAAAAAD9VVVVVVVVVX8AAAAAAAAA/VVVVVVVVVV8AAAAAAAAA/VVVVVVVVVVfAAAAAAAAAP1VVVVVVVVVXwAAAAAAAAD1VVVVVVVVVV8AAAAAAAAD9VVVVVVVVVVfAAAAAAAAA/VVVVVVVVVVXwAAAAAAAA/VVVVVVVVVVVfAAAAAAAAP1VVVVVVVVVVXwAAAAAAAP1VVVVVVVVVVV8AAAAAAAD9VVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVfAAAAAAA////////////1V8AAAAAAA////////////9XwAAAAAAAAAAAAAAAAAAD9fAAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAAD1X/AAAAAAAAAAAAAAAAAAPVV///////////////////1VVf/////////////////9VVVVVVVVVVVVVVVVVVVVVVU="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////9VVVVVVVVVVVVVV/////////9VVVVVVVVVVVX/8AAAAAAP/9VVVVVVVVVVf8AAAAAAAAAP9VVVVVVVVV/wAAAAAAAAAAP9VVVVVVVX/AAAAAAAAAAAAP9VVVVVVX8AAAAAAAAAAAAAP1VVVVVXwAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAA9VV8AAAAAAAP//wAAAAAAAPVVfAAAAAAA////wAAAAAAD1VfAAAAAAA/VVV/wAAAAAA9VXwAAAAAA9VVVV/AAAAAAPVV8AAAAAA9VVVVV8AAAAAD1VfAAAAAAPVVVVVfAAAAAAPVXwAAAAAD1VVVVV8AAAAAD1V8AAAAAD1VVVVVfAAAAAA9VfAAAAAA9VVVVVXwAAAAAPVXwAAAAAPVVVVVV8AAAAAPVV8AAAAAD1VVVVVfAAAAAD1VfAAAAAA9VVVVVXwAAAAA9VV8AAAAA9VVVVVV8AAAAAPVVXwAAAA9VVVVVVfAAAAAD1VVf////9VVVVVVXwAAAAA9VVV////9VVVVVVV8AAAAAPVVVVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVf//AAAAAAD1VVVVVVVVVVV///AAAAAAD1VVVVVVVVVVV/AAAAAAAAA9VVVVVVVVVVV8AAAAAAAAA9VVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVVVfAAAAAAAAA/VVVVVVVVVVVXwAAAAAAAA/VVVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVXwAAAAAAAA/VVVVVVVVVVVV8AAAAAAAAD/VVVVVVVVVVVfAAAAAAAAAD9VVVVVVVVVVXwAAAAAAAAAD1VVVVVVVVVVfAAAAAAAAAAPVVVVVVVVVVXwAAAAAAAAAA9VVVVVVVVVVfAAAAAAAAAAD1VVVVVVVVVV//8AAAAAAAA9VVVVVVVVVVX///AAAAAAAD1VVVVVVVVVVVVf/AAAAAAA9VVVVVVVVVVVVVX8AAAAAAD1VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVVVXwAAAAAPVX////VVVVVVVVV8AAAAAD1f/////VVVVVVVVfAAAAAA9fwAAAD9VVVVVVVXwAAAAAPfAAAAAPVVVVVVVV8AAAAAD3wAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAA9VVVVVVXwAAAAAD/AAAAAAPVVVVVVXwAAAAAD3wAAAAAA9VVVVVV8AAAAAA98AAAAAAD1VVVVX8AAAAAAPXwAAAAAAP1VVVX8AAAAAAD18AAAAAAA//1f/wAAAAAAA9fAAAAAAAA////wAAAAAAA9XwAAAAAAAAA/AAAAAAAAAPVfAAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVX8AAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAA/VVVVVVfwAAAAAAAAAAAAD/VVVVVVV/wAAAAAAAAAAAP9VVVVVVVV/wAAAAAAAAAA/1VVVVVVVVV/8AAAAAAAA//VVVVVVVVVVV//wAAAAD//9VVVVVVVVVVVVf///////9VVVVVVVVVVVVVVV/////1VVVVVVVVQ=="}, - {"width" : "73", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX//9VVVVVVVVVVVVVVVVVVVVf///9VVVVVVVVVVVVVVVVVVVfwAAP9VVVVVVVVVVVVVVVVVVfAAAAP1VVVVVVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVfAAAAAAD1VVV/VVVVVVVVVVVXwAAAAAD1VX////VVVVVVVVVXwAAAAAA9Vf/8D//VVVVVVVVV8AAAAAA9VfwAAAD9VVVVVVVV8AAAAAAPVfAAAAAD1VVVVVVVfAAAAAAPVXwAAAAA9VVVVVVVfAAAAAAD1XwAAAAAPVVVVVVVXwAAAAAD1V8AAAAAA9VVVVVVXwAAAAAA9VfAAAAAAPVVVVVVV8AAAAAA9VXwAAAAAD1VVVVVV8AAAAAAPVV8AAAAAA9VVVVVVfAAAAAAPVVfAAAAAAPVVVVVVfAAAAAAD1VXwAAAAAD1VVVVVXwAAAAAD1VV8AAAAAA9VVVVVXwAAAAAA9VVfAAAAAAPVVVVVV8AAAAAA9VVXwAAAAAD1VVVVV8AAAAAAD///wAAAAAAP/9VVVfAAAAAAAP//wAAAAAAA//9VVfAAAAAAAAAAAAAAAAAAAAP1VXwAAAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAAAA9V/wAAAAAAAAAAAAAAAAAAAD9VX///////////wAAAAAAA//9VVV///////////AAAAAAA//1VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVVVX8AAAAD9VVVVVVVVVVVVVVVVVf8AAAP9VVVVVVVVVVVVVVVVVVf////1VVVVVVVVVVVVVVVVVVVf///VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVX////////////////VVVVVV////////////////9VVVVV8AAAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAA9VVVVfAAAAAA//////////9VVVVXwAAAAA//////////9VVVVV8AAAAA9VVVVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVXwAAAAAP///VVVVVVVVVVVV8AAAAAA//////VVVVVVVVVfAAAAAAAAAD///9VVVVVVVXwAAAAAAAAAAAD//VVVVVVV8AAAAAAAAAAAAAP/1VVVVVfAAAAAAAAAAAAAAD/VVVVVXwAAAAAAAAAAAAAAA/VVVVV8AAAAAAAAAAAAAAAD9VVVVfAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAAPVVVfwAAAAAAAAAAAAAAAAD1VVV/////////wAAAAAAAAPVVVV/////////8AAAAAAAD1VVVVVVVVVVVV/8AAAAAAA9VVVVVVVVVVVVVfwAAAAAAD1VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVVVfAAAAAA9VX///9VVVVVVVVXwAAAAAPVf////9VVVVVVVV8AAAAAD1/wAAAP1VVVVVVVfAAAAAA9fAAAAA9VVVVVVVXwAAAAAPfAAAAAD1VVVVVVV8AAAAAD3wAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAAPVVVVVVV8AAAAAA/wAAAAAD1VVVVVVfAAAAAAP8AAAAAA9VVVVVVfAAAAAAPfAAAAAAD1VVVVVXwAAAAAD3wAAAAAAPVVVVVXwAAAAAA9fAAAAAAA9VVVVfwAAAAAAPXwAAAAAAD9VVVfwAAAAAAD18AAAAAAAP////AAAAAAAD1fAAAAAAAAP///AAAAAAAA9V8AAAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAPVVVVVX8AAAAAAAAAAAAAAPVVVVVVfwAAAAAAAAAAAAA/VVVVVVVfwAAAAAAAAAAAD/VVVVVVVV/8AAAAAAAAAA/9VVVVVVVVV//AAAAAAAAP/1VVVVVVVVVVf//AAAAA//9VVVVVVVVVVVVX////////VVVVVVVVVVVVVVVX////9VVVVVVVVQ=="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf//////VVVVVVVVVVVVVVX////////9VVVVVVVVVVVX//AAAAAAD//VVVVVVVVVVf/wAAAAAAAAP/VVVVVVVVV/wAAAAAAAAAAD/VVVVVVVX/AAAAAAAAAAAAD9VVVVVVX8AAAAAAAAAAAAAD9VVVVVXwAAAAAAAAAAAAAAP1VVVVXwAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAAD1XwAAAAAAAAP/8AAAAAAAA9V8AAAAAAAD////AAAAAAAD18AAAAAAAP/VVf8AAAAAAA9fAAAAAAAP1VVVXwAAAAAAPXwAAAAAAPVVVVVfAAAAAAD18AAAAAAPVVVVVV8AAAAAA98AAAAAAPVVVVVVXwAAAAAPfAAAAAAD1VVVVVV8AAAAAA/wAAAAAD1VVVVVVXwAAAAAP8AAAAAA9VVVVVVV8AAAAAD/AAAAAAPVVVVVVVfAAAAAD3wAAAAAPVVVVVVVXwAAAAA98AAAAAD1VVVVVVV8AAAAAPfAAAAAA9VVVVVVVXwAAAAD3wAAAAAPVVVVVVVVfwAAAP18AAAAAD1VVVVVVVV//8//1fAAAAAA9VVVVVVVVV////VXwAAAAAPVVVVVVVVVVVdVVV8AAAAAD1VVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVVVXwAAAAAPVVVf///9VVVVVVV8AAAAAD1Vf//////1VVVVVfAAAAAA9X//AAAAP/9VVVVXwAAAAAPX/AAAAAAA/9VVVV8AAAAAA/wAAAAAAAAP9VVVfAAAAAADwAAAAAAAAAP1VVXwAAAAAAAAAAAAAAAAAP1VV8AAAAAAAAAAAAAAAAAA/VVfAAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAAD18AAAAAAAAD//8AAAAAAAA9fAAAAAAAAP///8AAAAAAAPXwAAAAAAA/1VVfwAAAAAAA98AAAAAAA/VVVVfAAAAAAAPfAAAAAAA9VVVVV8AAAAAAD3wAAAAAA9VVVVVXwAAAAAA98AAAAAAPVVVVVVfAAAAAAPfAAAAAAPVVVVVVXwAAAAAD3wAAAAAD1VVVVVVfAAAAAA98AAAAAD1VVVVVVXwAAAAAPfAAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAA98AAAAAD1VVVVVVXwAAAAAPfAAAAAAPVVVVVVV8AAAAAD3wAAAAAD1VVVVVVfAAAAAA98AAAAAA9VVVVVVXwAAAAAPXwAAAAAD1VVVVVXwAAAAAD18AAAAAA9VVVVVXwAAAAAA9fAAAAAAD1VVVVV8AAAAAAPXwAAAAAAPVVVVX8AAAAAAPV8AAAAAAA/VVVX8AAAAAAD1XwAAAAAAD/1V/wAAAAAAA9V8AAAAAAAD///wAAAAAAA9VfAAAAAAAAA/8AAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAPVVVVVX8AAAAAAAAAAAAAA/VVVVVVfwAAAAAAAAAAAAA/VVVVVVVfwAAAAAAAAAAAD9VVVVVVVV/8AAAAAAAAAA/9VVVVVVVVV//AAAAAAAA//1VVVVVVVVVVf//AAAAA//9VVVVVVVVVVVVX///////9VVVVVVVVVVVVVVVX////9VVVVVVVVQ=="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVV//////////////////9VVX///////////////////9VX8AAAAAAAAAAAAAAAAAAP9XwAAAAAAAAAAAAAAAAAAAP3wAAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAP8AAAAAAP//////8AAAAAAD/AAAAAAP///////wAAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAAP8AAAAAD1VVVVVVV8AAAAAD/AAAAAA9VVVVVVVfAAAAAA/wAAAAAPVVVVVVVXwAAAAA98AAAAAD1VVVVVVXwAAAAAPfAAAAAA9VVVVVVV8AAAAAD3wAAAAAPVVVVVVVfAAAAAA98AAAAAPVVVVVVVfAAAAAA9XwAAAAD1VVVVVVXwAAAAAPVfwAAA/1VVVVVVXwAAAAAD1V/////1VVVVVVXwAAAAAD1VV///9VVVVVVVV8AAAAAA9VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVVVV/AAAAD1VVVVVVVVVVVVVVVX/AAAA9VVVVVVVVVVVVVVVVX/AAD9VVVVVVVVVVVVVVVVVX///9VVVVVVVVVVVVVVVVVVX//1VVVVVVVVVVVVQ=="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////9VVVVVVVVVVVVVVf////////1VVVVVVVVVVVX/8AAAAAAP/9VVVVVVVVVVf/AAAAAAAAA/9VVVVVVVVV/wAAAAAAAAAAP9VVVVVVVX/AAAAAAAAAAAAP1VVVVVVX8AAAAAAAAAAAAAPVVVVVVXwAAAAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAPVVV8AAAAAAAP//AAAAAAAA9VVfAAAAAAAP///AAAAAAAPVVXwAAAAAAPVVX8AAAAAAD1VXwAAAAAAPVVVXwAAAAAA9VV8AAAAAAPVVVVfAAAAAAPVVfAAAAAAPVVVVV8AAAAAD1VXwAAAAAPVVVVVfAAAAAAPVV8AAAAAD1VVVVXwAAAAAD1VfAAAAAA9VVVVVfAAAAAA9VXwAAAAAPVVVVVXwAAAAAPVV8AAAAAD1VVVVV8AAAAAPVVfAAAAAA9VVVVVfAAAAAD1VXwAAAAAPVVVVVXwAAAAA9VVfAAAAAD1VVVVXwAAAAAPVVXwAAAAA9VVVVV8AAAAAD1VV8AAAAAD1VVVVfAAAAAA9VVfAAAAAA9VVVVfAAAAAA9VVV8AAAAAD1VVVfAAAAAAPVVVfAAAAAAP1VV/AAAAAAD1VVXwAAAAAA////AAAAAAD1VVVfAAAAAAA//8AAAAAAA9VVVV8AAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAAAA9VVVVVX8AAAAAAAAAAAAAA9VVVVVVf8AAAAAAAAAAAAD9VVVVVVVfwAAAAAAAAAAAD9VVVVVVVVfAAAAAAAAAAAD1VVVVVVVVXwAAAAAAAAAAA9VVVVVVVVXwAAAAAAAAAAAD1VVVVVVVfwAAAAAAAAAAAAP1VVVVVVfwAAAAAAAAAAAAA/VVVVVV/AAAAAAAAAAAAAAA/VVVVV/AAAAAAAAAAAAAAAD9VVVV8AAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVV8AAAAAAAP//wAAAAAAD1VVfAAAAAAA////wAAAAAAPVVfAAAAAAD/VVV/AAAAAAD1VXwAAAAAD9VVVV8AAAAAAPVXwAAAAAD1VVVVXwAAAAAD1V8AAAAAD1VVVVVfAAAAAAPV8AAAAAA9VVVVVV8AAAAAD1fAAAAAA9VVVVVVfAAAAAAPXwAAAAAPVVVVVVV8AAAAAD18AAAAAPVVVVVVVfAAAAAA98AAAAAD1VVVVVVXwAAAAAPfAAAAAA9VVVVVVV8AAAAAD3wAAAAAPVVVVVVVfAAAAAA98AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAD1VVVVVVfAAAAAAP8AAAAAA9VVVVVVfAAAAAAD/AAAAAAPVVVVVVXwAAAAAD3wAAAAAA9VVVVVXwAAAAAA98AAAAAAD1VVVVV8AAAAAAPXwAAAAAAPVVVVX8AAAAAAD18AAAAAAA/VVVX8AAAAAAA9fAAAAAAAD/VV/wAAAAAAAPXwAAAAAAAD///wAAAAAAAPVfAAAAAAAAD/8AAAAAAAAD1XwAAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAP1VVVXwAAAAAAAAAAAAAAAP1VVVVfwAAAAAAAAAAAAAAPVVVVVV/wAAAAAAAAAAAAA/VVVVVVV/wAAAAAAAAAAAD/VVVVVVVV/wAAAAAAAAAAP9VVVVVVVVV//AAAAAAAAP/1VVVVVVVVVV//8AAAAA///VVVVVVVVVVVVX////////VVVVVVVVVVVVVVVf////9VVVVVVVVQ=="}, - {"width" : "67", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////9VVVVVVVVVVVVVVf////////1VVVVVVVVVVVf/8AAAAAAP/9VVVVVVVVVV//AAAAAAAAA/9VVVVVVVVX/AAAAAAAAAAAP9VVVVVVVX8AAAAAAAAAAAAP9VVVVVVXwAAAAAAAAAAAAAP1VVVVVfwAAAAAAAAAAAAAAPVVVVVfwAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAA9VfAAAAAAAAP//wAAAAAAAPVXwAAAAAAA////AAAAAAAA9V8AAAAAAD/VVV/AAAAAAAPV8AAAAAAD9VVVX8AAAAAAD1fAAAAAAD1VVVVXwAAAAAAPXwAAAAAD1VVVVVfAAAAAAD18AAAAAA9VVVVVV8AAAAAA9fAAAAAA9VVVVVVfAAAAAAPfAAAAAAPVVVVVVV8AAAAAD3wAAAAAD1VVVVVVfAAAAAA98AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAA/wAAAAAPVVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAAPVVVVVVV8AAAAAA/wAAAAAD1VVVVVVfAAAAAAP8AAAAAA9VVVVVVfAAAAAAD/AAAAAAD1VVVVVXwAAAAAA/wAAAAAAPVVVVVXwAAAAAAPfAAAAAAA9VVVVXwAAAAAAD3wAAAAAAD1VVVfwAAAAAAA98AAAAAAAP9VX/wAAAAAAAPfAAAAAAAA////AAAAAAAAD18AAAAAAAAP/wAAAAAAAAA9fAAAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1VX8AAAAAAAAAAAAAAAAAA9VVfwAAAAAAAAAAAAAAAAAPVVVfwAAAAAAAAA8AAAAAAD1VVV/wAAAAAAAD/wAAAAAA9VVVV//AAAAAAP9fAAAAAAPVVVVV///8AD//1XwAAAAAD1VVVVVX//////VV8AAAAAA9VVVVVVVVf/1VVVfAAAAAAPVVVVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVVVV8AAAAAA9VX//9VVVVVVVVVfAAAAAAPVf////VVVVVVVVXwAAAAAD1/wAAP9VVVVVVVV8AAAAAA9/AAAAD1VVVVVVVfAAAAAAPfAAAAAPVVVVVVVXwAAAAAD/AAAAAD1VVVVVVV8AAAAAA/wAAAAA9VVVVVVVfAAAAAAP8AAAAAD1VVVVVVXwAAAAAD/AAAAAA9VVVVVVV8AAAAAD3wAAAAAPVVVVVVV8AAAAAA98AAAAAD1VVVVVVfAAAAAAPfAAAAAAPVVVVVVfAAAAAAD3wAAAAAA9VVVVVfAAAAAAA98AAAAAAD1VVVVfAAAAAAAPfAAAAAAAP1VVV/AAAAAAAPV8AAAAAAA/////AAAAAAAD1fAAAAAAAA///8AAAAAAAA9XwAAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAA9VVVVVfwAAAAAAAAAAAAAA9VVVVVV/AAAAAAAAAAAAAD9VVVVVVV/AAAAAAAAAAAAP9VVVVVVVX/wAAAAAAAAAD/1VVVVVVVVX/8AAAAAAAD//VVVVVVVVVVV//wAAAAD//1VVVVVVVVVVVVf///////1VVVVVVVVVVVVVVV/////1VVVVVVVVQ=="}, - {"width" : "21", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf//VVVX///9VVfwAA/VV8AAAD1XwAAAA9fAAAAAPfAAAAAPfAAAAAP8AAAAAP8AAAAAD8AAAAAD8AAAAAD8AAAAADfAAAAAPfAAAAAPfAAAAAPXwAAAA9XwAAAD1V/AAAP1Vf/AP9VVV///1VVVV/1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/1VVVV///1VVf/AP/VV/AAAP1XwAAAA9XwAAAA9fAAAAAPfAAAAAPfAAAAAP8AAAAAD8AAAAAD8AAAAAD8AAAAAD8AAAAAPfAAAAAPfAAAAAPfAAAAA9XwAAAA9V8AAAD1VfwAA/VVX///9VVVf//VV"}, - ] -} - diff --git a/apps/contourclock/font-SairaCond.json b/apps/contourclock/font-SairaCond.json deleted file mode 100644 index b6757f7ad..000000000 --- a/apps/contourclock/font-SairaCond.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name":"SairaCond", - "size":"98", - "characters":[ - {"width" : "61", "buffer":"VVVVf///////////VVVVVVVV/////////////VVVVVVX/AAAAAAAAAAAD/VVVVVX8AAAAAAAAAAAAD9VVVVXwAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAA/8AAAAAAAA9fAAAAAAAA//wAAAAAAAD3wAAAAAAA9VfAAAAAAAA98AAAAAAAPVV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA/wAAAAAAA9VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAD/AAAAAAAD1VXwAAAAAAA98AAAAAAAPVV8AAAAAAAPfAAAAAAAD1V8AAAAAAAD3wAAAAAAAP/8AAAAAAAD18AAAAAAAA/8AAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAPVVVVfwAAAAAAAAAAAAA/VVVVV/AAAAAAAAAAAAA/VVVVVV/wAAAAAAAAAAD9VVVVVVX////////////9VVVVVVVV///////////1VVVUA==" }, - {"width" : "42", "buffer":"VVVVVVX///////VVVVVV////////VVVVVf8AAAAAAPVVVVX/AAAAAAAPVVVV/wAAAAAAAPVVVf8AAAAAAAAPVVX/AAAAAAAAAPVV/wAAAAAAAAAPVf8AAAAAAAAAAPX/AAAAAAAAAAAPfwAAAAAAAAAAAPfAAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAAAAAAAAAAP8AAAMAAAAAAAAP8AAD/AAAAAAAAP8AD/3wAAAAAAAP8D/9XwAAAAAAAP8/9VXwAAAAAAAPf9VVXwAAAAAAAPfVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVXwAAAAAAAPVVVVX/////////VVVVV/////////" }, - {"width" : "59", "buffer":"Vf/////////////1VVVVf//////////////9VVVV8AAAAAAAAAAAAAD/VVVXwAAAAAAAAAAAAAA/VVVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD1/////////wAAAAAAAAPX/////////wAAAAAAAA9VVVVVVVVVXwAAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAA9VVVVVVVVX8AAAAAAAAD1VVVVVVVX/AAAAAAAAAPVVVVVVVV/AAAAAAAAAD1VVVVVVV/AAAAAAAAAAPVVVVVVV/wAAAAAAAAAA9VVVVVV/wAAAAAAAAAAD1VVVVV/wAAAAAAAAAAA9VVVVV/wAAAAAAAAAAAD1VVVV/wAAAAAAAAAAAAPVVVV/wAAAAAAAAAAAAD1VVVfwAAAAAAAAAAAAAPVVVfwAAAAAAAAAAAAAD1VVX8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAP1VVV8AAAAAAAAAAAAAP9VVVXwAAAAAAAAAAAAP9VVVV8AAAAAAAAAAAAP9VVVVXwAAAAAAAAAAAP9VVVVVfAAAAAAAAAAAP9VVVVVV8AAAAAAAAAAP9VVVVVVfAAAAAAAAAAP9VVVVVVV8AAAAAAAAAP9VVVVVVVXwAAAAAAAAP9VVVVVVVVfAAAAAAAAD9VVVVVVVVV8AAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAD1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAA//////////3wAAAAAAAA///////////AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP///////////////////////////////////////w" }, - {"width" : "59", "buffer":"V//////////////VVVVV///////////////1VVVXwAAAAAAAAAAAAAP9VVVfAAAAAAAAAAAAAAD9VVV8AAAAAAAAAAAAAAA/VVXwAAAAAAAAAAAAAAA/VVfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAPX/////////AAAAAAAAA9f/////////AAAAAAAAD1VVVVVVVVVfAAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVf/////8AAAAAAAA9VVVX//////AAAAAAAAPVVVVfAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAA/VVVVVV8AAAAAAAAAAAP1VVVVVXwAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAP1VVVVVXwAAAAAAAAAAAP9VVVVVfAAAAAAAAAAAAD9VVVVV8AAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV//////wAAAAAAAA9VVVV//////wAAAAAAAA9VVVVVVVVVXwAAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVV8AAAAAAAA9//////////AAAAAAAAD//////////wAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAA/VV8AAAAAAAAAAAAAAAP1VXwAAAAAAAAAAAAAAP1VVf/AAAAAAAAAAAAA/9VVVf//////////////9VVVVVf////////////1VVVVQ" }, - {"width" : "66", "buffer":"VVVVVX/////////VVVVVVVVVVVVf/////////VVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVXwAAAAAAAD1VVVVVVVVVVVXwAAAAAAAD1VVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVXwAAAAAAAD1VVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVVV8AAAAAAAPVVf/////1VVVV8AAAAAAAPVV//////9VVVV8AAAAAAAPVV8AAAAA9VVVXwAAAAAAA9VV8AAAAA9VVVXwAAAAAAA9VXwAAAAA9VVVXwAAAAAAA9VXwAAAAA9VVVfAAAAAAAA9VfAAAAAA9VVVfAAAAAAAD1VfAAAAAA9VVVfAAAAAAAD1VfAAAAAA9VVVfAAAAAAAPVV8AAAAAA9VVVfAAAAAAAPVV8AAAAAA9VVV8AAAAAAAPVV8AAAAAA9VVV8AAAAAAAPVXwAAAAAA9VVXwAAAAAAA9VXwAAAAAA9VVXwAAAAAAA9VXwAAAAAA9VVXwAAAAAAA9VfAAAAAAA9VVXwAAAAAAD1VfAAAAAAA9VVfAAAAAAAD1V8AAAAAAA9VVfAAAAAAAD1V8AAAAAAA9VVfAAAAAAAPVXwAAAAAAA9VVfAAAAAAAD//AAAAAAAAP//8AAAAAAAA/8AAAAAAAAD//8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAP//////////8AAAAAAAAD/////////////AAAAAAAAP//VVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVVX////////9VVVVVVVVVVVVX////////9VV" }, - {"width" : "59", "buffer":"f/////////////////VV//////////////////VXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAD/////////VV8AAAAAAA/////////1VXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9Vf/1VVVVVVV8AAAAAAA//////VVVVVXwAAAAAAA/8AD//9VVVVfAAAAAAAAAAAAAP/VVVV8AAAAAAAAAAAAAA/1VVXwAAAAAAAAAAAAAAP1VVfAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD1/////////wAAAAAAAAPX/////////wAAAAAAAA9VVVVVVVVVXwAAAAAAAA9VVVVVVVVVXwAAAAAAAD1VVVVVVVVVfAAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAD3/////////8AAAAAAAAPf/////////AAAAAAAAA98AAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD9VV8AAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA/VVVf8AAAAAAAAAAAAA/1VVVf//////////////1VVVVV/////////////1VVVVQ" }, - {"width" : "62", "buffer":"VVVVVV////////////1VVVVVVV//////////////VVVVVV/wAAAAAAAAAAAD/VVVVVfwAAAAAAAAAAAAA9VVVVfwAAAAAAAAAAAAAD1VVVX8AAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAD///////8PVXwAAAAAAAA/////////9VfAAAAAAAAPVVVVVVVV/VV8AAAAAAAD1VVVVVVVVVVXwAAAAAAA9VVVVVVVVVVV8AAAAAAAD1VVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVX/9VVVVVV8AAAAAAAPVV/////1VVVXwAAAAAAA9V//AA//9VVVfAAAAAAAD1/wAAAAD/VVXwAAAAAAAPfwAAAAAA/1VfAAAAAAAAPwAAAAAAAP1V8AAAAAAAAMAAAAAAAAPVXwAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAA//wAAAAAAAD/AAAAAAAAP//wAAAAAAAP8AAAAAAAD1VXwAAAAAAA/wAAAAAAAPVVfAAAAAAAD/AAAAAAAA9VVfAAAAAAAP8AAAAAAAD1VV8AAAAAAA/wAAAAAAAPVVXwAAAAAAD/AAAAAAAA9VVfAAAAAAAPfAAAAAAAD1VV8AAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAPfAAAAAAAD1VV8AAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAPfAAAAAAAD1VV8AAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAPfAAAAAAAD1VV8AAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAPXwAAAAAAD1VXwAAAAAAA9fAAAAAAAD1V8AAAAAAAD18AAAAAAAD//AAAAAAAAPXwAAAAAAAD/wAAAAAAAA9fAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAPVVVVV/AAAAAAAAAAAAAP1VVVVV/AAAAAAAAAAAAP9VVVVVVfwAAAAAAAAAAP9VVVVVVVf///////////9VVVVVVVVX//////////9VVVVQ==" }, - {"width" : "58", "buffer":"///////////////////////////////////////wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP/////////8AAAAAAAAD3/////////wAAAAAAAD1VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAAPVVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVfAAAAAAAAD1VVVVVVVVXwAAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAAPVVVVVVVVVf/////////VVVVVVVVVX/////////1VVVVVVVU=" }, - {"width" : "64", "buffer":"VVVVX///////////1VVVVVVVVf////////////9VVVVVVV/wAAAAAAAAAAA/9VVVVVV/AAAAAAAAAAAAAP1VVVVV8AAAAAAAAAAAAAAPVVVVX8AAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAAD1fAAAAAAAAA/AAAAAAAAAPXwAAAAAAAD//AAAAAAAAD18AAAAAAAD9X8AAAAAAAA9fAAAAAAAD1VXwAAAAAAAPXwAAAAAAA9VV8AAAAAAAD18AAAAAAAPVVXwAAAAAAA9fAAAAAAAD1VV8AAAAAAAPXwAAAAAAA9VVfAAAAAAAD18AAAAAAAPVVXwAAAAAAA9fAAAAAAAD1VV8AAAAAAAPXwAAAAAAA9VVfAAAAAAAD18AAAAAAAPVVXwAAAAAAA9fAAAAAAAD1VV8AAAAAAAPXwAAAAAAA9VVfAAAAAAAD18AAAAAAAPVVXwAAAAAAD1fAAAAAAAD1VV8AAAAAAA9XwAAAAAAA9VVfAAAAAAAPVfAAAAAAAPVVXwAAAAAAD1XwAAAAAAD1VXwAAAAAAA9V8AAAAAAAPVV8AAAAAAA9VXwAAAAAAA//8AAAAAAAPVV8AAAAAAAD/8AAAAAAAPVVXwAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAD9VVVVX8AAAAAAAAAAAAAD9VVVVVfwAAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAAD9VVVVV/AAAAAAAAAAAAAAP9VVVV/AAAAAAAAAAAAAAAP1VVV8AAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAAPVXwAAAAAAAD/8AAAAAAAA9XwAAAAAAAD//wAAAAAAAPV8AAAAAAAD1VfAAAAAAAA9fAAAAAAAD1VV8AAAAAAAPXwAAAAAAA9VVfAAAAAAAD3wAAAAAAAPVVXwAAAAAAA98AAAAAAAD1VV8AAAAAAAPfAAAAAAAA9VVfAAAAAAAD3wAAAAAAAPVVXwAAAAAAAP8AAAAAAAPVVV8AAAAAAAD/AAAAAAAD1VVfAAAAAAAA/wAAAAAAA9VVXwAAAAAAAP8AAAAAAAPVVV8AAAAAAAD/AAAAAAAA9VVfAAAAAAAA/wAAAAAAAPVVXwAAAAAAAP8AAAAAAAD1VV8AAAAAAAD/AAAAAAAA9VVfAAAAAAAA/wAAAAAAAPVVXwAAAAAAAP8AAAAAAAD1VV8AAAAAAAD/AAAAAAAA9VV8AAAAAAAA/wAAAAAAAD//8AAAAAAAAP8AAAAAAAAP/8AAAAAAAAD/AAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAA9VVVVfwAAAAAAAAAAAAAA9VVVVV/wAAAAAAAAAAAAD9VVVVVV/wAAAAAAAAAAA/9VVVVVVV/////////////1VVVVVVVV///////////9VVVVU=" }, - {"width" : "62", "buffer":"VVVVX///////////VVVVVVVVX////////////1VVVVVVX/AAAAAAAAAAAP9VVVVVV/AAAAAAAAAAAAD9VVVVVfAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAPV8AAAAAAAA/8AAAAAAAA9XwAAAAAAAP/8AAAAAAAA9fAAAAAAAD1V8AAAAAAAD18AAAAAAA9VV8AAAAAAAPXwAAAAAAD1VXwAAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAPfAAAAAAAD1VV8AAAAAAA98AAAAAAAPVVXwAAAAAAD3wAAAAAAA9VVfAAAAAAAD/AAAAAAAD1VV8AAAAAAAP8AAAAAAAPVVXwAAAAAAA/wAAAAAAA9VVfAAAAAAAD/AAAAAAAD1VV8AAAAAAAP8AAAAAAAPVVXwAAAAAAA/wAAAAAAA9VVfAAAAAAAD/AAAAAAAD1VV8AAAAAAAP8AAAAAAAPVVXwAAAAAAA/wAAAAAAA9VVfAAAAAAAD/AAAAAAAD1VV8AAAAAAAP8AAAAAAAPVVfAAAAAAAA/wAAAAAAA9VV8AAAAAAAD/AAAAAAAA///AAAAAAAAP8AAAAAAAA//wAAAAAAAA/wAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAA9VfAAAAAAAAAwAAAAAAAD1VfAAAAAAAA/wAAAAAAAPVVfwAAAAAAP3wAAAAAAA9VVf/AAAAAP1fAAAAAAAD1VVX//////9V8AAAAAAAPVVVVf////9VXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVV8AAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVXwAAAAAAAPVVVVVVVVVVV8AAAAAAAA9Vf/VVVVVVVfAAAAAAAAPVX/////////wAAAAAAAA9VfAP//////8AAAAAAAAD1V8AAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAA/VVVVXwAAAAAAAAAAAAA/1VVVVX/AAAAAAAAAAAA/1VVVVVf/////////////1VVVVVVVf///////////1VVVVVQ==" }, - {"width" : "26", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX///////9/////////AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/////////3////////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/////////////////AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP8AAAAAAA/wAAAAAAD/AAAAAAAP/////////////////w==" } - ] -} diff --git a/apps/contourclock/fonts/Anton-p1.png b/apps/contourclock/fonts/Anton-p1.png new file mode 100644 index 000000000..c049dd4a5 Binary files /dev/null and b/apps/contourclock/fonts/Anton-p1.png differ diff --git a/apps/contourclock/fonts/Anton-p2.png b/apps/contourclock/fonts/Anton-p2.png new file mode 100644 index 000000000..ac00cd840 Binary files /dev/null and b/apps/contourclock/fonts/Anton-p2.png differ diff --git a/apps/contourclock/fonts/ArchivoNarrow-p1.png b/apps/contourclock/fonts/ArchivoNarrow-p1.png new file mode 100644 index 000000000..909245a2a Binary files /dev/null and b/apps/contourclock/fonts/ArchivoNarrow-p1.png differ diff --git a/apps/contourclock/fonts/ArchivoNarrow-p2.png b/apps/contourclock/fonts/ArchivoNarrow-p2.png new file mode 100644 index 000000000..c73725c58 Binary files /dev/null and b/apps/contourclock/fonts/ArchivoNarrow-p2.png differ diff --git a/apps/contourclock/fonts/Bangers-p1.png b/apps/contourclock/fonts/Bangers-p1.png new file mode 100644 index 000000000..e57f725f0 Binary files /dev/null and b/apps/contourclock/fonts/Bangers-p1.png differ diff --git a/apps/contourclock/fonts/Bangers-p2.png b/apps/contourclock/fonts/Bangers-p2.png new file mode 100644 index 000000000..b25d4f511 Binary files /dev/null and b/apps/contourclock/fonts/Bangers-p2.png differ diff --git a/apps/contourclock/fonts/FjallaOne-p1.png b/apps/contourclock/fonts/FjallaOne-p1.png new file mode 100644 index 000000000..382a3f271 Binary files /dev/null and b/apps/contourclock/fonts/FjallaOne-p1.png differ diff --git a/apps/contourclock/fonts/FjallaOne-p2.png b/apps/contourclock/fonts/FjallaOne-p2.png new file mode 100644 index 000000000..34a964cca Binary files /dev/null and b/apps/contourclock/fonts/FjallaOne-p2.png differ diff --git a/apps/contourclock/fonts/LuckiestGuy-p1.png b/apps/contourclock/fonts/LuckiestGuy-p1.png new file mode 100644 index 000000000..0d082e40e Binary files /dev/null and b/apps/contourclock/fonts/LuckiestGuy-p1.png differ diff --git a/apps/contourclock/fonts/LuckiestGuy-p2.png b/apps/contourclock/fonts/LuckiestGuy-p2.png new file mode 100644 index 000000000..3b93f83d6 Binary files /dev/null and b/apps/contourclock/fonts/LuckiestGuy-p2.png differ diff --git a/apps/contourclock/fonts/MouseMemoirs-p1.png b/apps/contourclock/fonts/MouseMemoirs-p1.png new file mode 100644 index 000000000..fc9bdedc0 Binary files /dev/null and b/apps/contourclock/fonts/MouseMemoirs-p1.png differ diff --git a/apps/contourclock/fonts/MouseMemoirs-p2.png b/apps/contourclock/fonts/MouseMemoirs-p2.png new file mode 100644 index 000000000..511e6addf Binary files /dev/null and b/apps/contourclock/fonts/MouseMemoirs-p2.png differ diff --git a/apps/contourclock/fonts/NerkoOne-p1.png b/apps/contourclock/fonts/NerkoOne-p1.png new file mode 100644 index 000000000..44f91aca9 Binary files /dev/null and b/apps/contourclock/fonts/NerkoOne-p1.png differ diff --git a/apps/contourclock/fonts/NerkoOne-p2.png b/apps/contourclock/fonts/NerkoOne-p2.png new file mode 100644 index 000000000..c931116d8 Binary files /dev/null and b/apps/contourclock/fonts/NerkoOne-p2.png differ diff --git a/apps/contourclock/fonts/Oswald-p1.png b/apps/contourclock/fonts/Oswald-p1.png new file mode 100644 index 000000000..872e72d26 Binary files /dev/null and b/apps/contourclock/fonts/Oswald-p1.png differ diff --git a/apps/contourclock/fonts/Oswald-p2.png b/apps/contourclock/fonts/Oswald-p2.png new file mode 100644 index 000000000..a7b353b9f Binary files /dev/null and b/apps/contourclock/fonts/Oswald-p2.png differ diff --git a/apps/contourclock/fonts/RubikOne-p1.png b/apps/contourclock/fonts/RubikOne-p1.png new file mode 100644 index 000000000..376909fe1 Binary files /dev/null and b/apps/contourclock/fonts/RubikOne-p1.png differ diff --git a/apps/contourclock/fonts/RubikOne-p2.png b/apps/contourclock/fonts/RubikOne-p2.png new file mode 100644 index 000000000..89a7453db Binary files /dev/null and b/apps/contourclock/fonts/RubikOne-p2.png differ diff --git a/apps/contourclock/fonts/TitanOne-p1.png b/apps/contourclock/fonts/TitanOne-p1.png new file mode 100644 index 000000000..a9f3ec1a8 Binary files /dev/null and b/apps/contourclock/fonts/TitanOne-p1.png differ diff --git a/apps/contourclock/fonts/TitanOne-p2.png b/apps/contourclock/fonts/TitanOne-p2.png new file mode 100644 index 000000000..a52b11e1c Binary files /dev/null and b/apps/contourclock/fonts/TitanOne-p2.png differ diff --git a/apps/contourclock/fonts/font-Anton.json b/apps/contourclock/fonts/font-Anton.json new file mode 100644 index 000000000..5013839b2 --- /dev/null +++ b/apps/contourclock/fonts/font-Anton.json @@ -0,0 +1,17 @@ +{ + "name":"Anton", + "size":"100", + "characters":[ + {"width" : "53", "buffer":"VVVVVV/////9VVVVVVVVVVX///////1VVVVVVVVX/wAAAAA//VVVVVVVX/AAAAAAAD/VVVVVVV/AAAAAAAAAP1VVVVVfAAAAAAAAAAP1VVVVXwAAAAAAAAAAD1VVVV8AAAAAAAAAAAD1VVVfAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAPV8AAAAAAP/wAAAAAA9fAAAAAAD//wAAAAAD18AAAAAA9VXwAAAAAPXwAAAAAPVVfAAAAAA9fAAAAAA9VVfAAAAAA98AAAAAD1VV8AAAAAD3wAAAAA9VVXwAAAAAPfAAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD3wAAAAAPVVXwAAAAAPfAAAAAA9VVfAAAAAA98AAAAAD1VXwAAAAAD3wAAAAAD1V8AAAAAAPfAAAAAAD9fAAAAAAD18AAAAAAD/wAAAAAAPV8AAAAAAA8AAAAAAA9XwAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAD1VXwAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAD1VVXwAAAAAAAAAAAA9VVVfAAAAAAAAAAAAPVVVVfwAAAAAAAAAAD1VVVVfwAAAAAAAAAA9VVVVVX8AAAAAAAAA/VVVVVVX/AAAAAAAA/1VVVVVVV/8AAAAAD/1VVVVVVVVf///////1VVVVVVVVVV//////VVVVVU="}, + {"width" : "34", "buffer":"VVVVVV/////VVVVVV/////9VVVVV8AAAAPVVVVVfAAAAD1VVVVfAAAAA9VVVVfAAAAAPVVVVfAAAAAD1VVVfAAAAAA9VVVfAAAAAAPVVV/AAAAAAD1VX/AAAAAAA9Vf8AAAAAAAPX/wAAAAAAAD//AAAAAAAAA/wAAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAMAAAAAAP8AAPwAAAAAD/AA/fAAAAAA/wP/XwAAAAAP//9V8AAAAAD//VVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVXwAAAAAPVVVV8AAAAAD1VVVfAAAAAA9VVVX///////VVVVf//////Q=="}, + {"width" : "52", "buffer":"VVVVVVX///9VVVVVVVVVVVX//////1VVVVVVVVV//wAAAP/9VVVVVVVX/wAAAAAA/9VVVVVVX8AAAAAAAAP1VVVVVfwAAAAAAAAAP1VVVVfwAAAAAAAAAA/VVVVfAAAAAAAAAAAA9VVVXwAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVXwAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAD1fAAAAAAA8AAAAAAA9fAAAAAAD/8AAAAAAD3wAAAAAD9fwAAAAAA98AAAAAD1VfAAAAAAPfAAAAAA9VV8AAAAAD3wAAAAA9VVfAAAAAA/wAAAAAPVVXwAAAAAP8AAAAAD1VVfAAAAAD/AAAAAA9VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVV8AAAAAD/AAAAAD1VVfAAAAAA/wAAAAA9VVXwAAAAAP8AAAAAPVVV8AAAAAD/AAAAAD1VVfAAAAAD3//////9VVfAAAAAA9///////VVXwAAAAAPVVVVVVVVVV8AAAAAD1VVVVVVVVV8AAAAAD1VVVVVVVVVfAAAAAA9VVVVVVVVVfAAAAAAPVVVVVVVVVXwAAAAAPVVVVVVVVVXwAAAAAD1VVVVVVVVV8AAAAAD1VVVVVVVVV8AAAAAA9VVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVV8AAAAAA9VVVVVVVVVfAAAAAA9VVVVVVVVVfAAAAAAPVVVVVVVVVXwAAAAAA////////9XwAAAAAAD////////18AAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA///////////////////////////////////Q=="}, + {"width" : "53", "buffer":"VVVVVVVX//1VVVVVVVVVVVVf/////9VVVVVVVVVV///AAD//1VVVVVVVV/8AAAAAA/9VVVVVVV/wAAAAAAAD/VVVVVVfwAAAAAAAAA/VVVVVXwAAAAAAAAAAPVVVVV8AAAAAAAAAAAPVVVVfAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAPVVXwAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAA9V8AAAAAAD/AAAAAAD1fAAAAAAA//AAAAAAD18AAAAAAPVfAAAAAAPXwAAAAAD1VfAAAAAA9fAAAAAA9VV8AAAAAD18AAAAAD1VV8AAAAAPXwAAAAA9VVXwAAAAA9fAAAAAD1VVfAAAAAD18AAAAAPVVV8AAAAAPXwAAAAA9VVXwAAAAA9fAAAAAD1VVfAAAAAD18AAAAAPVVV8AAAAAPX//////9VVXwAAAAA9f//////1VVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVfAAAAAAPVVVVVVVVVfwAAAAAD1VVVVVVVX/8AAAAAAPVVVVVVVX/8AAAAAAA9VVVVVVVfAAAAAAAAD1VVVVVVV8AAAAAAAA9VVVVVVVXwAAAAAAAD1VVVVVVVfAAAAAAAA9VVVVVVVV8AAAAAAAPVVVVVVVVXwAAAAAAP1VVVVVVVVfAAAAAAD9VVVVVVVVV8AAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVfAAAAAAA/VVVVVVVVV8AAAAAAA/VVVVVVVVXwAAAAAAAPVVVVVVVVfAAAAAAAA9VVVVVVVV8AAAAAAAA9VVVVVVVXwAAAAAAAD1VVVVVVVf/AAAAAAAD1VVVVVVV//wAAAAAAPVVVVVVVVVfwAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVV8AAAAAPX//////1VVXwAAAAA9///////VVVfAAAAAD3wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAPVVVfAAAAAA/wAAAAA9VVV8AAAAAD/AAAAAD1VVXwAAAAAP8AAAAAPVVVfAAAAAA/wAAAAA9VVV8AAAAAD/AAAAAD1VVXwAAAAAP8AAAAAPVVVfAAAAAD3wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAPVVVfAAAAAD3wAAAAA9VVV8AAAAAPXwAAAAA9VVXwAAAAA9fAAAAAD1VV8AAAAAD18AAAAAPVVXwAAAAAPXwAAAAAPVV8AAAAAA9fAAAAAAPVfAAAAAAPV8AAAAAAP/wAAAAAA9XwAAAAAAP8AAAAAAD1XwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAD1VVV8AAAAAAAAAAAA9VVVV8AAAAAAAAAAAP1VVVV8AAAAAAAAAAD9VVVVV/AAAAAAAAAA9VVVVVV/wAAAAAAAA/VVVVVVVf8AAAAAAD/1VVVVVVVX////////1VVVVVVVVV///////VVVVVU="}, + {"width" : "55", "buffer":"VVVVVVf/////////1VVVVVVVf//////////VVVVVVVXwAAAAAAAAD1VVVVVVV8AAAAAAAAA9VVVVVVV8AAAAAAAAAPVVVVVVVfAAAAAAAAAD1VVVVVVXwAAAAAAAAA9VVVVVVXwAAAAAAAAAPVVVVVVV8AAAAAAAAAD1VVVVVVfAAAAAAAAAA9VVVVVVXwAAAAAAAAAPVVVVVVXwAAAAAAAAAD1VVVVVV8AAAAAAAAAA9VVVVVVfAAAAAAAAAAPVVVVVVXwAAAAAAAAAD1VVVVVXwAAAAAAAAAA9VVVVVV8AAAAAAAAAAPVVVVVVfAAAAAAAAAAD1VVVVVfAAAAAAAAAAA9VVVVVXwAAAAAAAAAAPVVVVVV8AAAAAAAAAAD1VVVVVfAAAAMAAAAAA9VVVVVfAAAAPwAAAAAPVVVVVXwAAAPfAAAAAD1VVVVV8AAAD3wAAAAA9VVVVV8AAAA98AAAAAPVVVVVfAAAAPfAAAAAD1VVVVXwAAAPXwAAAAA9VVVVV8AAAD18AAAAAPVVVVV8AAAA9fAAAAAD1VVVVfAAAAPXwAAAAA9VVVVXwAAAPV8AAAAAPVVVVV8AAAD1fAAAAAD1VVVV8AAAA9XwAAAAA9VVVVfAAAAPV8AAAAAPVVVVXwAAAPVfAAAAAD1VVVXwAAAD1XwAAAAA9VVVV8AAAA9V8AAAAAPVVVVfAAAAPVfAAAAAD1VVVXwAAAD1XwAAAAA9VVVXwAAAD1V8AAAAAPVVVV8AAAA9VfAAAAAD1VVVfAAAAPVXwAAAAA9VVVXwAAAD1V8AAAAAPVVVXwAAAD1VfAAAAAD1VVV8AAAA9VXwAAAAA9VVVfAAAAPVV8AAAAAPVVVfAAAAD1VfAAAAAD1VVXwAAAD1VXwAAAAA9VVV8AAAA9VV8AAAAAPVVVfAAAAPVVfAAAAAD1VVfAAAAD1VXwAAAAA9VVXwAAAA9VV8AAAAAPVVV8AAAA9VVfAAAAAD1VVfAAAAPVVXwAAAAA9VVfAAAAD1VV8AAAAAPVVXwAAAA9VVfAAAAAD1VV8AAAA9VVXwAAAAA9VV8AAAAPVVV8AAAAAPVVfAAAAD1VVfAAAAAD1VXwAAAA9VVXwAAAAA9VV8AAAA9VVV8AAAAAPVV8AAAAPVVVfAAAAAD1VfAAAAD1VVXwAAAAA9VXwAAAA9VVV8AAAAAPVXwAAAA9VVVfAAAAAD1V8AAAAPVVVXwAAAAA9VfAAAAA////wAAAAAD//wAAAAD///wAAAAAAP/8AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAD//////////AAAAAAA///////////8AAAAAA//VVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVf//////1VVVVVVVVVVX//////1VQ=="}, + {"width" : "53", "buffer":"V///////////////1Vf///////////////1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAA/////////9VfAAAAAP/////////VV8AAAAD1VVVVVVVVVVXwAAAAPVVVVVVVVVVVfAAAAA9VVVVVVVVVVV8AAAAD1VVVVVVVVVVXwAAAAPVVVVVVVVVVVfAAAAA9VVVVVVVVVVV8AAAAD1VVVVVVVVVVXwAAAAPVVVVVVVVVVVfAAAAA9VVf/VVVVVVV8AAAAD1X////1VVVVXwAAAAPV/8AP/9VVVVfAAAAA9/AAAAD/VVVV8AAAAA/wAAAAA/VVVXwAAAAAwAAAAAAPVVVfAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9VfAAAAAAA8AAAAAAA9V8AAAAAA//AAAAAAD1XwAAAAAP1/AAAAAAPVfAAAAAD1VfAAAAAA9V8AAAAA9VV8AAAAAA9XwAAAAD1VV8AAAAAD1fAAAAA9VVXwAAAAAPV8AAAAD1VVfAAAAAA9X//////VVVfAAAAAD1f/////9VVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAP///////VVVfAAAAAA///////9VVV8AAAAAD/AAAAAD1VVXwAAAAAP8AAAAAPVVVfAAAAAA/wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAPVVVfAAAAAD3wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAPVVVfAAAAAD3wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAPVVVfAAAAAD3wAAAAA9VVV8AAAAAPfAAAAAD1VVXwAAAAA98AAAAAD1VV8AAAAAD3wAAAAAPVVXwAAAAA9fAAAAAAPVV8AAAAAD18AAAAAAPVfAAAAAAPXwAAAAAAP/wAAAAAA9fAAAAAAAP8AAAAAAD1fAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAPVVV8AAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVVXwAAAAAAAAAAAD1VVVX8AAAAAAAAAAA9VVVVX/AAAAAAAAAA/VVVVVV/wAAAAAAAAP1VVVVVVf8AAAAAAA/1VVVVVVVX////////9VVVVVVVVV///////1VVVVU="}, + {"width" : "53", "buffer":"VVVVVV/////9VVVVVVVVVVX///////1VVVVVVVVX/wAAAAA//VVVVVVVX/AAAAAAAD/VVVVVVV/AAAAAAAAAP1VVVVV/AAAAAAAAAAP1VVVVfwAAAAAAAAAAD1VVVXwAAAAAAAAAAAD1VVVfAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVfAAAAAAD/8AAAAAA9V8AAAAAA//8AAAAAD1XwAAAAAPVV8AAAAAPVfAAAAAD1VXwAAAAA9XwAAAAA9VVXwAAAAD1fAAAAAD1VVfAAAAAPV8AAAAAPVVV8AAAAA9XwAAAAA9VVXwAAAAD1fAAAAAD1VVXwAAAAPV8AAAAAPVVVfAAAAA9XwAAAAA9VVV8AAAAD18AAAAAD1VVXwAAAAPXwAAAAAPVVVf/////9fAAAAAA9VVV//////18AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VX///1VVVV8AAAAAD1f/////VVVXwAAAAAPX/AAAD/1VVfAAAAAA98AAAAAP1VV8AAAAAA/AAAAAAD1VXwAAAAAAwAAAAAAD1VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA98AAAAAAA/wAAAAAAD3wAAAAAAP/wAAAAAAPfAAAAAAD1XwAAAAAA98AAAAAA9VXwAAAAAD3wAAAAAPVVXwAAAAAPfAAAAAA9VVfAAAAAA98AAAAAD1VV8AAAAAA/wAAAAAPVVXwAAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAA98AAAAAD1VV8AAAAAD3wAAAAAPVVXwAAAAAPfAAAAAA9VVfAAAAAA9fAAAAAD1VV8AAAAAD18AAAAAPVVXwAAAAAPXwAAAAAPVV8AAAAAD1fAAAAAAP1fAAAAAAPV8AAAAAAP/wAAAAAA9V8AAAAAAD8AAAAAAD1XwAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAD1VVVfAAAAAAAAAAAA9VVVV/AAAAAAAAAAAPVVVVV/AAAAAAAAAAD1VVVVVfwAAAAAAAAD9VVVVVVf8AAAAAAAD/VVVVVVVX/wAAAAAD/VVVVVVVVV////////VVVVVVVVVVX//////VVVVVU="}, + {"width" : "51", "buffer":"f///////////////9/////////////////8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAA9/////////wAAAAAA9/////////8AAAAAA9VVVVVVVVVfAAAAAA9VVVVVVVVVfAAAAAA9VVVVVVVVV8AAAAAD1VVVVVVVVV8AAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAD1VVVVVVVVX///////1VVVVVVVVX///////VVVVVVVV"}, + {"width" : "52", "buffer":"VVVVVf//////VVVVVVVVVX////////1VVVVVVVf/AAAAAAD/1VVVVVV/wAAAAAAAA/1VVVVX/AAAAAAAAAA/VVVVX8AAAAAAAAAAA9VVVXwAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVV8AAAAAAAAAAAAA9VV8AAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAPXwAAAAAAPwAAAAAAD3wAAAAAAP/wAAAAAA98AAAAAAPV/AAAAAAPfAAAAAAPVV8AAAAAD3wAAAAAPVVfAAAAAA98AAAAAD1VV8AAAAAD/AAAAAD1VVfAAAAAA/wAAAAA9VVXwAAAAAP8AAAAAPVVV8AAAAAD/AAAAAD1VVfAAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVV8AAAAAD/AAAAAD1VVfAAAAAA/wAAAAA9VVXwAAAAAP8AAAAAPVVV8AAAAAPfAAAAAD1VVfAAAAAD3wAAAAAPVVXwAAAAA98AAAAAD1VXwAAAAAPfAAAAAAPVXwAAAAAD18AAAAAA//wAAAAAA9fAAAAAAD/wAAAAAA9XwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAD9VVXwAAAAAAAAAAAD9VVVfAAAAAAAAAAAD1VVVV8AAAAAAAAAAD1VVVV8AAAAAAAAAAAP1VVV8AAAAAAAAAAAA/VVV8AAAAAAAAAAAAA9VV8AAAAAAAAAAAAAD1V8AAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAA9XwAAAAAAP8AAAAAAD18AAAAAA//wAAAAAA9fAAAAAA/VfAAAAAAPfAAAAAA9VV8AAAAAD3wAAAAAPVVXwAAAAA98AAAAAPVVV8AAAAAPfAAAAAD1VVfAAAAAA/wAAAAA9VVXwAAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVVfAAAAAD/AAAAAD1VVXwAAAAA/wAAAAA9VVV8AAAAAP8AAAAAPVVV8AAAAAD/AAAAAD1VVfAAAAAD3wAAAAAPVVXwAAAAA98AAAAAD1VXwAAAAAPfAAAAAAPVV8AAAAAD18AAAAAA/X8AAAAAA9fAAAAAAD/8AAAAAAPXwAAAAAADwAAAAAAPV8AAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VfAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAA9VVfAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAPVVVXwAAAAAAAAAAAPVVVV8AAAAAAAAAAAPVVVVX8AAAAAAAAAAPVVVVVfwAAAAAAAAA/VVVVVVfwAAAAAAAD/VVVVVVV/8AAAAAAP9VVVVVVVV////////1VVVVVVVVVf//////VVVVVQ=="}, + {"width" : "53", "buffer":"VVVVVV/////9VVVVVVVVVVX///////1VVVVVVVVf/wAAAAA//VVVVVVVX/AAAAAAAD/VVVVVVX8AAAAAAAAAP1VVVVV/AAAAAAAAAAP1VVVVfAAAAAAAAAAAD1VVVXwAAAAAAAAAAAD1VVVfAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAPVVXwAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAA9XwAAAAAAP/AAAAAAD1fAAAAAAD//AAAAAAPV8AAAAAA9VfAAAAAAPXwAAAAAPVVfAAAAAA9fAAAAAA9VV8AAAAAD3wAAAAAPVVV8AAAAAPfAAAAAA9VVXwAAAAA98AAAAAD1VVfAAAAAD3wAAAAAPVVV8AAAAAPfAAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAA9VVXwAAAAAP8AAAAAD1VVfAAAAAA/wAAAAAPVVV8AAAAAD/AAAAAAPVVfAAAAAAP8AAAAAA9VV8AAAAAA/wAAAAAA9VfAAAAAAD/AAAAAAA//wAAAAAAPfAAAAAAA/8AAAAAAA98AAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAD1V8AAAAAAAMAAAAAAPVV8AAAAAAD8AAAAAA9VV/AAAAAA98AAAAAD1VV/8AAAA/XwAAAAAPVVVf/////1fAAAAAA9VVVV////1V8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1//////9VVV8AAAAAPX//////1VVXwAAAAA9fAAAAAPVVVfAAAAAD18AAAAA9VVV8AAAAAPXwAAAAD1VVXwAAAAA9fAAAAAPVVVfAAAAAD18AAAAA9VVV8AAAAAPXwAAAAD1VVXwAAAAA9fAAAAAPVVVfAAAAAPV8AAAAAPVVXwAAAAA9XwAAAAA9VVfAAAAAD1fAAAAAA9VXwAAAAAPV8AAAAAA/f8AAAAAA9XwAAAAAA//AAAAAAPVXwAAAAAAMAAAAAAA9VfAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAD1VVV8AAAAAAAAAAAA9VVVV8AAAAAAAAAAAPVVVVV/AAAAAAAAAAD1VVVVV/wAAAAAAAAD9VVVVVVf8AAAAAAAD/VVVVVVVX/wAAAAAP/VVVVVVVVV////////VVVVVVVVVVX/////9VVVVVU="}, + {"width" : "20", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////////////8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA//////////////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP/////////////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="} + ] +} diff --git a/apps/contourclock/fonts/font-ArchivoNarrow.json b/apps/contourclock/fonts/font-ArchivoNarrow.json new file mode 100644 index 000000000..9de7d4c53 --- /dev/null +++ b/apps/contourclock/fonts/font-ArchivoNarrow.json @@ -0,0 +1,17 @@ +{ + "name":"Archivo Narrow", + "size":"100", + "characters":[ + {"width" : "57", "buffer":"VVVVVVX/////1VVVVVVVVVVVX///////1VVVVVVVVVV/8AAAAAP/VVVVVVVVVX8AAAAAAAP9VVVVVVVV/AAAAAAAAA/VVVVVVVX8AAAAAAAAAD1VVVVVVfAAAAAAAAAAA9VVVVVV8AAAAAAAAAAAPVVVVVV8AAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAPVVVV8AAAAAAMAAAAAAPVVVV8AAAAAP//AAAAAD1VVXwAAAAA/3/wAAAAD1VVXwAAAAD1VV8AAAAD1VVXwAAAAPVVVfAAAAA9VVfAAAAA9VVVXwAAAA9VVfAAAAD1VVVXwAAAA9VVfAAAAD1VVVV8AAAAPVV8AAAAPVVVVV8AAAAPVV8AAAAPVVVVVfAAAAPVV8AAAA9VVVVVfAAAAD1V8AAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVXwAAAD1XwAAAA9VVVVVXwAAAD1XwAAAA9VVVVVXwAAAA9XwAAAD1VVVVVXwAAAA9XwAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAP8AAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVV8AAAAPfAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVXwAAAAPfAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9fAAAAD1VVVVVXwAAAA9XwAAAD1VVVVVXwAAAA9XwAAAA9VVVVVXwAAAA9XwAAAA9VVVVVXwAAAA9XwAAAA9VVVVVXwAAAD1XwAAAA9VVVVVXwAAAD1XwAAAA9VVVVVfAAAAD1V8AAAA9VVVVVfAAAAD1V8AAAAPVVVVVfAAAAD1V8AAAAPVVVVVfAAAAPVV8AAAAPVVVVV8AAAAPVVfAAAAD1VVVV8AAAAPVVfAAAAD1VVVXwAAAA9VVfAAAAA9VVVXwAAAA9VVXwAAAAPVVVfAAAAA9VVXwAAAAD1VV8AAAAD1VVXwAAAAA///wAAAAD1VVV8AAAAAP//AAAAAD1VVV8AAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAD1VVVVVfAAAAAAAAAAAPVVVVVVfAAAAAAAAAAA9VVVVVVX8AAAAAAAAAD1VVVVVVV/AAAAAAAAA/VVVVVVVVX/AAAAAAAP9VVVVVVVVV//AAAAAP/VVVVVVVVVVV///////1VVVVVVVVVVVV/////1VVVVVV"}, + {"width" : "60", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV///1VVVVVVVVVVVVVVVf///1VVVVVVVVVVVVVVV/AAD1VVVVVVVVVVVVVV/wAAD1VVVVVVVVVVVVVf/AAAD1VVVVVVVVVVVVX/AAAAD1VVVVVVVVVVVX/wAAAAD1VVVVVVVVVVX/8AAAAAD1VVVVVVVVVX/8AAAAAAD1VVVVVVVVX/8AAAAAAAD1VVVVVVV//8AAAAAAAAD1VVVVVV//8AAAAAAAAAD1VVVVVV/AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAD1VVVVVV///////wAAAAD1VVVVVV///////8AAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVfAAAAD1VVVVVV///////8AAAAA//////////////wAAAAAP//////8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAP////////////////////f///////////////////VVVVVVVVVVVVVVVVVVVV"}, + {"width" : "56", "buffer":"VVVVVVX//////VVVVVVVVVVVf///////9VVVVVVVVVf/AAAAAAP/VVVVVVVVf8AAAAAAAA/1VVVVVVX8AAAAAAAAAP1VVVVVX8AAAAAAAAAAD1VVVVV/AAAAAAAAAAAD1VVVVfAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAA9VV8AAAAAA//wAAAAAA9VfAAAAAAP//8AAAAAD1V8AAAAAD1VX8AAAAAD1XwAAAAA9VVV8AAAAAPV8AAAAAPVVVV8AAAAA9XwAAAAD1VVVV8AAAAD1fAAAAA9VVVVXwAAAAPXwAAAAD1VVVVfAAAAA9fAAAAAPVVVVVfAAAAA98AAAAD1VVVVV8AAAAD3wAAAAPVVVVVXwAAAAPfAAAAA9VVVVVfAAAAA98AAAAPVVVVVV8AAAAD3wAAAA9VVVVVXwAAAAPfAAAAD1VVVVVfAAAAA98AAAAPVVVVVV8AAAAD3wAAAA9VVVVVXwAAAA9fAAAAD1VVVVVfAAAAD18AAAAPVVVVVXwAAAAPXwAAAA9VVVVVfAAAAA9f/////1VVVVV8AAAAD1//////VVVVVXwAAAAPVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVXwAAAAPVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVXwAAAAPVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVXwAAAAD1VVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVV8AAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAPVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAA///////////18AAAAAA///////////fAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA//////////////////////////////////////VVVVVVVVVVVVVVVVVVU="}, + {"width" : "58", "buffer":"VVVVVVX/////9VVVVVVVVVVVV////////VVVVVVVVVVX/wAAAAAP/VVVVVVVVVX8AAAAAAAD/VVVVVVVVfwAAAAAAAAD9VVVVVVVfwAAAAAAAAAD9VVVVVVfAAAAAAAAAAAP1VVVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVXwAAAAAAPAAAAAAA9VVV8AAAAAD//wAAAAAD1VV8AAAAAD/X/AAAAAA9VVfAAAAAD1VV8AAAAAD1VfAAAAAD1VVXwAAAAA9VXwAAAAD1VVVfAAAAAPVV8AAAAD1VVVV8AAAAD1VfAAAAA9VVVVfAAAAAPVXwAAAA9VVVVV8AAAAD1XwAAAAPVVVVVfAAAAA9V8AAAAD1VVVVXwAAAAPVfAAAAA9VVVVV8AAAAD1XwAAAAPVVVVVfAAAAA9V8AAAAD1VVVVXwAAAAPVfAAAAD1VVVVV8AAAAD1XwAAAA9VVVVVfAAAAA9V8AAAAPVVVVVXwAAAAPVf/////1VVVVV8AAAAD1X/////9VVVVVfAAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVV8AAAAD1VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVX8AAAAA9VVVVVVVVVX//8AAAAAPVVVVVVVVVV//wAAAAAPVVVVVVVVVVfAAAAAAA/VVVVVVVVVVXwAAAAAA/VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAA9VVVVVVVVVVVXwAAAAA9VVVVVVVVVVVV8AAAAAD9VVVVVVVVVVVfAAAAAAP9VVVVVVVVVVXwAAAAAAP1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAD1VVVVVVVVV///AAAAAAPVVVVVVVVVf///AAAAAA9VVVVVVVVVVVX/AAAAAPVVVVVVVVVVVVX8AAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAD3//////VVVVVVXwAAAA9//////1VVVVVV8AAAAD/AAAAA9VVVVVVfAAAAA/wAAAAPVVVVVVXwAAAAP8AAAAD1VVVVVV8AAAAD/AAAAA9VVVVVVfAAAAA/wAAAAPVVVVVVXwAAAAP8AAAAD1VVVVVV8AAAAD3wAAAA9VVVVVVfAAAAD18AAAAPVVVVVVXwAAAA9fAAAAA9VVVVVXwAAAAPXwAAAAPVVVVVV8AAAAD18AAAAA9VVVVVfAAAAA9XwAAAAPVVVVVfAAAAAPV8AAAAA9VVVVfAAAAAPVfAAAAAD1VVVfAAAAAD1V8AAAAAP1VV/AAAAAA9VfAAAAAA////AAAAAA9VV8AAAAAA//8AAAAAAPVVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAD1VVVVVX8AAAAAAAAAAP1VVVVVVfwAAAAAAAAAP1VVVVVVVf8AAAAAAAD/VVVVVVVVV//AAAAAA//VVVVVVVVVVf///////1VVVVVVVVVVVX/////9VVVVVVQ=="}, + {"width" : "63", "buffer":"VVVVVVVVf/////VVVVVVVVVVVVVVV//////VVVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVVV8AAAAPVV//9VVVVVVVVVXwAAAA9VX//9VVVVVVVVVXwAAAA9VXwA9VVVVVVVVVXwAAAA9VXwA9VVVVVVVVVfAAAAD1VXwA9VVVVVVVVVfAAAAD1VfAA9VVVVVVVVVfAAAAD1VfAA9VVVVVVVVV8AAAAPVVfAA9VVVVVVVVV8AAAAPVV8AA9VVVVVVVVV8AAAA9VV8AA9VVVVVVVVXwAAAA9VV8AA9VVVVVVVVXwAAAA9VXwAA9VVVVVVVVXwAAAD1VXwAA9VVVVVVVVfAAAAD1VXwAA9VVVVVVVVfAAAAPVVfAAA9VVVVVVVV8AAAAPVVfAAA9VVVVVVVV8AAAA9VVfAAA9VVVVVVVV8AAAA9VV8AAA9VVVVVVVXwAAAA9VV8AAA9VVVVVVVXwAAAD1VXwAAA9VVVVVVVfAAAAD1VXwAAA9VVVVVVVfAAAAPVVXwAAA9VVVVVVVfAAAAPVVfAAAA9VVVVVVV8AAAA9VVfAAAA9VVVVVVV8AAAA9VV8AAAA9VVVVVVXwAAAD1VV8AAAA9VVVVVVXwAAAD1VV8AAAA9VVVVVVXwAAAPVVV8AAAA9VVVVVVfAAAAPVVV8AAAA9VVVVVVfAAAAPVVV8AAAA9VVVVVV8AAAA9VVV8AAAA9VVVVVV8AAAA9VVV8AAAA9VVVVVXwAAAD1VVV8AAAA9VVVVVXwAAAD1VVV8AAAA9VVVVVXwAAAPVVVV8AAAA9VVVVVfAAAA9VVVV8AAAA9VVVVVfAAAA9VVVV8AAAA9VVVVV8AAAD1VVVV8AAAA9VVVVV8AAAD1VVVV8AAAA9VVVVXwAAAPVVVVV8AAAA9VVVVXwAAAPVVVVV8AAAA9VVVVfAAAA9VVVVV8AAAA9VVVVfAAAA9VVVVV8AAAA9VVVV8AAAD1VVVVV8AAAA9VVVV8AAAPVVVVVV8AAAA9VVVXwAAAPVVVVVV8AAAA9VVVXwAAA9VVVVVV8AAAA9VVVfAAAA9VVVVVXwAAAA9VVVfAAAAP//////AAAAAP///8AAAAD/////8AAAAAD///8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAP///////////8AAAAAD///f///////////AAAAAP///VVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVVV/////9VVVVVVVVVVVVVVV/////9VVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "58", "buffer":"VVVVVVVVVVVVVVVVVVVVVf///////////////1VVX///////////////9VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVV8AAAAD//////////1VVfAAAAD//////////1VVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAD1VVVVVVVVVVVVVfAAAA9VVVVVVVVVVVVVXwAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VV///9VVVVVVVfAAAAPVf/////VVVVVVXwAAAD1/8AAAP/VVVVVV8AAAA9/AAAAAD/VVVVVfAAAAD8AAAAAAD9VVVVXwAAAAMAAAAAAAD9VVVV8AAAAAAAAAAAAAP1VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VV8AAAAAAD//AAAAAAD1VfAAAAAAP///AAAAAA9VXwAAAAAP1VX8AAAAAPVV8AAAAAPVVVXwAAAAA9VfAAAAAPVVVVfAAAAAPVXwAAAAD1VVVXwAAAAD1V//wAAD1VVVVfAAAAAPVf////D1VVVVXwAAAAD1VVV///9VVVVVfAAAAA9VVVVVX9VVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA//////9VVVVVVXwAAAAP//////1VVVVVV8AAAAD/AAAAA9VVVVVVfAAAAA/wAAAAPVVVVVVXwAAAAP8AAAAD1VVVVVV8AAAAD/AAAAA9VVVVVVfAAAAA98AAAAPVVVVVVXwAAAA9fAAAAD1VVVVVV8AAAAPXwAAAAPVVVVVV8AAAAD18AAAAD1VVVVVfAAAAA9fAAAAA9VVVVVXwAAAAPXwAAAAD1VVVVXwAAAAPVfAAAAA9VVVVV8AAAAD1XwAAAAD1VVVV8AAAAA9V8AAAAA9VVVV8AAAAA9VXwAAAAD1VVV8AAAAAPVV8AAAAAP1VV8AAAAAD1VXwAAAAA/9f8AAAAAD1VV8AAAAAA//8AAAAAA9VVXwAAAAAAPAAAAAAA9VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAPVVVVVfwAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAPVVVVVVV/AAAAAAAAAAPVVVVVVVX8AAAAAAAAA/VVVVVVVVX8AAAAAAAD/VVVVVVVVVf/wAAAAA/9VVVVVVVVVVf///////1VVVVVVVVVVVV/////9VVVVVVQ=="}, + {"width" : "56", "buffer":"VVVVVVX/////1VVVVVVVVVVVf//////9VVVVVVVVVVf/AAAAAD/VVVVVVVVVX8AAAAAAA/1VVVVVVVX8AAAAAAAAP1VVVVVVV/AAAAAAAAAD1VVVVVVfAAAAAAAAAAD1VVVVVXwAAAAAAAAAAD1VVVVVfAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAPVVVV8AAAAAAwAAAAAA9VVVXwAAAAD//AAAAAA9VVV8AAAAA/3/AAAAAD1VVXwAAAAPVVfAAAAAPVVV8AAAAD1VVfAAAAAPVVXwAAAA9VVVfAAAAA9VVfAAAAPVVVVfAAAAD1VXwAAAA9VVVV8AAAAD1VfAAAAPVVVVXwAAAAPVV8AAAA9VVVVXwAAAA9VfAAAAD1VVVVfAAAAD1V8AAAAPVVVVV8AAAAPVXwAAAD1VVVVXwAAAA9VfAAAAPVVVVVfAAAAA9V8AAAA9VVVVV8AAAAD1fAAAAD1VVVVXwAAAAPV8AAAAPVVVVVXwAAAA9XwAAAA9VVVVVfAAAAD1fAAAAD1VVVVV//////V8AAAAPVVVVVX/////9XwAAAA9VVVVVVVVVVVVfAAAAPVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVfAAAAD1VVVVVVVVVVVV8AAAAPVVVVVVVVVVVVXwAAAA9VVVf//VVVVVVfAAAAD1VX/////VVVVV8AAAAPVX/8AAP/1VVVXwAAAA9V/AAAAAP9VVVfAAAAD1/AAAAAAD9VVV8AAAAPfwAAAAAAA9VVXwAAAAPwAAAAAAAA9VVfAAAAAMAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAPVfAAAAAAAD//AAAAAAPV8AAAAAAA///AAAAAA9XwAAAAAAPVVfAAAAAA9fAAAAAAD1VVfAAAAAD18AAAAAA9VVVfAAAAAPXwAAAAAPVVVVfAAAAAPfAAAAAD1VVVVfAAAAA98AAAAAPVVVVV8AAAAD3wAAAAA9VVVVV8AAAAD/AAAAAPVVVVVXwAAAAP8AAAAA9VVVVVfAAAAA/wAAAAD1VVVVV8AAAAD/AAAAAPVVVVVV8AAAAP8AAAAD1VVVVVXwAAAA/wAAAAPVVVVVVfAAAAD/AAAAA9VVVVVV8AAAAP8AAAAD1VVVVVXwAAAA/wAAAAPVVVVVVfAAAAD/AAAAA9VVVVVV8AAAAPfAAAAD1VVVVVXwAAAA98AAAAPVVVVVVfAAAAD3wAAAA9VVVVVV8AAAAPfAAAAD1VVVVVXwAAAA98AAAAPVVVVVVfAAAAD3wAAAA9VVVVVV8AAAAPXwAAAA9VVVVVXwAAAA9fAAAAD1VVVVV8AAAAD18AAAAPVVVVVXwAAAAPXwAAAA9VVVVVfAAAAD1XwAAAA9VVVVXwAAAAPVfAAAAD1VVVVfAAAAA9V8AAAAD1VVVXwAAAAD1V8AAAAD1VVVfAAAAA9VXwAAAAPVVVXwAAAAD1VfAAAAAP1VX8AAAAA9VVfAAAAAP///AAAAAD1VV8AAAAAD//AAAAAA9VVV8AAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAD1VVVVVfwAAAAAAAAAA9VVVVVVfwAAAAAAAAAPVVVVVVVX8AAAAAAAAP1VVVVVVVX/AAAAAAAP9VVVVVVVVV/8AAAAA/9VVVVVVVVVVf//////9VVVVVVVVVVVV/////1VVVVVU="}, + {"width" : "59", "buffer":"VVVVVVVVVVVVVVVVVVVX///////////////////////////////////////8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAPX////////////8AAAAA9X////////////8AAAAPVVVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVXwAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAD1VVVVVVVVVVVVV8AAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVf//////VVVVVVVVVVVVV//////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "57", "buffer":"VVVVVVf/////1VVVVVVVVVVVf///////1VVVVVVVVVX/wAAAAAP/VVVVVVVVVfwAAAAAAAP1VVVVVVVX8AAAAAAAAA/VVVVVVVfwAAAAAAAAAP1VVVVVV8AAAAAAAAAAA9VVVVVXwAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAPVVVXwAAAAAD/AAAAAAPVVVfAAAAAA//8AAAAAD1VVfAAAAAD9V/AAAAAD1VVfAAAAAPVVXwAAAAA9VV8AAAAA9VVV8AAAAA9VV8AAAAD1VVVfAAAAA9VV8AAAAD1VVVXwAAAAPVXwAAAAPVVVVXwAAAAPVXwAAAAPVVVVV8AAAAPVXwAAAA9VVVVV8AAAAPVXwAAAA9VVVVV8AAAAPVXwAAAA9VVVVV8AAAAPVXwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAD1XwAAAA9VVVVVfAAAAPVXwAAAA9VVVVV8AAAAPVXwAAAA9VVVVV8AAAAPVXwAAAA9VVVVV8AAAAPVV8AAAAPVVVVV8AAAAPVV8AAAAPVVVVXwAAAA9VV8AAAAPVVVVXwAAAA9VVfAAAAD1VVVXwAAAA9VVfAAAAD1VVVfAAAAD1VVfAAAAA9VVV8AAAAD1VVXwAAAAPVVXwAAAAPVVVXwAAAAD///AAAAAPVVVV8AAAAA//8AAAAA9VVVVfAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAPVVVVVV8AAAAAAAAAAA9VVVVVVfAAAAAAAAAAD1VVVVVVXwAAAAAAAAAPVVVVVVVfAAAAAAAAAAD1VVVVVV8AAAAAAAAAAA9VVVVVfwAAAAAAAAAAAP1VVVV/AAAAAAAAAAAAD9VVVXwAAAAAAAAAAAAAPVVVXwAAAAAD/AAAAAAD1VVfAAAAAD///AAAAAD1VV8AAAAAP9V/wAAAAA9VV8AAAAA9VVV8AAAAA9VXwAAAAD1VVVfAAAAAPVXwAAAAPVVVVXwAAAAPVXwAAAA9VVVVV8AAAAD1fAAAAA9VVVVV8AAAAD1fAAAAD1VVVVVfAAAAD1fAAAAD1VVVVVfAAAAA98AAAAD1VVVVVfAAAAA98AAAAD1VVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAAP8AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAPVVVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVfAAAAA98AAAAD1VVVVVfAAAAA98AAAAD1VVVVVfAAAAA9fAAAAA9VVVVV8AAAAA9fAAAAA9VVVVV8AAAAD1fAAAAAPVVVVXwAAAAD1XwAAAAD1VVVfAAAAAD1XwAAAAA/VVV8AAAAAPVXwAAAAAP/X/wAAAAAPVV8AAAAAA///AAAAAAPVV8AAAAAAA8AAAAAAA9VVfAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAD9VVVVXwAAAAAAAAAAAP1VVVVV/AAAAAAAAAAA9VVVVVVfwAAAAAAAAAP1VVVVVVV/AAAAAAAAD/VVVVVVVVf/wAAAAAD/1VVVVVVVVV////////9VVVVVVVVVVVf/////9VVVVVV"}, + {"width" : "57", "buffer":"VVVVVVf/////VVVVVVVVVVVVX///////VVVVVVVVVVV/wAAAAA/9VVVVVVVVVf8AAAAAAA/1VVVVVVVV/AAAAAAAAD9VVVVVVVfwAAAAAAAAAPVVVVVVV/AAAAAAAAAAD9VVVVVXwAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAA9VVVXwAAAAAAMAAAAAAPVVVfAAAAAA//8AAAAAPVVVfAAAAAD/3/AAAAAD1VV8AAAAAPVVXwAAAAD1VV8AAAAA9VVV8AAAAA9VXwAAAAD1VVVfAAAAA9VXwAAAAPVVVVXwAAAA9VXwAAAAPVVVVXwAAAAPVfAAAAA9VVVVV8AAAAPVfAAAAA9VVVVV8AAAAPVfAAAAA9VVVVVfAAAAD1fAAAAD1VVVVVfAAAAD1fAAAAD1VVVVVfAAAAD1fAAAAD1VVVVVfAAAAD18AAAAD1VVVVVfAAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAA98AAAAD1VVVVVXwAAAAP8AAAAD1VVVVVfAAAAAPfAAAAD1VVVVVfAAAAAPfAAAAD1VVVVVfAAAAAPfAAAAD1VVVVVfAAAAAPfAAAAA9VVVVVfAAAAAPfAAAAA9VVVVV8AAAAAPfAAAAA9VVVVV8AAAAAPfAAAAAPVVVVV8AAAAAPXwAAAAPVVVVXwAAAAAPXwAAAAD1VVVXwAAAAAPXwAAAAD1VVVfAAAAAAPV8AAAAA9VVV8AAAAAAPV8AAAAAP1VfwAAAAAAPV8AAAAAD///AAAAAAAPVfAAAAAAP/wAAAAAAAPVfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAPVVfAAAAAAAAADAAAAAPVVXwAAAAAAAAPwAAAAPVVX8AAAAAAAA98AAAAPVVVfAAAAAAAD18AAAAPVVVX8AAAAAA/V8AAAAPVVVV/wAAAAP9V8AAAAPVVVVX/8AA//VV8AAAAPVVVVVf////1VV8AAAAPVVVVVVX//VVVXwAAAAPVVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVXwAAAA9VVVVVVVVVVVVXwAAAA9V//////VVVVVXwAAAA9V//////VVVVVXwAAAA9V8AAAAPVVVVVXwAAAA9V8AAAAPVVVVVXwAAAD1V8AAAAPVVVVVXwAAAD1V8AAAAPVVVVVXwAAAD1V8AAAAPVVVVVXwAAAD1V8AAAAPVVVVVfAAAAD1V8AAAAD1VVVVfAAAAPVV8AAAAD1VVVVfAAAAPVVfAAAAD1VVVVfAAAAPVVfAAAAD1VVVV8AAAAPVVfAAAAA9VVVV8AAAA9VVfAAAAA9VVVXwAAAA9VVXwAAAAPVVVXwAAAA9VVXwAAAAPVVVfAAAAD1VVXwAAAAD9VV8AAAAD1VVV8AAAAA///wAAAAPVVVV8AAAAAD//AAAAAPVVVVfAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAPVVVVVVfAAAAAAAAAAA9VVVVVVXwAAAAAAAAAD1VVVVVVV8AAAAAAAAAPVVVVVVVVfwAAAAAAAD9VVVVVVVVX/AAAAAAA/1VVVVVVVVVf8AAAAA/9VVVVVVVVVVV///////VVVVVVVVVVVVX/////VVVVVVV"}, + {"width" : "20", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP//////9//////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////////////8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA//////////////VVVVVVU="} + ] +} diff --git a/apps/contourclock/fonts/font-Bangers.json b/apps/contourclock/fonts/font-Bangers.json new file mode 100644 index 000000000..73f2ca1dd --- /dev/null +++ b/apps/contourclock/fonts/font-Bangers.json @@ -0,0 +1,17 @@ +{ + "name":"Bangers", + "size":"100", + "characters":[ + {"width" : "60", "buffer":"VVVVVVVVVVVVX//VVVVVVVVVVVVVVVVX////VVVVVVVVVVVVVVX/8AA/1VVVVVVVVVVVVV/8AAAA/VVVVVVVVVVVVX8AAAAAP1VVVVVVVVVVVfAAAAAAA9VVVVVVVVVVX8AAAAAAA9VVVVVVVVVVfwAAAAAAAPVVVVVVVVVV8AAAAAAAAD1VVVVVVVVXwAAAAAAAAD1VVVVVVVVfAAAAAAAAAA9VVVVVVVV8AAAAAAAAAA9VVVVVVVXwAAAAAAAAAA9VVVVVVVXwAAAAAAAAAAPVVVVVVVfAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAA9VVVXwAAAAAAD/wAAAAA9VVVfAAAAAAA//8AAAAA9VVVfAAAAAAP9VfAAAAAPVVV8AAAAAA/VVXwAAAAPVVV8AAAAAD1VVV8AAAAPVVV8AAAAAD1VVVfAAAAPVVXwAAAAAPVVVVfAAAAPVVXwAAAAA9VVVVXwAAAPVVfAAAAAD1VVVVXwAAAPVVfAAAAAD1VVVVXwAAAPVV8AAAAAPVVVVVXwAAAPVV8AAAAA9VVVVVV8AAA9VV8AAAAA9VVVVVV8AAA9VXwAAAAD1VVVVVV8AAA9VXwAAAAD1VVVVVV8AAA9VXwAAAAPVVVVVVV8AAA9VfAAAAAPVVVVVVV8AAA9VfAAAAAPVVVVVVV8AAA9VfAAAAA9VVVVVVV8AAA9VfAAAAA9VVVVVVV8AAA9V8AAAAA9VVVVVVV8AAA9V8AAAAD1VVVVVVXwAAD1V8AAAAD1VVVVVVXwAAD1V8AAAAD1VVVVVVXwAAD1XwAAAAPVVVVVVVXwAAD1XwAAAAPVVVVVVVXwAAD1XwAAAAPVVVVVVVfAAAD1XwAAAAPVVVVVVVfAAAPVXwAAAA9VVVVVVVfAAAPVXwAAAA9VVVVVVVfAAAPVfAAAAA9VVVVVVV8AAAPVfAAAAA9VVVVVVV8AAA9VfAAAAA9VVVVVVV8AAA9VfAAAAA9VVVVVVXwAAA9VfAAAAD1VVVVVVXwAAA9VfAAAAD1VVVVVVfAAAD1VfAAAAD1VVVVVVfAAAD1VfAAAAD1VVVVVV8AAAD1V8AAAAD1VVVVVV8AAAPVV8AAAAD1VVVVVXwAAAPVV8AAAAD1VVVVVfAAAAPVV8AAAAD1VVVVVfAAAA9VV8AAAAA9VVVVV8AAAA9VV8AAAAA9VVVVXwAAAA9VV8AAAAA9VVVVfAAAAD1VV8AAAAA9VVVV8AAAAD1VV8AAAAAPVVVXwAAAAPVVVfAAAAAD1VV/AAAAAPVVVfAAAAAA9Vf8AAAAA9VVVfAAAAAAP//AAAAAA9VVVfAAAAAAD/wAAAAAD1VVVfAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAPVVVVVVVXwAAAAAAAAAA9VVVVVVVXwAAAAAAAAAD1VVVVVVVV8AAAAAAAAAPVVVVVVVVVfAAAAAAAAA9VVVVVVVVVXwAAAAAAAP1VVVVVVVVVV8AAAAAAD/VVVVVVVVVVVfwAAAAA/1VVVVVVVVVVVX/wAAA/9VVVVVVVVVVVVVf/////VVVVVVVVVVVVVVVf///VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "40", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/VVVVVVVVVVf//1VVVVVVVV///89VVVVVVf///AAPVVVVX///8AAAPVVVf///AAAAAD1Vf//wAAAAAAA9V//AAAAAAAAAPVfAAAAAAAAAAPVXwAAAAAAAAAD1V8AAAAAAAAAA9VfAAAAAAAAAAPVfAAAAAAAAAAD1XwAAAAAAAAAA9V8AAAAAAAAAA9VfAAAAAAAAAAPVXwAAAAAAAAAD1V8AAAAAAAAAA9VfAAAAAAAAAAPVXwAAAAAAAAAPVXwAAAAAAAAAD1V8AAAAAAAAAA9VfAAAAAAAAAAPVXwAAAAAAAAAD1V8AAAAAAAAAD1VfAAAAAAAAAA9VXwAAAAAAAAAPVXwAD/8AAAAAD1V8P///wAAAAA9Vf//1VfAAAAA9VX/VVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAA9VVVVVVXwAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVV8AAAAAPVVVVVVfAAAAAD1VVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVVfAAAAAPVVVVVVXwAAAAD1VVVVVXwAAAAA9VVVVVV8AAAAAPVVVVVVfAAAAAD1VVVVVXwAAAAD1VVVVVV8AAAAA9VVVVVV8AAAAAPVVVVVVfAAAAAD1VVVVVXwAAAAA9VVVVVV8AAAAAPVVVVVVfAAAAAPVVVVVVfAAAAAD1VVVVVXwAAAAA9VVVVVV8AAAAAPVVVVVVfAAAAAD1VVVVVXwAAAAA9VVVVVXwAAAAD/VVVVVV8AAAD//VVVVVVfAAD//1VVVVVVXwD//1VVVVVVVV///1VVVVVVVVV//1VVVVVVVVVVXVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "65", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX///1VVVVVVVVVVVVVVVV//////VVVVVVVVVVVVVVV//AAAD/9VVVVVVVVVVVVV/wAAAAAP9VVVVVVVVVVVV/wAAAAAAA/VVVVVVVVVVV/wAAAAAAAA/VVVVVVVVVVfwAAAAAAAAAPVVVVVVVVVXwAAAAAAAAAAPVVVVVVVVX8AAAAAAAAAAAPVVVVVVVV/AAAAAAAAAAAA9VVVVVVVXwAAAAAAAAAAAA9VVVVVVV8AAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAA9VVVXwAAAAAAAMAAAAAAAD1VVVfAAAAAAA//AAAAAAAPVVVXwAAAAAAP9/AAAAAAA9VVVfAAAAAAD1VfAAAAAAD1VVV8AAAAAA9VVfAAAAAAPVVVXwAAAAAPVVV8AAAAAA9VVVfAAAAAA9VVV8AAAAAD1VVXwAAAAAPVVVXwAAAAAPVVVfAAAAAA9VVVfAAAAAA9VVV8AAAAAD1VVV8AAAAAD1VVXwAAAAAPVVVXwAAAAAPVVVfAAAAAA9VVVfAAAAAA9VVV8AAAAAD1VVV8AAAAAPVVVXwAAAAD/VVVfAAAAAA9VVVfAA////9VVXwAAAAAD1VVV//////VVVVfAAAAAAPVVVX//1VVVVVVXwAAAAAD1VVVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVXwAAAAAAPVVVVVVVVVVVVV8AAAAAAD1VVVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVV8AAAAAAAPVVVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVX8AAAAAAAD1VVVVVVVVVVV/AAAAAAAA9VVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVXwAAAAAAAD1VVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVV/AAAAAAAAD1VVVVVVVVVVfwAAAAAAAA9VVVVVVVVVVXwAAAAAAAAPVVVVVVVVVVV8AAAAAAAAD1VVVVVVVVVVfAAAAAAAAA9VVVVVVVVVVXwAAAAAAAA/VVVVVVVVVVV8AAAAAAAAP1VVVVVVVVVVfAAAAAAAAD1VVVVVVVVVVXwAAAAAAAA9VVVVVVVVVVV8AAAAAAAAPVVVVVVVVVVVfAAAAAAAAP1VVVVVVVVVVXwAAAAAAAD9VVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVfAAAAAAAA/VVVVVVVVVVVXwAAAAAAAP1VVVVVVVVVVV8AAAAAAAP1VVVVVVVVVVVfAAAAAAAD9VVVVVVVVVVVV8AAAAAAD9VVVVVVVVVVVVfAAAAAAA/VVVVVVVVVVVVXwAAAAAA/VVVVVVVVVVVVV8AAAAAAP1VVVVVVVVVVVVXwAAAAAD1VVVVVV//1VVVV8AAAAAA9VVf//////VVVVXwAAAAAA///////wA9VVVV8AAAAAAA//8AAAAAD1VVVXwAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAD9VVVV8AAAAAAAAAD//////1VVVXwAAAAAA/////////VVVVVfAA////////VVVVVVVVVVV///////1VVVVVVVVVVVVVX//1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "62", "buffer":"VVVVVVVVVVVVVVVV////1VVVVVVVX////////////1VVVX///////////wAAAPVVVVf///AAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAD1VVVVfAAA//wAAAAAAAAPVVVVV//////wAAAAAAAA9VVVVX///1VXwAAAAAAAD1VVVVVVVVVV8AAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVXwAAAAAAAAP1VVVVVVVVV8AAAAAAAAD9VVVVVVVVVfAAAAAAAAD9VVVVVVVVVfwAAAAAAAD/VVVVVVVVVX8AAAAAAAA/VVVVVVVVVV8AAAAAAAA/VVVVVVVVVVfAAAAAAAA/1VVVVVVVVVfwAAAAAAAP1VVVVVVVVVX8AAAAAAAP1VVVVVVVVVV8AAAAAAAP9VVVVVVVVVV/AAAAAAAP9VVVVVVVVVVfwAAAAAAD9VVVVVVVVVVfwAAAAAAD9VVVVVVVVVVX8AAAAAAA/VVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVV8AAAAAAAP1VVVVVVVVVVXwAAAAAAAP/1VVVVVVVVV8AAAAAAAAD/9VVVVVVVVXwAAAAAAAAAD/VVVVVVVVfAAAAAAAAAAA/1VVVVVVV8AAAAAAAAAAAP1VVVVVVXwAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAD/8AAAAAAAAD1VVVV8AP////8AAAAAAAPVVVVX////VV//AAAAAAAPVVVVf/9VVVVV/AAAAAAA9VVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVfAAAAAD1VVVVVf//9VVVXwAAAAA9VVf//////9VVV8AAAAAD1VV///8AAD1VVfAAAAAA9VVfAAAAAAD1VXwAAAAAD1VV8AAAAAAD1X8AAAAAA9VVXwAAAAAAD//AAAAAAD1VVfAAAAAAAD/AAAAAAA9VVV8AAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAP1VVVVVVV8AAAAAAAAAAD9VVVVVVVV8AAAAAAAAAD9VVVVVVVVV8AAAAAAAAD/VVVVVVVVVV/AAAAAAAD/VVVVVVVVVVV/wAAAAAP/VVVVVVVVVVVVf///////VVVVVVVVVVVVVX/////9VVVVVVVVVVU="}, + {"width" : "59", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/1VVVVVVVVVVVVf//////VVVVVVVVVVVVV////8D1VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVV8AAAAD1VVVVVf/1VVVVXwAAAAPVVVf////VVVVVfAAAAA9V////8A9VVVVXwAAAAD1f/8AAAD1VVVVfAAAAA9V8AAAAAPVVVVV8AAAAD1XwAAAAA9VVVVXwAAAAPV8AAAAAPVVVVV8AAAAD1XwAAAAA9VVVVXwAAAAPVfAAAAAD1VVVVfAAAAA9V8AAAAAPVVVVXwAAAAPVXwAAAAA9VVVVfAAAAA9V8AAAAAD1VVVV8AAAAD1XwAAAAA9VVVVXwAAAA9VfAAAAAD1VVVV8AAAAD1V8AAAAAPVVVVXwAAAAPVXwAAAAA9VVVVfAAAAA9VfAAAAAD1VVVXwAAAAPVXwAAAAAPVVVVfAAAAA9VfAAAAAD1VVVV8AAAAD1V8AAAAAPVVVVfAAAAA9VXwAAAAA9VVVV8AAAAD1VfAAAAAD1VVVXwAAAAPVV8AAAAAPVVVVfAAAAD1VfAAAAAA9VVVXwAAAAPVV8AAAAAD1VVVfAAAAA9VXwAAAAA9VVVV8AAAAD1VfAAAAAD1VVVfAAAAA9VV8AAAAAPVVVV8AAAAD1VXwAAAAA9VVVXwAAAAPVV8AAAAAD1VVV8AAAAD1VXwAAAAAPVVVXwAAAAPVVfAAAAAD1VVVfAAAAA9VV8AAAAAPVVVV8AAAAD1VXwAAAAA9VVVfAAAAA9VVfAAAAAD1VVV8AAAAD1VXwAAAAAPVVVXwAAAAPVVfAAAAAD1VVV8AAAAD1VV8AAAAAPVVVXwAAAAPVVXwAAAAA9VVVfAAAAA9VVfAAAAAD1VVXwAAAAA///wAAAAAD//VfAAAAAA//8AAAAAAD//V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAPVVV8AAAAA/wAAAAAAD/9VVXwD/////wAAAAAA//1VVX/////1XwAAAAAPVVVVVf/VVVVVfAAAAAD1VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVXwAAAAAD1VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVV8AAAAAA9VVVVVVVVVVVXwAAAA//VVVVVVVVVVVVfAAA///1VVVVVVVVVVVV8A///1VVVVVVVVVVVVVX///1VVVVVVVVVVVVVVVf/1VVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "57", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX////VVVVVVf////////////VVVX//////////8AAAPVVVf//wAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAD1VVfAAAAAAAD///////1VVfAAAAAA/////////VVVfAAAAAD/9VVVVVVVVVVfAAAAAPVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVXwAAAAAP/1VVVVVVVVVXwAAAAAD///1VVVVVVVXwAAAAAAAP//9VVVVVVXwAAAAAAAAAP/1VVVVVXwAAAAAAAAAAD/VVVVVXwAAAAAAAAAAAP9VVVVfAAAAAAAAAAAAA/VVVVfAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAPVV8AAP///8AAAAAAAAPVV8P//////8AAAAAAAPVV///1VVVX/AAAAAAAPVV/1VVVVVVXwAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAA9V//1VVVVVVVXwAAAAA9V/////1VVVVXwAAAAA9V8AP///1VVVXwAAAAA9V8AAAAP1VVVfAAAAAD1V8AAAAD1VVVfAAAAAD1V8AAAAD1VVV8AAAAAD1V8AAAAD1VVV8AAAAAPVV8AAAAD1VVXwAAAAAPVV8AAAAA9VVfAAAAAA9VV8AAAAAPVX8AAAAAA9VV8AAAAAD//wAAAAAA9VV8AAAAAA/8AAAAAAD1VV8AAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAA9VVVVVXwAAAAAAAAAAD1VVVVVXwAAAAAAAAAAPVVVVVVV8AAAAAAAAAA9VVVVVVVfAAAAAAAAAD1VVVVVVVfAAAAAAAAAPVVVVVVVVXwAAAAAAAD9VVVVVVVVV8AAAAAAAP1VVVVVVVVVfwAAAAAD9VVVVVVVVVVX/AAAAA/1VVVVVVVVVVVf/AAD/9VVVVVVVVVVVVV/////VVVVVVVVVVVVVVV//9VVVVVVVVVVV"}, + {"width" : "59", "buffer":"VVVVVVVVVVVX///1VVVVVVVVVVVVVVf/////VVVVVVVVVVVVVf/AAAD/VVVVVVVVVVVVf8AAAAAPVVVVVVVVVVVf8AAAAAAPVVVVVVVVVVX8AAAAAAAPVVVVVVVVVX8AAAAAAAAPVVVVVVVVV/AAAAAAAAAPVVVVVVVVfAAAAAAAAAAPVVVVVVVXwAAAAAAAAAA9VVVVVVV8AAAAAAAAAAA9VVVVVVfAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAA/AAAAAA9VVV8AAAAAAAP/AAAAAD1VVXwAAAAAAD1fAAAAD/VVV8AAAAAAA9VfAAAD/VVVXwAAAAAAPVVfAAA/VVVV8AAAAAAD1VV8AA/VVVVXwAAAAAAPVVXwA/1VVVV8AAAAAAD1VVfA/1VVVVXwAAAAAAPVVVf/1VVVVV8AAAAAAD1VVV/1VVVVVXwAAAAAAPVVVX1VVVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAA9VVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVfAAAAAAD3////1VVVVVV8AAAAAAD//////VVVVVXwAAAAAADAAAAD/VVVVV8AAAAAAAAAAAAAP1VVVXwAAAAAAAAAAAAAP1VVVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAP///8AAAAAPVfAAAAAAD/////AAAAA9V8AAAAAA9VVVV/AAAAD1XwAAAAAPVVVVVfAAAAPVfAAAAAA9VVVVV8AAAA9V8AAAAAD1VVVVV8AAAD1XwAAAAAPVVVVVXwAAAPVfAAAAAA9VVVVVfAAAA9V8AAAAAA9VVVVV8AAAD1V8AAAAAD1VVVVfAAAAPVXwAAAAAPVVVVV8AAAA9VfAAAAAA9VVVVfAAAAD1V8AAAAAD1VVVV8AAAAPVXwAAAAAD1VVVfAAAAA9VfAAAAAAPVVVfwAAAAD1VfAAAAAAPVVX8AAAAAPVV8AAAAAAP1/8AAAAAD1VXwAAAAAAP//AAAAAAPVVfAAAAAAADwAAAAAAA9VVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAA9VVVVVVXwAAAAAAAAAA/VVVVVVVX8AAAAAAAAAP1VVVVVVVX8AAAAAAAAP1VVVVVVVVV/AAAAAAAP9VVVVVVVVVV/8AAAAA/9VVVVVVVVVVVf/8AA//9VVVVVVVVVVVVV/////1VVVVVVVVVVVVVVV//1VVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "53", "buffer":"VVVVVVVVVVVVVV///9VVVVVVVVVV///////VVVVVVVf//////wAA9VVVf//////wAAAAAD1VVV///8AAAAAAAAA9VVVfAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAPVVVXwAAAAAAAAAAAD1VVVfAAAAAAAAAAAAPVVVV8AAAAAAAAAAAA9VVVXwAAAAAAAAAAAPVVVV8AAAAAAAAAAAA9VVVXwAAAAAAAAAAAD1VVVfAAAAAAAAAAAAPVVVV8AAAAAAAAAAAD1VVVXwAAAAAAAAAAAPVVVVfAAAAAAAAAAAA9VVVV8AAAAAAAAAAAPVVVVfAAAAAAAAAAAA9VVVV8AAAAAAAAAAAD1VVVXwAAAAAAAAAAA9VVVVfAAAAAAAAAAAD1VVVV8AAAAAAAAAAAPVVVVXwAAA/AAAAAAD1VVVV8AA///AAAAAAPVVVVXz///1fAAAAAA9VVVVf//1VV8AAAAAPVVVVV/VVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAA9VVVVVVVVVV8AAAAP/1VVVVVVVVVXwD////9VVVVVVVVVV/////9VVVVVVVVVVVX//VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "69", "buffer":"VVVVVVVVVVVVV//////VVVVVVVVVVVVVVVV////////VVVVVVVVVVVVVVf/AAAAAA/1VVVVVVVVVVVVX/AAAAAAAA/VVVVVVVVVVVV/wAAAAAAAAP1VVVVVVVVVVf8AAAAAAAAAA9VVVVVVVVVV/AAAAAAAAAAA9VVVVVVVVVfwAAAAAAAAAAAPVVVVVVVVV/AAAAAAAAAAAAD1VVVVVVVXwAAAAAAAAAAAAD1VVVVVVVfAAAAAAAAAAAAAA9VVVVVVV8AAAAAAAAAAAAAA9VVVVVVXwAAAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAAPVVVVXwAAAAAAD//wAAAAAAPVVVVXwAAAAAA////AAAAAAPVVVVfAAAAAAD9VVfwAAAAAPVVVVfAAAAAAPVVVV8AAAAAPVVVVfAAAAAA9VVVVfAAAAAPVVVVfAAAAAD1VVVVfAAAAAPVVVV8AAAAAPVVVVVXwAAAAPVVVV8AAAAAPVVVVVXwAAAA9VVVV8AAAAA9VVVVVXwAAAA9VVVV8AAAAA9VVVVVfAAAAA9VVVV8AAAAA9VVVVVfAAAAD1VVVV8AAAAA9VVVVVfAAAAD1VVVV8AAAAA9VVVVV8AAAAPVVVVV8AAAAA9VVVVXwAAAAPVVVVVfAAAAA9VVVVXwAAAA9VVVVVfAAAAA9VVVVfAAAAA9VVVVVfAAAAAPVVVV8AAAAD1VVVVVfAAAAAD1VVfwAAAAPVVVVVVXwAAAAA9VV/AAAAA9VVVVVVXwAAAAAP//wAAAAD1VVVVVVV8AAAAAD//AAAAAPVVVVVVVVfAAAAAAAAAAAAD9VVVVVVVVfAAAAAAAAAAAAP1VVVVVVVVXwAAAAAAAAAAA9VVVVVVVVVV8AAAAAAAAAAA9VVVVVVVVVVfAAAAAAAAAAA9VVVVVVVVVX8AAAAAAAAAAAPVVVVVVVVV/wAAAAAAAAAAAD1VVVVVVVX8AAAAAAAAAAAAD1VVVVVVVfAAAAAAAAAAAAAA9VVVVVVX8AAAAAAAAAAAAAA9VVVVVVfwAAAAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAAAAAD1VVVVXwAAAAAAP//AAAAAAD1VVVVfAAAAAAD///8AAAAAD1VVVV8AAAAAA/1VV/AAAAAA9VVVV8AAAAAD9VVVXwAAAAA9VVVXwAAAAA/VVVVV8AAAAA9VVVXwAAAAD9VVVVV8AAAAA9VVVfAAAAAPVVVVVVfAAAAA9VVVfAAAAA9VVVVVVfAAAAAPVVV8AAAAD1VVVVVVXwAAAAPVVV8AAAAD1VVVVVVXwAAAAPVVXwAAAAPVVVVVVVXwAAAAPVVXwAAAA9VVVVVVVXwAAAAPVVXwAAAA9VVVVVVVXwAAAAPVVXwAAAD1VVVVVVVXwAAAAPVVfAAAAD1VVVVVVVXwAAAAPVVfAAAAD1VVVVVVVXwAAAA9VVfAAAAD1VVVVVVVXwAAAA9VVfAAAAD1VVVVVVVfAAAAA9VVfAAAAD1VVVVVVVfAAAAA9VV8AAAAD1VVVVVVV8AAAAA9VV8AAAAD1VVVVVVV8AAAAA9VV8AAAAA9VVVVVVXwAAAAD1VV8AAAAA9VVVVVVfAAAAAD1VV8AAAAAPVVVVVX8AAAAAPVVV8AAAAAD9VVVV/wAAAAAPVVV8AAAAAA/////8AAAAAA9VVV8AAAAAAD////AAAAAAA9VVV8AAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAA9VVVVVV8AAAAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAAAAPVVVVVVVfAAAAAAAAAAAAAA9VVVVVVVfAAAAAAAAAAAAAP1VVVVVVVXwAAAAAAAAAAAA/VVVVVVVVV/AAAAAAAAAAAP1VVVVVVVVVf8AAAAAAAAAD/VVVVVVVVVVV/wAAAAAAAD/1VVVVVVVVVVVX/wAAAAAD/9VVVVVVVVVVVVVf///////9VVVVVVVVVVVVVVVf/////9VVVVVVVVVVV"}, + {"width" : "58", "buffer":"VVVVVVV/////9VVVVVVVVVVVVf///////VVVVVVVVVVV/8AAAAAP/VVVVVVVVVX/AAAAAAAD9VVVVVVVVX8AAAAAAAAD9VVVVVVVfwAAAAAAAAAP1VVVVVVfwAAAAAAAAAAPVVVVVVfAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VXwAAAAAAA/wAAAAAAPVXwAAAAAAD//wAAAAAD1V8AAAAAAP9V/AAAAAAPVfAAAAAAP1VV8AAAAAD1fAAAAAAPVVVXwAAAAA9XwAAAAAPVVVV8AAAAAD18AAAAAPVVVVXwAAAAA9fAAAAAD1VVVV8AAAAAPXwAAAAD1VVVVfAAAAAD18AAAAA9VVVVXwAAAAAPfAAAAAPVVVVVfAAAAAD/AAAAAD1VVVVXwAAAAA/wAAAAA9VVVVV8AAAAAP8AAAAAPVVVVV8AAAAAD/AAAAAD1VVVVfAAAAAA/wAAAAAPVVVVXwAAAAAP8AAAAAA9VVVXwAAAAAD3wAAAAAD1VVXwAAAAAA98AAAAAAP1VfwAAAAAAPfAAAAAAA///wAAAAAAD3wAAAAAAA//AAAAAAAA98AAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAD1VV/AAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAPVVVVfwAAAAA8AAAAAAD1VVVV/8AAAP/wAAAAAA9VVVVV/////9fAAAAAAPVVVVVVf///VXwAAAAAD1VVVVVVVVVVV8AAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVfwAAAAAAPVVVVVVVVVVfwAAAAAAD1VVVVVVVVVfAAAAAAAD1VVVVVVVVV/AAAAAAAD1VVVVVVVVf/AAAAAAAA9VVVVVVV//8AAAAAAAA9VVVVVVV//AAAAAAAAAPVVVVVVVfAAAAAAAAAAPVVVVVVVXwAAAAAAAAAPVVVVVVVV8AAAAAAAAAPVVVVVVVVfAAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVVfAAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAP1VVVVVVVVVXwAAAAAAP1VVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAA/VVVVVVVVVVVXwAAAAD/VVVVVVVVVVVV8AAAAP9VVVVVVVVVVVVfAAAA/1VVVVVVVVVVVVXwAAP/VVVVVVVVVVVVVV8AD/9VVVVVVVVVVVVVVf///VVVVVVVVVVVVVVVX//1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "28", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX9VVVVVVf//VVVX////z1VVX///AA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAD1VVV8AAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAD1VVV8AAAA9VVVfAAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAPVVVXwAAAD1VVV8AAAA9VVVfAAAAPVVVf/////1VVX/////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/////1VVX/////9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AAAAPVVVfAAAAD1VVXwAAAA9VVV8AD//9VVV8P////VVVf//1VVVVVX/VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="} + ] +} diff --git a/apps/contourclock/font-BarlowCond.json b/apps/contourclock/fonts/font-BarlowCond.json similarity index 99% rename from apps/contourclock/font-BarlowCond.json rename to apps/contourclock/fonts/font-BarlowCond.json index 2388b7383..bc1d9ddf3 100644 --- a/apps/contourclock/font-BarlowCond.json +++ b/apps/contourclock/fonts/font-BarlowCond.json @@ -1,5 +1,5 @@ { - "name":"BarlowCond", + "name":"Barlow Cond", "size":"100", "characters":[ {"width" : "61", "buffer":"VVVVVVX//////1VVVVVVVVVVVVf///////9VVVVVVVVVVX/wAAAAAA/9VVVVVVVVVX/AAAAAAAAP9VVVVVVVVfwAAAAAAAAAP9VVVVVVVfwAAAAAAAAAAP1VVVVVV/AAAAAAAAAAAAPVVVVVV/AAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAA/AAAAAAAAD3wAAAAAAAA/8AAAAAAAA98AAAAAAAA9XwAAAAAAAD/AAAAAAAAPV8AAAAAAAA/wAAAAAAAPVfAAAAAAAAP8AAAAAAAD1XwAAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9VfAAAAAAAA/wAAAAAAAPVXwAAAAAAAP8AAAAAAAD1V8AAAAAAAD/AAAAAAAA9V8AAAAAAAA/wAAAAAAAPVfAAAAAAAAP8AAAAAAAA9XwAAAAAAAD/AAAAAAAAPXwAAAAAAAD3wAAAAAAAA/wAAAAAAAA9fAAAAAAAADwAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAP1VVVVVVfwAAAAAAAAAAP1VVVVVVV/wAAAAAAAAA/VVVVVVVVV/wAAAAAAAD/VVVVVVVVVV/////////9VVVVVVVVVVV////////1VVVVVQ=="}, diff --git a/apps/contourclock/font-BebasNeue.json b/apps/contourclock/fonts/font-BebasNeue.json similarity index 99% rename from apps/contourclock/font-BebasNeue.json rename to apps/contourclock/fonts/font-BebasNeue.json index 1109f1523..8be708322 100644 --- a/apps/contourclock/font-BebasNeue.json +++ b/apps/contourclock/fonts/font-BebasNeue.json @@ -1,5 +1,5 @@ { - "name":"BebasNeue", + "name":"Bebas Neue", "size":"98", "characters":[ {"width" : "47", "buffer":"VVVVf//////VVVVVVVVf///////1VVVVVVf8AAAAAAP9VVVVVX8AAAAAAAD9VVVVV8AAAAAAAAA/VVVV/AAAAAAAAAA/VVVXwAAAAAAAAAAPVVV8AAAAAAAAAAA9VVfAAAAAAAAAAAA9VXwAAAAAAAAAAAA9VfAAAAAAAAAAAAD1V8AAAAAAAAAAAAPVfAAAAAAAAAAAAAPV8AAAAAP/wAAAAA9fAAAAAD//wAAAAA98AAAAA9VXwAAAAD3wAAAAPVVXwAAAAPfAAAAD1VVXwAAAA98AAAA9VVVXwAAAA/wAAAD1VVVfAAAAD/AAAAPVVVV8AAAAP8AAAA9VVVXwAAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAPVVVVXwAAAD/AAAA9VVVVfAAAAP8AAAD1VVVV8AAAA/wAAAD1VVVXwAAAD/AAAAPVVVV8AAAAP8AAAA9VVVXwAAAA/wAAAD1VVVfAAAAD/AAAAD1VVXwAAAA98AAAAPVVVfAAAAD3wAAAAP1VfwAAAAPfAAAAAP//8AAAAA9fAAAAAD/8AAAAAD18AAAAAAAAAAAAA9XwAAAAAAAAAAAAD1XwAAAAAAAAAAAA9VfAAAAAAAAAAAAD1VfAAAAAAAAAAAA9VVfAAAAAAAAAAAPVVV8AAAAAAAAAAA9VVV8AAAAAAAAAAPVVVV/AAAAAAAAAP1VVVV/AAAAAAAAD9VVVVVfwAAAAAAD9VVVVVVf////////VVVVVVVX///////VVVVQ" }, diff --git a/apps/contourclock/font-Dekko.json b/apps/contourclock/fonts/font-Dekko.json similarity index 100% rename from apps/contourclock/font-Dekko.json rename to apps/contourclock/fonts/font-Dekko.json diff --git a/apps/contourclock/font-DinAlternate.json b/apps/contourclock/fonts/font-DinAlternate.json similarity index 100% rename from apps/contourclock/font-DinAlternate.json rename to apps/contourclock/fonts/font-DinAlternate.json diff --git a/apps/contourclock/fonts/font-FjallaOne.json b/apps/contourclock/fonts/font-FjallaOne.json new file mode 100644 index 000000000..2ff8240e0 --- /dev/null +++ b/apps/contourclock/fonts/font-FjallaOne.json @@ -0,0 +1,17 @@ +{ + "name":"Fjalla One", + "size":"100", + "characters":[ + {"width" : "49", "buffer":"VVVVV//////9VVVVVVVVX///////9VVVVVVVf8AAAAAAP9VVVVVV/wAAAAAAAP1VVVVV/AAAAAAAAAPVVVVV8AAAAAAAAAA9VVVV8AAAAAAAAAAD1VVVfAAAAAAAAAAAPVVVfAAAAAAAAAAAA9VVfAAAAAP/wAAAAPVVXwAAAA///wAAAA9VXwAAAA/VV/AAAAPVV8AAAA9VVV8AAAA9V8AAAA9VVVXwAAAPVfAAAA9VVVVfAAAD1XwAAA9VVVVXwAAAPXwAAAPVVVVV8AAAD18AAAD1VVVVXwAAA9fAAAA9VVVVV8AAAPXwAAA9VVVVVfAAAA98AAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAPfAAAD1VVVVV8AAAD3wAAA9VVVVVfAAAA98AAAPVVVVVXwAAA9fAAAA9VVVVV8AAAPXwAAAPVVVVVfAAAD1fAAAD1VVVVfAAAA9XwAAAPVVVVXwAAA9V8AAAD1VVVXwAAAPVXwAAAPVVVXwAAAD1V8AAAA/VVXwAAAD1VXwAAAD///wAAAA9VV8AAAAD//wAAAA9VVXwAAAAAAAAAAAPVVV8AAAAAAAAAAAPVVVXwAAAAAAAAAAPVVVVfAAAAAAAAAAPVVVVV8AAAAAAAAAPVVVVVX8AAAAAAAA/VVVVVVf8AAAAAAA/VVVVVVVf8AAAAAP9VVVVVVVVf//////9VVVVVVVVVf/////VVVVVQ=="}, + {"width" : "46", "buffer":"VVVVVVVf///9VVVVVVVVVVf////VVVVVVVVVVfAAAD1VVVVVVVVVfAAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVXwAAAA9VVVVVVVVXwAAAAPVVVVVVVVXwAAAAD1VVVVVVVXwAAAAA9VVVVVVVXwAAAAAPVVVVVVVXwAAAAAD1VVVVVVXwAAAAAA9VVVVVVXwAAAAAAPVVVVVVXwAAAAAAD1VVVVVXwAAAAAAA9VVVVVXwAAAAAAAPVVVVVXwAAAAAAAD1VVVVXwAAAAAAAA9VVVVXwAAAAAAAAPVVVVXwAAADAAAAD1VVVXwAAAD8AAAA9VVVXwAAAP3wAAAPVVVXwAAAP18AAAD1VVVfAAAPVfAAAA9VVVV8AAPVXwAAAPVVVVXwAPVV8AAAD1VVVV8APVVfAAAA9VVVVXwPVVXwAAAPVVVVVf/VVV8AAAD1VVVVV/VVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVV////AAAAD///9VVf///AAAAAP///VVXwAAAAAAAAAAD1VV8AAAAAAAAAAA9VVfAAAAAAAAAAAPVVXwAAAAAAAAAAD1VV8AAAAAAAAAAA9VVfAAAAAAAAAAAPVVXwAAAAAAAAAAD1VV8AAAAAAAAAAA9VVf////////////VVX////////////w=="}, + {"width" : "48", "buffer":"VVVVX//////9VVVVVVVX////////1VVVVVVf8AAAAAAD/VVVVVX8AAAAAAAAP9VVVVfwAAAAAAAAA/VVVV8AAAAAAAAAAD1VVXwAAAAAAAAAAA9VVfAAAAAAAAAAAA9VVfAAAAAAAAAAAAPVV8AAAAD//wAAAAD1V8AAAA///8AAAAD1XwAAAD9VVfAAAAA9XwAAAPVVVXwAAAA9fAAAA9VVVV8AAAA9fAAAD1VVVV8AAAA9fAAAD1VVVVfAAAAP8AAAD1VVVVfAAAAP8AAAPVVVVVfAAAAP8AAAPVVVVVfAAAAP8AAAPVVVVVfAAAAP8AAA9VVVVVfAAAAP8AAA9VVVVVfAAAAP8AAA9VVVVVfAAAAP8AAA9VVVVVfAAAAP8AAA9VVVVVfAAAAP8AAA9VVVVVfAAAAPfAAA9VVVVVfAAAAPfAAA9VVVVVfAAAAPfAAA9VVVVV8AAAA9fAAA9VVVVV8AAAA9fAAA9VVVVV8AAAA9X///9VVVVXwAAAA9X///9VVVVXwAAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAD1VVVVVVVVVfAAAAPVVVVVVVVVV8AAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAPVVVVVVVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAPVVVVVVVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAPVVVVVVVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAPVVVVVVVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAPVVVVVVVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAD1VVVVVVVVVfAAAAA/////////9fAAAAAP////////98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA98AAAAAAAAAAAAAA9///////////////9///////////////9"}, + {"width" : "49", "buffer":"VVVVX//////9VVVVVVVVf///////9VVVVVVV/wAAAAAAP9VVVVVX/AAAAAAAAP9VVVVX8AAAAAAAAAP1VVVXwAAAAAAAAAAPVVVXwAAAAAAAAAAA9VVV8AAAAAAAAAAAPVVV8AAAAAAAAAAAA9VV8AAAAD//AAAAAPVVfAAAAD///AAAAA9VfAAAAD1VX8AAAAPVXwAAAD1VVXwAAAA9XwAAAD1VVVfAAAAPV8AAAD1VVVXwAAAD1fAAAD1VVVVfAAAA9fAAAA9VVVVXwAAAPXwAAA9VVVVV8AAAA98AAAPVVVVVXwAAAPfAAAD1VVVVV8AAAD3wAAA9VVVVVfAAAA98AAAPVVVVVXwAAAPfAAAPVVVVVV8AAAD3wAAD1VVVVVfAAAA98AAA9VVVVVXwAAAPfAAAPVVVVVV8AAAD18AAD1VVVVVfAAAA9fAAA9VVVVVXwAAAPXwAAPVVVVVV8AAAD18AAD1VVVVVfAAAA9fAAA9VVVVVXwAAAPV////VVVVVV8AAAD1f///1VVVVVfAAAD1VVVVVVVVVVXwAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1VVVVVVVVVVfAAAD1VVVVVVVVVVXwAAA9VVVVVVVVVVXwAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVXwAAAPVVVVVVVVVVXwAAAPVVVVVVVX///wAAAD1VVVVVVX///wAAAP1VVVVVVV8AAAAAAP1VVVVVVVfAAAAAA/VVVVVVVVXwAAAAA/VVVVVVVVV8AAAAA9VVVVVVVVVfAAAAAD9VVVVVVVVXwAAAAAP1VVVVVVVV8AAAAAAP1VVVVVVVfAAAAAAA/VVVVVVVX///wAAAA9VVVVVVVf///wAAAD1VVVVVVVVVV/AAAAPVVVVVVVVVVV8AAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVVfAAAA9VVVVVVVVVVV8AAAPVVVVVVVVVVVfAAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVVfAAAD1VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1VVVVVVVVVVfAAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1////VVVVVVfAAAA9f///1VVVVVXwAAAPfAAA9VVVVVV8AAAD3wAAPVVVVVVfAAAA98AAD1VVVVVXwAAAPfAAA9VVVVVV8AAAD3wAAPVVVVVVfAAAA98AAD1VVVVVXwAAAPfAAAPVVVVVV8AAAD/AAAD1VVVVVfAAAA/wAAA9VVVVVXwAAAP8AAAPVVVVVV8AAAD/AAAD1VVVVVfAAAA/wAAA9VVVVVXwAAAP8AAAD1VVVVV8AAAD3wAAA9VVVVVfAAAA98AAAPVVVVVfAAAA9fAAAA9VVVVXwAAAPXwAAAPVVVVXwAAAD18AAAA9VVVXwAAAA9XwAAAD9VVXwAAAA9V8AAAAP/X/wAAAAPVfAAAAAP//wAAAAPVV8AAAAADwAAAAAD1VXwAAAAAAAAAAAD1VV8AAAAAAAAAAAD1VVXwAAAAAAAAAAD1VVVfAAAAAAAAAAD9VVVV/AAAAAAAAAP1VVVVX/AAAAAAAA/1VVVVVX/wAAAAAP/VVVVVVVX///////9VVVVVVVVV//////VVVVVQ=="}, + {"width" : "57", "buffer":"VVVVVVVVVX//////VVVVVVVVVVVVX//////VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVV8AAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAA8AAAPVVVVVVVVVVfAAD/AAAPVVVVVVVVVVfAAPXwAAPVVVVVVVVVV8AAPXwAAPVVVVVVVVVV8AAPfAAAPVVVVVVVVVV8AA9fAAAPVVVVVVVVVXwAA9fAAAPVVVVVVVVVXwAA9fAAAPVVVVVVVVVfAAD1fAAAPVVVVVVVVVfAAD1fAAAPVVVVVVVVVfAAD1fAAAPVVVVVVVVV8AAPVfAAAPVVVVVVVVV8AAPVfAAAPVVVVVVVVXwAAPVfAAAPVVVVVVVVXwAA9VfAAAPVVVVVVVVXwAA9VfAAAPVVVVVVVVfAAA9VfAAAPVVVVVVVVfAAD1VfAAAPVVVVVVVV8AAD1VfAAAPVVVVVVVV8AAD1VfAAAPVVVVVVVV8AAPVVfAAAPVVVVVVVXwAAPVVfAAAPVVVVVVVXwAAPVVfAAAPVVVVVVVfAAA9VVfAAAPVVVVVVVfAAA9VVfAAAPVVVVVVVfAAA9VVfAAAPVVVVVVV8AAD1VVfAAAPVVVVVVV8AAD1VVfAAAPVVVVVVXwAAD1VVfAAAPVVVVVVXwAAPVVVfAAAPVVVVVVXwAAPVVVfAAAPVVVVVVfAAAPVVVfAAAPVVVVVVfAAA9VVVfAAAPVVVVVV8AAA9VVVfAAAPVVVVVV8AAA9VVV8AAAPVVVVVV8AAD1VVV8AAAPVVVVVXwAAD1VVV8AAAPVVVVVXwAAD1VVV8AAAPVVVVVXwAAPVVVV8AAAPVVVVVfAAAPVVVV8AAAPVVVVVfAAAPVVVV8AAAPVVVVV8AAA9VVVV8AAAPVVVVV8AAA9VVVV8AAAPVVVVV8AAA9VVVV8AAAPVVVVXwAAD1VVVV8AAAPVVVVXwAAD1VVVV8AAAPVVVVfAAAD1VVVV8AAAPVVVVfAAAPVVVVV8AAAPVVVVfAAAPVVVVV8AAAPVVVV8AAAPVVVVV8AAAPVVVV8AAA9VVVVV8AAAPVVVXwAAA9VVVVV8AAAPVVVXwAAA9VVVVV8AAAD1VVXwAAAP/////wAAAA///fAAAAD/////AAAAAP//fAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP///////////AAAAA///f//////////wAAAD///VVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV8AAAPVVVVVVVVVVVVVV/////VVVVVVVVVVVVVVf////VVV"}, + {"width" : "47", "buffer":"X//////////////Vf/////////////9V8AAAAAAAAAAAAD1XwAAAAAAAAAAAAPVfAAAAAAAAAAAAA9V8AAAAAAAAAAAAD1XwAAAAAAAAAAAAPVfAAAAAAAAAAAAA9V8AAAAAAAAAAAAD1XwAAAAAAAAAAAAPVfAAAAAAAAAAAAA9V8AAAAAAAAAAAAD1XwAAA//////////VfAAAP/////////9V8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VVVVVVVVVVXwAAPVVVVVVVVVVVfAAA9VVVVVVVVVVV8AAD1VV////VVVVXwAAPVX/////1VVVfAAA9V/wAAAP9VVV8AAD1/AAAAAD9VVXwAAPfwAAAAAA9VVfAAAPwAAAAAAA9VV8AAAMAAAAAAAA9VXwAAAAAAAAAAAD1VfAAAAAAAAAAAAD1V8AAAAAAAAAAAAPVXwAAAAAAAAAAAAPVfAAAAAP/wAAAAA9V8AAAAP//8AAAAD1XwAAAD9VX8AAAAD1fAAAA9VVV8AAAAPV8AAAPVVVV8AAAA9XwAAD1VVVXwAAAD1f///9VVVVXwAAAD1////VVVVVfAAAAPVVVVVVVVVV8AAAA9VVVVVVVVVXwAAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAA9VVVVVVVVVV8AAAD1VVVVVVVVVXwAAAPVVVVVVVVVVfAAAA9f///1VVVVV8AAAD3////VVVVVXwAAAPfAAA9VVVVVfAAAA98AAD1VVVVV8AAAD3wAAPVVVVVXwAAAP8AAA9VVVVVfAAAA/wAAD1VVVVV8AAAD/AAAPVVVVVXwAAAP8AAA9VVVVVfAAAA/wAAD1VVVVV8AAAD/AAAPVVVVVXwAAAP8AAA9VVVVVfAAAA/wAAD1VVVVXwAAAD/AAAPVVVVVfAAAAP8AAA9VVVVV8AAAD3wAAA9VVVVXwAAAPfAAAD1VVVVfAAAA98AAAPVVVVXwAAAD18AAAPVVVVfAAAA9XwAAAPVVVXwAAAD1fAAAAPVVV8AAAA9VfAAAAP9f/AAAAD1V8AAAAP//wAAAAPVV8AAAAA8AAAAAD1VXwAAAAAAAAAAA9VVXwAAAAAAAAAAD1VVXwAAAAAAAAAA9VVVXwAAAAAAAAAPVVVVXwAAAAAAAAP1VVVVX/AAAAAAAP9VVVVVX/wAAAAA/9VVVVVVVf//////9VVVVVVVVX/////1VVVVU="}, + {"width" : "49", "buffer":"VVVVV///////VVVVVVVVX////////VVVVVVVf8AAAAAAD/VVVVVV/wAAAAAAAD/VVVVV/AAAAAAAAAD9VVVV8AAAAAAAAAAD1VVV8AAAAAAAAAAAPVVV8AAAAAAAAAAAD1VVfAAAAAP/AAAAAPVVfAAAAA///wAAAD1VfAAAAD/VX/AAAAPVXwAAAD9VVV8AAAD1V8AAAD1VVVXwAAA9V8AAAD1VVVVfAAAD1fAAAA9VVVVXwAAA9fAAAA9VVVVV8AAAPXwAAAPVVVVVXwAAD18AAAPVVVVVV8AAA9fAAAD1VVVVVfAAAPXwAAA9VVVVVXwAAD3wAAAPVVVVVV8AAA98AAAD1VVVVVfAAAPfAAAA9VVVVVV8AAD3wAAAPVVVVVVfAAA98AAAD1VVVVVXwAAPfAAAA9VVVVVV8AAPXwAAAPVVVVVVf///18AAAD1VVVVVX///9fAAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1VVVVVVVVVVfAAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1VVVVVVVVVVfAAAA9VVVVVVVVVVXwAAAPVVVVVVVVVVV8AAAD1VVVVVVVVVVfAAAA9VVX//9VVVVXwAAAPVX/////VVVV8AAAD1X/wAAP/VVVfAAAA9fwAAAAD/VVXwAAAPfwAAAAAD9VV8AAAA/AAAAAAAD1VfAAAADAAAAAAAAPVXwAAAAAAAAAAAAA9V8AAAAAAAAAAAAAPVfAAAAAAAAAAAAAA9XwAAAAAA//AAAAAD18AAAAAD///AAAAA9fAAAAAD9VX8AAAAPXwAAAAP1VVXwAAAA98AAAAP1VVVfAAAAPfAAAAPVVVVXwAAAD3wAAAPVVVVVfAAAA98AAAD1VVVVXwAAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA/wAAAPVVVVVXwAAAP8AAAD1VVVVV8AAAD/AAAA9VVVVVfAAAA98AAAPVVVVVXwAAAPfAAAD1VVVVV8AAAPXwAAAPVVVVVfAAAD18AAAD1VVVVfAAAA9fAAAA9VVVVXwAAAPV8AAAD1VVVXwAAAPVfAAAAPVVVV8AAAD1V8AAAA9VVX8AAAA9VfAAAAD/1/8AAAA9VXwAAAAP//wAAAAPVVfAAAAAA8AAAAAPVVV8AAAAAAAAAAAD1VVfAAAAAAAAAAAD1VVV8AAAAAAAAAAD1VVVXwAAAAAAAAAD1VVVVfwAAAAAAAAD1VVVVV/wAAAAAAAP1VVVVVV/8AAAAAD/1VVVVVVV////////VVVVVVVVVf/////1VVVVQ=="}, + {"width" : "46", "buffer":"///////////////////////////////wAAAAAAAAAAAAAP8AAAAAAAAAAAAAD/AAAAAAAAAAAAAA/wAAAAAAAAAAAAAP8AAAAAAAAAAAAAD/AAAAAAAAAAAAAA/wAAAAAAAAAAAAAP8AAAAAAAAAAAAAPfAAAAAAAAAAAAAD3wAAAAAAAAAAAAA9/////////8AAAAPf/////////wAAAPVVVVVVVVVVfAAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAA9VVVVVVVVVV8AAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAPVVVVVVVVVV8AAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAA9VVVVVVVVVXwAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAD1VVVVVVVVVfAAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAAPVVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVXwAAAD1VVVVVVVVV8AAAA9VVVVVVVVVfAAAAPVVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVV8AAAA9VVVVVVVVV8AAAA9VVVVVVVVVfAAAAPVVVVVVVVVXwAAAD1VVVVVVVVXwAAAA9VVVVVVVVV8AAAA9VVVVVVVVVf/////VVVVVVVVVX/////VVVVVVVVVQ=="}, + {"width" : "51", "buffer":"VVVVV///////VVVVVVVVV////////9VVVVVVVX/AAAAAAA/1VVVVVV/AAAAAAAAD/VVVVVX8AAAAAAAAAP1VVVVfAAAAAAAAAAA9VVVV8AAAAAAAAAAAPVVVXwAAAAAAAAAAAD1VVfAAAAAAAAAAAAD1VVfAAAAAP/8AAAAA9VV8AAAAD///wAAAA9VV8AAAAP1VX8AAAAPVV8AAAA9VVVfAAAAPVXwAAAD1VVVXwAAAD1XwAAAD1VVVXwAAAD1XwAAAPVVVVV8AAAD1XwAAAPVVVVV8AAAD1fAAAAPVVVVV8AAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9XwAAA9VVVVVfAAAD1XwAAA9VVVVVfAAAD1XwAAA9VVVVV8AAAD1XwAAAPVVVVV8AAAPVV8AAAPVVVVV8AAAPVV8AAAPVVVVXwAAAPVVfAAAD1VVVXwAAA9VVfAAAA9VVVfAAAA9VVXwAAAPVVV8AAAD1VVV8AAAD///wAAAPVVVV/AAAA///AAAA9VVVVfwAAAAAAAAAD1VVVVV8AAAAAAAAA/VVVVVVfAAAAAAAAD9VVVVVVXwAAAAAAAPVVVVVVVXwAAAAAAAD1VVVVVV/AAAAAAAAA/VVVVVX8AAAAAAAAAP1VVVVfAAAAAAAAAAA9VVVV8AAAA///AAAAPVVVXwAAAD///wAAAD1VVfAAAA/VVV8AAAA9VV8AAAD9VVVfAAAAPVV8AAAPVVVVXwAAAPVXwAAAPVVVVV8AAAD1XwAAA9VVVVVfAAAD1fAAAA9VVVVVfAAAA9fAAAD1VVVVVfAAAA9fAAAD1VVVVVXwAAA9fAAAD1VVVVVXwAAA98AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVXwAAAP8AAAD1VVVVVfAAAAP8AAAA9VVVVVfAAAA9fAAAA9VVVVVfAAAA9fAAAA9VVVVV8AAAA9fAAAAPVVVVV8AAAA9fAAAAD1VVVXwAAAD1XwAAAA9VVV/AAAAD1XwAAAAP/V/8AAAAD1V8AAAAD///AAAAAPVV8AAAAAA/AAAAAA9VVfAAAAAAAAAAAAA9VVXwAAAAAAAAAAAD1VVX8AAAAAAAAAAAPVVVV/AAAAAAAAAAA9VVVVX8AAAAAAAAAP1VVVVV/wAAAAAAAD/VVVVVVX/wAAAAAD/1VVVVVVVf///////9VVVVVVVVVf/////9VVVVV"}, + {"width" : "50", "buffer":"VVVVX//////9VVVVVVVVX////////VVVVVVVX/AAAAAAA/1VVVVVV/AAAAAAAAP9VVVVVfAAAAAAAAAD9VVVVXwAAAAAAAAAA9VVVV8AAAAAAAAAAA9VVVfAAAAAAAAAAAA9VVXwAAAAAAAAAAAD1VVfAAAAD//8AAAAD1VXwAAAA////AAAAPVVfAAAAPVVV/AAAAPVXwAAAD1VVVfAAAA9VfAAAA9VVVVfAAAA9V8AAAD1VVVV8AAAD1fAAAA9VVVVV8AAAPV8AAAD1VVVVXwAAA9XwAAAPVVVVVfAAAA9fAAAD1VVVVV8AAAD18AAAPVVVVVV8AAAPXwAAA9VVVVVXwAAA9fAAAD1VVVVVfAAAD3wAAAPVVVVVV8AAAPfAAAA9VVVVVXwAAA98AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA/wAAAPVVVVVV8AAAD/AAAA9VVVVVXwAAAP8AAAD1VVVVVfAAAA98AAAD1VVVVV8AAAD3wAAAPVVVVVXwAAAPfAAAA9VVVVV8AAAA98AAAA9VVVVXwAAAD3wAAAD1VVVV8AAAAPfAAAAD1VVV/AAAAA9fAAAAD1VVfwAAAAD18AAAAD/V/wAAAAAPV8AAAAD//8AAAAAA9XwAAAAAPwAAAAAAD1XwAAAAAAAAAAAAAPVfAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAD1VfAAAAAAAA8AAAAPVVfAAAAAAAP8AAAA9VVfwAAAAAP18AAAD1VVf8AAAAP9XwAAAPVVVX//AD/9VfAAAA9VVVV////9VV8AAAD1VVVVVf/VVVXwAAAPVVVVVVVVVVVfAAAA9VVVVVVVVVVV8AAAD1VVVVVVVVVVXwAAAPVVVVVVVVVVVfAAAA9VVVVVVVVVVV8AAAD1VVVVVVVVVVXwAAAPVVVVVVVVVVVfAAAA9VVVVVVVVVVV8AAAD1VVVVVVVVVVXwAAAPX////VVVVVVfAAAA9f///9VVVVVV8AAAD18AAD1VVVVVXwAAAPXwAAPVVVVVVfAAAA98AAA9VVVVVV8AAAPXwAAD1VVVVVXwAAA9fAAAPVVVVVV8AAAD18AAA9VVVVVXwAAAPXwAAD1VVVVVfAAAA9fAAAPVVVVVV8AAAD18AAAPVVVVVXwAAA9XwAAA9VVVVV8AAAD1fAAAD1VVVVXwAAAPV8AAAD1VVVV8AAAD1V8AAAD1VVVfAAAAPVXwAAAD1VVXwAAAA9VfAAAAD/1/8AAAAPVVfAAAAD///AAAAA9VV8AAAAADwAAAAAPVVV8AAAAAAAAAAAA9VVXwAAAAAAAAAAAPVVVXwAAAAAAAAAAD1VVVXwAAAAAAAAAA9VVVVX8AAAAAAAAA/VVVVVX/AAAAAAAA/1VVVVVV/8AAAAAA/1VVVVVVVf///////1VVVVVVVVV//////1VVVVU="}, + {"width" : "18", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX//1VV////VX8AAP1XwAAA9fAAAA9fAAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAPfAAAAPfAAAA9XwAAA9V/AAP1Vf///VVV//1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/VVVf//9VV/wA/1XwAAD9fAAAA9fAAAAPfAAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAPfAAAAPfAAAAPXwAAA9X8AAD1V////VVX//9V"} + ] +} diff --git a/apps/contourclock/font-Impact.json b/apps/contourclock/fonts/font-Impact.json similarity index 100% rename from apps/contourclock/font-Impact.json rename to apps/contourclock/fonts/font-Impact.json diff --git a/apps/contourclock/fonts/font-LuckiestGuy.json b/apps/contourclock/fonts/font-LuckiestGuy.json new file mode 100644 index 000000000..a5aac8567 --- /dev/null +++ b/apps/contourclock/fonts/font-LuckiestGuy.json @@ -0,0 +1,17 @@ +{ + "name":"Luckiest Guy", + "size":"100", + "characters":[ + {"width" : "65", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVdVVVVVVVVVVVVVVVVVVV////9VVVVVVVVVVVVVVVX//8///VVVVVVVVVVVVVVX/wAAAA/1VVVVVVVVVVVVX/AAAAAAP9VVVVVVVVVVVV/AAAAAAAD9VVVVVVVVVVVfAAAAAAAAA/VVVVVVVVVVXwAAAAAAAAA/VVVVVVVVVX8AAAAAAAAAAPVVVVVVVVVfAAAAAAAAAAAPVVVVVVVVXwAAAAAAAAAAAPVVVVVVVV8AAAAAAAAAAAAPVVVVVVVfAAAAAAAAAAAAA9VVVVVVXwAAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAD1VfAAAAAAAAP8AAAAAAAAPVV8AAAAAAAP//AAAAAAAAPVfAAAAAAAD9V/AAAAAAAA9V8AAAAAAA9VVfAAAAAAAD1XwAAAAAAPVVVfAAAAAAAPVfAAAAAAD1VVVfAAAAAAAPV8AAAAAAPVVVVfAAAAAAA9fAAAAAAD1VVVV8AAAAAAD18AAAAAAPVVVVV8AAAAAAPXwAAAAAD1VVVVXwAAAAAA9fAAAAAAPVVVVVXwAAAAAD18AAAAAD1VVVVVfAAAAAAD3wAAAAAPVVVVVV8AAAAAAP8AAAAAA9VVVVVV8AAAAAA/wAAAAAD1VVVVVXwAAAAAD/AAAAAA9VVVVVVfAAAAAAP8AAAAAD1VVVVVV8AAAAAA/wAAAAAPVVVVVVXwAAAAAD/AAAAAA9VVVVVVfAAAAAAP8AAAAAD1VVVVVVfAAAAAA/wAAAAAPVVVVVVV8AAAAAD/AAAAAA9VVVVVVXwAAAAAP8AAAAAD1VVVVVVfAAAAAA/wAAAAAPVVVVVVV8AAAAAD/AAAAAA9VVVVVVXwAAAAAP8AAAAAD1VVVVVV8AAAAAA/wAAAAAPVVVVVVXwAAAAAD3wAAAAA9VVVVVVfAAAAAAPfAAAAAD1VVVVVV8AAAAAD18AAAAAD1VVVVVXwAAAAAPXwAAAAAPVVVVVV8AAAAAA9fAAAAAA9VVVVVXwAAAAAD18AAAAAA9VVVVVfAAAAAAPV8AAAAAD1VVVVXwAAAAAA9XwAAAAAPVVVVVfAAAAAAPVfAAAAAAPVVVVXwAAAAAA9V8AAAAAA9VVVVfAAAAAAD1V8AAAAAA9VVVXwAAAAAAPVXwAAAAAA9VVVfAAAAAAD1VfAAAAAAA9VVXwAAAAAAPVVfAAAAAAA9VX8AAAAAAA9VV8AAAAAAA///AAAAAAAPVVXwAAAAAAA//AAAAAAAA9VVXwAAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAAA9VVVVVV8AAAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAAA9VVVVVVXwAAAAAAAAAAAAPVVVVVVVXwAAAAAAAAAAAD1VVVVVVVXwAAAAAAAAAAA9VVVVVVVVXwAAAAAAAAAAPVVVVVVVVVXwAAAAAAAAAD1VVVVVVVVVX8AAAAAAAAA9VVVVVVVVVVX/AAAAAAAA/VVVVVVVVVVVV/AAAAAAA/1VVVVVVVVVVVVf8AAAAA/1VVVVVVVVVVVVVf/wAAD/1VVVVVVVVVVVVVVV/////1VVVVVVVVVVVVVVVVX///VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "41", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/9VVVVVVVVVVV////////1VVVVfAA//////VVVVXwAAAAAAA9VVVV8AAAAAAAD1VVVfAAAAAAAAPVVVXwAAAAAAAA9VVV8AAAAAAAAD1VVfAAAAAAAAAPVVfwAAAAAAAAA9VX8AAAAAAAAAD1V8AAAAAAAAAAPVfAAAAAAAAAAA9XwAAAAAAAAAAD18AAAAAAAAAAAPfAAAAAAAAAAAA/wAAAAAAAAAAAD3wAAAAAAAAAAAPfAAAAAAAAAAAA98AAAAAAAAAAAD18AAAAAAAAAAAPXwAAAAAAAAAAA9fAAAAAAAAAAAD1fAAAAAAAAAAAPV8AAAAAAAAAAA9XwAAAAAAAAAAD1XwAAAAAAAAAAPVfAAAAAAAAAAA9V8AAAAAAAAAAD1V8AAAAAAAAAAPVXwAAAAAAAAAA9VXwADAAAAAAAD1VfAA/AAAAAAAPVV8APfAAAAAAA9VV8D18AAAAAAPVVXz9XwAAAAAA9VVf/VfAAAAAAD1VVfVV8AAAAAAPVVV1VXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAPVVVVVV8AAAAAA9VVVVVXwAAAAAD1VVVVVfAAAAAAPVVVVVV8AAAAAA9VVVVVXwAAAAAD1VVVVVfAAAAAAPVVVVVV8AAAAAA9VVVVVXwAAAAAD1VVVVVfAAAAAAPVVVVVV8AAAAAA9VVVVVXwAAAAAD1VVVVVfAAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVfAAAAAAD1VVVVV8AAAAAAPVVVVVXwAAAAAA9VVVVVf///////1VVVVVX//////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "52", "buffer":"VVVVVVVVVVVVVVVVVVVVVVf////VVVVVVVVVVVf//////1VVVVVVVVV//AAAAD/1VVVVVVVX/AAAAAAA/1VVVVVVX8AAAAAAAA/1VVVVVfwAAAAAAAAA/VVVVVfwAAAAAAAAAA9VVVVfAAAAAAAAAAAD1VVVXwAAAAAAAAAAAPVVVV8AAAAAAAAAAAA9VVVfAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVV8AAAAAAAAAAAAD1VVfAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAD1VfAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAA9VXwAADwAAAAAAAAAPVV8AD///AAAAAAAAD1VfAP/1/8AAAAAAAA9VXwP1VVXwAAAAAAAD1V8/VVVVfAAAAAAAA9Vf/VVVVV8AAAAAAAPVV9VVVVVfAAAAAAAD1VdVVVVVV8AAAAAAA9VVVVVVVVfAAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVV8AAAAAAD1VVVVVVVV8AAAAAAA9VVVVVVVVfAAAAAAA9VVVVVVVVfAAAAAAAPVVVVVVVVfAAAAAAAPVVVVVVVVfAAAAAAAD1VVVVVVVXwAAAAAAD1VVVVVVVXwAAAAAAD1VVVVVVVXwAAAAAAA9VVVVVVVfwAAAAAAA9VVVVVVVfwAAAAAAA9VVVVVVVfAAAAAAAA9VVVVVVVfAAAAAAAA9VVVVVVV/AAAAAAAA9VVVVVVV/AAAAAAAA9VVVVVVX8AAAAAAAA9VVVVVVf8AAAAAAAA9VVVVVV/wAAAAAAAAD/////X/AAAAAAAAAAP////38AAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAD1/////AAAAAAAAAAA9X//////////wAAAAPVVVVVX///////////1VVVVVVVVVVV/////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "54", "buffer":"VVVVX//////1VVVVVVVVVV////////1VVVVVVVVf8AAAAAAP/VVVVVVVX/AAAAAAAAP9VVVVVV/wAAAAAAAAA/VVVVVX8AAAAAAAAAAD1VVVVfAAAAAAAAAAAA9VVVX8AAAAAAAAAAAAPVVVfwAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAA9VfAAAD//wAAAAAAAA9VfAAD////AAAAAAAA9VfAA/9VVfwAAAAAAA9VfAP9VVVV8AAAAAAA9VfA/VVVVVfAAAAAAA9VX/1VVVVVXwAAAAAA9VX/VVVVVVV8AAAAAA9VX1VVVVVVV8AAAAAA9VX1VVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVX8AAAAAAD1VVVVVVVVfwAAAAAAPVVVVVVVV/8AAAAAAAPVVVVVX///wAAAAAAA9VVVVVX//AAAAAAAAD1VVVVVXwAAAAAAAAAD1VVVVVXwAAAAAAAAAPVVVVVVXwAAAAAAAAA9VVVVVVXwAAAAAAAAAPVVVVVVXwAAAAAAAAAD1VVVVVXwAAAAAAAAAA9VVVVVXwAAAAAAAAAAPVVVVVXwAAAAAAAAAAPVVVVVXwAAAAAAAAAAD1VVVVXwAAAAAAAAAAD1VVVVXwAAAAAAAAAAA9VVVVXwAAAAAAAAAAA9VVVVXwAAAAAAAAAAA9VVVVXw//AAAAAAAAA9VVVVX///8AAAAAAAAPVVVVX/VV/wAAAAAAAPVVVVVVVVX8AAAAAAAPVVVVVVVVVfAAAAAAAPVVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAPX1VVVVVVVV8AAAAAAPX9VVVVVVVV8AAAAAAPX/VVVVVVVV8AAAAAAPXz9VVVVVVV8AAAAAAPXw/1VVVVVV8AAAAAAPXwD/VVVVVXwAAAAAAPfAAP9VVVVfAAAAAAAPfAAA//VVX8AAAAAAAPfAAAD////wAAAAAAAPfAAAAA//8AAAAAAAAPfAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAA9VV/AAAAAAAAAAAAAD1VVX8AAAAAAAAAAAAP1VVV/AAAAAAAAAAAA9VVVVX8AAAAAAAAAAP1VVVVV/wAAAAAAAAA/VVVVVVX/wAAAAAAA/1VVVVVVVf/wAAAAAP/VVVVVVVVVf//AAP//VVVVVVVVVVVf/////1VVVVVVVVVVVVV//1VVVVVVVV"}, + {"width" : "56", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX///////1VVVVVVVVVVf///////VVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAD1VVV//////9fAAAAAAPVVVX//////18AAAAAA9VVVfAAAAAPXwAAAAAD1VVXwAAAAA9fAAAAAAPVVVfAAAAAD18AAAAAA9VVV8AAAAA9XwAAAAAD1VVXwAAAAD1fAAAAAAPVVVfAAAAAPV8AAAAAA9VVV8AAAAA9XwAAAAAD1VVXwAAAAD1fAAAAAAPVVV8AAAAAPV8AAAAAA9VVXwAAAAA9XwAAAAAD1VVfAAAAAPVfAAAAAAPVVV8AAAAA9V8AAAAAA9VVXwAAAAD1XwAAAAAD1VVfAAAAAPVfAAAAAAPVVV8AAAAA9V8AAAAAA9VVXwAAAAD1fAAAAAAD1VV8AAAAAPV8AAAAAAPVVXwAAAAD1XwAAAAAA9VVfAAAAAPVfAAAAAAD1VV8AAAAA9V8AAAAAAPVVXwAAAAD1XwAAAAAA9VVfAAAAAPVfAAAAAAD1VV8AAAAA9V8AAAAAAPVVfAAAAAD1XwAAAAAA9VV8AAAAA9VfAAAAAAD1VXwAAAAD1V8AAAAAAPVVfAAAAAPVXwAAAAAA9VV8AAAAA9VfAAAAAAD1VXwAAAAD1V8AAAAAAPVVfAAAAAPVXwAAAAAA9VV8AAAAD1VfAAAAAAPVVfAAAAAPVV8AAAAAA9VV8AAAAA9VXwAAAAAD1VXwAAAAD1VfAAAAAAPVVfAAAAAPVV8AAAAAA9VV8AAAAA9VXwAAAAAD1VXwAAAAD1VfAAAAAAD31fAAAAAPVV8AAAAAAD/XwAAAAAP//AAAAAAAD9fAAAAAAP/wAAAAAAAD18AAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAP8AAAAP///wAAAAAAAP//////////wAAAAAAD9f////9VVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAP/9VVVVVVVVVVfAP/////VVVVVVVVVVV/////9VVVVVVVVVVVVX/9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "55", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV9VVVVVVVf//////////VVVV//////////////1VVVf///AAAAAAAAAA9VVVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAD1VVVfAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAD1VVVfAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAPVVVfAAAAAD////AAAD1VVXwAAAAD////////9VVV8AAAAD1VVVX////VVVfAAAAA9VVVVVVVVVVVXwAAAAPVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVfAAAAA9VVVVVVVVVVVXwAAAAPVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVfAAAAA9VVVVVVVVVVVXwAAAAPVVVVVVVVVVVV8AAAAD1f///VVVVVVVfAAAAAP/////1VVVVVXwAAAAA/AAAD/1VVVVV8AAAAAAAAAAA/VVVVVfAAAAAAAAAAAA/VVVVXwAAAAAAAAAAAD9VVVV8AAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAPVV8////8AAAAAAAAAA9Vf//////wAAAAAAAAPVX9VVVVf/wAAAAAAAD1VVVVVVVV/wAAAAAAAPVVVVVVVVV/AAAAAAAD1VVVVVVVVV8AAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAAPX1VVVVVVVV8AAAAAAPX/VVVVVVVV8AAAAAAD18/1VVVVVV8AAAAAAA9fD/1VVVVV8AAAAAAAPXwA/9VVVX8AAAAAAAD18AA/////8AAAAAAAA9fAAAP///wAAAAAAAAPXwAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAP1VVfwAAAAAAAAAAAAP1VVV/wAAAAAAAAAAAPVVVVV/8AAAAAAAAAA/VVVVVV/8AAAAAAAAD/VVVVVVVf/wAAAAAA/9VVVVVVVVf/8AAAA//1VVVVVVVVVV//////9VVVVVVVVVVVVf///9VVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "59", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX////1VVVVVVVVVVVVV///////1VVVVVVVVVVV//AAAAD//VVVVVVVVVV/wAAAAAAD/1VVVVVVVVfwAAAAAAAAP9VVVVVVVfwAAAAAAAAAD1VVVVVVX8AAAAAAAAAAPVVVVVVV8AAAAAAAAAAA9VVVVVVfAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAADwAAAA9VVVfAAAAAAAA////AAD1VVV8AAAAAAA//X//8APVVVXwAAAAAAP1VVVf/w9VVV8AAAAAAD1VVVVV//1VVXwAAAAAA9VVVVVVX/VVVfAAAAAAPVVVVVVVV9VVV8AAAAAD1VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVfAAAAAA9VVV//9VVVVVV8AAAAAD1VX////9VVVVXwAAAAAPVf/wAA//VVVVfAAAAAD1f/AAAAA/1VVV8AAAAAPf8AAAAAAP1VVXwAAAAAP8AAAAAAAD1VVfAAAAAAMAAAAAAAAD1VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAPfAAAAAAAAD/wAAAAAAA98AAAAAAAD//8AAAAAAD3wAAAAAAA/VX8AAAAAAPfAAAAAAAPVVV8AAAAAA98AAAAAAD1VVV8AAAAAD3wAAAAAA9VVVV8AAAAAPfAAAAAAD1VVVXwAAAAA98AAAAAA9VVVVXwAAAAD3wAAAAAD1VVVVfAAAAAPfAAAAAAPVVVVV8AAAAA98AAAAAA9VVVVXwAAAAD3wAAAAAD1VVVVfAAAAAPXwAAAAAPVVVVV8AAAAA9fAAAAAAPVVVVfAAAAAD18AAAAAA9VVVV8AAAAAPXwAAAAAA9VVVfAAAAAA9fAAAAAAA9VVXwAAAAAD1fAAAAAAA/VX8AAAAAAPV8AAAAAAA///AAAAAAA9XwAAAAAAAP/AAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAA/VVVVVVVfAAAAAAAAAAP1VVVVVVVfAAAAAAAAAD1VVVVVVVVfwAAAAAAAD9VVVVVVVVVf8AAAAAAD/VVVVVVVVVVX/AAAAAP/VVVVVVVVVVVV///////VVVVVVVVVVVVVf////9VVVVVVU="}, + {"width" : "52", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////8AAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAD3wAAAAAP/8AAAAAAA98A///////wAAAAAAPf//////VVfAAAAAAPX/9VVVVVVXwAAAAAD1VVVVVVVVV8AAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAPVVVVVVVVVXwAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVfAAAAAAAPVVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVV8AAAAAAA9VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVXwAAAAAAD1VVVVVVVV8AAAAAAA9VVVVVVVV8AAAAAAA9VVVVVVVVfAAAAAAAPVVVVVVVVXwAAAAAAD1VVVVVVVXwAAAAAAD1VVVVVVVV8AAAAAAA9VVVVVVVVfAAAAAAAPVVVVVVVVfAAAAAAAD1VVVVVVVXwAAAAAAD1VVVVVVVXwAAAAAAA9VVVVVVVV8AAAAAAAPVVVVVVVVfAAAAAAAPVVVVVVVVfAAAAAAAD1VVVVVVVXwAAAAAAA9VVVVVVVV8AAAAAAAPVVVVVVVV8AAAAAAAPVVVVVVVVfAAAAAAAD1VVVVVVVfAAAAAAAA9VVVVVVVXwAAAAAAA9VVVVVVVV8AAAAAAAPVVVVVVVV8AAAAAAAD1VVVVVVVfAAAAAAAA9VVVVVVVXwAAAAAAA9VVVVVVVXwAAAAAAAPVVVVVVVV8AAAAAAAD1VVVVVVVfAAAAAAAD1VVVVVVVfAAAAAAAA9VVVVVVVXwAAAAAAAPVVVVVVVXwAAAAAAAD1VVVVVVV8AAAAAAAD1VVVVVVVfAAAAAAAA9VVVVVVVfwAAAAAAAPVVVVVVVX////wAAAPVVVVVVVVV////////1VVVVVVVVVVVV////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "58", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/1VVVVVVVVVVVVVVV/////1VVVVVVVVVVVVf//AA//9VVVVVVVVVVV/8AAAAA/9VVVVVVVVVX/AAAAAAAP1VVVVVVVVf8AAAAAAAAP1VVVVVVVfwAAAAAAAAA/VVVVVVVfAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAA/8AAAAAAA9VfAAAAAAD//8AAAAAAD1XwAAAAAD9VfwAAAAAA9V8AAAAAD1VVfAAAAAAPVfAAAAAD1VVV8AAAAAD1XwAAAAD1VVVXwAAAAA9V8AAAAA9VVVVfAAAAAPVfAAAAA9VVVVXwAAAAD1XwAAAAPVVVVVfAAAAA9V8AAAAD1VVVVXwAAAA9VfAAAAD1VVVVV8AAAAPVXwAAAA9VVVVVfAAAAD1VfAAAAD1VVVVXwAAAA9VXwAAAA9VVVVV8AAAAPVV8AAAAPVVVVV8AAAAPVVfAAAAA9VVVVfAAAAD1VV8AAAAPVVVVfAAAAA9VVfAAAAA9VVVfAAAAA9VVV8AAAAD9VVfAAAAAPVVVfAAAAAP9V/AAAAAPVVVV8AAAAAP//AAAAAD1VVVfAAAAAAP8AAAAAD1VVVV8AAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAA9VXwAAAAAA//wAAAAAAD1XwAAAAAA///wAAAAAA9V8AAAAAA9VV/AAAAAAPV8AAAAAA9VVV8AAAAAA9fAAAAAA9VVVXwAAAAAPXwAAAAA9VVVVfAAAAAD18AAAAA9VVVVXwAAAAAP8AAAAAPVVVVV8AAAAAD/AAAAAD1VVVVXwAAAAA/wAAAAA9VVVVV8AAAAAP8AAAAAPVVVVVfAAAAAD/AAAAAD1VVVVXwAAAAA/wAAAAA9VVVVV8AAAAAP8AAAAAPVVVVV8AAAAAD/AAAAAA9VVVVfAAAAAA/wAAAAAPVVVVfAAAAAAP8AAAAAA9VVVXwAAAAAD/AAAAAAD1VVfwAAAAAA/wAAAAAAP1VfwAAAAAAP8AAAAAAA///AAAAAAAD/AAAAAAAA//AAAAAAAA98AAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAA/VVVVVV/AAAAAAAAAAA/VVVVVVX/AAAAAAAAAD9VVVVVVVX/AAAAAAAAP9VVVVVVVVX/wAAAAAP/1VVVVVVVVVX//AAAP//VVVVVVVVVVVV//////VVVVVVVVVVVVVVX///VVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "57", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf//VVVVVVVVVVVVVVX/////1VVVVVVVVVVVV//wAA//VVVVVVVVVVVf8AAAAAP9VVVVVVVVVX/AAAAAAA/1VVVVVVVVfwAAAAAAAD9VVVVVVVV8AAAAAAAAAPVVVVVVVfwAAAAAAAAAD1VVVVVV/AAAAAAAAAAA9VVVVVXwAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVfAAAAAAA//wAAAAAAD1fAAAAAAD//8AAAAAAD1fAAAAAAPVVfAAAAAAD1fAAAAAA9VVXwAAAAAD1fAAAAAD1VVV8AAAAAA9fAAAAAPVVVVfAAAAAA98AAAAAPVVVVfAAAAAA98AAAAA9VVVVfAAAAAA98AAAAA9VVVVXwAAAAA98AAAAA9VVVVXwAAAAA98AAAAA9VVVVXwAAAAAP8AAAAA9VVVVXwAAAAAP8AAAAAPVVVVfAAAAAAP8AAAAAPVVVVfAAAAAAP8AAAAAD1VVV8AAAAAAPfAAAAAA9VVXwAAAAAAPfAAAAAAPVVfAAAAAAAPfAAAAAAD//8AAAAAAAPfAAAAAAA//wAAAAAAAPfAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA9VVV/AAAAAAA8AAAAAA9VVVf8AAAAAP/AAAAAA9VVVV/8AAA//XwAAAAA9VVVVX/////1XwAAAAA9VVVVVX///VVXwAAAAA9VVVVVVVVVVVXwAAAAD1VVVVVVVVVVVfAAAAAD1VVVVVVVVVVVfAAAAAD1VVVVVVVVVVV8AAAAAD1VVVVVVVVVVV8AAAAAD1VVdVVVVVVVXwAAAAAPVVVf1VVVVVVfAAAAAAPVVV//VVVVVVfAAAAAAPVVV8P/VVVVX8AAAAAAPVVV8A//VVV/wAAAAAA9VVV8AA////8AAAAAAA9VVV8AAA///AAAAAAAA9VVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAD1VVVVVfAAAAAAAAAAA/VVVVVVX8AAAAAAAAAD9VVVVVVV/wAAAAAAAA/VVVVVVVVX/AAAAAAAP9VVVVVVVVVf/8AAAAP/VVVVVVVVVVV///////1VVVVVVVVVVVVX////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "22", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/1VVVVf//1VVV/wA/1VV/AAA/VV8AAAD1VfAAAAPVfAAAAA9XwAAAAPXwAAAAA98AAAAAPfAAAAAD/AAAAAA/wAAAAAP8AAAAAD/AAAAAA/wAAAAAP8AAAAAD3wAAAAA98AAAAAPfAAAAAD3wAAAAD1fAAAAA9XwAAAAPVfAAAAPVV8AAAPVVXwAA/VVVf///VVVV//9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/9VVVV///9VVV/AAP1VV8AAAPVV8AAAA9V8AAAAPVfAAAAA9fAAAAAPXwAAAAA98AAAAAPfAAAAAD/AAAAAA/wAAAAAP8AAAAAD/AAAAAA/wAAAAAP8AAAAAD3wAAAAA98AAAAAPfAAAAAD18AAAAD1fAAAAA9V8AAAA9VfAAAAPVV/AAA/VVX/AD/VVVX//9VVVVX/1VVVVVVVVVQ=="} + ] +} diff --git a/apps/contourclock/fonts/font-MouseMemoirs.json b/apps/contourclock/fonts/font-MouseMemoirs.json new file mode 100644 index 000000000..e98b6712e --- /dev/null +++ b/apps/contourclock/fonts/font-MouseMemoirs.json @@ -0,0 +1,17 @@ +{ + "name":"Mouse Memoirs", + "size":"100", + "characters":[ + {"width" : "63", "buffer":"VVVVVVVVX///1VVVVVVVVVVVVVVVf/////9VVVVVVVVVVVVVX/8AAAP/1VVVVVVVVVVVV/wAAAAAD/VVVVVVVVVVVf8AAAAAAAP9VVVVVVVVVV/AAAAAAAAA/VVVVVVVVVXwAAAAAAAAAD9VVVVVVVVfAAAAAAAAAAA/VVVVVVVV8AAAAAAAAAAAD1VVVVVVXwAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAA9VVVVVV8AAAAAA/AAAAAAPVVVVVXwAAAAAP/8AAAAAD1VVVVXwAAAAA/V/AAAAAD1VVVVfAAAAAD1VXwAAAAA9VVVVfAAAAAPVVV8AAAAA9VVVV8AAAAAPVVVfAAAAAPVVVV8AAAAA9VVVfAAAAAPVVVXwAAAAD1VVVXwAAAAD1VVXwAAAAD1VVVXwAAAAD1VVXwAAAAPVVVVV8AAAAA9VVfAAAAAPVVVVV8AAAAA9VVfAAAAAPVVVVVfAAAAA9VVfAAAAA9VVVVVfAAAAAPVV8AAAAA9VVVVVfAAAAAPVV8AAAAA9VVVVVfAAAAAPVV8AAAAA9VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAA9fAAAAAD1VVVVVV8AAAAA9fAAAAAD1VVVVVV8AAAAA9fAAAAAPVVVVVVV8AAAAA9fAAAAAPVVVVVVV8AAAAA9fAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVVfAAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAP8AAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAAPfAAAAAPVVVVVVV8AAAAA9fAAAAAPVVVVVVV8AAAAA9fAAAAAD1VVVVVV8AAAAA9fAAAAAD1VVVVVV8AAAAA9XwAAAAD1VVVVVXwAAAAA9XwAAAAD1VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAD1XwAAAAD1VVVVVXwAAAAD1V8AAAAD1VVVVVXwAAAAD1V8AAAAA9VVVVVfAAAAAD1V8AAAAA9VVVVVfAAAAAPVVfAAAAA9VVVVVfAAAAAPVVfAAAAAPVVVVVfAAAAAPVVfAAAAAPVVVVV8AAAAA9VVfAAAAAPVVVVV8AAAAA9VVXwAAAAD1VVVV8AAAAD1VVXwAAAAD1VVVXwAAAAD1VVV8AAAAA9VVVXwAAAAD1VVV8AAAAA9VVVfAAAAAPVVVVfAAAAAPVVVfAAAAAPVVVVfAAAAAD1VV8AAAAA9VVVVXwAAAAA9VXwAAAAA9VVVVXwAAAAAP3/AAAAAD1VVVVV8AAAAAD/8AAAAAPVVVVVVfAAAAAAMAAAAAAPVVVVVVXwAAAAAAAAAAAA9VVVVVVXwAAAAAAAAAAAD1VVVVVVV8AAAAAAAAAAAPVVVVVVVVfwAAAAAAAAAA9VVVVVVVVX8AAAAAAAAAP1VVVVVVVVVfwAAAAAAAA/VVVVVVVVVVX/AAAAAAAP1VVVVVVVVVVVf8AAAAAP/VVVVVVVVVVVVV//AAA//1VVVVVVVVVVVVVX/////1VVVVVVVVVVVVVVVV///VVVVVVVVV"}, + {"width" : "43", "buffer":"VVVVVVVVVVVVVVVVVVVVVVf////9VVVVVVVVf//////VVVVVVV/AAAAAP1VVVVVV/AAAAAA9VVVVVX8AAAAAAPVVVVVX8AAAAAAD1VVVVXwAAAAAAA9VVVVfwAAAAAAAPVVVVfwAAAAAAAD1VVV/AAAAAAAAA9VVV/AAAAAAAAAPVVX8AAAAAAAAAD1VX8AAAAAAAAAA9VfwAAAAAAAAAAPVfwAAAAAAAAAAD1/AAAAAAAAAAAA9/AAAAAAAAAAAAP8AAAAAAAAAAAAPfAAAAAAAAAAAAD18AAAAAPAAAAAA9XwAAAA/8AAAAAPVfAAAA/XwAAAAD1V8AAD9V8AAAAA9VfAAD9VfAAAAAPVV8AP1VXwAAAAD1VX8P1VV8AAAAA9VVf/VVVfAAAAAPVVVfVVVXwAAAAD1VVVVVVV8AAAAA9VVVVVVVfAAAAAPVVVVVVVXwAAAAD1VVVVVVV8AAAAA9VVVVVVVfAAAAAPVVVVVVVXwAAAAD1VVVVVVV8AAAAA9VVVVVVVfAAAAAPVVVVVVVXwAAAAD1VVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVV8AAAAPVVVVVVVVfAAAAD1VVVVVVVXwAAAA9VVVVVVVV8AAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAD1VVVVVVVfAAAAA9VVVVVVVXwAAAAPVVVVVVVV8AAAAP1VVVVVVVf/////9VVVVVVVVf////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "50", "buffer":"VVVVVVf//9VVVVVVVVVVV///////VVVVVVVVX//8AAA//1VVVVVVf/wAAAAAAP9VVVVVf/AAAAAAAAD/VVVVX8AAAAAAAAAA/VVVX8AAAAAAAAAAAPVVV/AAAAAAAAAAAAPVVfAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAPVXwAAAAAAAAAAAAA9VfAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAD1XwAAAAAAAAAAAAAPVfAAAAD///AAAAAAPVfAAAP////AAAAAA9V8AAP/VVVfAAAAAD1XwAP9VVVVfAAAAAD1XwD9VVVVVfAAAAAPVfD9VVVVVVfAAAAA9V//VVVVVVV8AAAAD1V/VVVVVVVV8AAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAA9VVVVVVVVVV8AAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAA9VVVVVVVVVV8AAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAA9VVVVVVVVVV8AAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAD1VVVVVVVVVV8AAAAPVVVVVVVVVVfAAAAA9VVVVVVVVVV8AAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAD1VVVVVVVVVXwAAAAPVVVVVVVVVVfAAAAA9VVVVVVVVVV8AAAAD1VVVVVVVVVfAAAAAPVVVVVVVVVV8AAAAD1VVVVVVVVVfAAAAAPVVVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAPVVVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVXwAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVfAAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVfAAAAAD1VVVVVVVVV8AAAAA9VVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVVfAAAAAPVVVVVVVVVXwAAAAA9VVVVVVVVV8AAAAAPVVVVVVVVVfAAAAAD1VVVVVVVVV8AAAAAPVVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVV8AAAAAPVVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAAPVVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVV8AAAAAPVVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVVfAAAAAD1VVVVVVVVXwAAAAA9VVVVVVVVV8AAAAAD1VVVVVVVVXwAAAAAD////////1fAAAAAAD////////V8AAAAAAAAAAAAAA9XwAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAA9XwAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAA9XwAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAPV///////////////9X///////////////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "54", "buffer":"VVVVVX/////1VVVVVVVVVVX///////1VVVVVVVVX/8AAAAAP/VVVVVVVX/8AAAAAAAP9VVVVVV/8AAAAAAAAA/VVVVVX8AAAAAAAAAAD1VVVV/AAAAAAAAAAAA/VVVX8AAAAAAAAAAAAP1VVfAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAP//wAAAAAAA9VXwAAD////AAAAAAA9VV8AA/1VVfwAAAAAAPVV8AP9VVVV8AAAAAAPVVfA/VVVVVfAAAAAAPVVfP1VVVVVXwAAAAAPVVX/VVVVVVV8AAAAAPVVX1VVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVXwAAAAD1VVVVVVVVVVfAAAAAD1VVVVVVVVVVfAAAAAPVVVVVVVVVVV8AAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVV/AAAAAD1VVVVVVVVVf8AAAAA/VVVVVVVVVV/AAAAAD9VVVVVVVVVXwAAAAA/VVVVVVVVVVXwAAAAD9VVVVVVVVVVXwAAAAPVVVVVVVVVVVXwAAAAPVVVVVVVVVVVXwAAAAD1VVVVVVVVVVXwAAAAA/VVVVVVVVVVXwAAAAAP9VVVVVVVVVXwAAAAAA/1VVVVVVVVXwAAAAAAD9VVVVVVVVXwAAAAAAAP1VVVVVVVX/8AAAAAAD9VVVVVVVV//8AAAAAAPVVVVVVVVVX/wAAAAAD1VVVVVVVVVX/AAAAAA9VVVVVVVVVVfwAAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAAPVVVVVVVVVVVXwAAAAPVVVVVVVVVVVXwAAAAD1VVVVVVVVVVV8AAAAD1VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAA9VVVVVVVVVVXwAAAAA9VXVVVVVVVVXwAAAAA9Vf9VVVVVVVfAAAAAA9Vf/VVVVVVVfAAAAAA9VfD9VVVVVV8AAAAAA9V8A/1VVVVfwAAAAAD1V8AD/VVVV/AAAAAAD1V8AAP/1V/wAAAAAAD1V8AAA////AAAAAAAPVV8AAAAP/AAAAAAAA9VV8AAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAD9VVXwAAAAAAAAAAAAPVVVXwAAAAAAAAAAAD9VVVX/AAAAAAAAAAAP1VVVVf8AAAAAAAAAD9VVVVVV/8AAAAAAAD/1VVVVVVX//AAAAAD/9VVVVVVVVX///////9VVVVVVVVVVV/////9VVVVVVV"}, + {"width" : "54", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////9VVVVVVVVVVVf//////VVVVVVVVVVVfAAAAAPVVV/////VVVfAAAAAPVVf/////9VVfAAAAAPVV/AAAAA9VVfAAAAAPVV8AAAAA9VVfAAAAAPVV8AAAAA9VVfAAAAAPVV8AAAAA9VVfAAAAAPVV8AAAAD1VVfAAAAAPVV8AAAAD1VVfAAAAAPVV8AAAAD1VVfAAAAAPVV8AAAAD1VVfAAAAA9VV8AAAAD1VVfAAAAA9VV8AAAAD1VVfAAAAA9VV8AAAAPVVVfAAAAA9VV8AAAAPVVVfAAAAA9VV8AAAAPVVVfAAAAA9VV8AAAAPVVVfAAAAA9VXwAAAAPVVVfAAAAA9VXwAAAAPVVVfAAAAA9VXwAAAAPVVVfAAAAA9VXwAAAAPVVVfAAAAA9VXwAAAA9VVVfAAAAA9VXwAAAA9VVVfAAAAA9VXwAAAA9VVVfAAAAA9VXwAAAA9VVVfAAAAA9VXwAAAA9VVVfAAAAA9VfAAAAA9VVVfAAAAA9VfAAAAA9VVVXwAAAA9VfAAAAA9VVVXwAAAA9VfAAAAA9VVVXwAAAA9VfAAAAA9VVVXwAAAA9VfAAAAD1VVVXwAAAA9VfAAAAD1VVVXwAAAA9VfAAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVXwAAAA9V8AAAAD1VVVfAAAAA9XwAAAAD1VVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAD1f//8AAAAA9XwAAAAA////wAAAAA9fAAAAAAPwAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAA9///////////wAAAAA9f//////////8AAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVV//////9VVVVVVVVVVV//////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "52", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV////////////9VVVV/////////////VVVVfAAAAAAAAAAAD1VVVfAAAAAAAAAAAA9VVVXwAAAAAAAAAAAPVVVV8AAAAAAAAAAAD1VVVfAAAAAAAAAAAA9VVVXwAAAAAAAAAAAPVVVV8AAAAAAAAAAAD1VVVfAAAAAAAAAAAA9VVVXwAAAAAAAAAAAPVVVV8AAAAA///////1VVVfAAAAA///////1VVVXwAAAA9VVVVVVVVVVV8AAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAD1VVVVVVVVVVXwAAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAD1VVVVVVVVVVXwAAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAPVVVVVVVVVVVXwAAAD1VVVVVVVVVVXwAAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAD1VVVVVVVVVVXwAAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAD1VVVVVVVVVVXwAAAA9VVVVVVVVVVV8AAAAPVVVVVVVVVVVfAAAAD1VVVVVVVVVVXwAAAAP//9VVVVVVVV8AAAAA////1VVVVVV8AAAAAAAAP/1VVVVVfAAAAAAAAAA/1VVVVXwAAAAAAAAAA/1VVVV8AAAAAAAAAAA/VVVVfAAAAAAAAAAAA9VVVXwAAAAAAAAAAAD9VVV8AAAAAAAAAAAAP1VVfAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAD1VV8AD///AAAAAAAAPVV8D/////wAAAAAAA9Vf//1VVX/wAAAAAAPVX/1VVVVV/AAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVVXwAAAAAA9VVVVVVVVVfAAAAAAPVVVVVVVVVV8AAAAAD1VVVVVVVVVXwAAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVVfAAAAAPVVVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAA9VVVVVVVVVVfAAAAAPVVVVVVVVVVXwAAAAD1VVVVVVVVVV8AAAAA9VVVVVVVVVVfAAAAAPVVVVVVVVVVXwAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVVfAAAAAD1VVVVVVVVVXwAAAAD1VVVVVVVVVXwAAAAA9VVVVVVVVVV8AAAAAPVVVVVVVVVV8AAAAAD1VVVVVVVVVfAAAAAD1VVVVVVVVVfAAAAAA9VVVVVVVVVfAAAAAAPV/9VVVVVVfAAAAAAPVf/9VVVVV/AAAAAAD1XwP/VVVX/AAAAAAD1V8AP////8AAAAAAA9VfAAD///wAAAAAAA9VXwAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAPVVfAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAPVVV8AAAAAAAAAAAAPVVVfAAAAAAAAAAAA/VVVX/AAAAAAAAAAD/VVVVX/wAAAAAAAAP9VVVVVX//AAAAAAP/1VVVVVVV/////////VVVVVVVVVX//////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "59", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/////1VVVVVVVVVVVVf///////9VVVVVVVVVVf/wAAAAD//1VVVVVVVVf8AAAAAAAA/VVVVVVVVf8AAAAAAAAA9VVVVVVVX8AAAAAAAAAD1VVVVVVX8AAAAAAAAAAPVVVVVVV/AAAAAAAAAAD1VVVVVVfAAAAAAAAAAAPVVVVVVXwAAAAAAAAAAA9VVVVVV8AAAAAAAAAAAD1VVVVVXwAAAAAAAP//APVVVVVV8AAAAAAA////89VVVVVfAAAAAAA/9VVf/VVVVVV8AAAAAAP1VVVV1VVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVfAAAAAA9VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVXwAAAAD1VV///VVVVVVVfAAAAAPVf/////VVVVVV8AAAAA9f/wAAP/9VVVVXwAAAAA/8AAAAAP/VVVV8AAAAAA8AAAAAAA/VVVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAA//AAAAAAA9XwAAAAAAA///wAAAAAD1fAAAAAAAP1VfwAAAAAD18AAAAAAD1VVXwAAAAAPXwAAAAAA9VVVXwAAAAA9fAAAAAAPVVVVXwAAAAA98AAAAAD1VVVVfAAAAAD3wAAAAA9VVVVVfAAAAAPfAAAAAD1VVVVV8AAAAA98AAAAA9VVVVVV8AAAAA/wAAAAD1VVVVVXwAAAAD/AAAAAPVVVVVVfAAAAAP8AAAAA9VVVVVV8AAAAA/wAAAAD1VVVVVV8AAAAD/AAAAAPVVVVVVXwAAAAP8AAAAA9VVVVVVfAAAAA98AAAAD1VVVVVV8AAAAD3wAAAAPVVVVVVXwAAAAPfAAAAA9VVVVVVfAAAAA98AAAAD1VVVVVV8AAAAD3wAAAAPVVVVVVXwAAAAPfAAAAA9VVVVVVfAAAAA98AAAAD1VVVVVV8AAAAD3wAAAAD1VVVVVXwAAAAPXwAAAAPVVVVVVfAAAAA9fAAAAA9VVVVVXwAAAAD18AAAAD1VVVVVfAAAAAPXwAAAAPVVVVVV8AAAAA9fAAAAAPVVVVVXwAAAAPVfAAAAA9VVVVVfAAAAA9V8AAAAD1VVVVXwAAAAD1XwAAAAPVVVVVfAAAAAPVfAAAAAPVVVVV8AAAAD1VfAAAAA9VVVVfAAAAAPVV8AAAAA9VVVV8AAAAA9VXwAAAAA9VVVfAAAAAPVVXwAAAAD1VVXwAAAAA9VVfAAAAAD1VV8AAAAAPVVVfAAAAAD9V/AAAAAA9VVV8AAAAAD//wAAAAAPVVVV8AAAAAA/wAAAAAA9VVVXwAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAD9VVVVVVfwAAAAAAAAAA/VVVVVVVfwAAAAAAAAA/VVVVVVVVX8AAAAAAAA/1VVVVVVVVX/wAAAAAD/1VVVVVVVVVV//AAAAP/1VVVVVVVVVVVX//////VVVVVVVVVVVVVVf///9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "45", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf///9f////////////////////////wAAAP8AAAAAAAAAAAAA98AAAAAAAAAAAAA98AAAAAAAAAAAAA98AAAAAAAAAAAAA98AAAAAAAAAAAAD18AAAAAAAAAAAAD18AAAAAAAAAAAAD18AAAAAAAAAAAAPV8AAAAAAAAAAAAPV8AAAAAAAAAAAAPV///////8AAAAAPVf///////AAAAA9VVVVVVVVXwAAAA9VVVVVVVVXwAAAA9VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVV8AAAAPVVVVVVVVV8AAAAPVVVVVVVVV8AAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAPVVVVVVVVV8AAAAPVVVVVVVVV8AAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAD1VVVVVVVVfAAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVfAAAAAPVVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVV8AAAAA9VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVV8AAAAD1VVVVVVVXwAAAAD1VVVVVVVXwAAAAD1VVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVXwAAAAPVVVVVVVVfAAAAAPVVVVVVVVfAAAAAPVVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVVfAAAAA9VVVVVVVV8AAAAA9VVVVVVVV8AAAAA9VVVVVVVV8AAAAA9VVVVVVVV8AAAAA9VVVVVVVV8AAAAD1VVVVVVVXwAAAAD1VVVVVVVXwAAAAD1VVVVVVVXwAAAAD1VVVVVVVX//////1VVVVVVVV//////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "54", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVX//1VVVVVVVVVVVVX//////1VVVVVVVVVX//8AAP//1VVVVVVVV/8AAAAAAP/VVVVVVVf8AAAAAAAAP9VVVVVV/AAAAAAAAAA/VVVVVXwAAAAAAAAAAD1VVVVfAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVfAAAAAA//AAAAAA9VV8AAAAAD//wAAAAAPVV8AAAAAPVV8AAAAAPVXwAAAAA9VVfAAAAAD1XwAAAAD1VVXwAAAAD1XwAAAAD1VVV8AAAAD1XwAAAAPVVVV8AAAAA9fAAAAAPVVVVfAAAAA9fAAAAA9VVVVfAAAAA9fAAAAA9VVVVfAAAAAPfAAAAA9VVVVfAAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAPfAAAAD1VVVVXwAAAAPfAAAAD1VVVVXwAAAAPfAAAAA9VVVVXwAAAA9fAAAAA9VVVVfAAAAA9XwAAAA9VVVVfAAAAA9XwAAAA9VVVVfAAAAD1XwAAAAPVVVV8AAAAD1V8AAAAPVVVV8AAAAPVV8AAAAD1VVV8AAAAPVVfAAAAD1VVXwAAAAPVVfAAAAA9VVfAAAAA9VVXwAAAAPVV8AAAAD1VVV8AAAAD1XwAAAAD1VVVfAAAAA//AAAAAPVVVVfAAAAAP8AAAAA9VVVVXwAAAAAAAAAAD1VVVVV8AAAAAAAAAAPVVVVVVfAAAAAAAAAA9VVVVVVXwAAAAAAAAD1VVVVVVXwAAAAAAAAD1VVVVVVfAAAAAAAAAA/VVVVVX8AAAAAAAAAAP1VVVVfwAAAAAAAAAAA9VVVV8AAAAAAAAAAAAPVVVXwAAAAA//AAAAAD1VVfAAAAAD//wAAAAA9VVfAAAAAPVV8AAAAA9VV8AAAAA9VVfAAAAAPVV8AAAAD1VVXwAAAAD1XwAAAAPVVVV8AAAAD1XwAAAAPVVVV8AAAAA9fAAAAA9VVVVfAAAAA9fAAAAA9VVVVfAAAAA9fAAAAA9VVVVfAAAAAPfAAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAP8AAAAD1VVVVXwAAAAPfAAAAA9VVVVXwAAAAPfAAAAA9VVVVfAAAAA9fAAAAA9VVVVfAAAAA9XwAAAAPVVVVfAAAAA9XwAAAAPVVVV8AAAAD1V8AAAAD1VVXwAAAAD1V8AAAAA9VVfAAAAAPVVfAAAAAP1X8AAAAAPVVXwAAAAD//wAAAAA9VVXwAAAAAP8AAAAAD1VVV8AAAAAAAAAAAAPVVVVfwAAAAAAAAAAA9VVVVX/AAAAAAAAAAP1VVVVVf8AAAAAAAAD/VVVVVVV/wAAAAAAD/1VVVVVVVX/8AAAAD/9VVVVVVVVVf//////9VVVVVVVVVVVX////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "59", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX////1VVVVVVVVVVVVV///////VVVVVVVVVVVV//AAAAD/1VVVVVVVVVV/wAAAAAAP9VVVVVVVVV/wAAAAAAAD/VVVVVVVVfwAAAAAAAAA/VVVVVVVfwAAAAAAAAAAPVVVVVVX8AAAAAAAAAAAPVVVVVV8AAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAA9VVVXwAAAAAD/wAAAAAD1VVVfAAAAAA//8AAAAAD1VVXwAAAAAPVX8AAAAAD1VVfAAAAAD1VV8AAAAAPVVXwAAAAA9VVV8AAAAA9VVfAAAAAPVVVV8AAAAA9VXwAAAAA9VVVXwAAAAD1VfAAAAAPVVVVXwAAAAD1V8AAAAA9VVVVfAAAAAPVfAAAAAPVVVVVfAAAAA9V8AAAAA9VVVVV8AAAAD1XwAAAAPVVVVVXwAAAAD1fAAAAA9VVVVVXwAAAAPV8AAAAD1VVVVVfAAAAA9fAAAAAPVVVVVV8AAAAD18AAAAA9VVVVVXwAAAAPXwAAAAD1VVVVVfAAAAAPfAAAAA9VVVVVVfAAAAA98AAAAD1VVVVVV8AAAAD3wAAAAPVVVVVVXwAAAAPfAAAAA9VVVVVVfAAAAA98AAAAD1VVVVVV8AAAAD3wAAAAPVVVVVVXwAAAAD/AAAAA9VVVVVVfAAAAAP8AAAAD1VVVVVV8AAAAA/wAAAAPVVVVVVXwAAAAD/AAAAAPVVVVVVfAAAAAP8AAAAA9VVVVVV8AAAAA/wAAAAD1VVVVVXwAAAAD/AAAAAPVVVVVVfAAAAAP8AAAAA9VVVVVV8AAAAA98AAAAA9VVVVVXwAAAAD3wAAAAD1VVVVV8AAAAAPfAAAAAPVVVVVXwAAAAA98AAAAAPVVVVV8AAAAAD18AAAAAPVVVVfAAAAAAPXwAAAAA9VVVXwAAAAAA9fAAAAAA9VVV8AAAAAAD1fAAAAAA/VV/AAAAAAAPV8AAAAAA///wAAAAAAA9V8AAAAAAP/wAAAAAAAD1XwAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVVfwAAAAAAAAAAAAAA9VVVf8AAAAAADwAAAAAD1VVVX/wAAAAP/wAAAAAPVVVVV///AD//XwAAAAD1VVVVVX////9VfAAAAAPVVVVVVVVf/VVV8AAAAA9VVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVfAAAAAD1VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVX1VVVV/AAAAAAD1VVVVVf/VVV/wAAAAAAPVVVVVV/////wAAAAAAD1VVVVVfAP//wAAAAAAA9VVVVVV8AAAAAAAAAAAD1VVVVVXwAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAP1VVVVVVXwAAAAAAAAAD9VVVVVVV8AAAAAAAAAD9VVVVVVVXwAAAAAAAAD/VVVVVVVVf8AAAAAAAD/VVVVVVVVVf//AAAAA//VVVVVVVVVVV////////VVVVVVVVVVVVVf////1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "20", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX//1VVX///9VX/AAD/V/AAAA/XwAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA98AAAAPV/AAAP1V////9VVf//9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf//VVVf///1Vf8AAP9X8AAAD9fAAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD3wAAAA9X8AAA/VX/8D/1VV///1VVVV/VVU="} + ] +} diff --git a/apps/contourclock/fonts/font-NerkoOne.json b/apps/contourclock/fonts/font-NerkoOne.json new file mode 100644 index 000000000..14fb84fcb --- /dev/null +++ b/apps/contourclock/fonts/font-NerkoOne.json @@ -0,0 +1,17 @@ +{ + "name":"Nerko One", + "size":"99", + "characters":[ + {"width" : "60", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf////VVVVVVVVVVVVVVf//////VVVVVVVVVVVVX/wAAAA/9VVVVVVVVVVV/wAAAAAA/1VVVVVVVVVf8AAAAAAAD9VVVVVVVVV/AAAAAAAAAP1VVVVVVVXwAAAAAAAAAD9VVVVVVVfAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAD1VVVV8AAAAAAP8AAAAAA9VVVXwAAAAAA//AAAAAA9VVVXwAAAAAD1XwAAAAAPVVVXwAAAAAPVXwAAAAAPVVVfAAAAAAPVV8AAAAAD1VVfAAAAAA9VVfAAAAAD1VV8AAAAAA9VVfAAAAAD1VV8AAAAAA9VVfAAAAAA9VV8AAAAAD1VVXwAAAAA9VV8AAAAAD1VVXwAAAAAPVXwAAAAAD1VVXwAAAAAPVXwAAAAAD1VVXwAAAAAPVXwAAAAAD1VVXwAAAAAPVXwAAAAAD1VVV8AAAAAD1fAAAAAAPVVVV8AAAAAD1fAAAAAAPVVVV8AAAAAD1fAAAAAAPVVVV8AAAAAD1fAAAAAAPVVVV8AAAAAD1fAAAAAAPVVVV8AAAAAD1fAAAAAAPVVVV8AAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAAPVVVVfAAAAAA98AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAAP8AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAA98AAAAAA9VVVVfAAAAAA9fAAAAAA9VVVVfAAAAAA9fAAAAAA9VVVVfAAAAAA9fAAAAAA9VVVVfAAAAAA9fAAAAAA9VVVV8AAAAAA9fAAAAAAPVVVV8AAAAAA9fAAAAAAPVVVV8AAAAAA9fAAAAAAPVVVV8AAAAAA9XwAAAAAPVVVV8AAAAAA9XwAAAAAPVVVV8AAAAAD1XwAAAAAPVVVV8AAAAAD1XwAAAAAPVVVV8AAAAAD1XwAAAAAPVVVXwAAAAAD1V8AAAAAPVVVXwAAAAAD1V8AAAAAD1VVXwAAAAAD1V8AAAAAD1VVXwAAAAAD1V8AAAAAD1VVXwAAAAAPVVfAAAAAD1VVXwAAAAAPVVfAAAAAD1VVfAAAAAAPVVfAAAAAA9VVfAAAAAAPVVXwAAAAA9VV8AAAAAA9VVXwAAAAAPVV8AAAAAA9VVXwAAAAAPVXwAAAAAA9VVV8AAAAAD//AAAAAAD1VVV8AAAAAA/8AAAAAAD1VVVfAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAPVVVVVVVfAAAAAAAAAAD9VVVVVVVX8AAAAAAAAAP1VVVVVVVV/AAAAAAAAD9VVVVVVVVVX/AAAAAAA/1VVVVVVVVVV////////9VVVVVVVVVVVV///////VVVVVV"}, + {"width" : "42", "buffer":"VVVVVVf//9VVVVVVVVVX////1VVVVVVVV/wAAD9VVVVVVVX8AAAAPVVVVVVVfAAAAAD1VVVVVVfAAAAAD1VVVVVV8AAAAAD1VVVVVXwAAAAAD1VVVVVXwAAAAAA9VVVVVfAAAAAAA9VVVVVfAAAAAAA9VVVVV8AAAAAAA9VVVVV8AAAAAAA9VVVVXwAAAAAAA9VVVVXwAAAAAAA9VVVVfAAAAAAAA9VVVVfAAAAAAAA9VVVVfAAAAAAAA9VVVV8AAAAAAAA9VVVV8AAAAAAAA9VVVV8AAAAAAAA9VVVXwAAAAAAAA9VVVXwAAAAAAAAPVVVXwAAAAAAAAPVVVfAAAAAAAAAPVVVfAAAAAAAAAPVVVfAAAAAAAAAPVVV8AAAAAAAAAPVVV8AAAAAAAAAPVVV8AAAAAAAAAPVVV8AAAAAAAAAPVVXwAAAAAAAAAD1VXwAAAAAAAAAD1VXwAAAAAAAAAD1VfAAAAAAAAAAD1VfAAAAAAAAAAD1VfAAAAAAAAAAD1VfAAAAAAAAAAD1V8AAAAAAAAAAD1V8AAAAAAAAAAD1V8AAAAAAAAAAD1V8AAAAAAAAAAD1V8AAAAAAAAAAD1V8AAAAAAAAAAA9V8AAAAAAAAAAA9VfAAAAAAAAAAA9VfwAAAAAAAAAA9VV/wAAAAAAAAA9VVf//8AAAAAAA9VVVf//AAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAAPVVVVVXwAAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVfAAAAAAD1VVVVVXwAAAAAD1VVVVVXwAAAAAD1VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVXwAAAAAA9VVVVVV8AAAAAA9VVVVVV8AAAAAA9VVVVVV8AAAAAA9VVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVV8AAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAPVVVVVVfAAAAAAPVVVVVVXwAAAAAPVVVVVVXwAAAAAPVVVVVVXwAAAAA9VVVVVVV8AAAAA9VVVVVVV/AAAAA9VVVVVVVf/////1VVVVVVVV/////VA="}, + {"width" : "57", "buffer":"VVVVVX/////1VVVVVVVVVVVX///////9VVVVVVVVVV/8AAAAAP/9VVVVVVVVf8AAAAAAAD/1VVVVVVV/AAAAAAAAAD/VVVVVVfwAAAAAAAAAAP9VVVVV/AAAAAAAAAAAA/VVVVXwAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAD1VXwAAAAAAD/AAAAAAA9VXwAAAAAA//wAAAAAAPVfAAAAAAD9V8AAAAAAPVfAAAAAAPVV8AAAAAAPVfAAAAAA9VVfAAAAAAD18AAAAAA9VVfAAAAAAD18AAAAAD1VVfAAAAAAD18AAAAAD1VVfAAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA98AAAAAD1VVXwAAAAAA9fAAAAAPVVVXwAAAAAA9fAAAAAPVVVXwAAAAAA9XwAAAD9VVVfAAAAAAD1V/AAA/1VVVfAAAAAAD1Vf///9VVVVfAAAAAAD1VV///VVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAD1VVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAA9VVV//9VVVVV8AAAAAA9VX////9VVVXwAAAAAAP///AAD/VVVXwAAAAAAD/8AAAAD1VVfAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAD9XwAAAAAAAAAAAAAAA/1XwAAAAAAAAAAAAA//9VXwAAAAAAAAAAD////VVV8AAAAAAAAA////VVVVV8AAAAAAD///9VVVVVVVf/////////VVVVVVVVVX//////9VVVVVVVVVVA=="}, + {"width" : "57", "buffer":"VVVVVX////1VVVVVVVVVVVV///////9VVVVVVVVVV//8AAAAP/1VVVVVVVVf/AAAAAAAD/VVVVVVVX/AAAAAAAAAP9VVVVVVfwAAAAAAAAAA/VVVVVV8AAAAAAAAAAAD1VVVVXwAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAD1VVfAAAAD/8AAAAAAAD1VVf8AA////wAAAAAAD1VVX////9VX8AAAAAAA9VVVX//VVVVfAAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVX8AAAAAAD1VVVVVVVVX/wAAAAAAD1VVVVVVVV/8AAAAAAAD1VVVVVVVX8AAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAD1VVVVVVV8AAAAAAAAAA9VVVVVVV8AAAAAAAAAAPVVVVVVV8AAAAAAAAAAD1VVVVVV8AAAAAAAAAAA9VVVVVV8AAAAAAAAAAAPVVVVVVfAAAAAAAAAAAPVVVVVVf/AAAAAAAAAAPVVVVVVV///wAAAAAAAD1VVVVVVV//8AAAAAAAD1VVVVVVVVVfAAAAAAAD1VVVVVVVVVXwAAAAAAA9VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVVVVVVVVVVfAAAAAAPVVV//9VVVVVfAAAAAAPVV////9VVVVfAAAAAAPVX/AAD/VVVVfAAAAAAPVfAAAAD1VVVfAAAAAAPV8AAAAA9VVVfAAAAAAPXwAAAAA9VVVfAAAAAAPXwAAAAA9VVVfAAAAAAPfAAAAAAPVVVfAAAAAAPfAAAAAAPVVVfAAAAAAPfAAAAAAPVVVfAAAAAAPfAAAAAAPVVVfAAAAAAPfAAAAAAPVVVfAAAAAAPfAAAAAAD1VVfAAAAAAPfAAAAAAD1VVfAAAAAA9fAAAAAAA9VV8AAAAAA9fAAAAAAA9VV8AAAAAA9XwAAAAAAPVXwAAAAAA9XwAAAAAAD//AAAAAAA9XwAAAAAAA/8AAAAAAD1V8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA9VVV/AAAAAAAAAAAAAD1VVVfwAAAAAAAAAAAAPVVVVV/AAAAAAAAAAAA9VVVVVf8AAAAAAAAAAP1VVVVVV/wAAAAAAAAD/VVVVVVVX//////////1VVVVVVVVf////////9VVVVA=="}, + {"width" : "56", "buffer":"VVVVVVVVVVVVVVVVVVVVVf//VVVVVVVVVVVVVVVf///9VVVVVVVVVVVVVX8AAP9VVVVVVVVVVVVV8AAAA9VVVVVVVVVVVVfAAAAD1VVVVVVVVVVVXwAAAAD1VVVVVVVVVVVfAAAAAPVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVV8AAAAAPVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAD1VVVVVVVVVVV8AAAAAPVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAD1VVVVX//1VVV8AAAAAPVVVV/////VVXwAAAAA9VVVf/AAD/VVfAAAAAD1VVXwAAAAPVV8AAAAAPVVV8AAAAAPVXwAAAAA9VVfAAAAAA9VfAAAAAPVVV8AAAAAA9V8AAAAA9VVXwAAAAAD1XwAAAAD1VVfAAAAAAPVfAAAAAPVVV8AAAAAA9V8AAAAA9VVXwAAAAAD1XwAAAAD1VVfAAAAAAPVfAAAAAPVVV8AAAAAA9V8AAAAA9VVXwAAAAAA9XwAAAAD1VVfAAAAAAD1fAAAAAPVVV8AAAAAAPXwAAAAA9VVXwAAAAAA9fAAAAAD1VVfAAAAAAD18AAAAA9VVV8AAAAAAPXwAAAAD1VVXwAAAAAA9fAAAAAPVVVfAAAAAAD18AAAAA9VVV8AAAAAAPXwAAAAD1VVXwAAAAAA9fAAAAAPVVVfAAAAAAD18AAAAA9VVV8AAAAAAPXwAAAAD1VVXwAAAAAA9fAAAAAPVVVfAAAAAAD18AAAAA9VVV8AAAAAAPXwAAAAD1VVXwAAAAAA9fAAAAA9VVVfAAAAAAD18AAAAD1VVV8AAAAAAPXwAAAAPVVVXwAAAAAA9fAAAAA9VVVfAAAAAAD18AAAAD1VVV8AAAAAAPXwAAAAPVVVXwAAAAAA9fAAAAA9VVVXwAAAAAD18AAAAD1VVVfAAAAAAD3wAAAAPVVVXwAAAAAAPfAAAAA9X//8AAAAAAA98AAAAA////AAAAAAAD3wAAAAA/AAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD18AAAAAAA//AAAAAAAPV/AAAA/////AAAAAAA9V///////1VfAAAAAAD1Vf///1VVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVV8AAAAAAPVVVVVVVVVVXwAAAAAA9VVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAPVVVVVVVVVVV8AAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVVXwAAAAA9VVVVVVVVVVVfAAAAAPVVVVVVVVVVVVf/////9VVVVVVVVVVVVf////9V"}, + {"width" : "58", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////VVVVVVf/////////////1VVVX////////wAAAAD/VVVX/AAAAAAAAAAAAAA/VVXwAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAP1V8AAAAAAAAAAAAAAA/1VfAAAAAAAAAAAAAAP/VVXwAAAAAD////////9VVV8AAAAAD////////VVVVfAAAAAD1VVVVVVVVVVVXwAAAAA9VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVfAAAAAA9VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVfAAAAAA9VVVVVVVVVVVXwAAAAAPVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVfAAAAAA9VVVVVVVVVVVXwAAAAA9VVVVVVVVVVVV8AAAAAPVVVVVVVVVVVVfAAAAAA/////1VVVVVVfAAAAAAD//////VVVVVXwAAAAAAAAAAA//VVVVV8AAAAAAAAAAAAD9VVVVfAAAAAAAAAAAAAD9VVVXwAAAAAAAAAAAAAP1VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9V8AAAAAAAP8AAAAAAAPVXwAAAAAAP/wAAAAAAA9V8AAAAAAPVfAAAAAAAPVfAAAAAAPVV8AAAAAAD1XwAAAAAPVVXwAAAAAAPVfAAAAAPVVVfAAAAAAD1V/AAAAPVVVXwAAAAAA9VX/AAAP1VVV8AAAAAAD1VX/8D/VVVVXwAAAAAA9VVX///VVVVV8AAAAAAPVVVVf1VVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVXwAAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAAPVVVVVVVVVVVXwAAAAAD1VVVVVVVVVVV8AAAAAA9VVVVVVVVVVVfAAAAAAPVVVX/9VVVVVXwAAAAAD1VV////VVVVXwAAAAAA9VX/wAP9VVVV8AAAAAA9VX8AAAD1VVVfAAAAAAPVXwAAAAPVVVfAAAAAAD1XwAAAAA9VVXwAAAAAD1V8AAAAAD1VXwAAAAAA9VfAAAAAAP//wAAAAAAPVXwAAAAAA//wAAAAAAPVV8AAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAPVVVVfwAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAPVVVVVV/AAAAAAAAAAA/VVVVVVX8AAAAAAAAAA/VVVVVVVX/AAAAAAAAP9VVVVVVVVf/////////9VVVVVVVVVX////////VVVVVVA="}, + {"width" : "58", "buffer":"VVVVVVVVV//1VVVVVVVVVVVVVVVf///1VVVVVVVVVVVVVVf8AA/1VVVVVVVVVVVVVfAAAA/VVVVVVVVVVVVVfAAAAA/VVVVVVVVVVVVfAAAAAD9VVVVVVVVVVVfAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAA9VVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAD1VVVVVVVVVfAAAAAAAD1VVVVVVVVVXwAAAAAAD1VVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVV8AAAAAAD1VVVVVVVVVV8AAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAA////1VVVVVV8AAAAAAAD////9VVVVVfAAAAAAAAAAAA/9VVVVfAAAAAAAAAAAAAP1VVVXwAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VfAAAAAAAA/8AAAAAAD1XwAAAAAAD//8AAAAAA9V8AAAAAAD9VfwAAAAAPV8AAAAAAD1VVfAAAAAA9fAAAAAAD1VVV8AAAAAPXwAAAAAD1VVVXwAAAAA98AAAAAA9VVVV8AAAAAPfAAAAAA9VVVVXwAAAAD3wAAAAAPVVVVV8AAAAA98AAAAAD1VVVVfAAAAAPfAAAAAD1VVVVXwAAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAA9VVVVV8AAAAA/wAAAAAPVVVVVfAAAAAPfAAAAAD1VVVVXwAAAAD3wAAAAA9VVVVXwAAAAA98AAAAAPVVVVV8AAAAAPfAAAAAA9VVVVfAAAAAPXwAAAAAPVVVVXwAAAAD1fAAAAAD1VVVXwAAAAA9XwAAAAAPVVVV8AAAAAPV8AAAAAD1VVV8AAAAAD1fAAAAAAPVVVfAAAAAD1V8AAAAAA9VVfAAAAAA9VfAAAAAAD1V/AAAAAAPVXwAAAAAAP//AAAAAAPVVfAAAAAAA/8AAAAAAD1VXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAA9VVVVVfAAAAAAAAAAAA9VVVVVV8AAAAAAAAAAA9VVVVVVX8AAAAAAAAAA9VVVVVVVfwAAAAAAAAA9VVVVVVVVfwAAAAAAAD9VVVVVVVVV/wAAAAAAP9VVVVVVVVVV////////1VVVVVVVVVVV///////VVVVVVA="}, + {"width" : "55", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf1VVVVVVVVVVVV///////VVVVVVV/////////A//VVVV//////8AAAAAAD/VVf//8AAAAAAAAAAAD9Vf8AAAAAAAAAAAAAAPV/AAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAD3wAAAAAP/8AAAAAAAD1f8D//////wAAAAAAA9V//////VVfAAAAAAA9VVf1VVVVVXwAAAAAAPVVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAA9VVVVVVVVVXwAAAAAAPVVVVVVVVVV8AAAAAAD1VVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAA9VVVVVVVVVV8AAAAAAPVVVVVVVVVVfAAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVV//////1VVVVVVVVVVVX/////VVVVVVVVA"}, + {"width" : "58", "buffer":"VVVVVVVVf//9VVVVVVVVVVVVVV//////1VVVVVVVVVVVX//AAAP/9VVVVVVVVVVf8AAAAAA/1VVVVVVVVV/wAAAAAAAP1VVVVVVVX/AAAAAAAAA/1VVVVVVX8AAAAAAAAAA/VVVVVVXwAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAD1VVVVXwAAAAAPwAAAAAPVVVVXwAAAAA//AAAAAA9VVVV8AAAAA/V8AAAAAPVVVV8AAAAA9VXwAAAAA9VVV8AAAAA9VVfAAAAAD1VVfAAAAAPVVV8AAAAA9VVfAAAAAPVVVXwAAAAD1VXwAAAAD1VVV8AAAAA9VV8AAAAA9VVVfAAAAAPVV8AAAAA9VVVXwAAAAA9VfAAAAAPVVVV8AAAAAPVXwAAAAD1VVVfAAAAAD1V8AAAAA9VVVV8AAAAA9V8AAAAAPVVVVfAAAAAPVfAAAAAD1VVVXwAAAAD1XwAAAAA9VVVV8AAAAA9V8AAAAAPVVVVfAAAAAPVfAAAAAD1VVVXwAAAAD1XwAAAAA9VVVXwAAAAA9V8AAAAAPVVVV8AAAAAPVfAAAAAD1VVVfAAAAAD1XwAAAAA9VVVXwAAAAA9V8AAAAAD1VVV8AAAAAPVXwAAAAA9VVV8AAAAAD1V8AAAAAD1VVfAAAAAD1VfAAAAAAPVVfAAAAAA9VXwAAAAAA9VfAAAAAAPVVfAAAAAAD//AAAAAAD1VXwAAAAAAP/AAAAAAD1VV8AAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VV8AAAAAAD/AAAAAAAPVV8AAAAAAP//AAAAAAA9VfAAAAAAP1X8AAAAAAPVfAAAAAAPVVXwAAAAAA9XwAAAAAPVVVfAAAAAAPV8AAAAAD1VVV8AAAAAD1fAAAAAD1VVVXwAAAAA9fAAAAAA9VVVV8AAAAAPXwAAAAA9VVVVXwAAAAA98AAAAAPVVVVV8AAAAAPfAAAAAD1VVVVfAAAAAD3wAAAAD1VVVVV8AAAAA98AAAAA9VVVVVfAAAAAPfAAAAAPVVVVVXwAAAAD3wAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD/AAAAAD1VVVVV8AAAAA/wAAAAA9VVVVVfAAAAAP8AAAAAPVVVVVXwAAAAD3wAAAAD1VVVVV8AAAAA98AAAAA9VVVVVfAAAAA9fAAAAAPVVVVVXwAAAAPXwAAAAD1VVVVV8AAAAD18AAAAA9VVVVV8AAAAA9fAAAAAPVVVVVfAAAAAPV8AAAAD1VVVVXwAAAAPVfAAAAAPVVVVV8AAAAD1XwAAAAD1VVVV8AAAAA9VfAAAAAPVVVVfAAAAA9VXwAAAAD1VVVfAAAAAPVVfAAAAAPVVVfAAAAAD1VXwAAAAA9VVXwAAAAD1VV8AAAAAD/VfwAAAAA9VVXwAAAAAP//wAAAAA9VVVfAAAAAAD/AAAAAAPVVVXwAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAP1VVVVVV/AAAAAAAAAAP1VVVVVVX/AAAAAAAAAPVVVVVVVVX/AAAAAAAD/VVVVVVVVVX/////////VVVVVVVVVVX///////1VVVVVA="}, + {"width" : "61", "buffer":"VVVVVVVVV///1VVVVVVVVVVVVVVf//////1VVVVVVVVVVVX//8AAA//1VVVVVVVVVVf/AAAAAAA/1VVVVVVVVV/wAAAAAAAA/1VVVVVVVX/AAAAAAAAAA/VVVVVVVf8AAAAAAAAAAA9VVVVVVfwAAAAAAAAAAAD1VVVVV/AAAAAAAAAAAAAPVVVVV/AAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VV8AAAAAAA//AAAAAAAD1VfAAAAAAA///AAAAAAA9VfAAAAAAA9VX8AAAAAAPVXwAAAAAA9VVXwAAAAAA9V8AAAAAA9VVV8AAAAAAPV8AAAAAAPVVVXwAAAAAD1fAAAAAAPVVVVfAAAAAAPXwAAAAAD1VVVXwAAAAAD18AAAAAD1VVVV8AAAAAA9fAAAAAA9VVVVfAAAAAAPXwAAAAAPVVVVV8AAAAAD18AAAAAD1VVVVfAAAAAAPfAAAAAA9VVVVXwAAAAAD3wAAAAAPVVVVV8AAAAAA98AAAAAD1VVVVfAAAAAAPfAAAAAA9VVVVXwAAAAAD3wAAAAAPVVVVVfAAAAAA98AAAAAD1VVVVXwAAAAAPfAAAAAA9VVVVV8AAAAAD3wAAAAAD1VVVVfAAAAAA9fAAAAAA9VVVVXwAAAAAPXwAAAAAD1VVVV8AAAAAA98AAAAAAPVVVVfAAAAAAPfAAAAAAA9VVVfAAAAAAD18AAAAAAD/VX/AAAAAAA9fAAAAAAAP///AAAAAAAPXwAAAAAAAD/wAAAAAAAD1fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAPVVV/AAAAAAAAAAAAAAAD1VVX/AAAAAAAAAAAAAAA9VVVX/wAAAAAAAAAAAAAPVVVVX//AAAAAAAAAAAAD1VVVVV//////8AAAAAAA9VVVVVVX/////wAAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVV8AAAAAAPVVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVX//////VVVVVVVVVVVVVVf////9VA=="}, + {"width" : "25", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV////VVVX/////VVX8AAAD9VXwAAAAD1V8AAAAAPV8AAAAAA9fAAAAAAPXwAAAAAD18AAAAAAPfAAAAAAD3wAAAAAA98AAAAAAPfAAAAAAD18AAAAAD1fAAAAAA9V8AAAAA9VX8AAAD/VVf////9VVVf///1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/9VVVVV////VVVX/8AP/VVX8AAAD/VXwAAAAD9V8AAAAAPVfAAAAAA9fAAAAAAPXwAAAAAD18AAAAAA9fAAAAAAPXwAAAAAD18AAAAAA9XwAAAAAPV8AAAAAD1fAAAAAA9V8AAAAA/VX/AAAD/VVf////9VVVX///1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVA=="} + ] +} diff --git a/apps/contourclock/font-Nunito.json b/apps/contourclock/fonts/font-Nunito.json similarity index 100% rename from apps/contourclock/font-Nunito.json rename to apps/contourclock/fonts/font-Nunito.json diff --git a/apps/contourclock/font-OpenSansEC.json b/apps/contourclock/fonts/font-OpenSansEC.json similarity index 100% rename from apps/contourclock/font-OpenSansEC.json rename to apps/contourclock/fonts/font-OpenSansEC.json diff --git a/apps/contourclock/fonts/font-Oswald.json b/apps/contourclock/fonts/font-Oswald.json new file mode 100644 index 000000000..97c61911c --- /dev/null +++ b/apps/contourclock/fonts/font-Oswald.json @@ -0,0 +1,17 @@ +{ + "name":"Oswald", + "size":"100", + "characters":[ + {"width" : "54", "buffer":"VVVVVf//////9VVVVVVVVVX////////9VVVVVVVV/wAAAAAAD/1VVVVVVf8AAAAAAAAD9VVVVVV/AAAAAAAAAAPVVVVVXwAAAAAAAAAAD9VVVVfAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAA9XwAAAAAA//AAAAAAA9fAAAAAAD//wAAAAAA9fAAAAAAPVV8AAAAAA9fAAAAAA9VVfAAAAAAPfAAAAAA9VVXwAAAAAPfAAAAAD1VVXwAAAAAPfAAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVV8AAAAAP8AAAAAD1VVXwAAAAAPfAAAAAA9VVXwAAAAAPfAAAAAA9VVXwAAAAAPfAAAAAAPVVfAAAAAAPfAAAAAAD1V8AAAAAA9fAAAAAAA//wAAAAAA9XwAAAAAAP/AAAAAAA9XwAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAD1VVV8AAAAAAAAAAAAPVVVVfAAAAAAAAAAAA9VVVVX8AAAAAAAAAAD1VVVVV/AAAAAAAAAA/VVVVVVX8AAAAAAAAP9VVVVVVV/8AAAAAAD/VVVVVVVVX////////1VVVVVVVVVX//////9VVVVV"}, + {"width" : "34", "buffer":"VVVVVV/////1VVVVV/////9VVVVX8AAAAPVVVVX8AAAAD1VVVfwAAAAA9VVV/wAAAAAPVVV/AAAAAAD1VX8AAAAAAA9Vf8AAAAAAAPV/wAAAAAAAD3/AAAAAAAAA/8AAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAAAAAAAAP8AAAAAAAAAD/AAAAAAAAAA/wAAPAAAAAAP8AA/8AAAAAD/AP/XwAAAAA/w/9V8AAAAAP//VVfAAAAAD/9VVXwAAAAA/1VVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVXwAAAAA9VVVV8AAAAAPVVVVfAAAAAD1VVVX//////9VVVV///////VVVVVVVVVVVQ=="}, + {"width" : "55", "buffer":"VVVVVX///////1VVVVVVVVVf////////1VVVVVVVV/wAAAAAAA/1VVVVVVX/AAAAAAAAA/1VVVVVX8AAAAAAAAAA/VVVVVXwAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAA9V8AAAAAAD/wAAAAAAPV8AAAAAAP//AAAAAAD1fAAAAAAP1V8AAAAAAPXwAAAAAPVVXwAAAAAD3wAAAAAPVVVfAAAAAA98AAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAA98AAAAA9VVVV8AAAAAPfAAAAAPVVVVXwAAAAD/AAAAAD1VVVV8AAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVfAAAAAA/wAAAAA9VVVXwAAAAAP///////VVVV8AAAAAPf//////1VVV8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAA9VVVVVVVVVVfAAAAAA9VVVVVVVVVVfAAAAAAPVVVVVVVVVVfAAAAAAPVVVVVVVVVVXwAAAAAPVVVVVVVVVVXwAAAAAD1VVVVVVVVVXwAAAAAD1VVVVVVVVVV8AAAAAA9VVVVVVVVVV8AAAAAAD/////////1fAAAAAAAP////////9fAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAA9f/////////////////X/////////////////1VVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "54", "buffer":"VVVVVf//////9VVVVVVVVVf////////9VVVVVVVX/wAAAAAAD/1VVVVVVfwAAAAAAAAD9VVVVVX8AAAAAAAAAAP1VVVVfwAAAAAAAAAAD9VVVV8AAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAD1fAAAAAAA//AAAAAAA9fAAAAAAD//wAAAAAA9fAAAAAAPVV8AAAAAA9fAAAAAA9VVfAAAAAAP8AAAAAD1VVXwAAAAAP8AAAAAD1VVXwAAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP///////VVVV8AAAAAP///////VVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVXwAAAAD1VVVVVVVVVVfAAAAAD1VVVVVVVVVV8AAAAAPVVVVVVVVVVfwAAAAAPVVVVVVVX///AAAAAA9VVVVVVVf//wAAAAAD1VVVVVVVfAAAAAAAAP1VVVVVVVfAAAAAAAA/VVVVVVVVfAAAAAAAP1VVVVVVVVfAAAAAAA/VVVVVVVVVfAAAAAAD1VVVVVVVVVfAAAAAAA/VVVVVVVVVfAAAAAAAP9VVVVVVVVfAAAAAAAA/VVVVVVVVfAAAAAAAAD1VVVVVVVfAAAAAAAAA9VVVVVVVfAAAAAAAAA9VVVVVVVf//AAAAAAAPVVVVVVVX///AAAAAAD1VVVVVVVVV/wAAAAAD1VVVVVVVVVV8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAPVVVVVVVVVVV8AAAAAP//////9VVVV8AAAAAP///////VVVV8AAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVVfAAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAPVVVV8AAAAAP8AAAAAD1VVV8AAAAAPfAAAAAD1VVXwAAAAA9fAAAAAA9VVXwAAAAA9fAAAAAAPVV/AAAAAA9fAAAAAAD//8AAAAAA9XwAAAAAA//AAAAAAD1XwAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAA9VVVVfwAAAAAAAAAAD1VVVVX8AAAAAAAAAAPVVVVVVfwAAAAAAAAD9VVVVVVX/AAAAAAAD/1VVVVVVVf////////9VVVVVVVVV///////9VVVVV"}, + {"width" : "59", "buffer":"VVVVVVVVX///////9VVVVVVVVVVVf///////1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVfAAAAAAAD1VVVVVVVVVV8AAAAAAAPVVVVVVVVVVfAAAAAAAA9VVVVVVVVVV8AAAAAAAD1VVVVVVVVVfAAAAAAAAPVVVVVVVVVV8AAAAAAAA9VVVVVVVVVXwAAAAAAAD1VVVVVVVVV8AAAAAAAAPVVVVVVVVVXwAAAAAAAA9VVVVVVVVV8AAAAAAAAD1VVVVVVVVXwAAAAAAAAPVVVVVVVVV8AAAAAAAAA9VVVVVVVVXwAAAAAAAAD1VVVVVVVVfAAAAAAAAAPVVVVVVVVXwAAAAAAAAA9VVVVVVVVfAAADAAAAAD1VVVVVVVXwAAA/AAAAAPVVVVVVVVfAAAPfAAAAA9VVVVVVVXwAAA98AAAAD1VVVVVVVfAAAPXwAAAAPVVVVVVVV8AAA9fAAAAA9VVVVVVVfAAAD18AAAAD1VVVVVVV8AAA9XwAAAAPVVVVVVVfAAAD1fAAAAA9VVVVVVV8AAAPV8AAAAD1VVVVVVXwAAD1XwAAAAPVVVVVVV8AAAPVfAAAAA9VVVVVVXwAAD1V8AAAAD1VVVVVV8AAAPVXwAAAAPVVVVVVXwAAA9VfAAAAA9VVVVVV8AAAPVV8AAAAD1VVVVVXwAAA9VXwAAAAPVVVVVVfAAAD1VfAAAAA9VVVVVXwAAA9VV8AAAAD1VVVVVfAAAD1VXwAAAAPVVVVVXwAAAPVVfAAAAA9VVVVVfAAAD1VV8AAAAD1VVVVV8AAAPVVXwAAAAPVVVVVfAAAD1VVfAAAAA9VVVVV8AAAPVVV8AAAAD1VVVVfAAAA9VVXwAAAAPVVVVV8AAAPVVVfAAAAA9VVVVfAAAA9VVV8AAAAD1VVVV8AAAD1VVXwAAAAPVVVVXwAAA9VVVfAAAAA9VVVV8AAAD1VVV8AAAAD1VVVXwAAAPVVVXwAAAAPVVVV8AAAD1VVVfAAAAA9VVVXwAAAPVVVV8AAAAD1VVVfAAAA9VVVXwAAAAPVVVXwAAAPVVVVfAAAAA9VVVfAAAA9VVVV8AAAAD1VVXwAAAPVVVVXwAAAAPVVVfAAAA9VVVVfAAAAA9VVXwAAAA/////wAAAAA////AAAAA////8AAAAAA///8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAP//////////AAAAAAD/////////////AAAAAA///1VVVVVVVVVfAAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVV//////1VVVVVVVVVVVVX//////VVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "54", "buffer":"V///////////////1VV///////////////1VV8AAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VXwAAAAD/////////1VXwAAAAP/////////1VXwAAAA9VVVVVVVVVVVXwAAAA9VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VVVVVVVVVVVXwAAAD1VX///9VVVVVXwAAAD1X/////9VVVVXwAAAD1/8AAAD/1VVVfAAAAA/8AAAAAD9VVVfAAAAAPAAAAAAAPVVVfAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAPVfAAAAAAA/8AAAAAAPVfAAAAAAP//wAAAAAPVfAAAAAA/VX8AAAAAD1fAAAAAD1VVfAAAAAD1fAAAAAPVVVfAAAAAD1fAAAAAPVVVXwAAAAD1fAAAAA9VVVXwAAAAA9fAAAAA9VVVV8AAAAA9fAAAAA9VVVV8AAAAA9f/////1VVVV8AAAAA9f/////1VVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVV8AAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAAPVVVVVVVVVVVfAAAAA9VVVVVVVVVVVfAAAAA9///////VVVVfAAAAA9///////VVVVfAAAAA98AAAAAPVVVVfAAAAA98AAAAAPVVVVfAAAAA98AAAAAPVVVVfAAAAA98AAAAAPVVVV8AAAAA98AAAAAPVVVV8AAAAA9fAAAAAPVVVV8AAAAA9fAAAAAPVVVV8AAAAA9fAAAAAPVVVV8AAAAA9fAAAAAPVVVV8AAAAD1fAAAAAD1VVXwAAAAD1fAAAAAD1VVXwAAAAD1fAAAAAA9VVfAAAAAD1XwAAAAAPVV8AAAAAPVXwAAAAAD//wAAAAAPVXwAAAAAA//AAAAAAPVV8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAA9VVVVfAAAAAAAAAAAD1VVVVXwAAAAAAAAAA/VVVVVV/AAAAAAAAAP9VVVVVVf8AAAAAAAD/VVVVVVVV/////////1VVVVVVVVX///////9VVVVV"}, + {"width" : "55", "buffer":"VVVVVf///////VVVVVVVVVV/////////1VVVVVVVX/AAAAAAAD/1VVVVVVf8AAAAAAAAA/VVVVVVfwAAAAAAAAAA9VVVVVfAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAPVVfAAAAAAD/wAAAAAA9VXwAAAAAD//wAAAAAPVXwAAAAAD1V/AAAAAD1V8AAAAAD1VV8AAAAA9VfAAAAAD1VVfAAAAAPVfAAAAAA9VVXwAAAAA9XwAAAAAPVVVfAAAAAPV8AAAAAPVVVXwAAAAD1fAAAAAD1VVV8AAAAA9XwAAAAA9VVVfAAAAAPV8AAAAAPVVVXwAAAAD18AAAAAD1VVV8AAAAA9fAAAAAA9VVVf//////XwAAAAAPVVVV//////V8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAAPVVVVVVVVVVV8AAAAAD1VVVVVVVVVVfAAAAAA9VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VV///1VVVVXwAAAAA9Vf////9VVVV8AAAAAPV/8AAA/9VVVfAAAAAD1/AAAAAP9VVXwAAAAAP8AAAAAAP1VV8AAAAAA8AAAAAAAPVVfAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAD18AAAAAAAD8AAAAAAA9fAAAAAAAP/8AAAAAAPXwAAAAAAP1fwAAAAAD18AAAAAAPVVfAAAAAAPfAAAAAAPVVV8AAAAAD3wAAAAAPVVVfAAAAAA98AAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAAP8AAAAAPVVVV8AAAAAD/AAAAAD1VVVfAAAAAA/wAAAAA9VVVXwAAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAA9VVVXwAAAAD3wAAAAAPVVVV8AAAAA98AAAAAD1VVV8AAAAAPXwAAAAA9VVVfAAAAAD18AAAAAPVVVXwAAAAA9fAAAAAD1VVV8AAAAAPXwAAAAA9VVVfAAAAAD18AAAAAD1VVXwAAAAD1XwAAAAA9VVXwAAAAA9V8AAAAAPVVV8AAAAAPVfAAAAAA9VV8AAAAAD1V8AAAAAD1V8AAAAAD1VfAAAAAAP/8AAAAAA9VV8AAAAAA/8AAAAAA9VVfAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAD1VVVfAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAA9VVVVXwAAAAAAAAAAA9VVVVVfwAAAAAAAAAA9VVVVVV/AAAAAAAAAD9VVVVVVV/AAAAAAAAP9VVVVVVVX/wAAAAAA/1VVVVVVVVX////////VVVVVVVVVVV//////9VVVVVQ=="}, + {"width" : "48", "buffer":"f///////////////////////////////8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP8AAAAAAAAAAAAAAP/////////AAAAAAP/////////wAAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAAPVVVVVVVVV8AAAAA9VVVVVVVVV8AAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAA9VVVVVVVVXwAAAAD1VVVVVVVVXwAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAD1VVVVVVVVfAAAAAPVVVVVVVVVfAAAAAPVVVVVVVVV///////VVVVVVVVV///////VVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "55", "buffer":"VVVVVX//////9VVVVVVVVVV////////9VVVVVVVVX/wAAAAAAP9VVVVVVVX8AAAAAAAAP9VVVVVVXwAAAAAAAAAP1VVVVVXwAAAAAAAAAAPVVVVVXwAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAPVVfAAAAAAD/wAAAAAD1VXwAAAAAD//AAAAAAPVV8AAAAAD1V8AAAAAD1VfAAAAAD1VXwAAAAA9VfAAAAAD1VV8AAAAAPVXwAAAAA9VVXwAAAAD1V8AAAAA9VVV8AAAAAPVfAAAAAPVVVXwAAAAD1XwAAAAD1VVV8AAAAA9V8AAAAA9VVVfAAAAAPVfAAAAA9VVVXwAAAAD1XwAAAAPVVVV8AAAAA9V8AAAAD1VVVfAAAAAPVfAAAAA9VVVXwAAAAD1XwAAAAPVVVV8AAAAA9V8AAAAD1VVVfAAAAAPVfAAAAAPVVVXwAAAAD1XwAAAAD1VVV8AAAAA9V8AAAAA9VVVfAAAAA9VfAAAAAPVVVXwAAAAPVV8AAAAD1VVXwAAAAD1VfAAAAA9VVV8AAAAA9VXwAAAAD1VVfAAAAAPVV8AAAAA9VVfAAAAAPVVXwAAAAD1VXwAAAAD1VV8AAAAAPVXwAAAAD1VVXwAAAAA/fwAAAAA9VVV8AAAAAD/wAAAAA9VVVXwAAAAADAAAAAAPVVVVfAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAPVVVVVfAAAAAAAAAAAD1VVVVV8AAAAAAAAAAD1VVVVVXwAAAAAAAAAD1VVVVVVfAAAAAAAAAD1VVVVVV/AAAAAAAAAAP1VVVVV/AAAAAAAAAAA/VVVVV8AAAAAAAAAAAA9VVVV8AAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAD1VVfAAAAAADwAAAAAAPVVfAAAAAAP/8AAAAAA9VXwAAAAAP1/wAAAAAPVXwAAAAAPVVfAAAAAD1V8AAAAAPVVV8AAAAAPVfAAAAAD1VVfAAAAAD1fAAAAAD1VVV8AAAAA9XwAAAAA9VVVfAAAAAD18AAAAAPVVVXwAAAAA9fAAAAAPVVVV8AAAAAPXwAAAAD1VVVXwAAAAD3wAAAAA9VVVV8AAAAA98AAAAAPVVVVfAAAAAPfAAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAD/AAAAAD1VVVXwAAAAA/wAAAAA9VVVV8AAAAAP8AAAAAPVVVVfAAAAAPfAAAAAD1VVVXwAAAAD18AAAAA9VVVV8AAAAA9fAAAAAPVVVV8AAAAAPXwAAAAA9VVVfAAAAAD18AAAAAPVVVXwAAAAA9fAAAAAD1VVXwAAAAAPV8AAAAAPVVV8AAAAAPVfAAAAAD1VV8AAAAAD1XwAAAAAP1V8AAAAAA9V8AAAAAA//8AAAAAA9VXwAAAAAA/8AAAAAAPVV8AAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAPVVVVV/AAAAAAAAAAA/VVVVVX8AAAAAAAAAA/VVVVVVX8AAAAAAAAD9VVVVVVVf/AAAAAAA/9VVVVVVVVf////////1VVVVVVVVVX//////9VVVVVQ=="}, + {"width" : "55", "buffer":"VVVVVX//////9VVVVVVVVVV/////////VVVVVVVVV/wAAAAAAP/VVVVVVVX8AAAAAAAAD9VVVVVVX8AAAAAAAAAD9VVVVVXwAAAAAAAAAAP1VVVVXwAAAAAAAAAAAPVVVVXwAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAD1VXwAAAAAD/8AAAAAA9VXwAAAAAD//wAAAAAD1V8AAAAAD1VfAAAAAA9VfAAAAAD1VV8AAAAAPVfAAAAAD1VVfAAAAAA9XwAAAAA9VVV8AAAAAPV8AAAAAPVVVfAAAAAD1fAAAAAPVVVXwAAAAA9fAAAAAD1VVV8AAAAAPXwAAAAA9VVVXwAAAAA98AAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAA98AAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAA98AAAAAPVVVV8AAAAAP8AAAAAD1VVVfAAAAAD/AAAAAA9VVVXwAAAAA/wAAAAAPVVVV8AAAAAP8AAAAAD1VVVfAAAAAD/AAAAAA9VVVXwAAAAA/wAAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAA98AAAAAPVVVV8AAAAAPfAAAAAD1VVVfAAAAAD3wAAAAA9VVVXwAAAAA98AAAAAD1VVV8AAAAAPfAAAAAA9VVVfAAAAAD3wAAAAAPVVVfAAAAAA9fAAAAAA9VVfAAAAAAPXwAAAAAD1VfAAAAAAD18AAAAAAP//AAAAAAA9fAAAAAAA//AAAAAAAPV8AAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAPVVXwAAAAAADwAAAAAD1VVfwAAAAAD/AAAAAA9VVV/wAAAA/18AAAAAPVVVV//wA//1fAAAAAD1VVVV////9VXwAAAAA9VVVVVV/9VVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1VVVVVVVVVVXwAAAAA9VVVVVVVVVVV8AAAAAPVVVVVVVVVVVfAAAAAD1f/////9VVVXwAAAAA9f//////VVVV8AAAAAPXwAAAAD1VVVfAAAAAD18AAAAA9VVVfAAAAAA9fAAAAAPVVVXwAAAAA9V8AAAAD1VVV8AAAAAPVfAAAAA9VVVfAAAAAD1XwAAAAD1VVXwAAAAA9V8AAAAA9VVV8AAAAAPVfAAAAAPVVV8AAAAAPVXwAAAAA9VV8AAAAAD1V8AAAAAD9V8AAAAAA9VfAAAAAAP/8AAAAAAPVV8AAAAAAP8AAAAAAPVVfAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAPVVVVVX8AAAAAAAAAA/VVVVVVf8AAAAAAAAD/VVVVVVVf/AAAAAAA/9VVVVVVVVf////////1VVVVVVVVVX//////9VVVVVQ=="}, + {"width" : "18", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV////////////8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP////////////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV////////////8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP8AAAAP//////f/////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"} + ] +} diff --git a/apps/contourclock/font-Phosphate.json b/apps/contourclock/fonts/font-Phosphate.json similarity index 100% rename from apps/contourclock/font-Phosphate.json rename to apps/contourclock/fonts/font-Phosphate.json diff --git a/apps/contourclock/font-Quicksand.json b/apps/contourclock/fonts/font-Quicksand.json similarity index 100% rename from apps/contourclock/font-Quicksand.json rename to apps/contourclock/fonts/font-Quicksand.json diff --git a/apps/contourclock/fonts/font-RubikOne.json b/apps/contourclock/fonts/font-RubikOne.json new file mode 100644 index 000000000..1077ad566 --- /dev/null +++ b/apps/contourclock/fonts/font-RubikOne.json @@ -0,0 +1,17 @@ +{ + "name":"Rubik One", + "size":"100", + "characters":[ + {"width" : "57", "buffer":"VVVVVVX/////9VVVVVVVVVVVX///////1VVVVVVVVVVf8AAAAAD/VVVVVVVVVX8AAAAAAAP9VVVVVVVVfwAAAAAAAA/VVVVVVVV8AAAAAAAAAD1VVVVVVXwAAAAAAAAAA9VVVVVV/AAAAAAAAAAAPVVVVVV8AAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAA9fAAAAAAAA/wAAAAAAA9fAAAAAAAD/8AAAAAAAPfAAAAAAAPVfAAAAAAAPfAAAAAAA9VXwAAAAAAPfAAAAAAD1VXwAAAAAAP8AAAAAAD1VXwAAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAPVVV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VV8AAAAAAP8AAAAAAD1VXwAAAAAAPfAAAAAAD1VXwAAAAAAPfAAAAAAA9VXwAAAAAAPfAAAAAAAPVfAAAAAAAPfAAAAAAAD/8AAAAAAAPfAAAAAAAA/wAAAAAAA9fAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAPVVVVVVfAAAAAAAAAAA9VVVVVVXwAAAAAAAAAD9VVVVVVV/AAAAAAAAAPVVVVVVVVfwAAAAAAAD9VVVVVVVVV/wAAAAAA/1VVVVVVVVVf///////9VVVVVVVVVVVf//////VVVVVV"}, + {"width" : "45", "buffer":"VVVVVVVV//////1VVVVVVVX//////9VVVVVVVfAAAAAAPVVVVVVV8AAAAAAPVVVVVVXwAAAAAAPVVVVVVXwAAAAAAPVVVVVVfAAAAAAAPVVVVVV8AAAAAAAPVVVVVXwAAAAAAAPVVVVVXwAAAAAAAPVVVVV/AAAAAAAAPVVVVV8AAAAAAAAPVVVVXwAAAAAAAAPVVVVfAAAAAAAAAPVVVV8AAAAAAAAAPVVVXwAAAAAAAAAPVVVXwAAAAAAAAAPVVVfAAAAAAAAAAPVVV8AAAAAAAAAAPVVXwAAAAAAAAAAPVVfAAAAAAAAAAAPVVfAAAAAAAAAAAPVX8AAAAAAAAAAAPVXwAAAAAAAAAAAPVfAAAAAAAAAAAAPV8AAAAAAAAAAAAPXwAAAAAAAAAAAAPfAAAAAAAAAAAAAPfAAAAAAAAAAAAAP8AAAAAAAAAAAAAP8AAAAAAAAAAAAAP8AAAAAAAAAAAAAP8AAAAAAAAAAAAAP8AAAAAAAAAAAAAPfAAAAAAAAAAAAAPfAAAAAAAAAAAAAPfAAAAAAAAAAAAAPXwAAAAAAAAAAAAPV8AAAAAAAAAAAAPV8AAAAAwAAAAAAPV8AAAAD8AAAAAAPVfAAAAPfAAAAAAPVfAAAA9fAAAAAAPVXwAAD1fAAAAAAPVV8AAPVfAAAAAAPVV8AAPVfAAAAAAPVV8AA9VfAAAAAAPVVfAD1VfAAAAAAPVVXwPVVfAAAAAAPVVXw9VVfAAAAAAPVVX/1VVfAAAAAAPVVV/1VVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVfAAAAAAPVVVVVVVX///////VVVVVVVX//////1"}, + {"width" : "58", "buffer":"VVVVVVf//////1VVVVVVVVVVV////////1VVVVVVVVVX/AAAAAAA/1VVVVVVVVX8AAAAAAAA/VVVVVVVVXwAAAAAAAAA9VVVVVVVfwAAAAAAAAAD1VVVVVVXwAAAAAAAAAAPVVVVVVXwAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VfAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVfAAAAAAAD/AAAAAAAD1XwAAAAAAD/8AAAAAAA9XwAAAAAAD1XwAAAAAAPV8AAAAAAD1V8AAAAAAD1fAAAAAAA9VfAAAAAAA9XwAAAAAA9VXwAAAAAAPV8AAAAAAPVV8AAAAAAD1fAAAAAAD1VfAAAAAAA9XwAAAAAD1VXwAAAAAAPV8AAAAAA9VV8AAAAAAD1X///////VVfAAAAAAA9V//////9VVXwAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVfAAAAAAAD1VVVVVVVVVfAAAAAAAD1VVVVVVVVVfAAAAAAAA9VVVVVVVVVfAAAAAAAAPVVVVVVVVVXwAAAAAAAPVVVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAA9VVVVVVVXwAAAAAAAAAD//////VXwAAAAAAAAAAP/////9V8AAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAD1//////////////////9f/////////////////9Q=="}, + {"width" : "59", "buffer":"VV///////////////9VVV////////////////9VVXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VVf///////wAAAAAAAPVVVf///////wAAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVX8AAAAAAD1VVVVVVVVVV/AAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVfwAAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVV/AAAAAAAPVVVVVVVVVVfwAAAAAAA9VVVVVVVVVV8AAAAAAAA9VVVVVVVVVXwAAAAAAAA/VVVVVVVVV8AAAAAAAAA/1VVVVVVVXwAAAAAAAAAP1VVVVVVVfAAAAAAAAAAD1VVVVVVV8AAAAAAAAAAD1VVVVVVXwAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAPVVVVVVXwAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAPVVVVVX/////wAAAAAAA9VVVVVX/////wAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVfAAAAAAD3//////9VVVV8AAAAAAP///////9VVVfAAAAAAA/wAAAAAA9VVV8AAAAAAD/AAAAAAA9VVfAAAAAAAP8AAAAAAA///wAAAAAAA/wAAAAAAA//8AAAAAAAD/AAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAD9VVVVVV8AAAAAAAAAAA/VVVVVVV/AAAAAAAAAAPVVVVVVVV/wAAAAAAAA/1VVVVVVVVf8AAAAAAA/9VVVVVVVVVX////////1VVVVVVVVVVV///////1VVVVVU="}, + {"width" : "62", "buffer":"VVVVVVVVVX//////9VVVVVVVVVVVVV///////9VVVVVVVVVVVVfAAAAAAD1VVVVVVVVVVVXwAAAAAAD1VVVVVVVVVVVfAAAAAAAPVVVVVVVVVVVXwAAAAAAA9VVVVVVVVVVVfAAAAAAAD1VVVVVVVVVVXwAAAAAAAPVVVVVVVVVVVfAAAAAAAA9VVVVVVVVVVXwAAAAAAAD1VVVVVVVVVVfAAAAAAAAPVVVVVVVVVVXwAAAAAAAA9VVVVVVVVVVfAAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVfAAAAAAAAAPVVVVVVVVVXwAAAAAAAAA9VVVVVVVVVfAAAAAAAAAD1VVVVVVVVXwAAAAAAAAAPVVVVVVVVVfAAAAAAAAAA9VVVVVVVVV8AAAAAAAAAD1VVVVVVVVfAAAAAAAAAAPVVVVVVVVXwAAAAAAAAAA9VVVVVVVVfAAAAAAAAAAD1VVVVVVVV8AAAAAAAAAAPVVVVVVVVfAAAAAAAAAAA9VVVVVVVXwAAAAAAAAAAD1VVVVVVVfAAAAAAAAAAAPVVVVVVVV8AAAAAAAAAAA9VVVVVVVfAAAAAAAAAAAD1VVVVVVV8AAAAAAAAAAAPVVVVVVVfAAAAAAAAAAAA9VVVVVVV8AAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAPVVVVVVV8AAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAAPVVVVVVfAAAAAAwAAAAAA9VVVVVV8AAAAAPwAAAAAD1VVVVVfAAAAAD3wAAAAAPVVVVVV8AAAAAPfAAAAAA9VVVVVfAAAAAD18AAAAAD1VVVVV8AAAAAPXwAAAAAPVVVVVfAAAAAD1fAAAAAA9VVVVV8AAAAAPV8AAAAAD1VVVVfAAAAAD1XwAAAAAPVVVVV8AAAAAPVfAAAAAA9VVVVfAAAAAD1V8AAAAAD1VVVV8AAAAAPVXwAAAAAPVVVVfAAAAAA9VfAAAAAA9VVVV8AAAAAPVV8AAAAAD1VVVfAAAAAA9VXwAAAAAPVVVV8AAAAAPVVfAAAAAA9VVVfAAAAAA9VV8AAAAAD1VVV8AAAAAPVVXwAAAAAD///XwAAAAAP//8AAAAAAD///8AAAAAAP//AAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA9/////////wAAAAAAA///3/////////wAAAAAAP//9VVVVVVVVVXwAAAAAD1VVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVfAAAAAA9VVVVVVVVVVVVV8AAAAAD1VVVVVVVVVVVVXwAAAAAPVVVVVVVVVVVVVf//////1VVVVVVVVVVVVVf/////9VVVU="}, + {"width" : "59", "buffer":"VVVf/////////////1VVVVX//////////////1VVVV8AAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAD1VVV8AAAAAP////////9VVVXwAAAAD/////////1VVVfAAAAA9VVVVVVVVVVVVV8AAAAD1VVVVVVVVVVVVXwAAAAPVVVVVVVVVVVVVfAAAAA9VVVVVVVVVVVVXwAAAAD1V///9VVVVVVVfAAAAAPV/////VVVVVVV8AAAAA9fwAAA/1VVVVVXwAAAAD3wAAAAP1VVVVVfAAAAAD8AAAAAD9VVVVV8AAAAADAAAAAAD9VVVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAPVV8AAAAAAA/AAAAAAAAPVXwAAAAAAP/AAAAAAAA9VfAAAAAAP1fAAAAAAAD1V8AAAAAD9VfAAAAAAAPVV//////9VVfAAAAAAA9VV//////VVVfAAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVVfAAAAAAD1VVVVVVVVVVV8AAAAAAPX//////9VVVXwAAAAAA9///////9VVV8AAAAAAD3wAAAAAA9VVXwAAAAAAP8AAAAAAD1VVfAAAAAAA/wAAAAAAD1VXwAAAAAAD/AAAAAAAPVVfAAAAAAAPfAAAAAAAP//wAAAAAAD18AAAAAAAP/8AAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAA9VVVVV8AAAAAAAAAAAAPVVVVVV8AAAAAAAAAAAD9VVVVVV8AAAAAAAAAAA9VVVVVVV/AAAAAAAAAAPVVVVVVVV/wAAAAAAAAP1VVVVVVVVf8AAAAAAAP9VVVVVVVVVX////////9VVVVVVVVVVV///////9VVVVVU="}, + {"width" : "59", "buffer":"VVVVVVVVX//////VVVVVVVVVVVVV///////1VVVVVVVVVVV/AAAAAAPVVVVVVVVVVVXwAAAAAA9VVVVVVVVVVV8AAAAAAD1VVVVVVVVVVXwAAAAAAPVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVV8AAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAD1VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAAP1VVVVVVVVVfAAAAAAAAP/VVVVVVVVXwAAAAAAAAD/VVVVVVVVfAAAAAAAAAAP1VVVVVVXwAAAAAAAAAAP1VVVVVVfAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAA/8AAAAAAAD18AAAAAAAP/8AAAAAAAPfAAAAAAAD1V8AAAAAAA98AAAAAAA9VV8AAAAAAA/wAAAAAAPVVV8AAAAAAD/AAAAAAA9VVXwAAAAAAP8AAAAAAPVVVXwAAAAAA/wAAAAAA9VVVfAAAAAAD/AAAAAAD1VVV8AAAAAAP8AAAAAAPVVVXwAAAAAA/wAAAAAA9VVVfAAAAAAD/AAAAAAD1VVV8AAAAAAP8AAAAAAPVVVXwAAAAAA/wAAAAAA9VVVfAAAAAAD/AAAAAAD1VVV8AAAAAAP8AAAAAAD1VVfAAAAAAA/wAAAAAAD1VXwAAAAAAPfAAAAAAAD1V8AAAAAAA98AAAAAAAD//AAAAAAAD3wAAAAAAAD/wAAAAAAAPXwAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAPVVVVVVVfAAAAAAAAAAD1VVVVVVVfwAAAAAAAAD9VVVVVVVVf8AAAAAAAA/VVVVVVVVVX/AAAAAAD/VVVVVVVVVVV////////1VVVVVVVVVVVf//////VVVVVVU="}, + {"width" : "52", "buffer":"X///////////////9X////////////////3wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAD3////////wAAAAAAD1/////////AAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVV8AAAAAAPVVVVVVVVV8AAAAAAD1VVVVVVVVfAAAAAAD1VVVVVVVVXwAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVXwAAAAAAPVVVVVVVVV8AAAAAAPVVVVVVVVVfAAAAAAD1VVVVVVVVfAAAAAAA9VVVVVVVVV8AAAAAA9VVVVVVVVVf//////9VVVVVVVVVV//////9VVVVVVVQ=="}, + {"width" : "58", "buffer":"VVVVVV///////VVVVVVVVVVVX////////VVVVVVVVVVf8AAAAAAD/VVVVVVVVV/wAAAAAAAD9VVVVVVVV/AAAAAAAAAD9VVVVVVV8AAAAAAAAAAP1VVVVVV8AAAAAAAAAAAPVVVVVV8AAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAA9VfAAAAAAA//wAAAAAAPVXwAAAAAA///AAAAAAD1V8AAAAAA9VV8AAAAAA9VfAAAAAA9VVXwAAAAAPVXwAAAAAPVVV8AAAAAD1V8AAAAAD1VVfAAAAAA9VfAAAAAA9VVV8AAAAAPVXwAAAAA9VVVfAAAAAD1V8AAAAAD1VVXwAAAAA9VfAAAAAA9VVXwAAAAAPVXwAAAAAPVVV8AAAAAD1V8AAAAAD1VVfAAAAAA9VfAAAAAAPVVfAAAAAAPVXwAAAAAD1VfAAAAAAPVVfAAAAAAP1fAAAAAAD1VXwAAAAAA//AAAAAAA9VV8AAAAAAA/AAAAAAAPVVfAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAA98AAAAAAAD/8AAAAAAAPfAAAAAAAP//wAAAAAAD3wAAAAAAP1VfAAAAAAA98AAAAAAPVVV8AAAAAAD/AAAAAAD1VVfAAAAAAA/wAAAAAD1VVV8AAAAAAP8AAAAAA9VVVfAAAAAAD/AAAAAAPVVVXwAAAAAA/wAAAAAD1VVV8AAAAAAP8AAAAAA9VVVfAAAAAAD/AAAAAAPVVVXwAAAAAA/wAAAAAD1VVV8AAAAAAP8AAAAAA9VVVfAAAAAAD/AAAAAAD1VVfAAAAAAA/wAAAAAAPVVXwAAAAAA98AAAAAAA9VfwAAAAAAPfAAAAAAAD//wAAAAAAD3wAAAAAAAP/AAAAAAAA98AAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAD1VVVVVVX/AAAAAAAAA/1VVVVVVVf/AAAAAAAD/1VVVVVVVVX////////9VVVVVVVVVVX///////1VVVVVQ=="}, + {"width" : "58", "buffer":"VVVVVVf/////9VVVVVVVVVVVV////////VVVVVVVVVVX/AAAAAAP/VVVVVVVVVX8AAAAAAAD/VVVVVVVVfwAAAAAAAAD9VVVVVVVfwAAAAAAAAAD1VVVVVVfAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAD3wAAAAAAAP/AAAAAAAA98AAAAAAAP/8AAAAAAAPfAAAAAAAPVXwAAAAAAD3wAAAAAAPVVfAAAAAAAP8AAAAAAPVVV8AAAAAAD/AAAAAAD1VVfAAAAAAA/wAAAAAA9VVV8AAAAAAP8AAAAAAPVVVfAAAAAAD/AAAAAAPVVVXwAAAAAA/wAAAAAD1VVV8AAAAAAP8AAAAAA9VVVfAAAAAAD/AAAAAAPVVVXwAAAAAA/wAAAAAA9VVV8AAAAAAP8AAAAAAPVVV8AAAAAAD/AAAAAAD1VVfAAAAAAA/wAAAAAAPVVXwAAAAAA98AAAAAAD1VXwAAAAAAPfAAAAAAAPVXwAAAAAAD3wAAAAAAA//wAAAAAAA98AAAAAAAD/wAAAAAAAPfAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAA9VVVVV/AAAAAAAAAAAA9VVVVVX8AAAAAAAAAAA9VVVVVVX8AAAAAAAAAAPVVVVVVVfwAAAAAAAAAD1VVVVVVVfwAAAAAAAAD1VVVVVVVV/8AAAAAAAA9VVVVVVVVV/wAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAAPVVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAAPVVVVVVVVVVXwAAAAAAD1VVVVVVVVVV8AAAAAAD1VVVVVVVVVVfAAAAAAA9VVVVVVVVVVXwAAAAAA9VVVVVVVVVVVf//////9VVVVVVVVVVVX//////9VVVVVVVVQ=="}, + {"width" : "20", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/////////////8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA9//////1/////9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/////////////wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD/AAAAAP8AAAAA/wAAAAD///////f/////8="} + ] +} diff --git a/apps/contourclock/font-SairaEC.json b/apps/contourclock/fonts/font-SairaEC.json similarity index 99% rename from apps/contourclock/font-SairaEC.json rename to apps/contourclock/fonts/font-SairaEC.json index ec3f6c990..7ba7fec3d 100644 --- a/apps/contourclock/font-SairaEC.json +++ b/apps/contourclock/fonts/font-SairaEC.json @@ -1,5 +1,5 @@ { - "name":"SairaEC", + "name":"Saira EC", "size":"100", "characters":[ {"width" : "51", "buffer":"VVVVf///////9VVVVVVVf/////////1VVVVVV/wAAAAAAAD/VVVVVfwAAAAAAAAAP1VVVV/AAAAAAAAAAA9VVVV8AAAAAAAAAAAPVVVXwAAAAAAAAAAAPVVVfAAAAAAAAAAAAD1VVfAAAAAAAAAAAAA9VV8AAAAAAAAAAAAA9VV8AAAAAAAAAAAAAPVXwAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAA9fAAAAAAA/AAAAAAA9fAAAAAAD/wAAAAAA9fAAAAAAPV8AAAAAA98AAAAAA9V8AAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAAP8AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAD1VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9VfAAAAAA98AAAAAA9V8AAAAAA9fAAAAAAPXwAAAAAA9fAAAAAAD/AAAAAAA9fAAAAAAA8AAAAAAA9fAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAPVV8AAAAAAAAAAAAA9VVfAAAAAAAAAAAAA9VVfAAAAAAAAAAAAD1VVXwAAAAAAAAAAAD1VVV8AAAAAAAAAAAPVVVV/AAAAAAAAAAD9VVVVfwAAAAAAAAAP1VVVVV/wAAAAAAAP9VVVVVVf/////////1VVVVVVVf///////1VVVV"}, diff --git a/apps/contourclock/font-Teko.json b/apps/contourclock/fonts/font-Teko.json similarity index 100% rename from apps/contourclock/font-Teko.json rename to apps/contourclock/fonts/font-Teko.json diff --git a/apps/contourclock/fonts/font-TitanOne.json b/apps/contourclock/fonts/font-TitanOne.json new file mode 100644 index 000000000..147d9624b --- /dev/null +++ b/apps/contourclock/fonts/font-TitanOne.json @@ -0,0 +1,17 @@ +{ + "name":"Titan One", + "size":"100", + "characters":[ + {"width" : "74", "buffer":"VVVVVVVVX///////VVVVVVVVVVVVVVVVf////////9VVVVVVVVVVVVVV//AAAAAAAP/1VVVVVVVVVVVV/8AAAAAAAAA/9VVVVVVVVVVV/wAAAAAAAAAAD/VVVVVVVVVVfwAAAAAAAAAAAA/VVVVVVVVVfwAAAAAAAAAAAAAP1VVVVVVVX8AAAAAAAAAAAAAAP1VVVVVVV8AAAAAAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAPAAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VXwAAAAAAAAA9fAAAAAAAAAD1VfAAAAAAAAAPVfAAAAAAAAAPVV8AAAAAAAAA9V8AAAAAAAAA9VXwAAAAAAAAD1V8AAAAAAAAD1V8AAAAAAAAA9VXwAAAAAAAAD1XwAAAAAAAAD1VfAAAAAAAAAPVfAAAAAAAAAPVV8AAAAAAAAA9V8AAAAAAAAA9VXwAAAAAAAAD1XwAAAAAAAAD1VfAAAAAAAAAD18AAAAAAAAAPVV8AAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9fAAAAAAAAAPVVXwAAAAAAAAD18AAAAAAAAA9VVfAAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9fAAAAAAAAAPVVXwAAAAAAAAD18AAAAAAAAA9VVfAAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9fAAAAAAAAAPVVXwAAAAAAAAA98AAAAAAAAA9VVfAAAAAAAAAD3wAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA/wAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA/wAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA/wAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA/wAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA/wAAAAAAAAA9VVfAAAAAAAAAD/AAAAAAAAAD1VV8AAAAAAAAAP8AAAAAAAAAPVVXwAAAAAAAAA98AAAAAAAAA9VVfAAAAAAAAAD3wAAAAAAAAD1VV8AAAAAAAAAPfAAAAAAAAAPVVXwAAAAAAAAA98AAAAAAAAA9VVfAAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9fAAAAAAAAAPVVXwAAAAAAAAD18AAAAAAAAA9VVfAAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9fAAAAAAAAAPVVXwAAAAAAAAD18AAAAAAAAA9VVfAAAAAAAAAPXwAAAAAAAAD1VV8AAAAAAAAA9XwAAAAAAAAD1VfAAAAAAAAAD1fAAAAAAAAAPVV8AAAAAAAAA9V8AAAAAAAAA9VXwAAAAAAAAD1XwAAAAAAAAD1VfAAAAAAAAAPVfAAAAAAAAAPVV8AAAAAAAAA9VfAAAAAAAAA9VXwAAAAAAAAD1V8AAAAAAAAA9VfAAAAAAAAA9VXwAAAAAAAAD1XwAAAAAAAAD1VfAAAAAAAAAD1fAAAAAAAAAPVVfAAAAAAAAAD/wAAAAAAAAA9VV8AAAAAAAAAD8AAAAAAAAAPVVXwAAAAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAAAAAAA9VVVVVV/AAAAAAAAAAAAAAAAPVVVVVVV/AAAAAAAAAAAAAAAP1VVVVVVVfAAAAAAAAAAAAAAD9VVVVVVVVfwAAAAAAAAAAAAD9VVVVVVVVVf8AAAAAAAAAAAA/VVVVVVVVVVX/AAAAAAAAAAD/VVVVVVVVVVVV//AAAAAAAAP/1VVVVVVVVVVVVf//////////VVVVVVVVVVVVVVVf///////9VVVVVVVU="}, + {"width" : "44", "buffer":"VVVVVVVVVVVVVVVVVVVVVVf////9VVVVVVX////////1VVVX///8AAAAA/1VVf//AAAAAAAAD1V//AAAAAAAAAAPV/8AAAAAAAAAAA9fwAAAAAAAAAAAD18AAAAAAAAAAAAPXwAAAAAAAAAAAA9fAAAAAAAAAAAAD3wAAAAAAAAAAAAPfAAAAAAAAAAAAA98AAAAAAAAAAAAD3wAAAAAAAAAAAAPfAAAAAAAAAAAAA98AAAAAAAAAAAAD3wAAAAAAAAAAAAP8AAAAAAAAAAAAA/wAAAAAAAAAAAAD/AAAAAAAAAAAAAP8AAAAAAAAAAAAA/wAAAAAAAAAAAAD/AAAAAAAAAAAAAP8AAAAAAAAAAAAA/wAAAAAAAAAAAAD/AAAAAAAAAAAAAP8AAAAAAAAAAAAA/wAAAAAAAAAAAAD/AAAAAAAAAAAAAP8AAAAAAAAAAAAA/wAAAAAAAAAAAAD/AAAAAAAAAAAAAPfAAAAAAAAAAAAA98AAAAAAAAAAAAD3///8AAAAAAAAAPX///8AAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAAA9VVVV8AAAAAAAAD1VVVXwAAAAAAAAPVVVVfAAAAAAAD/9VVVVf/////////VVVVVf///////VVVVVVVVVVVVVVVVU="}, + {"width" : "61", "buffer":"VVVVVV///////VVVVVVVVVVVX/////////9VVVVVVVVV//8AAAAAAD//VVVVVVVf/wAAAAAAAAAP/VVVVVV/8AAAAAAAAAAAD/VVVVV/AAAAAAAAAAAAAD9VVVX8AAAAAAAAAAAAAAD1VVX8AAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAD1fAAAAAD/wAAAAAAAAAA9XwAAAA///wAAAAAAAAAPVfAAAP/1V/AAAAAAAAAD1XwAA/9VVV8AAAAAAAAA9V8AA/VVVVfAAAAAAAAAPVXwD9VVVVXwAAAAAAAAD1VfP9VVVVV8AAAAAAAAA9VX/1VVVVVfAAAAAAAAA9VVXVVVVVVXwAAAAAAAAPVVVVVVVVVV8AAAAAAAAD1VVVVVVVVV8AAAAAAAAA9VVVVVVVVVfAAAAAAAAA9VVVVVVVVVfAAAAAAAAAPVVVVVVVVVfAAAAAAAAAD1VVVVVVVVXwAAAAAAAAD1VVVVVVVVXwAAAAAAAAA9VVVVVVVVXwAAAAAAAAA9VVVVVVVVXwAAAAAAAAAPVVVVVVVVXwAAAAAAAAAPVVVVVVVVV8AAAAAAAAAD1VVVVVVVV8AAAAAAAAAD1VVVVVVVV8AAAAAAAAAA9VVVVVVVV8AAAAAAAAAA9VVVVVVVV8AAAAAAAAAA9VVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAAPVVVVVVVV8AAAAAAAAAA/VVVVVVVV8AAAAAAAAAA/VVVVVVVV8AAAAAAAAAD9VVVVVVVV8AAAAAAAAAD9VVVVVVVV8AAAAAAAAAP1VVVVVVVV8AAAAAAAAAP1VVVVVVVV8AAAAAAAAAPVVVVVVVVV8AAAAAAAAAA///////1V8AAAAAAAAAAD///////VfAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1VX/////////////////9VVf////////////////9VVVVVVVVVVVVVVVVVVVVVQ=="}, + {"width" : "61", "buffer":"VVVVVf///////VVVVVVVVVVVf/////////9VVVVVVVVX//AAAAAAAD//VVVVVVVf/AAAAAAAAAAP/VVVVVVfwAAAAAAAAAAAD9VVVVV/AAAAAAAAAAAAAD9VVVV/AAAAAAAAAAAAAAP1VVV8AAAAAAAAAAAAAAAPVVV8AAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAD1XwAAAAD/8AAAAAAAAAA9V8AAAA///wAAAAAAAAAPVXwAAP/1VfAAAAAAAAAD1V8AA/9VVV8AAAAAAAAD1VfAD/VVVVXwAAAAAAAA9VV8P9VVVVV8AAAAAAAAPVVf/1VVVVVfAAAAAAAAD1VV/VVVVVVXwAAAAAAAA9VVVVVVVVVXwAAAAAAAAPVVVVVVVVVfwAAAAAAAAPVVVVVVVVX/wAAAAAAAAD1VVVVX////AAAAAAAAAA9VVVVX///wAAAAAAAAAA9VVVVV8AAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAPVVVVVVXwAAAAAAAAAAAA9VVVVVV8AAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAD1VVVVV8AAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAA9VVVVf///wAAAAAAAAAAD1VVVV////wAAAAAAAAAA9VVVVVVVV/wAAAAAAAAAPVVVVVVVVV/AAAAAAAAAD1VVVVVVVVV8AAAAAAAAA9VVVVVVVVVfAAAAAAAAAPVVXVVVVVVV8AAAAAAAAD1Vf/1VVVVVfAAAAAAAAAPVfz/1VVVVXwAAAAAAAAD1XwA/9VVVXwAAAAAAAAA9XwAA//VVXwAAAAAAAAAPV8AAAP///wAAAAAAAAAD18AAAAD//wAAAAAAAAAA9fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAP1VVV/AAAAAAAAAAAAAA/1VVVX/wAAAAAAAAAAAD/VVVVVX/8AAAAAAAAAAP9VVVVVVV//8AAAAAAAP/1VVVVVVVVf//////////VVVVVVVVVVVf///////VVVVVVVQ=="}, + {"width" : "62", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/////1VVVVVVVVVVVVf////////VVVVVVVVVVX//8AAAAAD/VVVVVVVVVf/8AAAAAAAAPVVVVVVVVf/AAAAAAAAAAPVVVVVVVf8AAAAAAAAAAA9VVVVVVX8AAAAAAAAAAAD1VVVVVVfAAAAAAAAAAAAPVVVVVVXwAAAAAAAAAAAA9VVVVVVfAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAPVVVVVVfAAAAAAAAAAAAA9VVVVVXwAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAA9VVVVVfAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAPVVVfAAAAAADAAAAAAAAA9VVV8AAAAAA/AAAAAAAAD1VVfAAAAAAPfAAAAAAAAPVVV8AAAAAA98AAAAAAAA9VVfAAAAAAD3wAAAAAAAD1VV8AAAAAAPfAAAAAAAAPVVXwAAAAAD18AAAAAAAA9VV8AAAAAAPXwAAAAAAAD1VXwAAAAAA9fAAAAAAAAPVV8AAAAAAD18AAAAAAAA9VXwAAAAAA9XwAAAAAAAD1V8AAAAAAD1fAAAAAAAAPVXwAAAAAAPV8AAAAAAAA9VfAAAAAAA9XwAAAAAAAD1XwAAAAAAPVfAAAAAAAAPVfAAAAAAA9V8AAAAAAAA9XwAAAAAAD1XwAAAAAAAD1fAAAAAAAPVfAAAAAAAAPXwAAAAAAD1V8AAAAAAAA9fAAAAAAAPVXwAAAAAAAD18AAAAAAA9VfAAAAAAAAPfAAAAAAAD1V8AAAAAAAA98AAAAAAA9VXwAAAAAAAD3wAAAAAAD1VfAAAAAAAAP8AAAAAAAPVV8AAAAAAAA/wAAAAAAA9VXwAAAAAAAD/AAAAAAAPVVfAAAAAAAAP8AAAAAAAP//wAAAAAAAA/wAAAAAAAP/8AAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAD1/////////8AAAAAAAAAPV/////////8AAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAAA9VVVVVVVVVV8AAAAAAAAD1VVVVVVVVVXwAAAAAAAAPVVVVVVVVVVfAAAAAAAD/9VVVVVVVVVVf/////////VVVVVVVVVVVf///////VVVVVVVVVVVVVVVVVVVVVVU="}, + {"width" : "61", "buffer":"VVVVVVVVVVVVVVVVVVVVVVf////////////////1VVX/////////////////VVXwAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAA9VV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAA9VVfAAAAAA//////////9VVXwAAAAA///////////VVV8AAAAA9VVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVV8AAAAA9VVVVVVVVVVVVVfAAAAAPVVVVVVVVVVVVVXwAAAAD1VVVVVVVVVVVVV8AAAAAP////VVVVVVVVVfAAAAAA/////9VVVVVVVXwAAAAAAAAAD//1VVVVVV8AAAAAAAAAAAP/1VVVVVfAAAAAAAAAAAAA/1VVVVXwAAAAAAAAAAAAA/1VVVV8AAAAAAAAAAAAAA/VVVVfAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAA9VVf/AAAAAAAAAAAAAAAD1VV////AAAAAAAAAAAAA9VVVX///8AAAAAAAAAAAPVVVVVVX/8AAAAAAAAAAA9VVVVVVVf8AAAAAAAAAAPVVVVVVVVfwAAAAAAAAAD1VVVVVVVVfAAAAAAAAAA9VVVVVVVVV8AAAAAAAAAPVV9VVVVVVXwAAAAAAAAD1V/9VVVVVV8AAAAAAAAA9V8P9VVVVVXwAAAAAAAAPVfAP9VVVVXwAAAAAAAAD1fAAP9VVVV8AAAAAAAAA9XwAAP9VVV8AAAAAAAAAPV8AAAP/VX8AAAAAAAAAD18AAAAP//8AAAAAAAAAA9fAAAAAD/wAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAAD3wAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAPXwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAD1VV8AAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAP1VVVfwAAAAAAAAAAAAAP1VVVV/wAAAAAAAAAAAA/VVVVVV/8AAAAAAAAAAP/VVVVVVV//wAAAAAAAP/9VVVVVVVVf//////////VVVVVVVVVVV////////VVVVVVVQ=="}, + {"width" : "67", "buffer":"VVVVVVVVf///////1VVVVVVVVVVVVf//////////1VVVVVVVVVV//AAAAAAAA//9VVVVVVVVX/AAAAAAAAAAA//VVVVVVVX8AAAAAAAAAAAAP9VVVVVVfwAAAAAAAAAAAAAD1VVVVVfwAAAAAAAAAAAAAAPVVVVVfAAAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAD1VVVXwAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVV8AAAAAAAAAAP//AAAA9VVVfAAAAAAAAAD/////wAPVVVXwAAAAAAAAD/VVX///D1VVXwAAAAAAAAD1VVVVV//1VVV8AAAAAAAAD1VVVVVVX1VVVfAAAAAAAAD1VVVVVVVVVVVXwAAAAAAAA9VVVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAAD1VVVVVVVVVVVXwAAAAAAAA9VVV///9VVVVV8AAAAAAAAPVV//////VVVVfAAAAAAAAPVX/8AAAP/VVVXwAAAAAAAD1f8AAAAAD9VVV8AAAAAAAA9/wAAAAAAD1VVfAAAAAAAAD/AAAAAAAAPVVXwAAAAAAAAMAAAAAAAAA9VV8AAAAAAAAAAAAAAAAAAD1VfAAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAAAPXwAAAAAAAAADwAAAAAAAAD18AAAAAAAAAD/AAAAAAAAAPfAAAAAAAAAD18AAAAAAAAD3wAAAAAAAAD1fAAAAAAAAA98AAAAAAAAA9V8AAAAAAAAPfAAAAAAAAAPVfAAAAAAAAD18AAAAAAAAPVXwAAAAAAAA9fAAAAAAAAD1V8AAAAAAAAD3wAAAAAAAA9VfAAAAAAAAA98AAAAAAAAPVV8AAAAAAAAPfAAAAAAAAD1VfAAAAAAAAD3wAAAAAAAA9VXwAAAAAAAA98AAAAAAAAPVV8AAAAAAAAPfAAAAAAAAD1VfAAAAAAAAD3wAAAAAAAA9VXwAAAAAAAA98AAAAAAAAPVV8AAAAAAAAPXwAAAAAAAD1VfAAAAAAAAD18AAAAAAAA9VXwAAAAAAAA9fAAAAAAAAPVV8AAAAAAAAPXwAAAAAAAD1VfAAAAAAAAD18AAAAAAAA9VXwAAAAAAAA9fAAAAAAAAPVV8AAAAAAAAPV8AAAAAAAD1VfAAAAAAAAD1fAAAAAAAA9VfAAAAAAAAA9XwAAAAAAAPVXwAAAAAAAAPV8AAAAAAAD1V8AAAAAAAAPVXwAAAAAAAPVfAAAAAAAAD1V8AAAAAAAD1XwAAAAAAAA9VfAAAAAAAA9V8AAAAAAAAPVV8AAAAAAAD18AAAAAAAAD1VfAAAAAAAAP8AAAAAAAAA9VXwAAAAAAAA8AAAAAAAAA9VVfAAAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAAAD1VVfAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVV8AAAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAAD1VVVVVfAAAAAAAAAAAAAAD1VVVVVV8AAAAAAAAAAAAAD1VVVVVVXwAAAAAAAAAAAAP1VVVVVVVfwAAAAAAAAAAAP1VVVVVVVV/wAAAAAAAAAA/VVVVVVVVVV/wAAAAAAAAP/VVVVVVVVVVV//AAAAAAD/9VVVVVVVVVVVV/////////VVVVVVVVVVVVVVX//////1VVVVVVQ=="}, + {"width" : "57", "buffer":"VVVVVVVVVVVVVVVVVVVV////////////////9VV/////////////////1XwAAAAAAAAAAAAAAAD9XwAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAPfAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAA98AAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAD18AAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAD1fAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVfAAAAAAAAAAAAAAAAPVX///////wAAAAAAAAPVX///////8AAAAAAAAPVVVVVVVVVfAAAAAAAAPVVVVVVVVVfAAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVV8AAAAAAAA9VVVVVVVVXwAAAAAAAA9VVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVXwAAAAAAAD1VVVVVVVVfAAAAAAAAD1VVVVVVVVfAAAAAAAAD1VVVVVVVVfAAAAAAAAPVVVVVVVVVfAAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVXwAAAAAAAA9VVVVVVVVXwAAAAAAAA9VVVVVVVVXwAAAAAAAA9VVVVVVVVXwAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAD1VVVVVVVVfAAAAAAAAD1VVVVVVVVfAAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAPVVVVVVVVV8AAAAAAAAPVVVVVVVVXwAAAAAAAAPVVVVVVVVXwAAAAAAAAPVVVVVVVVXwAAAAAAAAPVVVVVVVVXwAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVVfAAAAAAAAA9VVVVVVVV8AAAAAAAAA9VVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVV8AAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAPVVVVVVVVXwAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVVfAAAAAAAAAPVVVVVVVV8AAAAAAAAA9VVVVVVVV8AAAAAAAAA9VVVVVVVV8AAAAAAAAA9VVVVVVVV8AAAAAAAAA9VVVVVVVV8AAAAAAAAA9VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVXwAAAAAAAAD1VVVVVVVfAAAAAAAAAD1VVVVVVVXwAAAAAAD//VVVVVVVVX/////////9VVVVVVVVV///////9VVVVVVVVVVVVVVVVVVVVVVVVVV"}, + {"width" : "66", "buffer":"VVVVVVVX//////1VVVVVVVVVVVVVf/////////VVVVVVVVVVVf/8AAAAAAP/9VVVVVVVVVX/wAAAAAAAAA/9VVVVVVVV/wAAAAAAAAAAD/VVVVVVVX8AAAAAAAAAAAAD9VVVVVV/AAAAAAAAAAAAAA/VVVVVX8AAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAAA9VVVVfAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAwAAAAAAAAD1XwAAAAAAAAP8AAAAAAAAD1XwAAAAAAAA/fAAAAAAAAD1XwAAAAAAAD1XwAAAAAAAA9XwAAAAAAAD1V8AAAAAAAA9XwAAAAAAAD1V8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAPVV8AAAAAAAA9XwAAAAAAAD1V8AAAAAAAA9XwAAAAAAAD1XwAAAAAAAA9XwAAAAAAAA9XwAAAAAAAA9XwAAAAAAAAP/AAAAAAAAD1XwAAAAAAAAD8AAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAA9VVfAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAAAAAAAAAAA9fAAAAAAAAAAwAAAAAAAAA9fAAAAAAAAAP8AAAAAAAAAPfAAAAAAAAA/fAAAAAAAAAPfAAAAAAAAD1XwAAAAAAAAPfAAAAAAAAD1V8AAAAAAAAP8AAAAAAAAD1V8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAP8AAAAAAAAPVV8AAAAAAAAPfAAAAAAAAPVV8AAAAAAAAPfAAAAAAAAD1V8AAAAAAAAPfAAAAAAAAD1V8AAAAAAAAPfAAAAAAAAD1XwAAAAAAAA9fAAAAAAAAA9XwAAAAAAAA9XwAAAAAAAAP/AAAAAAAAA9XwAAAAAAAAD8AAAAAAAAA9XwAAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAAPVVfAAAAAAAAAAAAAAAAAA9VVXwAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAA9VVVVX8AAAAAAAAAAAAAAD1VVVVV/AAAAAAAAAAAAAA/VVVVVVX8AAAAAAAAAAAAD9VVVVVVV/wAAAAAAAAAAA/VVVVVVVVX/AAAAAAAAAA/9VVVVVVVVVf/wAAAAAAA//VVVVVVVVVVV//////////VVVVVVVVVVVVVf///////VVVVVVV"}, + {"width" : "67", "buffer":"VVVVVVVX//////VVVVVVVVVVVVVVX////////1VVVVVVVVVVVVf/wAAAAAD/9VVVVVVVVVVV/wAAAAAAAA/9VVVVVVVVVX/AAAAAAAAAAP1VVVVVVVVX8AAAAAAAAAAAP1VVVVVVVXwAAAAAAAAAAAA/VVVVVVVXwAAAAAAAAAAAAA9VVVVVVXwAAAAAAAAAAAAAD1VVVVVXwAAAAAAAAAAAAAAPVVVVVXwAAAAAAAAAAAAAAA9VVVVXwAAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAAA9VVVV8AAAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAAPVVXwAAAAAAAAAAAAAAAAAD1VV8AAAAAAAAD8AAAAAAAA9VVfAAAAAAAAD/wAAAAAAAD1VfAAAAAAAAD1fAAAAAAAA9VXwAAAAAAAA9XwAAAAAAAD1V8AAAAAAAAPVfAAAAAAAA9V8AAAAAAAAPVXwAAAAAAAPVfAAAAAAAAD1V8AAAAAAAD1XwAAAAAAAA9VfAAAAAAAAPV8AAAAAAAAPVXwAAAAAAAD1fAAAAAAAAD1V8AAAAAAAA9XwAAAAAAAA9VfAAAAAAAAPV8AAAAAAAAPVXwAAAAAAAA98AAAAAAAAD1VfAAAAAAAAPfAAAAAAAAA9VXwAAAAAAAD3wAAAAAAAAPVV8AAAAAAAA98AAAAAAAAD1VfAAAAAAAAPfAAAAAAAAA9VXwAAAAAAAD3wAAAAAAAAPVV8AAAAAAAA9fAAAAAAAAD1VfAAAAAAAAPXwAAAAAAAA9VfAAAAAAAAA98AAAAAAAAPVXwAAAAAAAAPfAAAAAAAAD1V8AAAAAAAAD3wAAAAAAAA9VfAAAAAAAAA98AAAAAAAAPVXwAAAAAAAAPfAAAAAAAAD1V8AAAAAAAAD3wAAAAAAAA9VfAAAAAAAAA98AAAAAAAAD1XwAAAAAAAAPXwAAAAAAAA9XwAAAAAAAAD18AAAAAAAAD18AAAAAAAAA9fAAAAAAAAAP8AAAAAAAAAPXwAAAAAAAAA8AAAAAAAAAD1fAAAAAAAAAAAAAAAAAAAA9XwAAAAAAAAAAAAAAAAAAAPV8AAAAAAAAAAAAAAAAAAAD1XwAAAAAAAAAAAAAAAAAAA9V8AAAAAAAAAAAAAAAAAAAPVXwAAAAAAAAAAAAAAAAAAD1V8AAAAAAAAAAAAAAAAAAA9VXwAAAAAAAAAAAAAAAAAAPVV8AAAAAAAAAAAAAAAAAAD1VXwAAAAAAAAAAAAAAAAAA9VVfAAAAAAAADAAAAAAAAAPVVV8AAAAAAAP8AAAAAAAAD1VVX8AAAAAP/3wAAAAAAAA9VVVf/AAAP//V8AAAAAAAAPVVVVf/////VVfAAAAAAAAPVVVVVX///VVVXwAAAAAAAD1VVVVVVVVVVVV8AAAAAAAA9VVVVVVVVVVVVfAAAAAAAAPVVVVVVVVVVVVfAAAAAAAAD1VVVVVVVVVVVXwAAAAAAAA9VVVVVVVVVVVV8AAAAAAAAPVVVVVVVVVVVV8AAAAAAAAD1VVVVVVVVVVVfAAAAAAAAD1VVVf9VVVVVV/AAAAAAAAA9VVVf///1VVX/AAAAAAAAAPVVVXwP/////8AAAAAAAAAD1VVXwAAA///wAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAPVVVfAAAAAAAAAAAAAAAAAD1VVXwAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAA9VVVfAAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAPVVVXwAAAAAAAAAAAAAAAAD1VVV8AAAAAAAAAAAAAAAAD1VVVfAAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAA9VVVXwAAAAAAAAAAAAAAAAPVVVV8AAAAAAAAAAAAAAAAPVVVVfAAAAAAAAAAAAAAAAPVVVVXwAAAAAAAAAAAAAAAD1VVVV8AAAAAAAAAAAAAAAD1VVVVfAAAAAAAAAAAAAAAD1VVVVXwAAAAAAAAAAAAAAP1VVVVV8AAAAAAAAAAAAAAP1VVVVVf8AAAAAAAAAAAAA/VVVVVVVf/AAAAAAAAAAAD/VVVVVVVVf//AAAAAAAAD/9VVVVVVVVVX///////////1VVVVVVVVVVVX////////1VVVVVVVQ=="}, + {"width" : "23", "buffer":"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX///9VVX/////VV/AAAA/VfAAAAAPV8AAAAAPfAAAAAA98AAAAAD3wAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA/wAAAAAD3wAAAAAPfAAAAAA98AAAAAD18AAAAAPXwAAAAD1XwAAAA9VX/AAD/VVX////1VVVf//VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//VVVVX////VVX/wAP/VV/AAAAPVfAAAAAPV8AAAAAPfAAAAAA98AAAAAD3wAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA/wAAAAAD/AAAAAAP8AAAAAA98AAAAAD3wAAAAAPfAAAAAA98AAAAAD18AAAAA9XwAAAAD1X8AAAD9VX/////VVV////VU="} + ] +} diff --git a/apps/contourclock/font-Yumaro.json b/apps/contourclock/fonts/font-Yumaro.json similarity index 100% rename from apps/contourclock/font-Yumaro.json rename to apps/contourclock/fonts/font-Yumaro.json diff --git a/apps/contourclock/font-YuseiMagic.json b/apps/contourclock/fonts/font-YuseiMagic.json similarity index 99% rename from apps/contourclock/font-YuseiMagic.json rename to apps/contourclock/fonts/font-YuseiMagic.json index 5aa3ee6e4..9289a8a99 100644 --- a/apps/contourclock/font-YuseiMagic.json +++ b/apps/contourclock/fonts/font-YuseiMagic.json @@ -1,5 +1,5 @@ { - "name":"YuseiMagic", + "name":"Yusei Magic", "size":"96", "characters":[ {"width" : "67", "buffer":"VVVVVVVVVX///1VVVVVVVVVVVVVVVVf//////VVVVVVVVVVVVVVX//wAAA//1VVVVVVVVVVVV//AAAAAAD/1VVVVVVVVVVX/wAAAAAAAA/1VVVVVVVVVX8AAAAAAAAAA/VVVVVVVVVfwAAAAAAAAAAA/VVVVVVVVfwAAAAAAAAAAAD9VVVVVVVfAAAAAAAAAAAAAD1VVVVVVfAAAAAA//8AAAAA9VVVVVVfAAAAAD///wAAAAD1VVVVVfAAAAAD9VVfAAAAAPVVVVVXwAAAAD1VVV8AAAAA9VVVVXwAAAAD1VVVXwAAAAPVVVVV8AAAAD1VVVVfAAAAA9VVVV8AAAAD1VVVVXwAAAAPVVVV8AAAAA9VVVVVfAAAAA9VVVfAAAAA9VVVVVV8AAAAD1VVfAAAAA9VVVVVVfAAAAA9VVXwAAAAPVVVVVVV8AAAAPVVXwAAAAPVVVVVVVfAAAAA9VV8AAAAD1VVVVVVXwAAAAPVVfAAAAD1VVVVVVVfAAAAD1VfAAAAA9VVVVVVVXwAAAAPVXwAAAAPVVVVVVVV8AAAAD1V8AAAAPVVVVVVVVXwAAAA9V8AAAAD1VVVVVVVV8AAAAD1fAAAAA9VVVVVVVVfAAAAA9XwAAAAPVVVVVVVVXwAAAAPV8AAAAPVVVVVVVVVfAAAAD1fAAAAD1VVVVVVVVXwAAAA9fAAAAA9VVVVVVVVV8AAAAPXwAAAAPVVVVVVVVVfAAAAA98AAAAD1VVVVVVVVXwAAAAPfAAAAD1VVVVVVVVV8AAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAAP8AAAAPVVVVVVVVVV8AAAAD/AAAAD1VVVVVVVVVfAAAAA/wAAAA9VVVVVVVVVXwAAAAP8AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVV8AAAAPfAAAAD1VVVVVVVVVfAAAAD3wAAAA9VVVVVVVVVXwAAAA98AAAAPVVVVVVVVVXwAAAAPfAAAAD1VVVVVVVVV8AAAAD3wAAAAPVVVVVVVVVfAAAAA98AAAAD1VVVVVVVVXwAAAAPfAAAAA9VVVVVVVVV8AAAAPXwAAAAPVVVVVVVVVfAAAAD1fAAAAD1VVVVVVVVXwAAAA9XwAAAA9VVVVVVVVXwAAAAPV8AAAAD1VVVVVVVV8AAAAD1fAAAAA9VVVVVVVVfAAAAA9XwAAAAPVVVVVVVVXwAAAA9VfAAAAD1VVVVVVVXwAAAAPVXwAAAAPVVVVVVVV8AAAAD1V8AAAAD1VVVVVVVfAAAAA9VfAAAAA9VVVVVVVXwAAAA9VV8AAAAD1VVVVVVXwAAAAPVVfAAAAA9VVVVVVV8AAAAD1VXwAAAAPVVVVVVVfAAAAD1VVfAAAAA9VVVVVVfAAAAA9VVXwAAAAPVVVVVVXwAAAAPVVVfAAAAA9VVVVVXwAAAAPVVVXwAAAAPVVVVVV8AAAAD1VVVfAAAAA9VVVVV8AAAAD1VVVXwAAAAPVVVVVfAAAAA9VVVVfAAAAA9VVVVfAAAAA9VVVVXwAAAAD1VVVfAAAAAPVVVVVfAAAAAPVVVfAAAAAPVVVVVXwAAAAA9VVfAAAAAPVVVVVVfAAAAAD1VfAAAAAD1VVVVVV8AAAAAP//AAAAAD1VVVVVVXwAAAAA//AAAAAD1VVVVVVVfAAAAAAAAAAAAD1VVVVVVVV8AAAAAAAAAAAD1VVVVVVVVX8AAAAAAAAAAP1VVVVVVVVVf8AAAAAAAAA/1VVVVVVVVVVf8AAAAAAAD/VVVVVVVVVVVVf/AAAAAA/9VVVVVVVVVVVVVf///////1VVVVVVVVVVVVVVX/////9VVVVVVVV" }, diff --git a/apps/contourclock/fonts/temp b/apps/contourclock/fonts/temp deleted file mode 100644 index 8b1378917..000000000 --- a/apps/contourclock/fonts/temp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index eb0dd39fb..f29dc7f6b 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,9 +1,10 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.27", + "version": "0.33", "icon": "app.png", - "description": "A Minimalist clockface with large Digits. Now with more fonts!", + "readme": "README.md", + "description": "A Minimalist clockface with large Digits.", "screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}], "tags": "clock", "custom": "custom.html", @@ -14,5 +15,6 @@ {"name":"contourclock.settings.js","url":"contourclock.settings.js"}, {"name":"contourclock","url":"lib.js"}, {"name":"contourclock.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"contourclock.json"}] } diff --git a/apps/contourclock/screenshot.png b/apps/contourclock/screenshot.png deleted file mode 100644 index 9e263152c..000000000 Binary files a/apps/contourclock/screenshot.png and /dev/null differ diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index ad6f0742d..30c775a49 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -1,3 +1,5 @@ 0.01: New app 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 cb12624ae..2b7de0bf0 100644 --- a/apps/coretemp/metadata.json +++ b/apps/coretemp/metadata.json @@ -1,7 +1,7 @@ { "id": "coretemp", "name": "CoreTemp", - "version": "0.03", + "version": "0.05", "description": "Display CoreTemp device sensor data", "icon": "coretemp.png", "type": "app", diff --git a/apps/coretemp/settings.js b/apps/coretemp/settings.js index 3fc2dfbf2..23ea09167 100644 --- a/apps/coretemp/settings.js +++ b/apps/coretemp/settings.js @@ -35,7 +35,6 @@ const menu = { '< Back' : back, 'Enabled' : { value : !!s.enabled, - format : v => v ? "Yes" : "No", onchange : v => { s.enabled = v; updateSettings(); diff --git a/apps/counter/ChangeLog b/apps/counter/ChangeLog index f3f1c4eac..950c892dc 100644 --- a/apps/counter/ChangeLog +++ b/apps/counter/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Added decrement and touch functions 0.03: Set color - ensures widgets don't end up coloring the counter's text +0.04: Adopted for BangleJS 2 +0.05: Support translations diff --git a/apps/counter/counter.js b/apps/counter/counter.js index 3e0687944..29413f600 100644 --- a/apps/counter/counter.js +++ b/apps/counter/counter.js @@ -1,45 +1,104 @@ var counter = 0; +const BANGLEJS2 = process.env.HWVERSION == 2; + +if (BANGLEJS2) { + var drag; + var y = 45; + var x = 5; +} else { + var y = 100; + var x = 25; +} function updateScreen() { - g.clearRect(0, 50, 250, 150); - g.setColor(0xFFFF); + if (BANGLEJS2) { + g.clearRect(0, 50, 250, 130); + } else { + g.clearRect(0, 50, 250, 150); + } + g.setBgColor(g.theme.bg).setColor(g.theme.fg); g.setFont("Vector",40).setFontAlign(0,0); g.drawString(Math.floor(counter), g.getWidth()/2, 100); - g.drawString('-', 45, 100); - g.drawString('+', 185, 100); + if (!BANGLEJS2) { + g.drawString('-', 45, 100); + g.drawString('+', 185, 100); + } } -// add a count by using BTN1 or BTN5 -setWatch(() => { - counter += 1; - updateScreen(); -}, BTN1, {repeat:true}); +if (BANGLEJS2) { + setWatch(() => { + counter = 0; + updateScreen(); + }, BTN1, {repeat:true}); + Bangle.on("drag", e => { + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + if (dx < dy) { + //console.log("left " + dx + " " + dy); + } else { + //console.log("right " + dx + " " + dy); + } + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + if (dx < dy) { + //console.log("down " + dx + " " + dy); + if (counter > 0) counter -= 1; + updateScreen(); + } else { + //console.log("up " + dx + " " + dy); + counter += 1; + updateScreen(); + } + } else { + //console.log("tap " + e.x + " " + e.y); + } + } + }); + } else { -setWatch(() => { - counter += 1; - updateScreen(); -}, BTN5, {repeat:true}); + // add a count by using BTN1 or BTN5 + setWatch(() => { + counter += 1; + updateScreen(); + }, BTN1, {repeat:true}); + + setWatch(() => { + counter += 1; + updateScreen(); + }, BTN5, {repeat:true}); + + // subtract a count by using BTN3 or BTN4 + setWatch(() => { + if (counter > 0) counter -= 1; + updateScreen(); + }, BTN4, {repeat:true}); + + setWatch(() => { + if (counter > 0) counter -= 1; + updateScreen(); + }, BTN3, {repeat:true}); + + // reset by using BTN2 + setWatch(() => { + counter = 0; + updateScreen(); + }, BTN2, {repeat:true}); +} -// subtract a count by using BTN3 or BTN4 -setWatch(() => { - counter -= 1; - updateScreen(); -}, BTN4, {repeat:true}); - -setWatch(() => { - counter -= 1; - updateScreen(); -}, BTN3, {repeat:true}); - -// reset by using BTN2 -setWatch(() => { - counter = 0; - updateScreen(); -}, BTN2, {repeat:true}); g.clear(1).setFont("6x8"); -g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', 25, 200); +g.setBgColor(g.theme.bg).setColor(g.theme.fg); +if (BANGLEJS2) { + g.drawString([/*LANG*/"Swipe up to increase", /*LANG*/"Swipe down to decrease", /*LANG*/"Press button to reset"].join("\n"), x, 100 + y); +} else { + g.drawString([/*LANG*/"Tap right or BTN1 to increase", /*LANG*/"Tap left or BTN3 to decrease", /*LANG*/"Press BTN2 to reset"].join("\n"), x, 100 + y); +} Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/counter/metadata.json b/apps/counter/metadata.json index e455fda95..827caa9ec 100644 --- a/apps/counter/metadata.json +++ b/apps/counter/metadata.json @@ -1,11 +1,11 @@ { "id": "counter", "name": "Counter", - "version": "0.03", + "version": "0.05", "description": "Simple counter", "icon": "counter_icon.png", "tags": "tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "screenshots": [{"url":"bangle1-counter-screenshot.png"}], "allow_emulator": true, "storage": [ diff --git a/apps/counter2/ChangeLog b/apps/counter2/ChangeLog new file mode 100644 index 000000000..d952c505b --- /dev/null +++ b/apps/counter2/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Added Settings & readme +0.03: Fix lint warnings +0.04: Fix lint warnings +0.05: Fix on not reading counter defaults in Settings 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..04b00487f --- /dev/null +++ b/apps/counter2/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "counter2", + "name": "Counter2", + "version": "0.05", + "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..d74d9269f --- /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.max0, + min: -99, max: 99, + onchange: v => { + settings.max0 = v; + writeSettings(); + } + }, + 'Default C2': { + value: settings.max1, + 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/cprassist/metadata.json b/apps/cprassist/metadata.json index d832e98c5..94ba71d1b 100644 --- a/apps/cprassist/metadata.json +++ b/apps/cprassist/metadata.json @@ -13,5 +13,6 @@ {"name":"cprassist.app.js","url":"cprassist.js"}, {"name":"cprassist.img","url":"cprassist-icon.js","evaluate":true}, {"name":"cprassist.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"cprassist.settings.json"}] } diff --git a/apps/cprassist/settings.js b/apps/cprassist/settings.js index 5776baa0b..5099d0b7d 100644 --- a/apps/cprassist/settings.js +++ b/apps/cprassist/settings.js @@ -61,4 +61,4 @@ } }; E.showMenu(menu); -}); +}) diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog index 4f48bdd14..1c4f6f43b 100644 --- a/apps/crowclk/ChangeLog +++ b/apps/crowclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". 0.03: Fix the clock for dark mode. +0.04: Tell clock widgets to hide. diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js index eee1653cb..7e608ef19 100644 --- a/apps/crowclk/crow_clock.js +++ b/apps/crowclk/crow_clock.js @@ -136,9 +136,9 @@ Bangle.on('lcdPower', (on) => { g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json index 6985cf11a..265a0398b 100644 --- a/apps/crowclk/metadata.json +++ b/apps/crowclk/metadata.json @@ -1,7 +1,7 @@ { "id": "crowclk", "name": "Crow Clock", - "version": "0.03", + "version": "0.04", "description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face", "icon": "crow_clock.png", "screenshots": [{"url":"screenshot_crow.png"}], diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index a98be5c0f..30bfdd560 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -6,3 +6,9 @@ 0.06: Now read wheel rev as well as cadence sensor 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 4006789ef..0029c4b82 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,15 +2,19 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.07", + "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", + "tags": "outdoors,exercise,ble,bluetooth,bike,cycle,bicycle", + "dependencies" : { "blecsc":"module" }, "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, {"name":"cscsensor.settings.js","url":"settings.js"}, {"name":"cscsensor.img","url":"cscsensor-icon.js","evaluate":true} + ], + "data": [ + {"name":"cscsensor.json"} ] } diff --git a/apps/cscsensor/settings.js b/apps/cscsensor/settings.js index d7a7d565d..4fac5d0c1 100644 --- a/apps/cscsensor/settings.js +++ b/apps/cscsensor/settings.js @@ -23,17 +23,17 @@ } } const menu = { - '': { 'title': 'Cycle speed sensor' }, + '': { 'title': /*LANG*/'Cycle speed sensor' }, '< Back': back, - 'Wheel circ.(mm)': { + /*LANG*/'Wheel circ.(mm)': { value: s.wheelcirc, min: 800, max: 2400, step: 5, onchange: save('wheelcirc'), }, - 'Reset total distance': function() { - E.showPrompt("Zero total distance?", {buttons: {"No":false, "Yes":true}}).then(function(v) { + /*LANG*/'Reset total distance': function() { + E.showPrompt(/*LANG*/"Zero total distance?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) { if (v) { s['totaldist'] = 0; storage.write(SETTINGS_FILE, s); 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/cutelauncher/ChangeLog b/apps/cutelauncher/ChangeLog new file mode 100644 index 000000000..6eedfa435 --- /dev/null +++ b/apps/cutelauncher/ChangeLog @@ -0,0 +1 @@ +0.01: New app introduced to the app loader! diff --git a/apps/cutelauncher/app-icon.js b/apps/cutelauncher/app-icon.js new file mode 100644 index 000000000..4798f2299 --- /dev/null +++ b/apps/cutelauncher/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lUvwIFCjwKDj/gAgf+Agf/BQUP+f8AgXwn/AAgP4gYKBAgMAv/Ag4EBAQIECBQODDgfxAgUA/wxIAgIUD///GIX/HgUP/f8GII2B/4xDIoN/w/4JgIXB/hKCEwX/DoIEBLQQlC+B9CCgUHwAUCgJFBCgV4Cix3BCgMH74UCgfn+AUBgZZBCgIjBWoMegP+AIMHwKGBn/h8ISBgf+vAEBBQPeAgUDRQKaCv//IgIzBAgYKBAgZTCAAoA=")) \ No newline at end of file diff --git a/apps/cutelauncher/app.js b/apps/cutelauncher/app.js new file mode 100644 index 000000000..464d137b3 --- /dev/null +++ b/apps/cutelauncher/app.js @@ -0,0 +1,207 @@ +{ + let s = require('Storage'); + let settings = Object.assign( + { + showClocks: false, + scrollbar: true + }, + s.readJSON('cutelauncher.settings.json', true) || {} + ); + + // Borrowed caching from Icon Launcher, code by halemmerich. + let launchCache = s.readJSON('launch.cache.json', true) || {}; + let launchHash = s.hash(/\.info/) + JSON.stringify(settings).length; + if (launchCache.hash != launchHash) { + launchCache = { + hash: launchHash, + apps: s + .list(/\.info$/) + .map((app) => { + var a = s.readJSON(app, 1); + return a && { name: a.name, type: a.type, icon: a.icon, sortorder: a.sortorder, src: a.src }; + }) + .filter((app) => app && (app.type == 'app' || (app.type == 'clock' && settings.showClocks) || !app.type)) + .sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }), + }; + s.writeJSON('launch.cache.json', launchCache); + } + let apps = launchCache.apps; + apps.forEach((app) => { + if (app.icon) app.icon = s.read(app.icon); + else app.icon = s.read('placeholder.img'); + }); + + require('Font8x16').add(Graphics); + Bangle.drawWidgets = () => { }; + Bangle.loadWidgets = () => { }; + + const ITEM_HEIGHT = 95; + + // Create scroll indicator overlay + const overlayWidth = 30; + const overlayHeight = 35; + const overlay = Graphics.createArrayBuffer(overlayWidth, overlayHeight, 16, { msb: true }); + + // Function to create app backdrop + function createAppBackdrop(y) { + return [ + 58, y + 5, // Top edge + 118, y + 5, + 133 - 15 * 0.7, y + 5, + 133 - 15 * 0.4, y + 5 + 15 * 0.1, + 133 - 15 * 0.1, y + 5 + 15 * 0.4, // Top right corner + 133, y + 5 + 15 * 0.7, + 133, y + 20, + 133, y + 75, // Right edge + 133, y + 90 - 15 * 0.7, + 133 - 15 * 0.1, y + 90 - 15 * 0.4, + 133 - 15 * 0.4, y + 90 - 15 * 0.1, // Bottom right corner + 133 - 15 * 0.7, y + 90, + 118, y + 90, + 58, y + 90, // Bottom edge + 43 + 15 * 0.7, y + 90, + 43 + 15 * 0.4, y + 90 - 15 * 0.1, + 43 + 15 * 0.1, y + 90 - 15 * 0.4, // Bottom left corner + 43, y + 90 - 15 * 0.7, + 43, y + 75, + 43, y + 20, // Left edge + 43, y + 5 + 15 * 0.7, + 43 + 15 * 0.1, y + 5 + 15 * 0.4, + 43 + 15 * 0.4, y + 5 + 15 * 0.1, // Top left corner + 43 + 15 * 0.7, y + 5 + ]; + } + + // Helper function for creating rounded rectangle points + function createRoundedRectPoints(x1, y1, x2, y2, r) { + return [ + x1 + r, y1, // Top edge + x2 - r, y1, + x2 - r * 0.7, y1, + x2 - r * 0.4, y1 + r * 0.1, + x2 - r * 0.1, y1 + r * 0.4, // Top right corner + x2, y1 + r * 0.7, + x2, y1 + r, + x2, y2 - r, // Right edge + x2, y2 - r * 0.7, + x2 - r * 0.1, y2 - r * 0.4, + x2 - r * 0.4, y2 - r * 0.1, // Bottom right corner + x2 - r * 0.7, y2, + x2 - r, y2, + x1 + r, y2, // Bottom edge + x1 + r * 0.7, y2, + x1 + r * 0.4, y2 - r * 0.1, + x1 + r * 0.1, y2 - r * 0.4, // Bottom left corner + x1, y2 - r * 0.7, + x1, y2 - r, + x1, y1 + r, // Left edge + x1, y1 + r * 0.7, + x1 + r * 0.1, y1 + r * 0.4, + x1 + r * 0.4, y1 + r * 0.1, // Top left corner + x1 + r * 0.7, y1 + ]; + } + + // Update initScrollIndicator to use the new function + function initScrollIndicator() { + overlay.setBgColor(g.theme.bg).clear(); + const points = createRoundedRectPoints(0, 0, overlayWidth, overlayHeight, 10); + overlay.setColor(g.theme.bgH).fillPoly(points); + + // Add horizontal lines for scroll thumb aesthetic with outlines + const lineY1 = overlayHeight / 3; + const lineY2 = overlayHeight * 2 / 3; + const lineLeft = 9; + const lineRight = overlayWidth - 9; + + // Draw inner lines (increased from ±1 to ±2) + overlay.setColor(g.theme.fg2); + overlay.fillRect(lineLeft - 2, lineY1 - 1, lineRight + 2, lineY1 + 1); + overlay.fillRect(lineLeft - 2, lineY2 - 1, lineRight + 2, lineY2 + 1); + + overlay.fillRect(lineLeft, lineY1 - 2, lineRight, lineY1 + 2); + overlay.fillRect(lineLeft, lineY2 - 2, lineRight, lineY2 + 2); + } + initScrollIndicator(); + + // Function to update scroll indicator + function updateScrollIndicator(idx) { + const marginX = 1; + const marginY = 5; + let scrollPercent = (idx) / (apps.length - 1); + let scrollableHeight = g.getHeight() - marginY * 2 - overlayHeight; + let indicatorY = scrollPercent * scrollableHeight + marginY; + + Bangle.setLCDOverlay(overlay, g.getWidth() - overlayWidth - marginX, indicatorY, { id: "scrollIndicator" }); + } + + let prev_idx = -1; + let second_call = false; + + E.showScroller({ + h: ITEM_HEIGHT, + c: apps.length, + draw: (idx, rect) => { + g.setFontAlign(0, -1, 0).setFont('8x16'); + // Calculate icon dimensions + let icon = apps[idx].icon; + let iconSize = 48; + // Define rectangle size (independent of icon size) + const rectSize = 80; + const rectX = 48; + + // Draw rounded rectangle background using the new function + const points = createAppBackdrop(rect.y); + g.setColor(g.theme.bg2).fillPoly(points); + + // Draw icon centered in the top portion + let iconPadding = 8; + // Center icon within the rectangle + let iconXInRect = rectX + (rectSize - iconSize) / 2; + g.setColor(g.theme.fg).setBgColor(g.theme.bg2).drawImage(icon, iconXInRect, rect.y + iconPadding + 8); + + // Draw app name with ellipsis if too long + const maxWidth = rectSize - 8; + let text = apps[idx].name; + let textWidth = g.stringWidth(text); + if (textWidth > maxWidth) { + const ellipsis = "..."; + const ellipsisWidth = g.stringWidth(ellipsis); + while (textWidth + ellipsisWidth > maxWidth && text.length > 0) { + text = text.slice(0, -1); + textWidth = g.stringWidth(text); + } + text = text + ellipsis; + } + let textY = rect.y + iconPadding + iconSize + 15; + g.drawString(text, rectX + rectSize / 2, textY); + if (idx != prev_idx && !second_call && settings.scrollbar) { + updateScrollIndicator(idx); + if (prev_idx == -1) second_call = true; + prev_idx = idx; + } else if (second_call) second_call = false; + }, + select: (idx) => { + // Launch the selected app + load(apps[idx].src); + }, + remove: () => { + // Remove button handler + setWatch(() => { }, BTN1); + // Remove lock handler + Bangle.removeListener('lock'); + // Clear the scroll overlay + Bangle.setLCDOverlay(); + } + }); + + setWatch(Bangle.showClock, BTN1, { debounce: 100 }); + // Add lock handler to show clock when locked + Bangle.on('lock', (on) => { if (on) Bangle.showClock(); }); +} diff --git a/apps/cutelauncher/app.png b/apps/cutelauncher/app.png new file mode 100644 index 000000000..3580b4895 Binary files /dev/null and b/apps/cutelauncher/app.png differ diff --git a/apps/cutelauncher/metadata.json b/apps/cutelauncher/metadata.json new file mode 100644 index 000000000..d35815854 --- /dev/null +++ b/apps/cutelauncher/metadata.json @@ -0,0 +1,38 @@ +{ + "id": "cutelauncher", + "name": "Cute Launcher", + "shortName": "Cute Launcher", + "version": "0.01", + "description": "A simple launcher app for Bangle.js 2 that makes use of the full touchscreen", + "icon": "app.png", + "type": "launch", + "tags": "tool,system,launcher", + "screenshots": [ + { + "url": "screenshot.png" + } + ], + "supports": [ + "BANGLEJS2" + ], + "storage": [ + { + "name": "cutelauncher.app.js", + "url": "app.js" + }, + { + "name": "cutelauncher.img", + "url": "app-icon.js", + "evaluate": true + }, + { + "name": "cutelauncher.settings.js", + "url": "settings.js" + } + ], + "data": [ + { + "name": "cutelauncher.settings.json" + } + ] +} diff --git a/apps/cutelauncher/screenshot.png b/apps/cutelauncher/screenshot.png new file mode 100644 index 000000000..037dd08f3 Binary files /dev/null and b/apps/cutelauncher/screenshot.png differ diff --git a/apps/cutelauncher/settings.js b/apps/cutelauncher/settings.js new file mode 100644 index 000000000..fb24a3eb4 --- /dev/null +++ b/apps/cutelauncher/settings.js @@ -0,0 +1,37 @@ +(function (back) { + const SETTINGS_FILE = "cutelauncher.settings.json"; + + // initialize with default settings... + const storage = require('Storage'); + let settings = { + showClocks: false, + scrollbar: true + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key]; + } + + function save() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Cutelauncher' }, + '< Back': back, + 'Show Clocks': { + value: settings.showClocks, + onchange: () => { + settings.showClocks = !settings.showClocks; + save(); + } + }, + 'Scrollbar': { + value: settings.scrollbar, + onchange: () => { + settings.scrollbar = !settings.scrollbar; + save(); + } + } + }); +}) \ No newline at end of file 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 cb4260bb2..51e51b409 100644 --- a/apps/cycling/metadata.json +++ b/apps/cycling/metadata.json @@ -2,16 +2,19 @@ "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": [ + {"name":"cycling.json"} ] } diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index d5844c62b..11138f412 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -4,3 +4,12 @@ 0.04: added heart rate which is switched on when cycled to it through up/down touch on rhs 0.05: changed text to uppercase, just looks better, removed colons on text 0.06: better contrast for light theme, use fg color instead of dithered for ring +0.07: Use default Bangle formatter for booleans +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 +0.15: Ring can now show hours, minute, or seconds hand, day/night left, or battery; Allowed for 12hr time; Ring now goes up in 5% increments; Step goal can be changed; The info that is set on the watchface will retain when leaving the face diff --git a/apps/daisy/README.md b/apps/daisy/README.md index 491ed697f..5cc3060dc 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. @@ -18,8 +18,16 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248) * Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location * If your Sunrise, Sunset times look odd make sure you have setup your location using [MyLocation](https://banglejs.com/apps/?id=mylocation) -* The screen is updated every minute to save battery power +* The screen is updated every minute to save battery power, unless the ring is set to display seconds, then it updates every 3 seconds. * 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 +* In the settings, the ring can be set to: + * Hours - Displays the ring as though it's the hour hand on an analog clock. + * Minutes - Displays the ring as though it's the minute hand on an analog clock. + * Seconds - Displays the ring as though it's the seconds hand on an analog clock. + * Steps - Displays the ring as the amount of steps taken that day out of Step Target setting. + * Battery - Displays the ring as the amount of battery percentage left. + * Sun - Displays the ring as the amount of time that has passed from sunrise to sunset in the day and the amount of time between sunset and sunrise at night. ## 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 7c513726f..84d08e094 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,6 +1,6 @@ -var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +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"; const h = g.getHeight(); @@ -14,11 +14,12 @@ let warned = 0; let idle = false; let IDLE_MINUTES = 26; -let pal1; // palette for 0-40% +let pal1; // palette for 0-49% let pal2; // palette for 50-100% const infoLine = (3*h/4) - 6; const infoWidth = 56; const infoHeight = 11; +const sec_update = 3000; // This ms between updates when the ring is in Seconds mode var drawingSteps = false; function log_debug(o) { @@ -42,12 +43,12 @@ Graphics.prototype.setFontRoboto20 = function(scale) { function assignPalettes() { if (g.theme.dark) { - // palette for 0-40% + // palette for 0-49% pal1 = new Uint16Array([g.theme.bg, g.toColor(settings.gy), g.toColor(settings.fg), g.toColor("#00f")]); // palette for 50-100% pal2 = new Uint16Array([g.theme.bg, g.toColor(settings.fg), g.toColor(settings.gy), g.toColor("#00f")]); } else { - // palette for 0-40% + // palette for 0-49% pal1 = new Uint16Array([g.theme.bg, g.theme.fg, g.toColor(settings.fg), g.toColor("#00f")]); // palette for 50-100% pal2 = new Uint16Array([g.theme.bg, g.toColor(settings.fg), g.theme.fg, g.toColor("#00f")]); @@ -70,7 +71,7 @@ function getSteps() { try { return Bangle.getHealthStatus("day").steps; } catch (e) { - if (WIDGETS.wpedom !== undefined) + if (WIDGETS.wpedom !== undefined) return WIDGETS.wpedom.getSteps(); else return 0; @@ -83,7 +84,12 @@ function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.gy = settings.gy||'#020'; settings.fg = settings.fg||'#0f0'; - settings.idle_check = settings.idle_check||true; + settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); + settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours); + settings.hr_12 = (settings.hr_12 === undefined ? false : settings.hr_12); + settings.ring = settings.ring||'Steps'; + settings.idxInfo = settings.idxInfo||0; + settings.step_target = settings.step_target||10000; assignPalettes(); } @@ -97,20 +103,88 @@ function loadLocation() { function extractTime(d){ var h = d.getHours(), m = d.getMinutes(); + if (settings.hr_12) { + h = h % 12; + if (h == 0) h = 12; + } return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); } var sunRise = "00:00"; var sunSet = "00:00"; var drawCount = 0; +var sunStart; // In terms of ms +var sunEnd; // In terms of minutes +var sunFull; // In terms of ms +var isDaytime = true; -function updateSunRiseSunSet(now, lat, lon, line){ +function getMinutesFromDate(date) { + return (60 * date.getHours()) + date.getMinutes(); +} + +function updateSunRiseSunSet(now, lat, lon, sunLeftCalcs){ // get today's sunlight times for lat/lon - var times = SunCalc.getTimes(new Date(), lat, lon); + var times = SunCalc.getTimes(now, lat, lon); + var dateCopy = new Date(now.getTime()); // format sunrise time from the Date object sunRise = extractTime(times.sunrise); sunSet = extractTime(times.sunset); + if (!sunLeftCalcs) return; + + let sunLeft = times.sunset - dateCopy; + if (sunLeft < 0) { // If it's already night + dateCopy.setDate(dateCopy.getDate() + 1); + let timesTmrw = SunCalc.getTimes(dateCopy, lat, lon); + isDaytime = false; + sunStart = times.sunset; + sunFull = timesTmrw.sunrise - sunStart; + sunEnd = getMinutesFromDate(timesTmrw.sunrise); + } + else { + sunLeft = dateCopy - times.sunrise; + if (sunLeft < 0) { // If it's not morning yet. + dateCopy.setDate(dateCopy.getDate() - 1); + let timesYest = SunCalc.getTimes(dateCopy, lat, lon); + isDaytime = false; + sunStart = timesYest.sunset; + sunFull = times.sunrise - sunStart; + sunEnd = getMinutesFromDate(times.sunrise); + } + else { // We're in the middle of the day + isDaytime = true; + sunStart = times.sunrise; + sunFull = times.sunset - sunStart; + sunEnd = getMinutesFromDate(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 = { @@ -119,39 +193,32 @@ const infoData = { 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 } }; const infoList = Object.keys(infoData).sort(); -let infoMode = infoList[0]; -function nextInfo() { - let idx = infoList.indexOf(infoMode); +function nextInfo(idx) { if (idx > -1) { - if (idx === infoList.length - 1) infoMode = infoList[0]; - else infoMode = infoList[idx + 1]; + if (idx === infoList.length - 1) idx = 0; + else idx += 1; } - // power HRM on/off accordingly - Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); - resetHrm(); + return idx; } -function prevInfo() { - let idx = infoList.indexOf(infoMode); +function prevInfo(idx) { if (idx > -1) { - if (idx === 0) infoMode = infoList[infoList.length - 1]; - else infoMode = infoList[idx - 1]; + if (idx === 0) idx = infoList.length - 1; + else idx -= 1; } - // power HRM on/off accordingly - Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); - resetHrm(); + return idx; } function clearInfo() { g.setColor(g.theme.bg); //g.setColor(g.theme.fg); - g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight); + g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight); } function drawInfo() { @@ -195,18 +262,49 @@ function draw() { function drawClock() { var date = new Date(); - var timeStr = require("locale").time(date,1); - var da = date.toString().split(" "); - var time = da[4].substr(0,5); - var hh = da[4].substr(0,2); - var mm = da[4].substr(3,2); - var steps = getSteps(); - var p_steps = Math.round(100*(steps/10000)); - + var hh = date.getHours(); + var mm = date.getMinutes(); + var ring_percent; + var invertRing = false; + switch (settings.ring) { + case 'Hours': + ring_percent = Math.round((10*(((hh % 12) * 60) + mm))/72); + break; + case 'Minutes': + ring_percent = Math.round((10*mm)/6); + break; + case 'Seconds': + ring_percent = Math.round((10*date.getSeconds())/6); + break; + case 'Steps': + ring_percent = Math.round(100*(getSteps()/settings.step_target)); + break; + case 'Battery': + ring_percent = E.getBattery(); + break; + case 'Sun': + ring_percent = 100 * (date - sunStart) / sunFull; + if (ring_percent > 100) { // If we're now past a sunrise of sunset + updateSunRiseSunSet(date, location.lat, location.lon, true); + ring_percent = 100 * (date - sunStart) / sunFull; + } + // If we're exactly on the minute that the sun is setting/rising + if (getMinutesFromDate(date) == sunEnd) ring_percent = 100; + invertRing = !isDaytime; + break; + } + + if (settings.hr_12) { + hh = hh % 12; + if (hh == 0) hh = 12; + } + hh = hh.toString().padStart(2, '0'); + mm = mm.toString().padStart(2, '0'); + g.reset(); g.setColor(g.theme.bg); g.fillRect(0, 0, w, h); - g.drawImage(getGaugeImage(p_steps), 0, 0); + g.drawImage(getGaugeImage(ring_percent, settings.ring, invertRing), 0, 0); setLargeFont(); g.setColor(settings.fg); @@ -218,10 +316,10 @@ function drawClock() { g.drawString(mm, (w/2) + 1, h/2); drawInfo(); - + // recalc sunrise / sunset every hour if (drawCount % 60 == 0) - updateSunRiseSunSet(new Date(), location.lat, location.lon); + updateSunRiseSunSet(date, location.lat, location.lon, settings.ring == 'Sun'); drawCount++; } @@ -254,7 +352,7 @@ function resetHrm() { Bangle.on('HRM', function(hrm) { hrmCurrent = hrm.bpm; hrmConfidence = hrm.confidence; - log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")"); + log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")"); if (infoMode == "ID_HRM" ) drawHrm(); }); @@ -264,116 +362,191 @@ Bangle.on('HRM', function(hrm) { // putting into 1 function like this, rather than individual variables // reduces ram usage from 70%-13% -function getGaugeImage(p) { +function getGaugeImage(p, type, reverse) { + const endsDontShowList = ['Minutes', 'Seconds']; // Don't show non-5% increments with these ring types + if (reverse) p = 100 - p; + var endsDontShow = endsDontShowList.includes(type); // p0 - if (p < 2) return { + if (p < 2 || (p < 5 && endsDontShow)) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLLLzWEABLgbVgohEGopYaiofDBihWVHJpYYDgYPbKx1ACJhYZIwT4OcAZWYHyRYUIgQXQH4RqOThCXUYRpCHNyQVVQQTwVQiSZWIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) }; // p2 - if (p >= 2 && p < 4) return { + if (p < 5) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette: pal1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/ADNUFE8FqtVq2q1AqkFIIrDAAOAFMEBFQYrE1WgKsYrGLL4qFFY2pqDWeFZdUVkAhCAQMKFYdVLDUVFQYMHlWq0oMJKyoOJlQrCLDBWDB5clB5xWOoARMCARYWKwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) }; - // p4 - if (p >= 4 && p < 7) return { + // p5 + if (p < 10) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFY2loAqjFY1VqDWeFZdUVkAhEhQrDLDcVFQYMHlQrCBhBWVHJpYYDgYPbKx1ACJhYZIwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) - }; - - // p7 - if (p >= 7 && p < 10) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : pal1, - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgWlKzVACJgrCqBWYawgAJcAOlNBhWMCZ8qFYJYUgoqBC6ECFYJqOAApWSS4jCNQQ5uSCqqCCeCqESFQKZUIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A==")) + palette : (reverse ? pal2 : pal1), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZeUVkFUBQcKFYdVqArZioqDBg8qFYQMIKyoOJlWpBoJYYKwYPLlIPOKx1ACJgQCLCxWCawgAJgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) }; // p10 - if (p >= 10 && p < 20) return { + if (p < 15) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAOkQSdUFacK1WloCCSCaAAEFYKaQQSyEC0pvQirZTbomlIh6CYZAZFOQTBxDQhyCYOQhoPQS4bQHaBzaVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) }; - // p20 - if (p >= 20 && p < 30) return { + // p15 + if (p < 20) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, + palette : (reverse ? pal2 : pal1), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWAOpbRSucWAWVO5DaeZ5jaeJhgrBbTqkLbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A==")) + }; + + // p20 + if (p < 25) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4AWgNVoAEGAERSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) }; - // p30 - if (p >= 30 && p < 40) return { + // p25 + if (p < 30) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, + palette : (reverse ? pal2 : pal1), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4AKgNVoAr/Ff4r/Ff4r/Ff4rNqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) + }; + + // p30 + if (p < 35) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccFawkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) }; - // p40 - if (p >= 40 && p < 50) return { + // p35 + if (p < 40) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal1, + palette : (reverse ? pal2 : pal1), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIiorgjWqBI8FqtAFb1W1ArJbjz9BFZAKBbjxMBsALIFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) + }; + + // p40 + if (p < 45) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal2 : pal1), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjUBQR6EaiqCPQjVVQSATCqtUFSZvB1WACiSEUY4KCQQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) }; - // p50 - if (p >= 50 && p < 60) return { + // p45 + if (p < 50) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal2 : pal1), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrBqgqThQrBwAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMVFYNUCJsKFQOqFShYEoARMrRWXLAiFMiorCFSxYEFhQ6BFYJWXLAosIBgVWKzBYGcAsFBIdWKzIhGABI1EADArNoArcFhgqeWQwAEqAqeLJRVfcBLWdAH4A5A=")) + }; + + // p50 + if (p < 55) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAiFMQIRWWLAosKFQZWXLAosIFQZWYLAzgFawZWbAAMKFgmq1IoEAANUFTQABFZtAFbgsFFYwqeWQorFVjZZJFYhVfcAwrCazoA/AHI")) }; - // p60 - if (p >= 60 && p < 70) return { + // p55 + if (p < 60) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAmVQJxWWLAgcLFQZWXLAWpJJQqDKzBYC0ofDqjWHKzYhHABA1EADArNoArcFhgqegEBFRKsbLJxVfcBLWdAH4A5A==")) + }; + + // p60 + if (p < 65) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAelNBqCLVxqEC0oRPQS6EC0oSQQSyECFYKEVQSIABFYI/QAAcFFYJDRCgSCmYYjdSCqqYCLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) }; - // p70 - if (p >= 70 && p < 80) return { + // p65 + if (p < 70) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WkFb8FqtAFY+VbUArIlVVFcIJHhI1IAC9VqiNJXI7aYFZAKKbS5MJFcKkJXRLafBYbcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) + }; + + // p70 + if (p < 75) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZAgoAggNVoAr/FbdUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) }; - // p80 - if (p >= 80 && p < 90) return { + // p75 + if (p < 80) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4rOqtQFf4r/Ff4r/Ff4r/FZVUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) + }; + + // p80 + if (p < 85) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) }; - - // p90 - if (p >= 90 && p < 100) return { + + // p85 + if (p < 90) return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhEBtQrgbhEFrTacbhkFqzadbgQrIXRbcfqoribg5hJbjIrGXILlIbjIiGFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) + }; + + // p90 + if (p < 95) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESquq1ArTgqESNgOqwArTIYKERH4KCUQigSBbKTdGCKKCVQiTCCFSyERCALBQQjAPBoArXDZ7ARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) }; + // p95 + if (p < 98 || (p < 100 && endsDontShow)) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLsFFZrgbgNVFAeoGohYfiorDBhIACKzVVtQqIFgpYYDgVqB5xYXKwVVoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA==")) + }; + + // p98 + if (p < 100) return { + width : 176, height : 176, bpp : 2, + transparent : -1, + palette : (reverse ? pal1 : pal2), + buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtV1WpqtUFUlVAAIrCAANAFMEBEoQrFqtQKsQrHLL4jEFY5ZdawIrMcDasEEIo1FLDUVD4YMUKyo5NLDAcDB7ZWOoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) + }; + // p100 return { width : 176, height : 176, bpp : 2, transparent : -1, - palette : pal2, + palette : (reverse ? pal1 : pal2), buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLFjzWEABLgbVgohEGoqyaiofDBihWVHJpYYDgYPbKxz5NLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) }; } @@ -410,7 +583,7 @@ function BUTTON(name,x,y,w,h,c,f,tx) { // if pressed the callback BUTTON.prototype.check = function(x,y) { //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); - + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { log_debug(this.name + ":callback\n"); this.callback(); @@ -472,7 +645,7 @@ function checkIdle() { warned = false; return; } - + let hour = (new Date()).getHours(); let active = (hour >= 9 && hour < 21); //let active = true; @@ -501,7 +674,7 @@ function buzzer(n) { if (n-- < 1) return; Bangle.buzz(250); - + if (buzzTimeout) clearTimeout(buzzTimeout); buzzTimeout = setTimeout(function() { buzzTimeout = undefined; @@ -514,14 +687,15 @@ function buzzer(n) { // timeout used to update every minute var drawTimeout; -// schedule a draw for the next minute +// schedule a draw for the next minute or every sec_update ms function queueDraw() { + let delay = settings.ring == 'Seconds' ? sec_update - (Date.now() % sec_update) : 60000 - (Date.now() % 60000); if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; checkIdle(); draw(); - }, 60000 - (Date.now() % 60000)); + }, delay); } // Stop updates when LCD is off, restart when on @@ -535,20 +709,26 @@ Bangle.on('lcdPower',on=>{ }); Bangle.setUI("clockupdown", btn=> { - if (btn<0) prevInfo(); - if (btn>0) nextInfo(); + if (btn<0) settings.idxInfo = prevInfo(settings.idxInfo); + if (btn>0) settings.idxInfo = nextInfo(settings.idxInfo); + // power HRM on/off accordingly + infoMode = infoList[settings.idxInfo]; + Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); + resetHrm(); + log_debug("idxInfo=" + settings.idxInfo); draw(); + storage.write(SETTINGS_FILE, settings); // Retains idxInfo when leaving the face }); loadSettings(); loadLocation(); +var infoMode = infoList[settings.idxInfo]; +updateSunRiseSunSet(new Date(), location.lat, location.lon, true); g.clear(); Bangle.loadWidgets(); /* * we are not drawing the widgets as we are taking over the whole screen - * so we will blank out the draw() functions of each widget and change the - * area to the top bar doesn't get cleared. */ -for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +widget_utils.hide(); draw(); diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 5073db603..d75402153 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.06", + "version": "0.15", "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 044eee0d1..74b667e2a 100644 --- a/apps/daisy/settings.js +++ b/apps/daisy/settings.js @@ -5,47 +5,98 @@ let s = {'gy' : '#020', 'fg' : '#0f0', 'color': 'Green', - 'check_idle' : true}; + 'check_idle' : true, + 'batt_hours' : false, + 'hr_12' : false, + 'ring' : 'Steps', + 'idxInfo' : 0, + 'step_target' : 10000}; // ...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); } var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; var gy_code = ['#020','#220','#022','#202','#200','#002']; - - E.showMenu({ - '': { 'title': 'Daisy Clock' }, - '< Back': back, - 'Colour': { - value: 0 | color_options.indexOf(s.color), - min: 0, max: 5, - format: v => color_options[v], - onchange: v => { - s.color = color_options[v]; - s.fg = fg_code[v]; - s.gy = gy_code[v]; - save(); + var ring_options = ['Hours', 'Minutes', 'Seconds', 'Steps', 'Battery', 'Sun']; + var step_options = [100, 1000, 5000, 10000, 15000, 20000]; + + function showMainMenu() { + let appMenu = { + '': { 'title': 'Daisy Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(s.color), + min: 0, max: color_options.length - 1, + format: v => color_options[v], + onchange: v => { + s.color = color_options[v]; + s.fg = fg_code[v]; + s.gy = gy_code[v]; + save(); + }, }, - }, - 'Idle Warning': { - value: !!s.idle_check, - format: v => v ? /*LANG*/"Yes":/*LANG*/"No", - onchange: v => { - s.idle_check = v; - save(); + 'Idle Warning': { + value: !!s.idle_check, + onchange: v => { + s.idle_check = v; + save(); + }, }, - } - }); -}) + 'Expected Battery Life In Days Not Percentage': { + value: !!s.batt_hours, + onchange: v => { + s.batt_hours = v; + save(); + }, + }, + '12 Hr Time': { + value: !!s.hr_12, + onchange: v => { + s.hr_12 = v; + save(); + }, + }, + 'Ring Display': { + value: 0 | ring_options.indexOf(s.ring), + min: 0, max: ring_options.length - 1, + format: v => ring_options[v], + onchange: v => { + let prev = s.ring; + s.ring = ring_options[v]; + save(); + if (prev != s.ring && (prev === 'Steps' || s.ring === 'Steps')) { + // redisplay the menu with/without ring setting + // Reference https://github.com/orgs/espruino/discussions/7697 + setTimeout(showMainMenu, 0); + } + }, + } + }; + if (s.ring == 'Steps') { + appMenu[/*LANG*/"Step Target"] = { + value: 0 | step_options.indexOf(s.step_target), + min: 0, max: step_options.length - 1, + format: v => step_options[v], + onchange: v => { + s.step_target = step_options[v]; + save(); + }, + }; + } + E.showMenu(appMenu); + } + + showMainMenu(); +}) \ No newline at end of file diff --git a/apps/dane_tcr/ChangeLog b/apps/dane_tcr/ChangeLog index 4f6fe2edc..05ef79052 100644 --- a/apps/dane_tcr/ChangeLog +++ b/apps/dane_tcr/ChangeLog @@ -4,4 +4,6 @@ 0.04: Move code to Arwes Module 0.05: Add icon 0.06: remove app image as it is unused -0.07: Bump version number for change to apps.json causing 404 on upload \ No newline at end of file +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 817d0c59b..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.07", + "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/dane_tcr/settings.js b/apps/dane_tcr/settings.js index 9d28d1b30..c13a0825d 100644 --- a/apps/dane_tcr/settings.js +++ b/apps/dane_tcr/settings.js @@ -41,7 +41,6 @@ }, "Animation" : { value : settings.animation, - format : v => v?"On":"Off", onchange : saveChange('animation') }, "Frame rate" : { @@ -51,9 +50,8 @@ }, "Debug" : { value : settings.debug, - format : v => v?"On":"Off", onchange : saveChange('debug') }, '< Back': back }); -}); \ No newline at end of file +}) 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/daymoon/ChangeLog b/apps/daymoon/ChangeLog new file mode 100644 index 000000000..a00fe7982 --- /dev/null +++ b/apps/daymoon/ChangeLog @@ -0,0 +1,5 @@ +0.01: First functional release +0.02: move moon down, rotate sunrise/sunset, shift Hours/minutes to corners +0.03: Change day and night to have different dots +0.04: Update ChangeLog, coerce dark theme +0.05: Add more screenshots, fix bug in dot colors diff --git a/apps/daymoon/README.md b/apps/daymoon/README.md new file mode 100644 index 000000000..4b43b75aa --- /dev/null +++ b/apps/daymoon/README.md @@ -0,0 +1,20 @@ +# DayMoon Circadian +This started out with a goal to recreate the Pebble [Fair Circadian watchface](https://setpebble.com/app/fair-circadian) by Matthew Clark for the Bangle.js2. +It ended up with me making a mostly new watchface that has the moon phase more prominent, but keeps the single dial 24 hour clock with daylight and sunset highlighted. + +This uses the myLocation app to get your latitude and longitude for proper daylight calculations. If your location isn't set in that app, it will default to Nashua, NH. If your sunrise/sunset times aren't making sense, check that first! + +## Future Development +Feature roadmap: + - [x] 0.01 Fix blocking widgets + - [x] 0.03 Day and Night different color markers + - [x] 0.04 Add to App Loader + - [x] 0.05 Add more screenshots with different moon phases + - [ ] 0.06 add Day of week and month display + - [ ] 0.07 Seconds display + - [ ] 0.08 Color Themes (and settings/options) + - [ ] 0.09 Moon display angle represents how it looks in the sky + - [ ] 0.10 custom/bigger/fitted time digits + - [ ] 0.20 clockinfo support? + - [ ] 0.30 Tap/swipe actions? + \ No newline at end of file diff --git a/apps/daymoon/app-icon.js b/apps/daymoon/app-icon.js new file mode 100644 index 000000000..a87dd3ee2 --- /dev/null +++ b/apps/daymoon/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///hADBg/g/9/zuFA4Ocj+Nj+lgP4vP/o8AokAg8Ag0Ao//Mf4AptF2s+ICqNGulmkVkxF0wwsPo+EuUik13s14DBsIu1Iw4XBlGIs41BsAWKgl4vFnu1okVns+GsgyBtOZC5FkOQOHw1ItGGvGEAQWWhIuIxF2w92vAJDxGIs2JxFpC494wUos9HBQtHo2Sk+XRhFyk65ChWqqvAAoN5wUpuyMHxFos6FBCwOq5vM5sAzF3ygMCAAl3vDQBgEPCwOqr285nggFJw9IGA1os2IEIPz/QXB5m84vMGALbBNYsIBANnFwMz1Uzn/MqtbrhiBTgN2JAkHpCzBgGjmfzC4POGAmIo1nuAXEFwIHB+evmYXBrm89vM9nAhFnogvFZgNghU//8/mf69ns8vM93Ag1ovAvEAwOGUgIsB/55B9lc9ld5yRBtF2swXDw92sgXDR4P65h4B93M7gQBoyQEu0iRwKMBXwX/5p1B9nMr0Ao8ivAXDsUilAXC/4XBnQVBrncrfOgFCkUmC4eykUrC4R1B0f/O4PM53O3kAqUikIXDjci7YXB+Z3B1+qCwPsqoDBgG8kXlC4kV7x3B1//C4XMrovCO4O72NVC4fhiI6BhWqIoIXBIYPN5lc4EArtdjYXDhdd8PAC4M/18/F4PM4vF53Agsd7sQC4nB7jLBUgKOB1XFLgIAB4EBMgIzBAAUBiPBWYK+CAANc5wXCgHF8sdE4IvD5mx8JgCAAXO9neFwMAjobBI4kA2vMqOwGAnO9yoBgEF3fBroWEgHsjflWAIwD4tc6ouB5nc8sRC4sO9cd9wPBDAXlFwUO8ve8PQC4sAiPCldbAoIYB5hvC50SlfOCw0AivSkNb2IKF2Mc2Uu9YXHh0V9nrjbjEjsc3sV2qEBAAsigEe53hj3R3vbiMeitbfgK0BCAIADgQCBhtc2O7qOx8si7ns4PNqvgCAQXEDwUO93uj1c2Uilvh8vsaYIQDF4kgAgO+qsRjdSkUrjZKBCAxfEAAXt4PR5kijle5gQIABFdrxeBCBgA/ADg=")) \ No newline at end of file diff --git a/apps/daymoon/app.js b/apps/daymoon/app.js new file mode 100644 index 000000000..ce403fdfe --- /dev/null +++ b/apps/daymoon/app.js @@ -0,0 +1,260 @@ +const LOCATION_FILE = "mylocation.json"; +let location; + +var Utils = require("graphics_utils"); +var SunCalc = require("suncalc"); +var RADII = { + moon: 40, + arcMin: 48, + arcMax: 63, + dots: 55, + needle: 54, +}; +var COL = { + moon: 65535, // + txture: 33792, // 0.5 ,0.5,0 + shadow: 8196, // .125,0, .125 + day: 40159, //0.6, 0.6,1 + night: 6, // 0, 0, 0.2 + ndots: 2047, // 0, 1, 1 cyan + ddots: 0, + needle: 63488, // 1, 0, 0 red + stime: 2047 +}; +const TAU = 2.0 * Math.PI; +const MX = g.getWidth() / 2, + MY = 24 + 3 + RADII.arcMax; +const DAY_MILLIS = 86400000; +const M_POS = { + x: MX, + y: MY, + r: RADII.moon +}; +// images +const moon_texture = { + width: 80, + height: 80, + bpp: 1, + transparent: 0, + buffer: require("heatshrink").decompress(atob("ABsRqAJHkEiBA0N0uq1AIEgNVqtRqoJEgUiAAQJEioTBAAIzEl2q12oxATECQdVioJD/eqne60UCHQoADoAJBgf+xWrFIOACYUFCYo8Cj/73f70er0ROHAANUBIM//3///q1WIFAV1qtXCggJB//7CYO6keikBOHKAUDCIInClSgCgonBu4TK1W73ShBMQxkCh5OC//uFIInBi91q5PFCYISC3er//iOwXVE41UCYf+9//9AnCJopVBqEv/+/3//E4P6kUgJw4nDKAP+14TB1Xoq4hBEwYFBqgnB3Wr3e737KB/QnIqp3B32OKAYTBE4Z4BAoYnBEoRSC0fyE5ITBJ4WuCYP4J4J3CeQQFClbvBJgOqn5kBnRPKTwJMB1B4B92qEgQACJ4JTBkYnBYwOilYsBO5NUhYmB9+qxGC9TxBEYTvFqki3Y8B1Uikei3+oionIgGrO4OqwGC9H/xATK1/7E4UAnU7kATIqEAl/uE4WA12u0ATJgSgB/+ikUgnW70EFCY9AgGDE4PowEAlWowEBCZJ4BneggUgkRSBCZEAgEKJoIEBgEIAYQOCKYcVBIMqJgIEBgQSCgAQBqiJDRQIOBEwYAEMgNRiITBqKKBCYJJBE4xQGMQIABlBPHHgInDHQQjEAQJTCHgbFEABg8EBg5SDCgxNDABI=")) +}; +const needle = { + width: 23, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob("//+B///D///AAAHgAADgAAHAAA9///j//+H//gA=") +}; + +/* + now use SunCalc.getMoonIllumination() + previously used these: + https://github.com/espruino/BangleApps/blob/master/apps/widmp/widget.js + https://github.com/deirdreobyrne/LunarPhase + modified to be based on millisec instead of sec, and to use tau = 2*pi +*/ + +// requires the myLocation app +function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE, 1) || { + "lat": 45, + "lon": -71.3, + "location": "Nashua" + }; //{"lat":51.5072,"lon":0.1276,"location":"London"}; +} + +function drawMoon(shadowShape) { + g.setColor(0, 0, 0).fillCircle(MX, MY, RADII.arcMax + 3); + g.setColor(COL.moon).fillCircle(MX, MY, RADII.moon - 1); + g.setColor(COL.txture).drawImage(moon_texture, MX, MY, { + rotate: 0 + }); + // TODO: can set the rotation here to the parallacticAngle from getMoonPosition + g.setColor(COL.shadow).fillPoly(shadowShape); + // TODO: set rotation of the fillPoly? parallactic-mp.angle I think. + // Use g.transformVertices to do the rotation +} + +function drawDayRing(times) { + let r_ = RADII.arcMin; + let rm = RADII.arcMax; + let rd = RADII.dots; + let radT = [tToRad(times[0]), tToRad(times[1])]; + let hhmm = [require("locale").time(times[0], 1), require("locale").time(times[1], 1)]; + g.setColor(COL.day); + Utils.fillArc(g, MX, MY, r_, rm, radT[0], radT[1]); + g.setColor(COL.night); + Utils.fillArc(g, MX, MY, r_, rm, radT[1] - TAU, radT[0]); + // write sunrise/sunset times + g.setFont('6x8').setColor(COL.stime); + g.setFontAlign(0, 1, 3).drawString(hhmm[0], MX - rm - 2, MY); + g.setFontAlign(0, 1, 1).drawString(hhmm[1], MX + rm + 2, MY); + // draw dots + let edges = []; + let isDay = false; + let flag = false; + if (radT[1] > TAU) { + edges = [radT[1] - TAU, radT[0]]; + g.setColor(COL.ddots); + isDay = true; + } else { + edges = [radT[0], radT[1]]; + g.setColor(COL.ndots); + isDay = false; + } + for (var i = 0; i < 24; i++) { + let a = i * TAU / 24; + if (!flag && a > edges[0] && a < edges[1]) { + //first cross + if (isDay) { + g.setColor(COL.ndots); + } else { + g.setColor(COL.ddots); + } + flag = true; + } else if (flag && a > edges[1]) { + //second cross + if (isDay) { + g.setColor(COL.ddots); + } else { + g.setColor(COL.ndots); + } + flag = false; + } + let dotSize = (i % 3 == 0) ? 2 : 1; + let pX = MX + Math.cos(a) * rd; + let pY = MY + Math.sin(a) * rd; + g.fillCircle(pX, pY, dotSize); + } + let labels = ['6P', '12A', '6A', '12P']; + let qX = [rd - 9, 2, 11 - rd, 2]; + let qY = [1, rd - 10, 1, 12 - rd]; + g.setFont('4x6').setFontAlign(0, 0, 0).setColor(COL.ndots); + for (var j = 0; j < 4; j++) { + g.drawString(labels[j], MX + qX[j], MY + qY[j]); + } +} + + +function drawHHMM(d) { + var HM = require("locale").time(d, 1 /*omit seconds*/ ).split(":"); + // write digital time + g.setBgColor(0, 0, 0).setColor(1, 1, 1).setFontVector(45); + g.setFontAlign(1, 1, 0).drawString(" " + HM[0], MX - 20, g.getHeight() + 3); + g.setFontAlign(-1, 1, 0).drawString(HM[1] + " ", MX + 30, g.getHeight() + 3); + // TODO: use the meridian text AM/PM or blank for 24 hr. + // var meridian = require("locale").meridian(d); +} + +function moonShade(pos, mp) { + pos = pos !== undefined ? pos : M_POS; + mp = mp !== undefined ? mp : SunCalc.getMoonIllumination(new Date()); + // pos has x,y, r for the drawing, mp is from SunCalc Moon Illumination + let k = mp.fraction; + // k is the percent along the equator of the terminator + const pts = Math.min(pos.r >> 1, 32); + // this gives r/2 pts on the way down and up, capped at 64 total for polyfill + let a = [], + b = [], + s1 = 1, + s2 = 0; + // scale s1 is 1 or -1 for fixed edge of the shadow; defined via case switches below + // scale s2 factor for the moving edge of the shadow + // need to do some computation to simplify for new/full moon if k 'close enough' to 0 or 1/-1 + // + let isWaxing = (mp.phase < 0.5); + s1 = isWaxing ? -1 : 1; + s2 = isWaxing ? 1 - 2 * k : 2 * k - 1; + let tr = (pos.r + 1); + for (var i = 0; i < pts; i++) { + // down stroke on the outer shadow + var t = i * Math.PI / (pts + 1); //pts+1 so we leave the last point for the starting of going up + let cirX = Math.sin(t) * tr; + let cirY = Math.cos(t) * tr; + a.push(pos.x + s1 * cirX); //x + a.push(pos.y + cirY); //y + b.push(pos.x + s2 * cirX); //x for shadow edge + b.push(pos.y - cirY); //y going up for shadow edge + } + return a.concat(b); +} + +function tToRad(date) { + date = (date !== undefined) ? new Date(date.getTime()) : new Date(); + let milli = date - new Date(date.setHours(0, 0, 0, 0)); + return (milli / DAY_MILLIS + 0.25) * TAU; +} + +function draw(date) { + var d = date !== undefined ? date : new Date(); + var a = tToRad(d), + shape = moonShade(M_POS, SunCalc.getMoonIllumination(d)), + sTimes = SunCalc.getTimes(d, location.lat, location.lon), + daylight = [sTimes.sunrise, sTimes.sunset]; + //clear time area + g.clearRect(Bangle.appRect); //g.setColor(0).fillRect(0, 176 - 45, 176, 176); + drawMoon(shape); + drawDayRing(daylight); + drawHHMM(d); + // draw pointer + // TODO: Maybe later make this an overlay that can be removed?? -avoid drawing so much every minute/second + g.setColor(COL.needle).drawImage(needle, MX + RADII.needle * Math.cos(a), MY + RADII.needle * Math.sin(a), { + rotate: a + }); + +} +/* +const shotTimes = [1720626960000, 1729184400000, 1738298880000, 1717575420000]; +let desc =`first quarter -2 days moon at 10:20 in the summer + jun 10 2024 10:56 +full moon at 12 noon near fall equinox + Sep 17 2024 12:00 +new moon at 11pm in winter + dec 30 2024 23:48 +3rd quarter moon at 03:17 am + May 5 2024 03:17` + +function screenshots(times) { + let d = new Date(); + for (let t of times) { + d.setTime(t); + draw(d); + g.dump(); + } +} +*/ +// Clear the screen once, at startup +g.reset(); +// requires the myLocation app +loadLocation(); +g.setBgColor(0, 0, 0).clear(); +// draw immediately at first +draw(); +// now draw every second +// eventually maybe update the moon just every hour?? +var secondInterval = setInterval(draw, 10000); //was 1000 +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower', on => { + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(draw, 10000); //was 1000 + draw(); // draw immediately + } +}); +/* Show launcher when middle button pressed +This should be done *before* Bangle.loadWidgets so that +widgets know if they're being loaded into a clock app or not */ +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +g.setTheme({ + fg: "#fff", + bg: "#000", + fg2: "#fff", + bg2: "#004", + fgH: "#fff", + bgH: "#00f", + dark: true +}); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/daymoon/daymoon.png b/apps/daymoon/daymoon.png new file mode 100644 index 000000000..f9cd84178 Binary files /dev/null and b/apps/daymoon/daymoon.png differ diff --git a/apps/daymoon/metadata.json b/apps/daymoon/metadata.json new file mode 100644 index 000000000..f06e216bc --- /dev/null +++ b/apps/daymoon/metadata.json @@ -0,0 +1,17 @@ +{ "id": "daymoon", + "name": "DayMoon Circadian Clock", + "version": "0.05", + "dependencies": {"mylocation":"app"}, + "description": "A 24 hour clockface showing the Moon Phase and portion of the day that the Sun is up inspired by Matthew Clark's *Fair Circadian* Pebble watchface", + "icon": "daymoon.png", + "screenshots": [{"url":"s1.png"},{"url":"s2.png"},{"url":"s3.png"},{"url":"s4.png"}], + "type": "clock", + "tags": "clock,moon,lunar", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"daymoon.app.js","url":"app.js"}, + {"name":"daymoon.img","url":"app-icon.js","evaluate":true} + ] +} \ No newline at end of file diff --git a/apps/daymoon/s1.png b/apps/daymoon/s1.png new file mode 100644 index 000000000..cb64c2f01 Binary files /dev/null and b/apps/daymoon/s1.png differ diff --git a/apps/daymoon/s2.png b/apps/daymoon/s2.png new file mode 100644 index 000000000..22eddc14b Binary files /dev/null and b/apps/daymoon/s2.png differ diff --git a/apps/daymoon/s3.png b/apps/daymoon/s3.png new file mode 100644 index 000000000..463be5d21 Binary files /dev/null and b/apps/daymoon/s3.png differ diff --git a/apps/daymoon/s4.png b/apps/daymoon/s4.png new file mode 100644 index 000000000..6b8ce5021 Binary files /dev/null and b/apps/daymoon/s4.png differ diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js index d2c3893d5..914234060 100644 --- a/apps/dclock/clock-dev.js +++ b/apps/dclock/clock-dev.js @@ -70,7 +70,7 @@ function drawSimpleClock() { var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate(); //Days since full moon - var knownnew = new Date(2020,02,24,09,28,0); + var knownnew = new Date(2020,2,24,9,28,0); // Get millisecond difference and divide down to cycles var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53; diff --git a/apps/dedreckon/ChangeLog b/apps/dedreckon/ChangeLog new file mode 100644 index 000000000..263d4078d --- /dev/null +++ b/apps/dedreckon/ChangeLog @@ -0,0 +1 @@ +0.01: attempt to import diff --git a/apps/dedreckon/README.md b/apps/dedreckon/README.md new file mode 100644 index 000000000..706c7f191 --- /dev/null +++ b/apps/dedreckon/README.md @@ -0,0 +1,20 @@ +# Ded Reckon + +Dead Reckoning using compass and step counter. + +This allows logging track using "dead reckoning" -- that's logging +angles from compass and distances from step counter. You need to mark +turns, and point watch to direction of the turn. Simultaneously, it +tries to log positions using GPS. You can use it to calibrate your +step length by comparing GPS and step counter data. It can also get +pretty accurate recording of track walked in right circumstances. + +Tap bottom part of the screen to select display (text or map for +now). Point watch to new direction, then tap top left part of screen +to indicate a turn. + +Map shows blue line for track from dead reckonging, and green line for +track from GPS. + +You probably want magnav installed (and calibrated) for useful +results, as it provides library with better compass. \ No newline at end of file diff --git a/apps/dedreckon/app-icon.js b/apps/dedreckon/app-icon.js new file mode 100644 index 000000000..39b72f00b --- /dev/null +++ b/apps/dedreckon/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AFsAFtoADF1wwqF4wwhEI5goGGIjFYN4wFF1KbHGUolIMc4lGSdIwJd9DstAH7FrBywwgad4veDwojJBIIvcFwIACGBYICGDYvEGBYvdFwqyLL8i+LF7oxFRxgveGAQ0EF5IwfMY4vpL5AFLAEYv/F8owoE44vrAY4vmAQIEEF85dGGE0AE4gvoFwpmHd0oINAH4A/AH4AvA")) diff --git a/apps/dedreckon/app.png b/apps/dedreckon/app.png new file mode 100644 index 000000000..db3fcfb88 Binary files /dev/null and b/apps/dedreckon/app.png differ diff --git a/apps/dedreckon/dedreckon.app.js b/apps/dedreckon/dedreckon.app.js new file mode 100644 index 000000000..449bf9c1b --- /dev/null +++ b/apps/dedreckon/dedreckon.app.js @@ -0,0 +1,442 @@ +/* Ded Reckon */ +/* eslint-disable no-unused-vars */ + +/* fmt library v0.1.3 */ +let fmt = { + icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3", + icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + + /* 0 .. DD.ddddd + 1 .. DD MM.mmm' + 2 .. DD MM'ss" + */ + geo_mode : 1, + + init: function() {}, + fmtDist: function(km) { + if (km >= 1.0) return km.toFixed(1) + this.icon_km; + return (km*1000).toFixed(0) + this.icon_m; + }, + fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); }, + fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; }, + draw_dot : 1, + add0: function(i) { + if (i > 9) { + return ""+i; + } else { + return "0"+i; + } + }, + fmtTOD: function(now) { + this.draw_dot = !this.draw_dot; + let dot = ":"; + if (!this.draw_dot) + dot = "."; + return now.getHours() + dot + this.add0(now.getMinutes()); + }, + fmtNow: function() { return this.fmtTOD(new Date()); }, + fmtTimeDiff: function(d) { + if (d < 180) + return ""+d.toFixed(0); + d = d/60; + return ""+d.toFixed(0)+"m"; + }, + fmtAngle: function(x) { + switch (this.geo_mode) { + case 0: + return "" + x; + case 1: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + return "" + d + " " + m.toFixed(3) + "'"; + } + case 2: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + let mf = Math.floor(m); + let s = m - mf; + s = s*60; + return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; + } + } + return "bad mode?"; + }, + fmtPos: function(pos) { + let x = pos.lat; + let c = "N"; + if (x<0) { + c = "S"; + x = -x; + } + let s = c+this.fmtAngle(x) + "\n"; + c = "E"; + if (x<0) { + c = "W"; + x = -x; + } + return s + c + this.fmtAngle(x); + }, + fmtFix: function(fix, t) { + if (fix && fix.fix && fix.lat) { + return this.fmtSpeed(fix.speed) + " " + + this.fmtAlt(fix.alt); + } else { + return "N/FIX " + this.fmtTimeDiff(t); + } + }, + fmtSpeed: function(kph) { + return kph.toFixed(1) + this.icon_kph; + }, +}; + +/* gps library v0.1.1 */ +let gps = { + emulator: -1, + init: function(x) { + this.emulator = (process.env.BOARD=="EMSCRIPTEN" + || process.env.BOARD=="EMSCRIPTEN2")?1:0; + }, + state: {}, + on_gps: function(f) { + let fix = this.getGPSFix(); + f(fix); + + /* + "lat": number, // Latitude in degrees + "lon": number, // Longitude in degrees + "alt": number, // altitude in M + "speed": number, // Speed in kph + "course": number, // Course in degrees + "time": Date, // Current Time (or undefined if not known) + "satellites": 7, // Number of satellites + "fix": 1 // NMEA Fix state - 0 is no fix + "hdop": number, // Horizontal Dilution of Precision + */ + this.state.timeout = setTimeout(this.on_gps, 1000, f); + }, + off_gps: function() { + clearTimeout(this.state.timeout); + }, + getGPSFix: function() { + if (!this.emulator) + return Bangle.getGPSFix(); + let fix = {}; + fix.fix = 1; + fix.lat = 50; + fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */ + fix.alt = 200; + fix.speed = 5; + fix.course = 30; + fix.time = Date(); + fix.satellites = 5; + fix.hdop = 12; + return fix; + }, + gps_start : -1, + start_gps: function() { + Bangle.setGPSPower(1, "libgps"); + this.gps_start = getTime(); + }, + stop_gps: function() { + Bangle.setGPSPower(0, "libgps"); + }, +}; + +/* ui library 0.1 */ +let ui = { + display: 0, + numScreens: 2, + drawMsg: function(msg) { + g.reset().setFont("Vector", 35) + .setColor(1,1,1) + .fillRect(0, this.wi, 176, 176) + .setColor(0,0,0) + .drawString(msg, 5, 30); + }, + drawBusy: function() { + this.drawMsg("\n.oO busy"); + }, + nextScreen: function() { + print("nextS"); + this.display = this.display + 1; + if (this.display == this.numScreens) + this.display = 0; + this.drawBusy(); + }, + prevScreen: function() { + print("prevS"); + this.display = this.display - 1; + if (this.display < 0) + this.display = this.numScreens - 1; + this.drawBusy(); +}, + onSwipe: function(dir) { + this.nextScreen(); +}, + h: 176, + w: 176, + wi: 32, + last_b: 0, + touchHandler: function(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || this.last_b != 0) { + this.last_b = d.b; + return; + } + + print("touch", x, y, this.h, this.w); + + /* + if ((xthis.h/2) && (ythis.w/2)) { + print("prev"); + this.prevScreen(); + } + if ((x>this.h/2) && (y>this.w/2)) { + print("next"); + this.nextScreen(); + } + }, + init: function() { + } +}; + +var last_steps = Bangle.getStepCount(), last_time = getTime(), speed = 0, step_phase = 0; + +var mpstep = 0.719 * 1.15; + +function updateSteps() { + if (step_phase ++ > 9) { + step_phase =0; + let steps = Bangle.getStepCount(); + let time = getTime(); + + speed = 3.6 * mpstep * ((steps-last_steps) / (time-last_time)); + last_steps = steps; + last_time = time; + } + return "" + fmt.fmtSpeed(speed) + " " + step_phase + "\n" + fmt.fmtDist(log_dist/1000) + " " + fmt.fmtDist(log_last/1000); +} + +/* compensated compass */ +var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; +const tiltfixread = require("magnav").tiltfixread; +var heading; + + +var cancel_gps = false; + +function drawStats() { + let fix = gps.getGPSFix(); + + let msg = fmt.fmtFix(fix, getTime() - gps.gps_start); + + msg += "\n" + fmt.fmtDist(gps_dist/1000) + " " + fmt.fmtDist(gps_last/1000) + "\n" + updateSteps(); + let c = Bangle.getCompass(); + if (c) msg += "\n" + c.heading.toFixed(0) + "/" + heading.toFixed(0) + "deg " + log.length + "\n"; + + g.reset().clear().setFont("Vector", 31) + .setColor(1,1,1) + .fillRect(0, 24, 176, 100) + .setColor(0,0,0) + .drawString(msg, 3, 25); +} + +function updateGps() { + if (cancel_gps) + return; + heading = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + if (ui.display == 0) { + setTimeout(updateGps, 1000); + drawLog(); + drawStats(); + } + if (ui.display == 1) { + setTimeout(updateGps, 1000); + drawLog(); + } +} + +function stopGps() { + cancel_gps=true; + gps.stop_gps(); +} + +var log = [], log_dist = 0, gps_dist = 0; +var log_last = 0, gps_last = 0; + +function logEntry() { + let e = {}; + e.time = getTime(); + e.fix = gps.getGPSFix(); + e.steps = Bangle.getStepCount(); + if (0) { + let c = Bangle.getCompass(); + if (c) + e.dir = c.heading; + else + e.dir = -1; + } else { + e.dir = heading; + } + return e; +} + +function onTurn() { + let e = logEntry(); + log.push(e); +} + +function radians(a) { return a*Math.PI/180; } +function degrees(a) { return a*180/Math.PI; } +// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km +// https://www.movable-type.co.uk/scripts/latlong.html +// (Equirectangular approximation) +function calcDistance(a,b) { + var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + return Math.sqrt(x*x + y*y) * 6371000; +} + +var dn, de; +function initConv(fix) { + let n = { lat: fix.lat+1, lon: fix.lon }; + let e = { lat: fix.lat, lon: fix.lon+1 }; + + dn = calcDistance(fix, n); + de = calcDistance(fix, e); + print("conversion is ", dn, 108000, de, 50000); +} +function toM(start, fix) { + return { x: (fix.lon - start.lon) * de, y: (fix.lat - start.lat) * dn }; +} +var mpp = 4; +function toPix(q) { + let p = { x: q.x, y: q.y }; + p.x /= mpp; /* 10 m / pix */ + p.y /= -mpp; + p.x += 85; + p.y += 85; + return p; +} + +function drawLog() { + let here = logEntry(); + if (!here.fix.lat) { + here.fix.lat = 50; + here.fix.lon = 14; + } + initConv(here.fix); + log.push(here); + let l = log; + log_dist = 0; + log_last = -1; + gps_last = -1; + + g.reset().clear(); + g.setColor(0, 0, 1); + let last = { x: 0, y: 0 }; + for (let i = l.length - 2; i >= 0; i--) { + let next = {}; + let m = (l[i+1].steps - l[i].steps) * mpstep; + let dir = radians(180 + l[i].dir); + next.x = last.x + m * Math.sin(dir); + next.y = last.y + m * Math.cos(dir); + print(dir, m, last, next); + let lp = toPix(last); + let np = toPix(next); + g.drawLine(lp.x, lp.y, np.x, np.y); + g.drawCircle(np.x, np.y, 3); + last = next; + if (log_last == -1) + log_last = m; + log_dist += m; + } + g.setColor(0, 1, 0); + last = { x: 0, y: 0 }; + gps_dist = 0; + for (let i = l.length - 2; i >= 0; i--) { + let fix = l[i].fix; + if (fix.fix && fix.lat) { + let next = toM(here.fix, fix); + let lp = toPix(last); + let np = toPix(next); + let d = Math.sqrt((next.x-last.x)*(next.x-last.x)+(next.y-last.y)*(next.y-last.y)); + if (gps_last == -1) + gps_last = d; + gps_dist += d; + g.drawLine(lp.x, lp.y, np.x, np.y); + g.drawCircle(np.x, np.y, 3); + last = next; + } + } + log.pop(); +} + +function testPaint() { + let pos = gps.getGPSFix(); + log = []; + let e = { fix: pos, steps: 100, dir: 0 }; + log.push(e); + e = { fix: pos, steps: 200, dir: 90 }; + log.push(e); + e = { fix: pos, steps: 300, dir: 0 }; + log.push(e); + print(log, log.length, log[0], log[1]); + drawLog(); +} + +function touchHandler(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || ui.last_b != 0) { + ui.last_b = d.b; + return; + } + + + if ((xui.h/2) && (y ui.onSwipe(s), + clock : 0 +}); + +if (0) + testPaint(); +if (1) { + g.reset(); + updateGps(); +} diff --git a/apps/dedreckon/metadata.json b/apps/dedreckon/metadata.json new file mode 100644 index 000000000..79bf8868e --- /dev/null +++ b/apps/dedreckon/metadata.json @@ -0,0 +1,13 @@ +{ "id": "dedreckon", + "name": "Ded Reckon", + "version": "0.01", + "description": "Dead Reckoning using compass and step counter", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors", + "storage": [ + {"name":"dedreckon.app.js","url":"dedreckon.app.js"}, + {"name":"dedreckon.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/dejivaisu/ChangeLog b/apps/dejivaisu/ChangeLog new file mode 100644 index 000000000..62e2d0c20 --- /dev/null +++ b/apps/dejivaisu/ChangeLog @@ -0,0 +1 @@ +0.1: New App! \ No newline at end of file diff --git a/apps/dejivaisu/app-icon.js b/apps/dejivaisu/app-icon.js new file mode 100644 index 000000000..aa3f5f2b1 --- /dev/null +++ b/apps/dejivaisu/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cBnnH///BIO6q2+++GoUwwmYmUkyVJAWspBhcSAgVKqOggEBA4VAwEAgnb9IRDqeQk3bvtAPAWbtv0gEP0QRCzmAFgQRDGoQEBugEB0nkUJkOxMk1IYCAAJWD7ASECIsDpILDyVgAgUB6MlhMkyEAjQxFpgEDnUoFoOQg2QgcAm3AhkAhMwCQdCNoU0kmbeYMYgKPBkwRDiQ1ByOhCIQABhuA4EELgsEwMJjmSnxrBuEGSQcgCQcNpHjxsl2XZkm44EAHAJDBtgRBtEBjlrsuS5dly4uBaoMEydtCwNog8Drcs21Zlmy8Eg3//0zdB2j0Bg3aOAQCCrgRDzFtl//pEAi1W7dt23btXIug1BmvAtf+y/9QQIRGnwyB0mSr+l/VdgPWCItIm/SQYMArt+y/r0GyCIvZg3brh6Brt1/QRIrIRBoARGywRF5IRJEYwRBI4IRCI4eSGo7FDNYdw2wRGgrFDhaPCgPSR40oYocNWYNLwCzG5TFDwEB+jOBYo/KYokAm//OIMCdItR3zFDNoMD9ADBrNlyXLsuywO1YoYACtACBhcs23LluUhuk6/8CAcAjomBgMk2Vbkmgts2ydgCIkNCIIIBI4MAjdN027CIQCCgeggFJ2AGBm3TpO17YGBg7+CgF0gUJPYNt03atOu7AMB/UpLgUOydp2matt07VtuyMBgPRkmuEgU6pk06VtmnbpM2BQMGxMkyoXBAAPpky0CyXtJoU+CIOS3YRCgbLBpMl7dsBAMB2i7CqdgggOBEYgMBRIP0CIVSpp0BNAIRC3dt2kbtsiCIVKcwoCFpAKJAVoA==")) \ No newline at end of file diff --git a/apps/dejivaisu/app.js b/apps/dejivaisu/app.js new file mode 100644 index 000000000..30193f805 --- /dev/null +++ b/apps/dejivaisu/app.js @@ -0,0 +1,192 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-undef */ + +const storage = require('Storage'); +require("Font8x16").add(Graphics); + +let appsettings = storage.readJSON('setting.json'); + +//MASCOT +if (appsettings.showMascot) { + var L1 = { + width : 16, height : 16, bpp : 1, + transparent : 0, + palette : new Uint16Array([65535,0]), + buffer : atob("AAAH4AgQcMiAaIDohgh4CEAQP4gpLylFGM40YlPVfn8=") + }; + var L2 = { + width : 16, height : 16, bpp : 1, + transparent : 0, + palette : new Uint16Array([65535,0]), + buffer : atob("B+AIEHDIgGiA6IYIeAhA0D8QGMgpGDnHDF0zxlKqfv4=") + }; + var R1 = { + width : 16, height : 16, bpp : 1, + transparent : 0, + palette : new Uint16Array([65535,0]), + buffer : atob("B+AIEBMOFgEXARBhEB4LAgj8ExgYlOOcujBjzFVKf34=") + }; + var R2 = { + width : 16, height : 16, bpp : 1, + transparent : 0, + palette : new Uint16Array([65535,0]), + buffer : atob("AAAH4AgQEw4WARcBEGEQHggCEfz0lKKUcxhGLKvK/n4=") + }; + + // Initial position and direction + var x = 40; + var y = 25; + var direction = 1; // 1 for right, -1 for left + var currentFrame = 0; // 0 for L1/R1, 1 for L2/R2 + var prevX = x; // Track the previous position of the sprite + + function drawSprite() { + g.clearRect(prevX, y, prevX + 32, y + 32); + if (direction === 1) { + g.drawImage(currentFrame === 0 ? R1 : R2, x, y, {scale:2}); + } else { + g.drawImage(currentFrame === 0 ? L1 : L2, x, y, {scale:2}); + } + prevX = x; + } + + function updatePosition() { + if (Math.random() < 0.3) { + direction = Math.random() < 0.5 ? -1 : 1; + } + + x += direction * 2; + + if (x > g.getWidth() - 70) { + x = g.getWidth() - 70; + direction = -1; + } else if (x < 0) { + x = 0; + direction = 1; + } + } + + function alternateFrame() { + currentFrame = 1 - currentFrame; + } +} + +//BARS + +if (appsettings.showDJSeconds) { + let barCount = 0; + let increasing = true; + + function drawBars() { + const barWidth = 5; + const barSpacing = 3; + const barHeight = 15; + const startX = (g.getWidth() - (5 * barWidth + 4 * barSpacing)) / 2 -60; + const startY = g.getHeight() / 2 + 30; + + for (let i = 0; i < barCount; i++) { + g.fillRect( + startX + i * (barWidth + barSpacing), + startY - barHeight / 2, + startX + i * (barWidth + barSpacing) + barWidth, + startY + barHeight / 2 + ); + } + } + + function updateBars() { + if (increasing) { + barCount++; + if (barCount >= 5) { + increasing = false; + } + } else { + barCount--; + if (barCount <= 0) { + increasing = true; + } + } + } +} + + +//ACTUAL WATCH + +{ +let drawTimeout; +let queueMillis = 1000; +let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + drawWatchface(); + }, queueMillis - (Date.now() % queueMillis)); +}; +let updateState = function() { + if (Bangle.isLCDOn()) { + if (Bangle.isLocked()){ + queueMillis = 60000; + } else { + queueMillis = 1000; + } + drawWatchface(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}; + +function drawWatchface() { + var date = new Date(); + var day = date.getDate(); + var month = date.getMonth() + 1; // Months are 0-indexed + var year = date.getFullYear(); + var seconds = date.getSeconds(); + + g.reset().clearRect(Bangle.appRect); + g.setFontAlign(0, 0); + g.setFontVector(60); + var timeString = require("locale").time(date, 1); + var AMPM = require("locale").meridian(new Date()).toUpperCase(); + var timeWidth = g.stringWidth(timeString)/2; + var jpclX = (g.getWidth() - timeWidth ); + var jpclY = g.getHeight() / 2; + g.drawString(timeString, jpclX, jpclY); + if (!Bangle.isLocked()) { + if (appsettings.showMascot) { + updatePosition(); + alternateFrame(); + drawSprite(); + } + if (appsettings.showDJSeconds) { + g.setFontVector(20); + g.drawString(seconds.toString().padStart(2, '0'), jpclX + timeWidth / 2+25, jpclY + 33); + updateBars(); + drawBars(); + } + g.drawString(AMPM, jpclX+60, jpclY-38); + } + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// Set dynamic state and perform initial drawing +updateState(); +// Register hooks for LCD on/off event and screen lock on/off event +Bangle.on('lcdPower', updateState); +Bangle.on('lock', updateState); +Bangle.setUI({ + mode: "clock", + remove: function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', updateState); + Bangle.removeListener('lock', updateState); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); +} \ No newline at end of file diff --git a/apps/dejivaisu/app.png b/apps/dejivaisu/app.png new file mode 100644 index 000000000..c9bc5cbc2 Binary files /dev/null and b/apps/dejivaisu/app.png differ diff --git a/apps/dejivaisu/metadata.json b/apps/dejivaisu/metadata.json new file mode 100644 index 000000000..4095b3335 --- /dev/null +++ b/apps/dejivaisu/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "dejivaisu", + "name": "Dejivaisu", + "version": "0.1", + "description": "A clock loosely inspired by a certain digital device. Includes an (optional) animated mascot and a seconds animation.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + { + "name": "dejivaisu.app.js", + "url": "app.js" + }, + { + "name": "dejivaisu.img", + "url": "app-icon.js", + "evaluate": true + }, + { "name":"dejivaisu.settings.js", + "url":"settings.js" + } + ], + "data": [{"name":"dejivaisu.json"}] +} diff --git a/apps/dejivaisu/screenshot.png b/apps/dejivaisu/screenshot.png new file mode 100644 index 000000000..7f465fa6e Binary files /dev/null and b/apps/dejivaisu/screenshot.png differ diff --git a/apps/dejivaisu/settings.js b/apps/dejivaisu/settings.js new file mode 100644 index 000000000..b808e47bd --- /dev/null +++ b/apps/dejivaisu/settings.js @@ -0,0 +1,28 @@ +(function back() { + const storage = require('Storage'); + // Load existing settings or initialize defaults + let settings = storage.readJSON('setting.json') || {}; + + function saveSettings() { + storage.write('setting.json', settings); + } + + E.showMenu({ + '': { 'title': 'Dejivaisu Settings' }, + 'Show Mascot': { + value: settings.showMascot, + onchange: v => { + settings.showMascot = v; + saveSettings(); + } + }, + 'Show Seconds': { + value: settings.showDJSeconds, + onchange: v => { + settings.showDJSeconds = v; + saveSettings(); + } + }, + '< Back': () => load() + }); +}) \ No newline at end of file diff --git a/apps/dejivaisu/settings.json b/apps/dejivaisu/settings.json new file mode 100644 index 000000000..09c433461 --- /dev/null +++ b/apps/dejivaisu/settings.json @@ -0,0 +1,4 @@ +{ + "showMascot": true, + "showDJSeconds": true +} diff --git a/apps/deko/Building_Typeface.ttf b/apps/deko/Building_Typeface.ttf new file mode 100644 index 000000000..d5a3933ab Binary files /dev/null and b/apps/deko/Building_Typeface.ttf differ diff --git a/apps/deko/ChangeLog b/apps/deko/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/deko/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/deko/README.md b/apps/deko/README.md new file mode 100644 index 000000000..91e83bd23 --- /dev/null +++ b/apps/deko/README.md @@ -0,0 +1,10 @@ +# Deko Clock + +A simple clock with an Art Deko font + +The font was obtained from https://dafonttop.com/building.font and is free for personal use + + +![](screenshot.png) + +Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/deko/app-icon.js b/apps/deko/app-icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/deko/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) diff --git a/apps/deko/app.js b/apps/deko/app.js new file mode 100644 index 000000000..8ae2c1d31 --- /dev/null +++ b/apps/deko/app.js @@ -0,0 +1,64 @@ +Graphics.prototype.setFontBuildingTypeface = function(scale) { + // Actual height 100 (102 - 3) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAAAAAAAAAAAAAAH/gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAD///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAD/////gAAAAAAAAAAAAAB//////gAAAAAAAAAAAAB///////gAAAAAAAAAAAB////////gAAAAAAAAAAA////////4AAAAAAAAAAA////////4AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAf///////8AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAP///////+AAAAAAAAAAAH////////AAAAAAAAAAAAH///////AAAAAAAAAAAAAH//////AAAAAAAAAAAAAAH/////gAAAAAAAAAAAAAAH////gAAAAAAAAAAAAAAAH///wAAAAAAAAAAAAAAAAH//wAAAAAAAAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA/4AAAAAAAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAH/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAAAAAAAAf4AAAAAAAAAAAAAAAAAAA/4AAAAAAAAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAH/4AAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/wAAf///////+AAAAAAB//wAB////////+AAAAAAH//wAD////////+AAAAAAH//wAH////////+AAAAAAP//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////+AAAAAAf//wAf////////+AAAAAAf//wAf////////+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf8AAAf8AAAAAAP+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAP/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAH/////8AAAA///+AAAAAAB/////8AAAA///+AAAAAAAf////8AAAA///+AAAAAAAAAAAAAAAAA///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//gAAAAAAAf//+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf//wAAAAAAA///+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAAAAAAAAP+AAAAAAf8AAAAEAAAAAAP+AAAAAAf8AAAB8AAAAAAP+AAAAAAf8AAAP8AAAAAAP+AAAAAAf8AAD/8AAAAAAP+AAAAAAf8AA//8AAAAAAP+AAAAAAf8AP//8AAAAAAP+AAAAAAf8B///8AAAAAAP+AAAAAAf8f///8AAAAAAP+AAAAAAf/////8AAAAAAP+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf///8P////////+AAAAAAf///gH////////+AAAAAAf//4AD////////+AAAAAAf/+AAB////////+AAAAAAf/gAAA////////+AAAAAAfwAAAAD///////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAB//gAAAAAAAAAAAAAAAAAH//gAAAAAAAAAAAAAAAAA///gAAAAAAAAAAAAAAAAH///gAAAAAAAAAAAAAAAAf///gAAAAAAAAAAAAAAAD////gAAAAAAAAAAAAAAAf////gAAAAAAAAAAAAAAB/////gAAAAAAAAAAAAAAP///z/gAAAAAAAAAAAAAB///+D/gAAAAAAAAAAAAAH///wD/gAAAAAAAAAAAAA///+AD/gAAAAAAAAAAAAH///wAD/gAAAAAAAAAAAAf//+AAD/gAAAAAAAAAAAD///wAAD/gAAAAAAAAAAAf///AAAD/gAAAAAAAAAAB///4AAAD/gAAAAAAAAAAP///AAAAD/gAAAAAAAAAB///4AAAAD/gAAAAAAAAAH///AAAAAD/gAAAAAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAAAAAAf/////8AAAA///wAAAAAAf/////8AAAA///4AAAAAAf/////8AAAA///8AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA///+AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf/////8AAAA////AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf8AAAf8AAAAAAH/AAAAAAf//wAf8AAAAAAH/AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAf////////+AAAAAAf//wAP////////+AAAAAAf//wAP////////8AAAAAAf//wAH////////8AAAAAAf//wAD////////wAAAAAAf//wAA////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////AAAAAAAD/////////////wAAAAAAP/////////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA/4AAAf8AAAAAAH/AAAAAA///wAf8AAAAAAH/AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAA///wAf/////////AAAAAAf//wAf/////////AAAAAAf//wAP////////+AAAAAAP//wAP////////+AAAAAAH//wAH////////8AAAAAAB//wAD////////4AAAAAAAf/wAB////////wAAAAAAAAAAAAf///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAAAAAAAAf//wAAAAAAAAAAeAAAAAAf//wAAAAAAAAAP+AAAAAAf//wAAAAAAAAD/+AAAAAAf//wAAAAAAAA//+AAAAAAf//wAAAAAAAf//+AAAAAAf//wAAAAAAH///+AAAAAAf//wAAAAAD////+AAAAAAf8AAAAAAA/////+AAAAAAf8AAAAAAP/////+AAAAAAf8AAAAAH//////8AAAAAAf8AAAAB///////AAAAAAAf8AAAAf//////wAAAAAAAf8AAAP//////4AAAAAAAAf8AAD//////+AAAAAAAAAf8AB///////gAAAAAAAAAf8Af//////4AAAAAAAAAAf8H//////+AAAAAAAAAAAf////////gAAAAAAAAAAAf///////wAAAAAAAAAAAAf//////8AAAAAAAAAAAAAf//////AAAAAAAAAAAAAAf/////wAAAAAAAAAAAAAAf////8AAAAAAAAAAAAAAAf////AAAAAAAAAAAAAAAAf///wAAAAAAAAAAAAAAAAf//4AAAAAAAAAAAAAAAAAf/+AAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///gf//////+AAAAAAAB////4////////wAAAAAAH////9////////4AAAAAAP/////////////8AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAf/////////////+AAAAAA///////////////AAAAAA///////////////AAAAAA//////4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA/4AAA/4AAAAAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////+AAAAAAP/////////////8AAAAAAH////9////////4AAAAAAB////4////////gAAAAAAAH///gP//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////+AAAAAAAAAAAAD////////gAD//AAAAAAAP////////wAD//wAAAAAAP////////4AD//4AAAAAAf////////8AD//8AAAAAAf////////8AD//+AAAAAA/////////+AD//+AAAAAA/////////+AD///AAAAAA/////////+AD///AAAAAA/4AAAAAAP+AD///AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA/4AAAAAAP+AAAH/AAAAAA///////////////AAAAAA///////////////AAAAAA///////////////AAAAAAf/////////////+AAAAAAf/////////////+AAAAAAP/////////////8AAAAAAP/////////////4AAAAAAD/////////////wAAAAAAA/////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAD/wAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='), + 46, + atob("FCYpGigoKigoJykoFA=="), + 126+(scale<<8)+(1<<16) + ); + return this; +}; + + + +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date).toUpperCase(); + // draw time + g.setFontAlign(0,0).setFont("BuildingTypeface"); + g.clearRect(0, 24, g.getWidth(), y+35); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 60; + g.setFontAlign(0,0).setFont("6x8",2); + g.clearRect(0,y-8,g.getWidth(),y+8); // clear the background + g.drawString(dateStr,x,y); + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first, queue update +draw(); +// 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; + } +}); +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/deko/app.png b/apps/deko/app.png new file mode 100644 index 000000000..6f11e7019 Binary files /dev/null and b/apps/deko/app.png differ diff --git a/apps/deko/metadata.json b/apps/deko/metadata.json new file mode 100644 index 000000000..9bdd15429 --- /dev/null +++ b/apps/deko/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "deko", + "name": "Deko Clock", + "version": "0.01", + "description": "Clock with Art Deko font", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"deko.app.js","url":"app.js"}, + {"name":"deko.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/deko/screenshot.png b/apps/deko/screenshot.png new file mode 100644 index 000000000..91ce2ea38 Binary files /dev/null and b/apps/deko/screenshot.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..7441d822b --- /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/demoapp/metadata.json b/apps/demoapp/metadata.json index df6554ef5..2fb30f718 100644 --- a/apps/demoapp/metadata.json +++ b/apps/demoapp/metadata.json @@ -12,6 +12,5 @@ "storage": [ {"name":"demoapp.app.js","url":"app.js"}, {"name":"demoapp.img","url":"app-icon.js","evaluate":true} - ], - "sortorder": -9 + ] } diff --git a/apps/denseclock/ChangeLog b/apps/denseclock/ChangeLog new file mode 100644 index 000000000..7ae520fa5 --- /dev/null +++ b/apps/denseclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Begin rewrite from old code. +0.02: Changed visuals: uA > mA, info order, battery state indication +0.03: Update app icon diff --git a/apps/denseclock/app-icon.js b/apps/denseclock/app-icon.js new file mode 100644 index 000000000..b8a0761f8 --- /dev/null +++ b/apps/denseclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcBkmSpIC/AS0nwEHCh5yBggROgP4jgROh1ICKWT4Hkz0AC4NPgFypPgAQIRDyBLBCIMAiVAgECpAGBCInwn4RBg4dBoH/yV4j+ACI0Dz05kARB8gRJgARFgYRBgEB5IRKBwICCI4/8CIdJ/kCGoP+CIMPUIIRBkgRIYop9DgJHCPowDBTwUAyAREiSkBCITCOAX4CuwE5kmT4D1BBYSMByShBhwRCgEkWYQRDUIIRCUIWfEALXBCIsDCIMf+QICvEECIILBBAV5CIUcBAYRFpEEBYIRKnARIgFyHwfk+PAGogREgQIBPQMk+EACI9J/hrCyUHCIMHCJEnwAIByEBCIJrFCI/wWYIROwP5CIShECI7FBgjFDPoTFBgTXGBYICCCI6PDAX4C/ARoA=")) diff --git a/apps/denseclock/app.js b/apps/denseclock/app.js new file mode 100644 index 000000000..62a40154e --- /dev/null +++ b/apps/denseclock/app.js @@ -0,0 +1,157 @@ +// FONTS + +/* + Share Tech Mono: https://fonts.google.com/specimen/Share+Tech+Mono + Converted with: https://www.espruino.com/Font+Converter +*/ + +Graphics.prototype.setFontShareTechMonoBig = function(scale) { + // Actual height 56 (55 - 0) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAHwAAAAAAAA/wAAAAAAAD/wAAAAAAAf/wAAAAAAB//gAAAAAAP/8AAAAAAA//wAAAAAAH/+AAAAAAA//4AAAAAAD//AAAAAAAf/8AAAAAAB//gAAAAAAP/8AAAAAAB//wAAAAAAH/+AAAAAAA//4AAAAAAD//AAAAAAAf/4AAAAAAB//gAAAAAAP/8AAAAAAA//wAAAAAAA/+AAAAAAAA/4AAAAAAAA/AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///4AAAAD/////AAAAP/////wAAAf/////4AAA//////8AAA//////8AAB/AAB/j+AAB+AAH/B+AAB8AAP8A+AAB8AA/4A+AAB8AB/gA+AAB8AH/AA+AAB8AP8AA+AAB8A/4AA+AAB8B/gAA+AAB+D/AAB+AAA//+AAP8AAA//////8AAAf/////4AAAP/////wAAAH/////gAAAB////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAeAAAfgAAAA+AAAfAAAAA+AAA/AAAAA+AAA+AAAAA+AAA+AAAAA+AAB+AAAAA+AAB8AAAAA+AAB//////+AAB//////+AAB//////+AAB//////+AAB//////+AAB//////+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAB8AAAAD+AAB8AAAAP+AAB8AAAAf+AAB8AAAB/+AAB8AAAD/+AAB8AAAH/+AAB8AAAf8+AAB8AAA/4+AAB8AAD/w+AAB8AAH/A+AAB+AAf+A+AAB+AA/4A+AAA////wA+AAA////gA+AAAf//+AA+AAAP//8AA+AAAH//wAA+AAAB/+AAA+AAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAA+AAB8AAAAA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AD8AA+AAB+AH+AB+AAA////gD+AAA//////8AAAf/////8AAAP//P//4AAAH/+H//wAAAA/4D//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAP/wAAAAAAD//wAAAAAB///wAAAAAf///wAAAAH////wAAAB///+HwAAAB///gHwAAAB//wAHwAAAB/4AAHwAAAB+AAAHwAAABAAAAHwAAAAAAP///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAf///+AAAAAAAHwAAAAAAAAHwAAAAAAAAHwAAAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB///wAA+AAB8AHwAA+AAB8AHwAA+AAB8AHwAA+AAB8AD4AA+AAB8AD4AA+AAB8AD4AB+AAB8AD+AD+AAB8AD///8AAB8AB///8AAB8AA///4AAB8AAf//wAAB4AAP//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAP/////gAAAf/////4AAAf/////4AAA//////8AAA/APgAD+AAB+APgAB+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8APgAA+AAB8AHwAA+AAB8AHwAB+AAB8AH+AP8AAB8AH///8AAB8AD///4AAAAAB///wAAAAAA///gAAAAAAP/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAAAAB8AAAAAOAAB8AAAAD+AAB8AAAAf+AAB8AAAD/+AAB8AAA//+AAB8AAH//+AAB8AB///gAAB8AP//8AAAB8D///AAAAB8f//4AAAAB////AAAAAB///wAAAAAB//+AAAAAAB//gAAAAAAB/8AAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//AAAAD/8H//wAAAP//P//4AAAf/////8AAA//////8AAA//////+AAB/AP+AB+AAB8AD8AA+AAB8AD8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AB8AA+AAB8AD8AA+AAB+AH+AA+AAA////AD+AAA//////8AAAf/////8AAAP//P//4AAAH/+H//wAAAA/4D//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAAAP//+AAAAAAf///AAAAAAf///AA+AAA////gA+AAB/AAfgA+AAB+AAPgA+AAB8AAPwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwA+AAB8AAHwB+AAB+AAHwB+AAA//////8AAA//////8AAAf/////4AAAP/////wAAAH/////gAAAB////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAB+AAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), + 46, + 32, + 60+(scale<<8)+(1<<16) + ); + return this; +}; + +Graphics.prototype.setFontShareTechMono = function(scale) { + // Actual height 38 (37 - 0) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAeAAAAAB4AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAfAAAAAP8AAAAD/gAAAB/4AAAAf+AAAAP/AAAAH/gAAAB/4AAAA/8AAAAP/AAAAH/gAAAD/wAAAA/8AAAAP+AAAAA/gAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//4AAA///8AAP///8AA////wAHwAfvgAeAH4eABwA/A4AHAHwDgAcB+AOAB4PgB4AHj8AHgAf///+AA////wAB///+AAD///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAOAA8AAA4ADwAADgAeAAAOAB4AAA4AH////gAf///+AB////4AH////gAAAAAOAAAAAA4AAAAADgAAAAAOAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AHAAAfgAcAAD+ABwAA/4AHAAH/gAcAA/OABwAP44AHgB+DgAfAfwOAA//8A4AD//gDgAH/4AOAAH+AA4AAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAOABwBwA4AHAHADgAcAcAOABwBwA4AHAHADgAeA+AOAB8H4B4AD////gAP///8AAf+f/gAAPgf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAP+AAAAP/4AAAf//gAAf//OAAB//A4AAH+ADgAAcAAOAAAAH//4AAA///gAAD//+AAAP//4AAAAHgAAAAAOAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/+ADgAf/4AOAB//gA4AH/+ADgAcA4AOABwDgA4AHAPADgAcA8AeABwD//4AHAH//AAcAP/4AAAAf/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAH///4AA////wAH////gAeB4AeABwHgA4AHAeADgAcB4AOABwHgA4AHAeAHgAcA//+ABwD//wAAAH/+AAAAP/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAABwAAAAAHAAAAgAcAAAeABwAAf4AHAAP/gAcAH/+ABwH/+AAHD//AAAf//AAAB//gAAAH/gAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAP+f/gAD////AAP///+AB//8B4AHgPgDgAcAcAOABwBwA4AHAHADgAcAcAOAB4D4B4AH////gAP///8AAf/f/wAAfw/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAH/+AAAA//8A4AH4HwDgAeAHgOABwAeA4AHAB4DgAcAHgOABwAeB4AHgB4HgAf///+AA////wAB///+AAB///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AHgAAHgAeAAAeAB4AAB4AHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='), + 46, + 22, + 40+(scale<<8)+(1<<16) + ); + return this; +}; + +Graphics.prototype.setFontShareTechMonoSmall = function(scale) { + // Actual height 23 (22 - 0) + this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zgP/zgP/zgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAPwAAPwAAAAAAPwAAPwAAPAAAAAAAAAAAAAAAAAAAAAAAAwYAP//gP//gP//gAwYAP//gP//gP//gAwYAAAAAAAAAAAAAAAAAB4AAD+BgH/Bg+HB8+DB8+DB8GD/gGB/AAAcAAAAAAAAAABAAH7AAP7AAMLAAMbAAP7AAHzfAAG/gAGxgAGxgAG/gAGfAAGAAAAAAAAAADz/AH//gP+DgMOBgMMBgMMBgMP/gAP/gAMAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAPwAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wP//8/AA+4AAGgAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAC4AAG/AB+P//8D//wAAAAAAAAAAAAAAAAAAAAAAAADAAADuAAB+AAP4AAfwAAP8AAB+AADsAADAAAAAAAAAAAAAAAAAAAADgAADgAADgAAf8AAf8AAf8AADgAADgAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAD+AAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAADgAADgAADgAADgAADgAADgAADgAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAADgAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAeAAD8AAfwAB+AAP4AA/AAH8AAfgAA8AAAwAAAAAAAAAAAAAAAD/+AH//APB/gMDxgMHBgMeBgP//gH//AD/+AAAAAAAAAAAAAAAAAGAAgGABgOABgOABgP//gP//gAABgAABgAABgAAAAAAAAAAAAAAAAAABgMAHgMAPgMA9gMB5gOHxgH/BgD+BgAABgAAAAAAAAAAAAAAAAAAAAMCBgMGBgMGBgMHBgP/DgH//gD5/AAAAAAAAAAAAAAAAAAAAAAD4AA/4AP/4APwYAMAYAAP/gAP/gAAcAAAYAAAAAAAAAAAAAAAAAAAAAP+BgP+BgMGBgMGBgMHDgMH/gMD/AAAAAAAAAAAAAAAAAAAAAD/+AH//AP//gMMBgMMBgMOBgMP/gMH/AAD+AAAAAAAAAAAAAAAAAMAAAMAAAMADgMAfgMH/AM/4AP/AAPwAAGAAAAAAAAAAAAAAAAAAAD5/AH//gP/DgMHBgMGBgMHBgP/jgH//gD5/AAAAAAAAAAAAAAAAAD+AAH/AgP/hgMBhgMBhgMBhgP//gH//AD/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcDgAcDgAcDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAcD+AcD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAHgAAPwAAMwAAc4AAY4AA4cAA4cABwMAAAAAAAAAAAAAAAAAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAMYAAAAAAAAAAAAAAAAABwMAA4cAA4cAAY4AAc4AAMwAAPwAAHgAAHgAAAAAAAAAAAAAAAAAAAAAMAAAMABgMDzgMHzgOeAAH8AAD4AAAAAAAAAAAAAAB/8AH//AP//gMHhgMf5gM/9gMwNgM/5gM/8gMAMAP/8AH/8AA/wAAAAAAADgAA/gAP/gD/4AP8YAPAYAP8YAD/4AAP/gAA/gAADgAAAAAAAAAAAAP//gP//gMGBgMGBgMGBgMGBgP/DgH//gD5/AAAAAAAAAAAAAAAAAB/8AH//AP//gOABgMABgMABgMABgMABgMABgAAAAAAAAAAAAAAAAP//gP//gMABgMABgMABgMABgOADgH//gH//AAAAAAAAAAAAAAAAAP//gP//gP//gMGBgMGBgMGBgMGBgMGBgMABgAAAAAAAAAAAAAAAAP//gP//gP//gMHAAMHAAMHAAMHAAMHAAMAAAAAAAAAAAAAAAAAAAD/+AH//AP//gMABgMDBgMDBgMD/gMD/gAD/gAAAAAAAAAAAAAAAAP//gP//gAHAAAHAAAHAAAHAAAHAAP//gP//gAAAAAAAAAAAAAAAAAAAAMABgMABgP//gP//gP//gMABgMABgAAAAAAAAAAAAAAAAAAAAAAAAAABgMABgMABgMABgMAHgP//AP/+AAAAAAAAAAAAAAAAAAAAAP//gP//gAPAAAfgAB/4ADw+APAfgOAHgIABgAAAAAAAAAAAAAAAAAAAAP//gP//gAABgAABgAABgAABgAABgAAAAAAAAAAAAAAAAP//gP//gPgAAP+AAB/gAAHwAB/gAP8AAPgAAP//gP//gAAAAAAAAAAAAP//gP//gPwAAP/AAA/8AAD/gAAPgP//gP//gAAAAAAAAAAAAA/8AH//AH//gOADgMABgMABgMABgOADgH//gH//AA/4AAAAAAAAAAAAAP//gP//gMDgAMDgAMDgAMDgAP/AAH/AAD+AAAAAAAAAAAAAAA/8AH//AH//gOADgMABgMABgMABgOADgH//gH//wA/4wAAAAAAAAH//gP//gP//gMDAAMDAAMDgAOD8AP//AH+PgB4DgAAAgAAAAAAAAAAAAD4AAH+BgP+BgOGBgMHBgMHBgMH/gMD/AAA+AAAAAAAAAAAAAMAAAMAAAMAAAMAAAP//gP//gP//gMAAAMAAAMAAAMAAAAAAAAAAAAAAAP//AP//gAADgAABgAABgAABgAADgP//gP//AAAAAAAAAAAAAMAAAP4AAP/wAA//AAB/gAAHgAD/gB/+AP/AAPwAAIAAAAAAAAAAAP/gAP//gAP/gAAfgAH+AAHwAAD/gAAfgAf/gP//gP8AAAAAAAAAAAAAgOADgPgPgH4+AB/4AAfwAB/4AH4/APgPgOADgAAAgAAAAAAAAMAAAPAAAPwAAD8AAA//gAP/gAf/gD8AAPwAAPAAAIAAAAAAAAAAAAAAAMADgMAPgMA/gMD5gMPhgM+BgP4BgPgBgOABgAAAAAAAAAAAAAAAAAAAAAAAA///+///+wAAGwAAGwAAGAAAAAAAAAAAAAAAAAAAAwAAA+AAAfgAAH8AAA/AAAP4AAB+AAAPwAAD8AAAeAAAGAAAAAAAAAAAAAAAAAAAAwAAGwAAGwAAG///+///+AAAAAAAAAAAAAAAAAAAAAAAAA4AAD4AAPwAAeAAAYAAAeAAAPwAAD4AAAYAAAAAAAAAAAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAGAAAAAAAAAAAAAAAAIAAAMAAAOAAAGAAACAAAAAAAAAAAAAAAAAAAAAAAAAOAAY/gAY/gAZxgAZxgAZxgAZxgAf/gAP/gAABgAAAgAAAAAAAAAAAAP//gP//gAcBgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAAAAAP/AAf/gAYBgAYBgAYBgAYBgAYBgAAAAAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAYDgP//gP//gAAAAAAAAAAAAAAAAAP+AAf/gAf/gAYxgAYxgAYxgAfxgAPxgAHwAAAAAAAAAAAAAAAAAAYBgAYBgD//gP//gP//gMYBgMYBgMYBgMQAAAAAAAAAAAAAAAAAAAPwGAf/GAYfGAYfGAYfGAYfGAY7mAf7+Afx8AAAAAAAAAAAAAAAAP//gP//gAcAAAYAAAYAAAYAAAcAAAf/gAP/gAAAAAAAAAAAAAAAAAYAAAYAAAYAAMf/gOf/gMf/gAABgAABgAABgAAAAAAAAAAAAAAAAAAAAAAAGAYAGAYAGAYAGMf/+Of/+Mf/8AAAAAAAAAAAAAAAAAAAAP//gP//gP//gADwAAH8AAefAAcHgAYDgAQAgAAAAAAAAAAAAEAAAMAAAMAAAMAAAP//AP//gAADgAABgAABgAABgAAAgAAAAAAAAAf/gAf/gAYAAAYAAAf/gAf/gAf/gAYAAAYAAAf/gAP/gAAAAAAAAAAAAAf/gAf/gAcAAAYAAAYAAAYAAAcAAAf/gAP/gAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAf/+Af/+AcBgAYBgAYBgAYBgAcDgAf/gAP/AAAAAAAAAAAAAAAAAAP/AAf/gAcDgAYBgAYBgAYBgAYDgAf/+Af/+AAAAAAAAAAAAAAAAAYBgAYBgAf/gAf/gAcBgAYBgAYBgAYAAAYAAAAAAAAAAAAAAAAAAAAAAAPhgAfxgAZxgAYxgAYxgAY/gAYfAAAAAAAAAAAAAAAAAAAAAAYAAAYAAAYAAD//AD//gAYBgAYBgAYBgAYBgAAAAAAAAAAAAAAAAAf+AAf/gAf/gAABgAABgAABgAf/gAf/gAf/gAAAAAAAAAAAAAQAAAeAAAf4AAH/AAAfgAADgAAfgAH/AAf4AAeAAAQAAAAAAAAAAAfAAAf/AAD/gAAPgAD/gAH4AAH/gAAPgAD/gAf+AAfAAAAAAAAAAAAAAAYBgAcHgAfPAAH+AAD4AAH+AAfPgAcDgAQBgAAAAAAAAAAAAAQAAAeAAAfwGAH+GAA/uAAD+AAf8AH/AAf4AAeAAAQAAAAAAAAAAAAAAAABgAYHgAYPgAY/gAZ5gAfxgAfBgAeBgAYAAAAAAAAAAAAAAAAAAAAAAADgAADgAf//8/+/+4AAGwAAGwAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//+///+f//+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAGwAAG4AAG/+/+f//8ADgAADgAAAAAAAAAAAAAAAAAAAAAADgAADAAADAAADAAADAAADgAABgAADgAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), + 32, + atob("DQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0H"), + 24+(scale<<8)+(1<<16) + ); + return this; +}; + +{ + + // VARS + + let FONT_NAME = "ShareTechMono"; + let BIG_FONT_HEIGHT = 60; + //let NORMAL_FONT_HEIGHT = 40; + let SMALL_FONT_HEIGHT = 24; + + let timeDrawTimeout; + let infoDrawTimeout; + let lockState = Bangle.isLocked(); + let pressure; + + + + // LISTENERS + + Bangle.on('lock', function(isLocked) { + lockState = isLocked; + timeDraw(); + infoDraw(); + }); + + + + // DRAW FUNCTIONS + + let timeDraw = function() { + g.reset(); + g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y + BIG_FONT_HEIGHT); + + var date = new Date(); + var timeArray = [date.getHours().toString().padStart(2, "0"), + date.getMinutes().toString().padStart(2, "0")]; + if (!lockState) timeArray.push(date.getSeconds().toString().padStart(2, "0")); + var timeString = timeArray.join(":"); + g.setFontAlign(0, 0).setColor(g.theme.fg).setFont(FONT_NAME + (lockState ? "Big" : "")); + g.drawString(timeString, Bangle.appRect.x2/2, Bangle.appRect.y + BIG_FONT_HEIGHT/2); + + if (timeDrawTimeout) clearTimeout(timeDrawTimeout); + timeDrawTimeout = setTimeout(function() { + timeDrawTimeout = undefined; + timeDraw(); + }, (lockState ? 10000 - (Date.now() % 10000) : 1000 - (Date.now() % 1000))); // if locked, every clock's 10s, otherwise every 1s + }; + + let infoDraw = function() { + g.reset(); + + var date = new Date(); + var dateString = [date.getFullYear().toString().padStart(4,"0"), + (date.getMonth()+1).toString().padStart(2,"0"), + date.getDate().toString().padStart(2,"0")].join("-"); + + var tzOffset = -(date.getTimezoneOffset())/60; + var tzOffsetString = (tzOffset >= 0 ? "+" + tzOffset : tzOffset); + + var batteryString = (Bangle.isCharging() ? "+" : "") + E.getBattery() + "%"; + + var pressureString = (pressure ? pressure + "hPa" : "(hPa)"); + + var powerString = (E.getPowerUsage().total / 1000) + "mA"; + + var stepsString = Bangle.getHealthStatus("day").steps + "ST"; + + var bluetoothStatus = NRF.getSecurityStatus(); + var bluetoothString = (bluetoothStatus.connected ? bluetoothStatus.connected_addr.split(" ")[0].substr(-5) : "N/C"); + + var infoMatrix = [ + [dateString + tzOffsetString ], + [batteryString, pressureString], + [powerString ], + [stepsString, bluetoothString ] + ]; + + g.clearRect(Bangle.appRect.x, Bangle.appRect.y + BIG_FONT_HEIGHT, Bangle.appRect.x2, Bangle.appRect.y2); + g.setFontAlign(0, -1).setColor(g.theme.fg2).setFont(FONT_NAME+"Small"); + + infoMatrix.forEach((lineArray, lineNumber) => { + g.drawString(lineArray.join(" "), Bangle.appRect.x2/2, Bangle.appRect.y + BIG_FONT_HEIGHT + SMALL_FONT_HEIGHT*lineNumber); + }); + + Bangle.getPressure().then(baroValue => { pressure=Math.round(baroValue.pressure); }); + + if (infoDrawTimeout) clearTimeout(infoDrawTimeout); + infoDrawTimeout = setTimeout(function() { + infoDrawTimeout = undefined; + infoDraw(); + }, (lockState ? 60000 : 10000)); // if locked, a minute from now, otherwise in 10s + }; + + + + // DRAW CALLS + + g.clear(); + + Bangle.setUI({ + mode: "clock", + remove: function() { + if (timeDrawTimeout) clearTimeout(timeDrawTimeout); + timeDrawTimeout = undefined; + if (infoDrawTimeout) clearTimeout(infoDrawTimeout); + infoDrawTimeout = undefined; + + delete Graphics.prototype.setFontShareTechMono; + delete Graphics.prototype.setFontShareTechMonoBig; + delete Graphics.prototype.setFontShareTechMonoSmall; + }}); + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + timeDraw(); + infoDraw(); +} diff --git a/apps/denseclock/app.png b/apps/denseclock/app.png new file mode 100644 index 000000000..4f27b473c Binary files /dev/null and b/apps/denseclock/app.png differ diff --git a/apps/denseclock/metadata.json b/apps/denseclock/metadata.json new file mode 100644 index 000000000..c361313ff --- /dev/null +++ b/apps/denseclock/metadata.json @@ -0,0 +1,18 @@ +{ "id": "denseclock", + "name": "Dense Clock", + "shortName":"Dense Clock", + "version":"0.03", + "description": "A clockface dense with text-only information. Switches between showing seconds and minutes when unlocked/locked, in the interest of saving power.", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"denseclock.app.js","url":"app.js"}, + {"name":"denseclock.img","url":"app-icon.js","evaluate":true} + ], + "screenshots": [ + {"url":"screenshot_locked.png"}, + {"url":"screenshot_unlocked.png"} + ] +} diff --git a/apps/denseclock/screenshot_locked.png b/apps/denseclock/screenshot_locked.png new file mode 100644 index 000000000..61a05ca44 Binary files /dev/null and b/apps/denseclock/screenshot_locked.png differ diff --git a/apps/denseclock/screenshot_unlocked.png b/apps/denseclock/screenshot_unlocked.png new file mode 100644 index 000000000..f1af09cba Binary files /dev/null and b/apps/denseclock/screenshot_unlocked.png differ 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/dinoClock/metadata.json b/apps/dinoClock/metadata.json index a61ce122b..1455e84a6 100644 --- a/apps/dinoClock/metadata.json +++ b/apps/dinoClock/metadata.json @@ -6,7 +6,7 @@ "icon": "app.png", "version": "0.01", "type": "clock", - "tags": "clock, weather, dino, trex, chrome", + "tags": "clock,weather,dino,trex,chrome", "supports": ["BANGLEJS2"], "allow_emulator": true, "readme": "README.md", 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 new file mode 100644 index 000000000..11be002af --- /dev/null +++ b/apps/distortclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New face! +0.02: Improved clock +0.03: Minor code improvements diff --git a/apps/distortclk/README.md b/apps/distortclk/README.md new file mode 100644 index 000000000..8c7c433c1 --- /dev/null +++ b/apps/distortclk/README.md @@ -0,0 +1,17 @@ +# Distort Watchface +Was playing around with custom fonts and made something with it +Made for Bangle.js 2 + +![screenshot (3)](https://user-images.githubusercontent.com/44651387/157507228-100452bf-94a6-476f-aec6-d13d5dad86d5.png) + +## Features + +Has a dark mode + +## Requests + +If you have any issues or would like to suggest a feature, click here to send a message -> [here](https://github.com/elykittytee/BangleApps/issues/new?title=Poketch%20Clock%20Bug). + +## Creator + +Eleanor Tayam diff --git a/apps/distortclk/app-icon.js b/apps/distortclk/app-icon.js new file mode 100644 index 000000000..56a3c6b6f --- /dev/null +++ b/apps/distortclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMB/4AFgYCB4H//kAAoMAn/w+IFBx8P8fjAoPH4/n4/gg/j8/Px4rB+Pz58ch/wnHzz0wv/+hl5zlhDoOGnOY44FB8cZyOP/1/+OJwcfAoP44OGn4FB/lh5giBAIMz7n/AoP/nf4Aocf/IFDz5YBAoWP+YFD54FFMgIFD84FD84FM/0AApKfDApiaCAAJBCApKyCWgRlBAAWfOIIACj/8Aoc//g/BJ4KTBn4FBBIUfAoIbCx4CBFoUHAQPgDIMhAoOEV4NwVgMOn/4/jdBn8fDILpBUIfwh5TBIAYABA=")) diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js new file mode 100644 index 000000000..715899fbb --- /dev/null +++ b/apps/distortclk/app.js @@ -0,0 +1,65 @@ +Graphics.prototype.setFontSixCaps = function(scale) { + // Actual height 60 (59 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0VniarM3u4AAAAAAAAAAAAAAAAAAAAAAAAAAABEZoiqzN3u////////8AAAAAAAAAAAAAAAAAAAJFZ4iqzN3u////////////////8AAAAAAAAAAARGZ4mrzd7v////////////////////////8AAAAAAAqszd7v////////////////////////////7u3MoAAAAAAA//////////////////////////7t3MqohmRCAAAAAAAAAA//////////////////7t3MqohmRAAAAAAAAAAAAAAAAAAA/////////+7dzKqYdlQwAAAAAAAAAAAAAAAAAAAAAAAAAA/+7dzKqIZkQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAa7//////////////////////////////////+oQAAAAAAC/////////////////////////////////////+wAAAAAAf//////////////////////////////////////3AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA//8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///AAAAAA///GREREREREREREREREREREREREREREREREbP//AAAAAA3///7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////////////////////////+wAAAAAAAa7//////////////////////////////////+oQAAAAAAAAWazMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACqoAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAA//3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA//////////////////////////////////////8AAAAAAA3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADu4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAes3d3d3d0AAAAAAAAAAAAAAAAAAAAAAAADVorAAAAAAAA8////////8AAAAAAAAAAAAAAAAAAAAlaKze///wAAAAAAHf////////8AAAAAAAAAAAAAAAFGis3v///////wAAAAAAn/////////8AAAAAAAAAAARom83v///////////wAAAAAA7//+3d3d3d0AAAAAAEV5rN7//////////////v/wAAAAAA//xTAAAAAAAAA1eKze//////////////7cqXVP/wAAAAAA//UAAAAAFGis3v/////////////+3Kl2QAAAAP/wAAAAAA//6XZoq83v/////////////+26hkEAAAAAAAAP/wAAAAAA3//////////////////tyoZSAAAAAAAAAAAAAP/wAAAAAAb//////////////tuoZAAAAAAAAAAAAAAAAAAP/wAAAAAACv/////////tuXUwAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAI3////9yoZAAAAAAAAAAAAAAAAAAAAAAAAAAP/wAAAAAAAAJ4qodRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqgAAAAAAAAWazMzMzMzMzAAAAAAAAAAAAAzMzMzMzMzMqVAAAAAAAAAa7//////////wAAAAAAAAAAAA///////////+oQAAAAAADP///////////wAAAAAAAAAAAA/////////////AAAAAAAf////////////wAAAAiIgAAAAA/////////////3AAAAAA3///7u7u7u7u7gAAAA//8AAAAA7u7u7u7u7u///9AAAAAA//11RERERERERAAAAA//8AAAAAREREREREREV9//AAAAAA//QAAAAAAAAAAAAAAB//8QAAAAAAAAAAAAAAAE//AAAAAA//11RERERERERERERa//+lREREREREREREREV9//AAAAAA3///7u7u7u7u7u7u7/////7u7u7u7u7u7u7u///9AAAAAAf//////////////////////////////////////3AAAAAAC/////////////////+q//////////////////+wAAAAAAAa7///////////////0i3////////////////+oQAAAAAAAAWazMzMzMzMzMzMyoIAKKzMzMzMzMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqoAAAAAAAAAAAAAAAAAAAAAAkRGZniIqqrMzd3u7v//////8AAAAAAAAAAAAAAAqqrMzd3u7v////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA//////////////////////////////8AAAAAAAAAAAAAAA///////////////+7t3czKqpiHZm//8AAAAAAAAAAAAAAA//7u3dzMuqqIhmZUQwAAAAAAAAAA//8AAAAAAAAAAAAAAAZmREAAAAAAAAAARERERERERERERE//9EREREREQAAAAAAAAAAAAAAAAAAAAA7u7u7u7u7u7u7u///u7u7u7u4AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAA////////////////////////8AAAAAAAAAAAAAAAAAAAAAzMzMzMzMzMzMzMzMzMzMzMzMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARERERERERERERERERERAAABEREREREREREREAAAAAAAAAA7u7u7u7u7u7u7u7u7u7gAADu7u7u7u7u7u7u2TAAAAAAAA///////////////////wAAD//////////////+YAAAAAAA///////////////////wAAD///////////////4wAAAAAA///////////////////wAAD///////////////+gAAAAAA//zMzMzMzMzMzMzM3//AAADMzMzMzMzMzMzM7//wAAAAAA//AAAAAAAAAAAAAG//cAAAAAAAAAAAAAAAAAKf/wAAAAAA//AAAAAAAAAAAAAN//UAAAAAAAAAAAAAAAAAB//wAAAAAA//AAAAAAAAAAAAAP//6qqqqqqqqqqqqqqqqqv//wAAAAAA//AAAAAAAAAAAAAP///////////////////////AAAAAAA//AAAAAAAAAAAAAN//////////////////////9AAAAAAA//AAAAAAAAAAAAAF7/////////////////////gAAAAAAA//AAAAAAAAAAAAAAWu//////////////////7GAAAAAAAAZmAAAAAAAAAAAAAAADZmZmZmZmZmZmZmZmZmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADREREREREREREREREREREREREREREREREMAAAAAAAAAADne7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZMAAAAAAABd////////////////////////////////////1QAAAAAALv/////////////////////////////////////iAAAAAAr//////////////////////////////////////6AAAAAA///szMzMzMzMzMzMzP//zMzMzMzMzMzMzMzM3///AAAAAA//ogAAAAAAAAAAAAC//5AAAAAAAAAAAAAAAACf//AAAAAA//cAAAAAAAAAAAAAD//zAAAAAAAAAAAAAAAABf//AAAAAA//+6qqqqqqqqAAAAD//8qqqqqqqqqqqqqqqqrv//AAAAAAz///////////AAAAD//////////////////////8AAAAAAT///////////AAAADv/////////////////////0AAAAAACP//////////AAAAB/////////////////////+AAAAAAAAGzv////////AAAAAGzv/////////////////sYAAAAAAAAABGZmZmZmZmAAAAAABGZmZmZmZmZmZmZmZmZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAACRGZ4iqrM3e4AAAAAAA//AAAAAAAAAAAAAAACRGZ4iqrM3e7v////////8AAAAAAA//AAAAACRGZ4iqrM3e7v//////////////////8AAAAAAA//iqrM3e7v////////////////////////////8AAAAAAA//////////////////////////////////7u3cwAAAAAAA///////////////////////+7t3MyqmIZmRCAAAAAAAAAA/////////////u3dzLqoiGZUQgAAAAAAAAAAAAAAAAAAAA//7t3cyqqIhmVEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZlRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAa7//////////+yWEAAAAVi97////////////+oQAAAAAAC///////////////2VAGrf////////////////+wAAAAAAf////////////////+vP///////////////////3AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAA///GRERERERmis3////////typhmREREREREbP//AAAAAA//8wAAAAAAAAAAJ8/////8hgAAAAAAAAAAAAA///AAAAAA///GRERERERWis3////////typhmREREREREbP//AAAAAA3///7u7u7u7/////////////////7u7u7u7u///9AAAAAAf////////////////+vP///////////////////3AAAAAAC///////////////2VAGrf////////////////+wAAAAAAAa7//////////+yWIAAAAVi97////////////+oQAAAAAAAAWazMzMzMqphjAAAAAAAAAARniqrMzMzMzMqVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1ZmZmZmZmZmZmZmZmQgAAAAAAZmZmZmZmYwAAAAAAAAAFvv////////////////7rUAAAAA/////////rUAAAAAAAB+////////////////////9gAAAA//////////9wAAAAAAP//////////////////////gAAAA///////////zAAAAAAv//////////////////////wAAAA///////////7AAAAAA///7qqqqqqqqqqqqqqqq3//wAAAAqqqqqqqqrP//AAAAAA//9wAAAAAAAAAAAAAAAAT//wAAAAAAAAAAAAAH//AAAAAA//9wAAAAAAAAAAAAAAAAn//AAAAAAAAAAAAAAZ//AAAAAA///8zMzMzMzMzMzMzMzM///MzMzMzMzMzMzMzf//AAAAAAv//////////////////////////////////////7AAAAAAP//////////////////////////////////////zAAAAAABu////////////////////////////////////5gAAAAAAAEre7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7aQAAAAAAAAAAUREREREREREREREREREREREREREREREREQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAP//8AAAAAAAAAAA///wAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMwAAAAAAAAAAAzMzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("CAwODQ4PDw8QDA8QCA=="), 69+(scale<<8)+(4<<16)); + return this; +}; + +const offset = 25; +const width = g.getWidth(); +const height = g.getHeight(); + +var drawTimeout; +var fgTime = 0xf800; +var bgTime = 0x3333ff; +var dayDate = 0x000; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function time() { + require("Font4x5").add(Graphics); + var d = new Date(); + var day = d.getDate(); + var time = require("locale").time(d,1); + //var date = require("locale").date(d); + var mo = require("date_utils").month(d.getMonth()+1,0); + + g.setFontAlign(0,0); + g.setFontSixCaps(2).setColor(fgTime).drawString(time, width/2, height/2+10); + + g.setFont("4x5",2); + g.setFontAlign(0,0); + g.setColor(dayDate).drawString(mo,width-55, height-16); + g.drawString(day,width-10, height-16); +} + +function draw() { + g.setColor(bgTime).fillRect(0,40,width,height-offset); + time(); + queueDraw(); +} + +//program start +g.clear(); // Clear the screen once, at startup + +if (g.theme.dark==true){ + dayDate = 0xffff; +} +else { + dayDate=0x000; +} + +draw(); // draw immediately at first + + + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/distortclk/app.png b/apps/distortclk/app.png new file mode 100644 index 000000000..b82a0913e Binary files /dev/null and b/apps/distortclk/app.png differ diff --git a/apps/distortclk/metadata.json b/apps/distortclk/metadata.json new file mode 100644 index 000000000..cd1bf9d4d --- /dev/null +++ b/apps/distortclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "distortclk", + "name": "Distort Clock", + "shortName":"Distort Clock", + "version": "0.03", + "description": "A clockface", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"distortclk.app.js","url":"app.js"}, + {"name":"distortclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/distortclk/screenshot.png b/apps/distortclk/screenshot.png new file mode 100644 index 000000000..3207b4e1e Binary files /dev/null and b/apps/distortclk/screenshot.png differ 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/dotmatrixclock/ChangeLog b/apps/dotmatrixclock/ChangeLog index 7ab9e14a9..12edf33a3 100644 --- a/apps/dotmatrixclock/ChangeLog +++ b/apps/dotmatrixclock/ChangeLog @@ -1 +1,2 @@ 0.01: Create dotmatrix clock app +0.02: Added adjustment for Bangle.js magnetometer heading fix diff --git a/apps/dotmatrixclock/app.js b/apps/dotmatrixclock/app.js index ba34d4885..493a3c43f 100644 --- a/apps/dotmatrixclock/app.js +++ b/apps/dotmatrixclock/app.js @@ -186,7 +186,7 @@ function drawCompass(lastHeading) { 'NW' ]; const cps = Bangle.getCompass(); - let angle = cps.heading; + let angle = 360-cps.heading; let heading = angle? directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]: "-- "; @@ -351,4 +351,4 @@ Bangle.on('faceUp', (up) => { setSensors(1); resetDisplayTimeout(); } -}); \ No newline at end of file +}); diff --git a/apps/dotmatrixclock/metadata.json b/apps/dotmatrixclock/metadata.json index 3425dc1b2..fdfb5271f 100644 --- a/apps/dotmatrixclock/metadata.json +++ b/apps/dotmatrixclock/metadata.json @@ -1,7 +1,7 @@ { "id": "dotmatrixclock", "name": "Dotmatrix Clock", - "version": "0.01", + "version": "0.02", "description": "A clear white-on-blue dotmatrix simulated clock", "icon": "dotmatrixclock.png", "type": "clock", diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog index 77d82eff9..0af4145d7 100644 --- a/apps/doztime/ChangeLog +++ b/apps/doztime/ChangeLog @@ -5,3 +5,6 @@ 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 +0.08: Bangle1: fix scoping of variables `time` and `wait` diff --git a/apps/doztime/app-bangle1.js b/apps/doztime/app-bangle1.js index 38c5acbac..b9681ad91 100644 --- a/apps/doztime/app-bangle1.js +++ b/apps/doztime/app-bangle1.js @@ -28,6 +28,7 @@ let addTimeDigit = false; let dateFormat = false; let lastX = 999999999; let res = {}; +let calenDef; //var last_time_log = 0; var drawtime_timeout; @@ -60,7 +61,7 @@ g.flip = function() setWatch(function(){ modeTime(); }, BTN1, {repeat:true} ); setWatch(function(){ Bangle.showLauncher(); }, BTN2, { repeat: false, edge: "falling" }); -setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); +//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); // TODO: `modeWeather` is not yet implemented. setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); @@ -122,7 +123,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(); @@ -138,7 +139,7 @@ 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(); @@ -159,20 +160,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); + 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 @@ -210,8 +213,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 +226,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..f0bb84a8b 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.08", "description": "A dozenal Holocene calendar and dozenal diurnal digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index 48a1ffb03..77cc63c98 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -3,3 +3,9 @@ 0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights. 0.04: Now displays the opened text string at launch. 0.05: Now scrolls text when string gets longer than screen width. +0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present. +0.07: Settings for display colors +0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting + edge 2v18 ones), allowing compatability with the Back Swipe app. +0.09: Fix colors settings, where color was stored as string instead of the expected int. +0.10: Fix touch region for letters diff --git a/apps/dragboard/README.md b/apps/dragboard/README.md index 8960e5749..415be5449 100644 --- a/apps/dragboard/README.md +++ b/apps/dragboard/README.md @@ -12,5 +12,8 @@ Known bugs: - Initially developed for use with dark theme set on Bangle.js 2 - that is still the preferred way to view it although it now works with other themes. - When repeatedly doing 'del' on an empty text-string, the letter case is changed back and forth between upper and lower case. -To do: -- Possibly provide a dragboard.settings.js file +Settings: +- CAPS LOCK: all characters are displayed and typed in uppercase +- ABC Color: color of the characters row +- Num Color: color of the digits and symbols row +- Highlight Color: color of the currently highlighted character diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index b9b19f982..2e40f3a77 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -1,51 +1,53 @@ -//Keep banglejs screen on for 100 sec at 0.55 power level for development purposes -//Bangle.setLCDTimeout(30); -//Bangle.setLCDPower(1); - exports.input = function(options) { options = options||{}; var text = options.text; if ("string"!=typeof text) text=""; - + let settings = require('Storage').readJSON('dragboard.json',1)||{} + + var R = Bangle.appRect; + const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`); var BGCOLOR = g.theme.bg; - var HLCOLOR = g.theme.fg; - var ABCCOLOR = g.toColor(1,0,0);//'#FF0000'; - var NUMCOLOR = g.toColor(0,1,0);//'#00FF00'; + var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg; + var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000'; + var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00'; var BIGFONT = '6x8:3'; var BIGFONTWIDTH = parseInt(BIGFONT.charAt(0)*parseInt(BIGFONT.charAt(-1))); var SMALLFONT = '6x8:1'; var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1))); var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase(); - var ABCPADDING = (g.getWidth()-6*ABC.length)/2; + var ABCPADDING = ((R.x+R.w)-6*ABC.length)/2; var NUM = ' 1234567890!?,.- '; var NUMHIDDEN = ' 1234567890!?,.- '; - var NUMPADDING = (g.getWidth()-6*NUM.length)/2; + var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2; var rectHeight = 40; - var delSpaceLast; function drawAbcRow() { g.clear(); + try { // Draw widgets if they are present in the current app. + if (WIDGETS) Bangle.drawWidgets(); + } catch (_) {} g.setFont(SMALLFONT); g.setColor(ABCCOLOR); - g.drawString(ABC, ABCPADDING, g.getHeight()/2); - g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight()); + g.setFontAlign(-1, -1, 0); + g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); + g.fillRect(0, (R.y+R.h)-26, (R.x+R.w), (R.y+R.h)); } function drawNumRow() { g.setFont(SMALLFONT); g.setColor(NUMCOLOR); - g.drawString(NUM, NUMPADDING, g.getHeight()/4); + g.setFontAlign(-1, -1, 0); + g.drawString(NUM, NUMPADDING, (R.y+R.h)/4); - g.fillRect(NUMPADDING, g.getHeight()-rectHeight*4/3, g.getWidth()-NUMPADDING, g.getHeight()-rectHeight*2/3); + g.fillRect(NUMPADDING, (R.y+R.h)-rectHeight*4/3, (R.x+R.w)-NUMPADDING, (R.y+R.h)-rectHeight*2/3); } function updateTopString() { - "ram" g.setColor(BGCOLOR); g.fillRect(0,4+20,176,13+20); g.setColor(0.2,0,0); @@ -54,13 +56,10 @@ exports.input = function(options) { g.setColor(0.7,0,0); g.fillRect(rectLen+5,4+20,rectLen+10,13+20); g.setColor(1,1,1); + g.setFontAlign(-1, -1, 0); g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20); } - drawAbcRow(); - drawNumRow(); - updateTopString(); - var abcHL; var abcHLPrev = -10; var numHL; @@ -68,194 +67,191 @@ exports.input = function(options) { var type = ''; var typePrev = ''; var largeCharOffset = 6; - + function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) { - "ram" + "ram"; // Small character in list g.setColor(rowColor); g.setFont(SMALLFONT); - g.drawString(char, typePadding + HLPrev*6, g.getHeight()/heightDivisor); + g.setFontAlign(-1, -1, 0); + g.drawString(char, typePadding + HLPrev*6, (R.y+R.h)/heightDivisor); // Large character g.setColor(BGCOLOR); - g.fillRect(0,g.getHeight()/3,176,g.getHeight()/3+24); - //g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, g.getHeight()/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle. + g.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24); + //g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, (R.y+R.h)/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle. // mark in the list } function showChars(char, HL, typePadding, heightDivisor) { - "ram" + "ram"; // mark in the list g.setColor(HLCOLOR); g.setFont(SMALLFONT); - if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, g.getHeight()/heightDivisor); + g.setFontAlign(-1, -1, 0); + if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, (R.y+R.h)/heightDivisor); // show new large character g.setFont(BIGFONT); - g.drawString(char, typePadding + HL*6 -largeCharOffset, g.getHeight()/3); + g.drawString(char, typePadding + HL*6 -largeCharOffset, (R.y+R.h)/3); g.setFont(SMALLFONT); } - - function changeCase(abcHL) { - g.setColor(BGCOLOR); - g.drawString(ABC, ABCPADDING, g.getHeight()/2); - if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase(); - else ABC = ABC.toUpperCase(); - g.setColor(ABCCOLOR); - g.drawString(ABC, ABCPADDING, g.getHeight()/2); + + function initDraw() { + //var R = Bangle.appRect; // To make sure it's properly updated. Not sure if this is needed. + drawAbcRow(); + drawNumRow(); + updateTopString(); } - return new Promise((resolve,reject) => { - // Interpret touch input - Bangle.setUI({ - mode: 'custom', - back: ()=>{ - Bangle.setUI(); - g.clearRect(Bangle.appRect); - resolve(text); - }, - drag: function(event) { + initDraw(); + //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise. - // ABCDEFGHIJKLMNOPQRSTUVWXYZ - // Choose character by draging along red rectangle at bottom of screen - if (event.y >= ( g.getHeight() - 12 )) { - // Translate x-position to character - if (event.x < ABCPADDING) { abcHL = 0; } - else if (event.x >= 176-ABCPADDING) { abcHL = 25; } - else { abcHL = Math.floor((event.x-ABCPADDING)/6); } + let dragHandlerDB = function(event) { + "ram"; + // ABCDEFGHIJKLMNOPQRSTUVWXYZ + // Choose character by draging along red rectangle at bottom of screen + if (event.y >= ( (R.y+R.h) - 26 )) { + // Translate x-position to character + if (event.x < ABCPADDING) { abcHL = 0; } + else if (event.x >= 176-ABCPADDING) { abcHL = 25; } + else { abcHL = Math.floor((event.x-ABCPADDING)/6); } - // Datastream for development purposes - //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev)); + // Datastream for development purposes + //print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev)); - // Unmark previous character and mark the current one... - // Handling switching between letters and numbers/punctuation - if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + // Unmark previous character and mark the current one... + // Handling switching between letters and numbers/punctuation + if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - if (abcHL != abcHLPrev) { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2); - } - // Print string at top of screen - if (event.b == 0) { - text = text + ABC.charAt(abcHL); + if (abcHL != abcHLPrev) { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2); + } + // Print string at top of screen + if (event.b == 0) { + text = text + ABC.charAt(abcHL); + updateTopString(); + + // Autoswitching letter case + if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL); + } + // Update previous character to current one + abcHLPrev = abcHL; + typePrev = 'abc'; + } + + // 12345678901234567890 + // Choose number or puctuation by draging on green rectangle + else if ((event.y < ( (R.y+R.h) - 26 )) && (event.y > ( (R.y+R.h) - 52 ))) { + // Translate x-position to character + if (event.x < NUMPADDING) { numHL = 0; } + else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } + else { numHL = Math.floor((event.x-NUMPADDING)/6); } + + // Datastream for development purposes + //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev)); + + // Unmark previous character and mark the current one... + // Handling switching between letters and numbers/punctuation + if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + + if (numHL != numHLPrev) { + resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4); + } + // Print string at top of screen + if (event.b == 0) { + g.setColor(HLCOLOR); + // Backspace if releasing before list of numbers/punctuation + if (event.x < NUMPADDING) { + // show delete sign + showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); + delSpaceLast = 1; + text = text.slice(0, -1); + updateTopString(); + //print(text); + } + // Append space if releasing after list of numbers/punctuation + else if (event.x > (R.x+R.w)-NUMPADDING) { + //show space sign + showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); + delSpaceLast = 1; + text = text + ' '; + updateTopString(); + //print(text); + } + // Append selected number/punctuation + else { + text = text + NUMHIDDEN.charAt(numHL); updateTopString(); // Autoswitching letter case - if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL); + if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase(); } - // Update previous character to current one - abcHLPrev = abcHL; - typePrev = 'abc'; } - - - - - - - - + // Update previous character to current one + numHLPrev = numHL; + typePrev = 'num'; + } - // 12345678901234567890 - // Choose number or puctuation by draging on green rectangle - else if ((event.y < ( g.getHeight() - 12 )) && (event.y > ( g.getHeight() - 52 ))) { - // Translate x-position to character - if (event.x < NUMPADDING) { numHL = 0; } - else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; } - else { numHL = Math.floor((event.x-NUMPADDING)/6); } - - // Datastream for development purposes - //print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev)); - - // Unmark previous character and mark the current one... - // Handling switching between letters and numbers/punctuation - if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - - if (numHL != numHLPrev) { + // Make a space or backspace by swiping right or left on screen above green rectangle + else if (event.y > 20+4) { + if (event.b == 0) { + g.setColor(HLCOLOR); + if (event.x < (R.x+R.w)/2) { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4); - } - // Print string at top of screen - if (event.b == 0) { - g.setColor(HLCOLOR); - // Backspace if releasing before list of numbers/punctuation - if (event.x < NUMPADDING) { - // show delete sign - showChars('del', 0, g.getWidth()/2 +6 -27 , 4); - delSpaceLast = 1; - text = text.slice(0, -1); - updateTopString(); - //print(text); - } - // Append space if releasing after list of numbers/punctuation - else if (event.x > g.getWidth()-NUMPADDING) { - //show space sign - showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4); - delSpaceLast = 1; - text = text + ' '; - updateTopString(); - //print(text); - } - // Append selected number/punctuation - else { - text = text + NUMHIDDEN.charAt(numHL); - updateTopString(); - // Autoswitching letter case - if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase(); - } + // show delete sign + showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4); + delSpaceLast = 1; + + // Backspace and draw string upper right corner + text = text.slice(0, -1); + updateTopString(); + if (text.length==0) changeCase(abcHL); + //print(text, 'undid'); } - // Update previous character to current one - numHLPrev = numHL; - typePrev = 'num'; - } - - - - - - - - - // Make a space or backspace by swiping right or left on screen above green rectangle - else if (event.y > 20+4) { - if (event.b == 0) { - g.setColor(HLCOLOR); - if (event.x < g.getWidth()/2) { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); + else { + resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); + resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - // show delete sign - showChars('del', 0, g.getWidth()/2 +6 -27 , 4); - delSpaceLast = 1; + //show space sign + showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4); + delSpaceLast = 1; - // Backspace and draw string upper right corner - text = text.slice(0, -1); - updateTopString(); - if (text.length==0) changeCase(abcHL); - //print(text, 'undid'); - } - else { - resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR); - resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR); - - //show space sign - showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4); - delSpaceLast = 1; - - // Append space and draw string upper right corner - text = text + NUMHIDDEN.charAt(0); - updateTopString(); - //print(text, 'made space'); - } + // Append space and draw string upper right corner + text = text + NUMHIDDEN.charAt(0); + updateTopString(); + //print(text, 'made space'); } } } - }); -}); -/* return new Promise((resolve,reject) => { - Bangle.setUI({mode:"custom", back:()=>{ - Bangle.setUI(); - g.clearRect(Bangle.appRect); - Bangle.setUI(); - resolve(text); - }}); - }); */ + }; + let catchSwipe = ()=>{ + E.stopEventPropagation&&E.stopEventPropagation(); + }; + + function changeCase(abcHL) { + if (settings.uppercase) return; + g.setColor(BGCOLOR); + g.setFontAlign(-1, -1, 0); + g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); + if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase(); + else ABC = ABC.toUpperCase(); + g.setColor(ABCCOLOR); + g.drawString(ABC, ABCPADDING, (R.y+R.h)/2); + } + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(text); + }, + drag: dragHandlerDB, + }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + }); }; diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index f9c73ddde..c4596d7bd 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -1,14 +1,18 @@ { "id": "dragboard", "name": "Dragboard", - "version":"0.05", + "version":"0.10", "description": "A library for text input via swiping keyboard", "icon": "app.png", "type":"textinput", "tags": "keyboard", "supports" : ["BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ - {"name":"textinput","url":"lib.js"} + {"name":"textinput","url":"lib.js"}, + {"name":"dragboard.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"dragboard.json"} ] } diff --git a/apps/dragboard/settings.js b/apps/dragboard/settings.js new file mode 100644 index 000000000..2aac13b28 --- /dev/null +++ b/apps/dragboard/settings.js @@ -0,0 +1,48 @@ +(function(back) { + let settings = require('Storage').readJSON('dragboard.json',1)||{}; + const colors = { + 4095: /*LANG*/"White", + 4080: /*LANG*/"Yellow", + 3840: /*LANG*/"Red", + 3855: /*LANG*/"Magenta", + 255: /*LANG*/"Cyan", + 240: /*LANG*/"Green", + 15: /*LANG*/"Blue", + 0: /*LANG*/"Black", + '-1': /*LANG*/"Default" + }; + + const save = () => require('Storage').write('dragboard.json', settings); + function colorMenu(key) { + let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)}; + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings[key] == color, + onchange: () => { + if (color >= 0) { + settings[key] = parseInt(color); + } else { + delete settings[key]; + } + save(); + setTimeout(E.showMenu, 10, appMenu); + } + }; + }); + return menu; + } + + const appMenu = { + '': {title: 'Dragboard'}, '< Back': back, + /*LANG*/'CAPS LOCK': { + value: !!settings.uppercase, + onchange: v => {settings.uppercase = v; save();} + }, + /*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")), + /*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")), + /*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight")) + }; + + E.showMenu(appMenu); +}) diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog new file mode 100644 index 000000000..3f36dc4a6 --- /dev/null +++ b/apps/draguboard/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App based on dragboard, but with a U shaped drag area +0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting + edge 2v18 ones), allowing compatability with the Back Swipe app. +0.03: Fix "Uncaught Error: Unhandled promise rejection: ReferenceError: "dragHandlerDB" is not defined" diff --git a/apps/draguboard/README.md b/apps/draguboard/README.md new file mode 100644 index 000000000..2386c7658 --- /dev/null +++ b/apps/draguboard/README.md @@ -0,0 +1,8 @@ +Swipe along the drag bars and release to select a letter, number or punctuation. + +Tap on left for backspace or right for space. + +Settings: +- ABC Color: color of the characters row +- Num Color: color of the digits and symbols row +- Highlight Color: color of the currently shown character diff --git a/apps/draguboard/app.png b/apps/draguboard/app.png new file mode 100644 index 000000000..ae7262b47 Binary files /dev/null and b/apps/draguboard/app.png differ diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js new file mode 100644 index 000000000..57093de3f --- /dev/null +++ b/apps/draguboard/lib.js @@ -0,0 +1,164 @@ +exports.input = function(options) { + options = options||{}; + var text = options.text; + if ("string"!=typeof text) text=""; + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + + var R; + const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`); + var BGCOLOR = g.theme.bg; + var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg; + var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000'; + var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00'; + var BIGFONT = '6x8:3'; + var SMALLFONT = '6x8:1'; + + var LEFT = "IJKLMNOPQ"; + var MIDDLE = "ABCDEFGH"; + var RIGHT = "RSTUVWXYZ"; + + var NUM = ' 1234567890!?,.-@'; + var rectHeight = 40; + var vLength = LEFT.length; + var MIDPADDING; + var NUMPADDING; + var showCharY; + var middleWidth; + var middleStart; + var topStart; + + function drawAbcRow() { + g.clear(); + try { // Draw widgets if they are present in the current app. + if (WIDGETS) Bangle.drawWidgets(); + } catch (_) {} + g.setColor(ABCCOLOR); + g.setFont('6x8:2x1'); + g.setFontAlign(-1, -1, 0); + g.drawString(RIGHT.split("").join("\n\n"), R.x2-28, topStart); + g.drawString(LEFT.split("").join("\n\n"), R.x+22, topStart); + g.setFont('6x8:1x2'); + var spaced = MIDDLE.split("").join(" "); + middleWidth = g.stringWidth(spaced); + middleStart = (R.x2-middleWidth)/2; + g.drawString(spaced, (R.x2-middleWidth)/2, (R.y2)/2); + g.fillRect(MIDPADDING, (R.y2)-26, (R.x2-MIDPADDING), (R.y2)); + // Draw left and right drag rectangles + g.fillRect(R.x, R.y, 12, R.y2); + g.fillRect(R.x2, R.y, R.x2-12, R.y2); + } + + function drawNumRow() { + g.setFont('6x8:1x2'); + g.setColor(NUMCOLOR); + NUMPADDING = (R.x2-g.stringWidth(NUM))/2; + g.setFontAlign(-1, -1, 0); + g.drawString(NUM, NUMPADDING, (R.y2)/4); + g.drawString("<-", NUMPADDING+10, showCharY+5); + g.drawString("->", R.x2-(NUMPADDING+20), showCharY+5); + + g.fillRect(NUMPADDING, (R.y2)-rectHeight*4/3, (R.x2)-NUMPADDING, (R.y2)-rectHeight*2/3); + } + + function updateTopString() { + g.setFont(SMALLFONT); + g.setColor(BGCOLOR); + g.fillRect(R.x,R.y,R.x2,R.y+9); + var rectLen = text.length<27? text.length*6:27*6; + g.setColor(0.7,0,0); + //draw cursor at end of text + g.fillRect(R.x+rectLen+5,R.y,R.x+rectLen+10,R.y+9); + g.setColor(HLCOLOR); + g.setFontAlign(-1, -1, 0); + g.drawString(text.length<=27? text : '<- '+text.substr(-24,24), R.x+5, R.y+1); + } + + function showChars(chars) { + "ram"; + + // clear large character + g.setColor(BGCOLOR); + g.fillRect(R.x+65,showCharY,R.x2-65,showCharY+28); + + // show new large character + g.setColor(HLCOLOR); + g.setFont(BIGFONT); + g.setFontAlign(-1, -1, 0); + g.drawString(chars, (R.x2 - g.stringWidth(chars))/2, showCharY+4); + } + + var charPos; + var char; + var prevChar; + + function moveCharPos(list, select, posPixels) { + charPos = Math.min(list.length-1, Math.max(0, Math.floor(posPixels))); + char = list.charAt(charPos); + + if (char != prevChar) showChars(char); + prevChar = char; + + if (select) { + text += char; + updateTopString(); + } + } + + let dragHandlerUB = function(event) { + "ram"; + + // drag on middle bottom rectangle + if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) { + moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length)); + } + // drag on left or right rectangle + else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) { + moveCharPos(event.x ( (R.y2) - 52 ))) { + moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6); + } + // Make a space or backspace by tapping right or left on screen above green rectangle + else if (event.y > R.y && event.b == 0) { + if (event.x < (R.x2)/2) { + showChars('<-'); + text = text.slice(0, -1); + } else { + //show space sign + showChars('->'); + text += ' '; + } + prevChar = null; + updateTopString(); + } + }; + + 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 lister if it was added with `Bangle.prependListener()` (fw2v19 and up). + g.clearRect(Bangle.appRect); + resolve(text); + }, + drag: dragHandlerUB + }); + Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares. + + R = Bangle.appRect; + MIDPADDING = R.x + 35; + showCharY = (R.y2)/3; + topStart = R.y+12; + + drawAbcRow(); + drawNumRow(); + updateTopString(); + }); +}; diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json new file mode 100644 index 000000000..2f395f8a8 --- /dev/null +++ b/apps/draguboard/metadata.json @@ -0,0 +1,18 @@ +{ "id": "draguboard", + "name": "DragUboard", + "version":"0.03", + "description": "A library for text input via swiping U-shaped keyboard.", + "icon": "app.png", + "type":"textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"}, + {"name":"draguboard.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"draguboard.json"} + ] +} diff --git a/apps/draguboard/screenshot.png b/apps/draguboard/screenshot.png new file mode 100644 index 000000000..f2cb91717 Binary files /dev/null and b/apps/draguboard/screenshot.png differ diff --git a/apps/draguboard/settings.js b/apps/draguboard/settings.js new file mode 100644 index 000000000..58634b1b3 --- /dev/null +++ b/apps/draguboard/settings.js @@ -0,0 +1,44 @@ +(function(back) { + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + const colors = { + 4095: /*LANG*/"White", + 4080: /*LANG*/"Yellow", + 3840: /*LANG*/"Red", + 3855: /*LANG*/"Magenta", + 255: /*LANG*/"Cyan", + 240: /*LANG*/"Green", + 15: /*LANG*/"Blue", + 0: /*LANG*/"Black", + '-1': /*LANG*/"Default" + }; + + const save = () => require('Storage').write('draguboard.json', settings); + function colorMenu(key) { + let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)}; + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings[key] == color, + onchange: () => { + if (color >= 0) { + settings[key] = parseInt(color); + } else { + delete settings[key]; + } + save(); + setTimeout(E.showMenu, 10, appMenu); + } + }; + }); + return menu; + } + + const appMenu = { + '': {title: 'draguboard'}, '< Back': back, + /*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")), + /*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")), + /*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight")) + }; + + E.showMenu(appMenu); +}) diff --git a/apps/drained/ChangeLog b/apps/drained/ChangeLog new file mode 100644 index 000000000..af1ee299b --- /dev/null +++ b/apps/drained/ChangeLog @@ -0,0 +1,9 @@ +0.01: New app! +0.02: Allow boot exceptions, e.g. to load DST +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 +0.06: Display clock in green when charging, with "charging" text +0.07: Correctly restore full power when the charged threshold is reached +0.08: Redisplay immediately on changes to charging status diff --git a/apps/drained/README.md b/apps/drained/README.md new file mode 100644 index 000000000..7bef74d81 --- /dev/null +++ b/apps/drained/README.md @@ -0,0 +1,33 @@ +# Drained + +With this app installed, your Bangle will automatically switch into low power mode when the battery reaches 5% battery (or a preconfigured percentage), displaying a simple clock. When the battery is then charged above 20% (also configurable), normal operation is restored. + +Low power mode can also be exited manually, by tapping the primary watch button (an initial tap may be required to unlock the watch). + +# Features + +## Persistence +- [x] Restore normal operation with sufficient charge +- [x] Reactivate on watch startup + +## Time +- [x] Show simple date/time +- [ ] Disable alarms - with a setting? +- [ ] Smarter/backoff interval for checking battery percentage + +## No backlight (#2502) +- [x] LCD brightness +- [ ] LCD timeout? + +## Peripherals +- [x] Disable auto heart rate measurement in health app (#2502) +- [x] Overwrite setGPSPower() function (#2502) +- [x] Turn off already-running GPS / HRM + +## Features +- [x] Wake on twist -> off (#2502) +- [x] Emit `"drained"` event + +# Creator + +- [bobrippling](https://github.com/bobrippling/) diff --git a/apps/drained/app-icon.js b/apps/drained/app-icon.js new file mode 100644 index 000000000..36c3683a2 --- /dev/null +++ b/apps/drained/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4AwqwAHF84HOF/6OQA5pe/F/4v2X1AvHL1crF9srwMrMwWJAAIvlFwIABAwQvCGEJXCFwYvBSQIvDGEAuHqySBF4gwfK4IuDF4IDClowjFwovEMMYuHF4gwhFw16q16BAoweFwwvJGAwueGAQJIGAgufABYvYFypfYFy3+F6wuXF6wuYF6ouZL9QuEX9IuFF64wQFwwvYGBwuHF7IwMFxAvaGBQuJF7YwIFxQvcGAwuLF7owEFxgveGAQuNF74wBB5wvfAB4v/FDgANM1YA/AH4A/AHI")) diff --git a/apps/drained/app.js b/apps/drained/app.js new file mode 100644 index 000000000..9f8f6988f --- /dev/null +++ b/apps/drained/app.js @@ -0,0 +1,136 @@ +var app = "drained"; +if (typeof drainedInterval !== "undefined") + drainedInterval = clearInterval(drainedInterval); +Bangle.setLCDBrightness(0); +var powerNoop = function () { return false; }; +var forceOff = function (name) { + var _a; + if ((_a = Bangle._PWR) === null || _a === void 0 ? void 0 : _a[name]) + Bangle._PWR[name] = []; + Bangle["set".concat(name, "Power")](0, app); + Bangle["set".concat(name, "Power")] = powerNoop; +}; +forceOff("GPS"); +forceOff("HRM"); +try { + NRF.disconnect(); + NRF.sleep(); +} +catch (e) { + console.log("couldn't disable ble: ".concat(e)); +} +Bangle.removeAllListeners(); +clearWatch(); +Bangle.setOptions({ + wakeOnFaceUp: 0, + wakeOnTouch: 0, + wakeOnTwist: 0, +}); +var nextDraw; +var draw = function () { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2 - 48; + var date = new Date(); + var timeStr = require("locale").time(date, 1); + var dateStr = require("locale").date(date, 0).toUpperCase() + + "\n" + + require("locale").dow(date, 0).toUpperCase(); + var x2 = x + 6; + var y2 = y + 66; + var charging = Bangle.isCharging(); + g.reset() + .clearRect(Bangle.appRect) + .setFont("Vector", 55) + .setFontAlign(0, 0) + .setColor(charging ? "#0f0" : g.theme.fg) + .drawString(timeStr, x, y) + .setFont("Vector", 24) + .drawString(dateStr, x2, y2); + if (charging) + g.drawString("charging: ".concat(E.getBattery(), "%"), x2, y2 + 48); + else + g.drawString("".concat(E.getBattery(), "%"), x2, y2 + 48); + if (nextDraw) + clearTimeout(nextDraw); + nextDraw = setTimeout(function () { + nextDraw = undefined; + draw(); + }, 60000 - (date.getTime() % 60000)); +}; +var reload = function () { + var scroller; + 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 (scroller) { + menu[""] = { selected: scroller.scroll }; + } + if (nextDraw) + clearTimeout(nextDraw); + (scroller = E.showMenu(menu).scroller); + }; + Bangle.setUI({ + mode: "custom", + remove: function () { + if (nextDraw) + clearTimeout(nextDraw); + nextDraw = undefined; + }, + btn: showMenu + }); + Bangle.CLOCK = 1; + g.clear(); + draw(); +}; +reload(); +Bangle.emit("drained", E.getBattery()); +var _a = require("Storage").readJSON("".concat(app, ".setting.json"), true) || {}, _b = _a.keepStartup, keepStartup = _b === void 0 ? true : _b, _c = _a.restore, restore = _c === void 0 ? 20 : _c, _d = _a.exceptions, exceptions = _d === void 0 ? ["widdst.0"] : _d, _e = _a.interval, interval = _e === void 0 ? 10 : _e; +function drainedRestore() { + if (!keepStartup) { + try { + eval(require('Storage').read('bootupdate.js')); + } + catch (e) { + console.log("error restoring bootupdate:" + e); + } + } + load(); +} +var checkCharge = function () { + if (E.getBattery() < restore) { + draw(); + return; + } + drainedRestore(); +}; +if (Bangle.isCharging()) + checkCharge(); +Bangle.on("charging", function (charging) { + if (drainedInterval) + drainedInterval = clearInterval(drainedInterval); + if (charging) + drainedInterval = setInterval(checkCharge, interval * 60 * 1000); + draw(); +}); +if (!keepStartup) { + var storage = require("Storage"); + for (var _i = 0, exceptions_1 = exceptions; _i < exceptions_1.length; _i++) { + var boot = exceptions_1[_i]; + try { + var js = storage.read("".concat(boot, ".boot.js")); + if (js) + eval(js); + } + catch (e) { + console.log("error loading boot exception \"".concat(boot, "\": ").concat(e)); + } + } +} diff --git a/apps/drained/app.ts b/apps/drained/app.ts new file mode 100644 index 000000000..57c71e727 --- /dev/null +++ b/apps/drained/app.ts @@ -0,0 +1,167 @@ +const app = "drained"; + +// from boot.js +declare let drainedInterval: IntervalId | undefined; +if(typeof drainedInterval !== "undefined") + drainedInterval = clearInterval(drainedInterval) as undefined; + +// backlight +Bangle.setLCDBrightness(0); + +// peripherals +const powerNoop = () => false; + +const forceOff = (name: "GPS" | "HRM" | "Compass" /*| "Barom"*/) => { + if ((Bangle as any)._PWR?.[name]) + (Bangle as any)._PWR[name] = []; + + // if(name === "Barom"){ setBarometerPower(...) } + // ^^^^ + Bangle[`set${name}Power`](0, app); + Bangle[`set${name}Power`] = powerNoop; +}; +forceOff("GPS"); +forceOff("HRM"); +try{ + NRF.disconnect(); + NRF.sleep(); +}catch(e){ + console.log(`couldn't disable ble: ${e}`); +} + +// events +Bangle.removeAllListeners(); +clearWatch(); + +// UI +Bangle.setOptions({ + wakeOnFaceUp: 0, + wakeOnTouch: 0, + wakeOnTwist: 0, +}); + +// clock +let nextDraw: TimeoutId | undefined; +const draw = () => { + const x = g.getWidth() / 2; + const y = g.getHeight() / 2 - 48; + + const date = new Date(); + + const timeStr = require("locale").time(date, 1); + const dateStr = require("locale").date(date, 0).toUpperCase() + + "\n" + + require("locale").dow(date, 0).toUpperCase(); + const x2 = x + 6; + const y2 = y + 66; + const charging = Bangle.isCharging(); + + g.reset() + .clearRect(Bangle.appRect) + .setFont("Vector", 55) + .setFontAlign(0, 0) + .setColor(charging ? "#0f0" : g.theme.fg) + .drawString(timeStr, x, y) + .setFont("Vector", 24) + .drawString(dateStr, x2, y2); + + if(charging) + g.drawString(`charging: ${E.getBattery()}%`, x2, y2 + 48); + else + g.drawString(`${E.getBattery()}%`, x2, y2 + 48); + + if(nextDraw) clearTimeout(nextDraw); + nextDraw = setTimeout(() => { + nextDraw = undefined; + draw(); + }, 60000 - (date.getTime() % 60000)); +}; + +const reload = () => { + let scroller: MenuInstance["scroller"] | undefined; + const showMenu = () => { + const menu: Menu = { + "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(scroller){ + menu[""] = { selected: scroller.scroll }; + } + + if(nextDraw) clearTimeout(nextDraw); + ({ scroller } = E.showMenu(menu)); + }; + + Bangle.setUI({ + mode: "custom", + remove: () => { + if (nextDraw) clearTimeout(nextDraw); + nextDraw = undefined; + }, + btn: showMenu + }); + Bangle.CLOCK=1; + + g.clear(); + draw(); +}; +reload(); + +// permit other apps to put themselves into low-power mode +Bangle.emit("drained", E.getBattery()); + +// restore normal boot on charge +const { keepStartup = true, restore = 20, exceptions = ["widdst.0"], interval = 10 }: DrainedSettings + = require("Storage").readJSON(`${app}.setting.json`, true) || {}; + +// re-enable normal boot code when we're above a threshold: +function drainedRestore() { // "public", to allow users to call + if(!keepStartup){ + try{ + eval(require('Storage').read('bootupdate.js')); + }catch(e){ + console.log("error restoring bootupdate:" + e); + } + } + load(); // necessary after updating boot.0 +} + +const checkCharge = () => { + if(E.getBattery() < restore) { + draw(); + return; + } + drainedRestore(); +}; + +if (Bangle.isCharging()) + checkCharge(); + +Bangle.on("charging", charging => { + if(drainedInterval) + drainedInterval = clearInterval(drainedInterval) as undefined; + if(charging) + drainedInterval = setInterval(checkCharge, interval * 60 * 1000); + draw(); // redraw to update charging status on screen +}); + +if(!keepStartup){ + const storage = require("Storage"); + for(const boot of exceptions){ + try{ + const js = storage.read(`${boot}.boot.js`); + if(js) eval(js); + }catch(e){ + console.log(`error loading boot exception "${boot}": ${e}`); + } + } +} diff --git a/apps/drained/boot.js b/apps/drained/boot.js new file mode 100644 index 000000000..48c1572bd --- /dev/null +++ b/apps/drained/boot.js @@ -0,0 +1,13 @@ +(function () { + var _a = require("Storage").readJSON("drained.setting.json", true) || {}, _b = _a.battery, threshold = _b === void 0 ? 5 : _b, _c = _a.interval, interval = _c === void 0 ? 10 : _c, _d = _a.keepStartup, keepStartup = _d === void 0 ? true : _d; + drainedInterval = setInterval(function () { + if (Bangle.isCharging()) + return; + if (E.getBattery() > threshold) + return; + var app = "drained.app.js"; + if (!keepStartup) + require("Storage").write(".boot0", "if(typeof __FILE__ === \"undefined\" || __FILE__ !== \"".concat(app, "\") setTimeout(load, 100, \"").concat(app, "\");")); + load(app); + }, interval * 60 * 1000); +})(); diff --git a/apps/drained/boot.ts b/apps/drained/boot.ts new file mode 100644 index 000000000..1fcb0591b --- /dev/null +++ b/apps/drained/boot.ts @@ -0,0 +1,21 @@ +(() => { +const { battery: threshold = 5, interval = 10, keepStartup = true }: DrainedSettings + = require("Storage").readJSON(`drained.setting.json`, true) || {}; + +drainedInterval = setInterval(() => { + if(Bangle.isCharging()) + return; + if(E.getBattery() > threshold) + return; + + const app = "drained.app.js"; + + if(!keepStartup) + require("Storage").write( + ".boot0", + `if(typeof __FILE__ === "undefined" || __FILE__ !== "${app}") setTimeout(load, 100, "${app}");` + ); + + load(app); +}, interval * 60 * 1000); +})() diff --git a/apps/drained/icon.png b/apps/drained/icon.png new file mode 100644 index 000000000..33311cf2c Binary files /dev/null and b/apps/drained/icon.png differ diff --git a/apps/drained/metadata.json b/apps/drained/metadata.json new file mode 100644 index 000000000..84addc803 --- /dev/null +++ b/apps/drained/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "drained", + "name": "Drained", + "version": "0.08", + "description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals", + "readme": "README.md", + "icon": "icon.png", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"drained.boot.js","url":"boot.js"}, + {"name":"drained.app.js","url":"app.js"}, + {"name":"drained.settings.js","url":"settings.js"}, + {"name":"drained.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"drained.setting.json"} + ] +} diff --git a/apps/drained/settings.js b/apps/drained/settings.js new file mode 100644 index 000000000..d82a9f6d4 --- /dev/null +++ b/apps/drained/settings.js @@ -0,0 +1,90 @@ +(function (back) { + var _a, _b, _c, _d, _e; + var SETTINGS_FILE = "drained.setting.json"; + var storage = require("Storage"); + var settings = storage.readJSON(SETTINGS_FILE, true) || {}; + (_a = settings.battery) !== null && _a !== void 0 ? _a : (settings.battery = 5); + (_b = settings.restore) !== null && _b !== void 0 ? _b : (settings.restore = 20); + (_c = settings.interval) !== null && _c !== void 0 ? _c : (settings.interval = 10); + (_d = settings.keepStartup) !== null && _d !== void 0 ? _d : (settings.keepStartup = true); + (_e = settings.exceptions) !== null && _e !== void 0 ? _e : (settings.exceptions = ["widdst.0"]); + var save = function () { + storage.writeJSON(SETTINGS_FILE, settings); + }; + var menu = { + "": { "title": "Drained" }, + "< Back": back, + "Trigger at batt%": { + value: settings.battery, + min: 0, + max: 95, + step: 5, + format: function (v) { return "".concat(v, "%"); }, + onchange: function (v) { + settings.battery = v; + save(); + }, + }, + "Poll interval": { + value: settings.interval, + min: 1, + max: 60 * 2, + step: 5, + format: function (v) { return "".concat(v, " mins"); }, + onchange: function (v) { + settings.interval = v; + save(); + }, + }, + "Restore watch at %": { + value: settings.restore, + min: 0, + max: 95, + step: 5, + format: function (v) { return "".concat(v, "%"); }, + onchange: function (v) { + settings.restore = v; + save(); + }, + }, + "Keep startup code": { + value: settings.keepStartup, + onchange: function (b) { + settings.keepStartup = b; + save(); + updateAndRedraw(); + }, + }, + }; + var updateAndRedraw = function () { + setTimeout(function () { E.showMenu(menu); }, 10); + if (settings.keepStartup) { + delete menu["Startup exceptions"]; + return; + } + menu["Startup exceptions"] = function () { return E.showMenu(bootExceptions); }; + var bootExceptions = { + "": { "title": "Startup exceptions" }, + "< Back": function () { return E.showMenu(menu); }, + }; + storage.list(/\.boot\.js/) + .map(function (name) { return name.replace(".boot.js", ""); }) + .forEach(function (name) { + bootExceptions[name] = { + value: settings.exceptions.indexOf(name) >= 0, + onchange: function (b) { + if (b) { + settings.exceptions.push(name); + } + else { + var i = settings.exceptions.indexOf(name); + if (i >= 0) + settings.exceptions.splice(i, 1); + } + save(); + }, + }; + }); + }; + updateAndRedraw(); +}) diff --git a/apps/drained/settings.ts b/apps/drained/settings.ts new file mode 100644 index 000000000..f972f51a7 --- /dev/null +++ b/apps/drained/settings.ts @@ -0,0 +1,104 @@ +type DrainedSettings = { + battery?: number, + restore?: number, + interval?: number, + keepStartup?: ShortBoolean, + exceptions?: string[], +}; + +(back => { + const SETTINGS_FILE = "drained.setting.json"; + + const storage = require("Storage") + const settings: DrainedSettings = storage.readJSON(SETTINGS_FILE, true) || {}; + settings.battery ??= 5; + settings.restore ??= 20; + settings.interval ??= 10; + settings.keepStartup ??= true; + settings.exceptions ??= ["widdst.0"]; // daylight savings + + const save = () => { + storage.writeJSON(SETTINGS_FILE, settings) + }; + + const menu: Menu = { + "": { "title": "Drained" }, + "< Back": back, + "Trigger at batt%": { + value: settings.battery, + min: 0, + max: 95, + step: 5, + format: (v: number) => `${v}%`, + onchange: (v: number) => { + settings.battery = v; + save(); + }, + }, + "Poll interval": { + value: settings.interval, + min: 1, + max: 60 * 2, + step: 5, + format: (v: number) => `${v} mins`, + onchange: (v: number) => { + settings.interval = v; + save(); + }, + }, + "Restore watch at %": { + value: settings.restore, + min: 0, + max: 95, + step: 5, + format: (v: number) => `${v}%`, + onchange: (v: number) => { + settings.restore = v; + save(); + }, + }, + "Keep startup code": { + value: settings.keepStartup as boolean, + onchange: (b: boolean) => { + settings.keepStartup = b; + save(); + updateAndRedraw(); + }, + }, + }; + + const updateAndRedraw = () => { + // will change the menu, queue redraw: + setTimeout(() => { E.showMenu(menu) }, 10); + + if (settings.keepStartup) { + delete menu["Startup exceptions"]; + return; + } + menu["Startup exceptions"] = () => E.showMenu(bootExceptions); + + const bootExceptions: Menu = { + "": { "title" : "Startup exceptions" }, + "< Back": () => E.showMenu(menu), + }; + + storage.list(/\.boot\.js/) + .map(name => name.replace(".boot.js", "")) + .forEach((name: string) => { + bootExceptions[name] = { + value: settings.exceptions!.indexOf(name) >= 0, + onchange: (b: boolean) => { + if (b) { + settings.exceptions!.push(name); + } else { + const i = settings.exceptions!.indexOf(name); + if (i >= 0) settings.exceptions!.splice(i, 1); + } + save(); + }, + }; + }); + }; + + updateAndRedraw(); +}) satisfies SettingsFunc diff --git a/apps/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog new file mode 100644 index 000000000..0541d11de --- /dev/null +++ b/apps/drinkcounter/ChangeLog @@ -0,0 +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 +0.26: Minor code improvements diff --git a/apps/drinkcounter/README.md b/apps/drinkcounter/README.md new file mode 100644 index 000000000..5638ee066 --- /dev/null +++ b/apps/drinkcounter/README.md @@ -0,0 +1,15 @@ +# Drink Counter + +Counts drinks you had for science. Calculates BAC. + +## Usage + +Swipe left/right to select drink. Swipe up/down to add/remove drinks. + +## Important notes + +No warranty whatsoever. Use at your own risk. Calculations might be wrong. Do not drink and drive - even if BAC is low. + +## Creator + +Hank - contact at http://forum.espruino.com diff --git a/apps/drinkcounter/app.js b/apps/drinkcounter/app.js new file mode 100644 index 000000000..b231930d7 --- /dev/null +++ b/apps/drinkcounter/app.js @@ -0,0 +1,291 @@ +g.reset().clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +require("Font8x16").add(Graphics); + +const BANGLEJS2 = process.env.HWVERSION == 2; +const SETTINGSFILE = "drinkcounter.json"; +setting = require("Storage").readJSON("setting.json",1); +E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ +var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; +var ampm = "AM"; +let drag; + +var icoBeer = require("heatshrink").decompress(atob("lEoxH+AG2BAAoecEpAoWC4fXAAIGGAAowTDxAmJE4YGGE5QeJE5QHHE7owJE0pQKE7pQJE86fnE5QJSE5YUHBAIJQYxIpFAAvGBBAJIExYoGDgIACBBApFExonCDYoAOFSAnbFJYnE6vVDYYFHAwakQE4YaFAoQGJEIYoME7QoEE7ogFE/4neTBgntY84n/E+7HUE64mDE8IAFEw4nDTBifIE9gmId7gALE5IGCAooGDE6gASE8yaME7gmOFIgAREqIAhA==")); +var icoCocktail = require("heatshrink").decompress(atob("lEoxH+AH4AJtgABEkgmiEiXGAAIllAAiXeEAPXAQQDCFBYmTEgYqDFBZNWAIZRME6IfBEAYuEE5J2UwIAaJ5QncFBB3DB4YGCACQnKTQgoXE5bIEE6qfKPAZRFA4MUABgmNPAonBCgQnPExgpFPIgoNEyBSF4wGBFBgmSABCjJTZwoXEzwoHE0AoFE0QnCFAQmhKAonjFAInCE0Qn/E/4n/E/4n/wInDFEAhBEwQoDFLYdCEwooEFTAjHAAwoYIYgAMPDglT")); +var icoShot = require("heatshrink").decompress(atob("lEoxH+AH4A/AH4A/AH4AqwIAgE+HXADRPME8ZQM5AnSZBQkGAAYngEYonfJA5QQE8zGJFAYfKFBwmKE4iYIE7rpIeYgAJE5woEEpQKHTxhQIIpJaHJxgn/E8zGQZBAnQYxxQRFQYnlFgon5FCYmDE6LjHZRQmPE5AAOE/4njFCTGQKCwmRKAgATE54oWEyAqTDZY")); +var icoReset = require("heatshrink").decompress(atob("j0egILI8ACBh4DC/4DBh4DCv8f4ED8EPwEPEQMAvEAnkB4EA+AKBCAM8DYOA8EB//HwED/wXBg/wnAOC+EAjkDDoMgg+AJoRFCEIIAB/kHgEB/l8FwP/DYIDBC4MD/ASBgYeCAAw")); +var drawTimeout; +var activeDrink = 0; +var drinks = [0,0,0]; +const maxDrinks = 2; // 3 drinks +var firstDrinkTime = null; +var firstDrinkTimeTime = null; + +//var confBeerSize; +var confSex; +var confWeight; +var confWeightUnit; + + +// Load Status =============== +var drinkStatus = require("Storage").open("drinkcounter.status.json", "r"); +var test = drinkStatus.read(drinkStatus.getLength()); +if(test!== undefined) { + drinkStatus = JSON.parse(test); + //console.log("read status: " + test); + for (let i = 0; i <= maxDrinks; i++) { + drinks[i] = drinkStatus.drinks[i]; + } + firstDrinkTime = Date.parse(drinkStatus.firstDrinkTime); + //console.log("read firstDrinkTime: " + firstDrinkTime); + if (firstDrinkTime) firstDrinkTimeTime = require("locale").time(new Date(firstDrinkTime), 1); + //console.log("read firstDrinkTimeTime: " + firstDrinkTimeTime); +} else { + drinkStatus = { + drinks: [0,0,0] + }; + //console.log("no status file - applying default"); +} +// Load Status =============== + + +var drinksAlcohol = [12,16,5.6]; // in gramm +// Beer: 0.3L 12g - 0.5L 20g +// Radler: 0.3L 6g - 0.5L 10g +// Wine: 0.2L 16g +// Jäger Shot: 0.02L 5.6g + +// sex: Women 60 - Men 70 (Percent) +// Formula: Alcohol in g /(Body weight in kg x sex) – (0,15 x Hours) = bac per mille +// Example: 5 Beer (0.3L=12g), 80KG, Male (70%), 5 hours +// (5 * 12) / (80 / 100 * 70) - (0.15 * 5) + +function drawBac(){ + if (firstDrinkTime) { + var sum_drinks = (drinks[0] * drinksAlcohol[0]) + (drinks[1] * drinksAlcohol[1]) + (drinks[2] * drinksAlcohol[2]); + + if (confSex == "male") { + sex = 70; + } else { + sex = 60; + } + var weight = confWeight; + + if (confWeightUnit == "US Pounds") { + weight = weight * 0.45359237; + } + var currentTime = new Date(); + var time_diff = Math.floor(((currentTime - firstDrinkTime) % 86400000) / 3600000); // in hours! + //console.log("currentTime: " + currentTime) + //console.log("firstDrinkTime: " + firstDrinkTime) + + //console.log("timediff: " + time_diff); + ebac = Math.round( ((sum_drinks) / (weight / 100 * sex) - (0.15 * time_diff) ) * 100) / 100; + + //console.log("BAC: " + ebac + " weight: " + confWeight + " weightInKilo: " + weight + " Unit: " + confWeightUnit); + //console.log("sum_drinks: " + sum_drinks); + g.clearRect(0,34 + 20 + 8,176,34 + 20 + 20 + 8); //Clear + g.setFontAlign(0,0).setFont("8x16").setColor(g.theme.fg).drawString("BAC: " + ebac, 90, 74); + } +} + + +// Load settings +function loadMySettings() { + // Helper function default setting + function def (value, def) {return value !== undefined ? value : def;} + + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + //confBeerSize = def(settings.beerSize, "0.3L"); + confSex = def(settings.sex, "male"); + confWeight = def(settings.weight, 80); + confWeightUnit = def(settings.weightUnit, "Kilo"); + //console.log("Read config - weight: " + confWeight); +} + + +function updateTime(){ + var d = require("locale").time(new Date(), 1); + + //console.log(d); + var time = d.split(":"); + var hours = time[0]; + var minutes = time[1]; + if (_12hour){ + //do 12 hour stuff + if (hours > 12) { + ampm = "PM"; + hours = hours - 12; + if (hours < 10) hours = doublenum(hours); + } else { + ampm = "AM"; + } + } else { + ampm = ""; + } + g.setBgColor(g.theme.bg).clearRect(0,24,176,44); //Clear + g.setFontAlign(0,0); // center font + g.setBgColor(g.theme.bg).setColor(g.theme.fg); + g.setFont("8x16").drawString("Time: " + hours + ":" + minutes + " " + ampm,90,34); + queueDrawTime(); +} + +function queueDrawTime() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + updateTime(); + }, 20000 - (Date.now() % 20000)); +} + + +function updateDrinks(){ + g.setBgColor(g.theme.bg).clearRect(0,145,176,176); //Clear + for (let i = 0; i <= maxDrinks; i++) { + if (i == activeDrink) { + g.setColor(g.theme.fg).fillRect((40 * (i + 1)) - 40 ,145,(40 * (i + 1)),176); + g.setColor(g.theme.bg); + } else { + g.setColor(g.theme.fg); + } + g.setFont("Vector",20).drawString(drinks[i], (40 * (i + 1)) - 20, 160); + g.setColor(g.theme.fg); + drinkStatus.drinks[i] = drinks[i]; + } + + g.setBgColor(g.theme.bg).setColor(g.theme.fg); + if (BANGLEJS2) { + g.drawImage(icoReset,145,145); + } + + drinkStatus.firstDrinkTime = firstDrinkTime; + settings_file = require("Storage").open("drinkcounter.status.json", "w"); + settings_file.write(JSON.stringify(drinkStatus)); + + drawBac(); +} + +function updateFirstDrinkTime(){ + if (firstDrinkTime){ + g.setFont("8x16"); + g.setFontAlign(0,0).drawString("1st drink @ " + firstDrinkTimeTime, 90, 34 + 20 ); + } +} + +function addDrink(){ + if (!firstDrinkTime){ + firstDrinkTime = new Date(); + firstDrinkTimeTime = require("locale").time(new Date(), 1); + //console.log("init drinking! " + firstDrinkTime); + } + drinks[activeDrink] = drinks[activeDrink] + 1; + updateFirstDrinkTime(); + updateDrinks(); +} + +function removeDrink(){ + if (drinks[activeDrink] > 0) drinks[activeDrink] = drinks[activeDrink] - 1; + updateDrinks(); + + if ((!BANGLEJS2) && (drinks[0] == 0) && (drinks[1] == 0) && (drinks[2] == 0)) { + resetDrinksFn() + } +} + +function previousDrink(){ + if (activeDrink > 0) activeDrink = activeDrink - 1; + updateDrinks(); +} + +function nextDrink(){ + if (activeDrink < maxDrinks) activeDrink = activeDrink + 1; + updateDrinks(); +} + +function showDrinks() { + g.setBgColor(g.theme.bg); + g.drawImage(icoBeer,0,100); + g.drawImage(icoCocktail,40,100); + g.drawImage(icoShot,80,100); +} + +function resetDrinksFn() { + g.clearRect(0,34,176,176); //Clear + resetDrinks = E.showPrompt("Reset drinks?", { + title: "Confirm", + buttons: { Yes: true, No: false }, + }); + resetDrinks.then((confirm) => { + if (confirm) { + for (let i = 0; i <= maxDrinks; i++) { + drinks[i] = 0; + } + //console.log("reset to default"); + } + //console.log("reset " + confirm); + firstDrinkTime = null; + showDrinks(); + updateDrinks(); + updateTime(); + updateFirstDrinkTime(); + }); +} + + +function initDragEvents() { + +if (BANGLEJS2) { + Bangle.on("drag", e => { + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + if (dx < dy) { + //console.log("left " + dx + " " + dy); + previousDrink(); + } else { + //console.log("right " + dx + " " + dy); + nextDrink(); + } + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + if (dx < dy) { + //console.log("down " + dx + " " + dy); + removeDrink(); + } else { + //console.log("up " + dx + " " + dy); + addDrink(); + } + } else { + //console.log("tap " + e.x + " " + e.y); + if (e.x > 145 && e.y > 145) { + resetDrinksFn(); + } + } + } + }); + } else { + setWatch(addDrink, BTN1, { repeat: true, debounce:50 }); + setWatch(removeDrink, BTN3, { repeat: true, debounce:50 }); + setWatch(previousDrink, BTN4, { repeat: true, debounce:50 }); + setWatch(nextDrink, BTN5, { repeat: true, debounce:50 }); + } +} + + +loadMySettings(); +showDrinks(); + + +if (drawTimeout) clearTimeout(drawTimeout); +drawTimeout = undefined; +updateTime(); +queueDrawTime(); +initDragEvents(); +updateDrinks(); +updateFirstDrinkTime(); + diff --git a/apps/drinkcounter/drinkcounter-icon.js b/apps/drinkcounter/drinkcounter-icon.js new file mode 100644 index 000000000..e7b95f9ef --- /dev/null +++ b/apps/drinkcounter/drinkcounter-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AAWBAAomkFpAweD4fXAAIGGAAo4bExAuJF4YGGF6QmJF5QHHF8o4JF1pgSF7pgRF96/vF5QJSF6YcHBAIJQdyIxFAAvGBBAJIFyYwGEgIACBBAxFFyovCEYoAOGTAvbGKYvE6vVEYYFHAwbEYF4YiFAoQGJFIYwUF7QwEF8ooFF/4v2XBgv1d94v/F/7vsF64uDF9IAFFx4vDXBi/IF+guQR6wvCFSIvOAwQFFAwYvcACQvuXSgvcFywxEACItZAH4A/AH4AlA==")) diff --git a/apps/drinkcounter/drinkcounter.png b/apps/drinkcounter/drinkcounter.png new file mode 100644 index 000000000..91a0cd4ad Binary files /dev/null and b/apps/drinkcounter/drinkcounter.png differ diff --git a/apps/drinkcounter/metadata.json b/apps/drinkcounter/metadata.json new file mode 100644 index 000000000..315a5845b --- /dev/null +++ b/apps/drinkcounter/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "drinkcounter", + "name": "Drink Counter", + "shortName": "Drink Counter", + "version": "0.26", + "description": "Counts drinks you had for science. Calculates blood alcohol content (BAC)", + "allow_emulator":true, + "icon": "drinkcounter.png", + "type": "app", + "tags": "health", + "screenshots": [{"url":"screenshot_drnkcnt.png"}], + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"drinkcounter.app.js","url":"app.js"}, + {"name":"drinkcounter.img","url":"drinkcounter-icon.js","evaluate":true}, + {"name":"drinkcounter.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"drinkcounter.settings.json"}, + {"name":"drinkcounter.json"}, + {"name":"drinkcounter.status.json"} + ] +} \ No newline at end of file diff --git a/apps/drinkcounter/screenshot_drnkcnt.png b/apps/drinkcounter/screenshot_drnkcnt.png new file mode 100644 index 000000000..7547eb63f Binary files /dev/null and b/apps/drinkcounter/screenshot_drnkcnt.png differ diff --git a/apps/drinkcounter/settings.js b/apps/drinkcounter/settings.js new file mode 100644 index 000000000..8353103e3 --- /dev/null +++ b/apps/drinkcounter/settings.js @@ -0,0 +1,58 @@ +(function(back) { + var FILE = "drinkcounter.json"; + var settings = Object.assign({ + secondsOnUnlock: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "Drink counter" + }, + "< Back": () => back(), + + "Beer size": stringInSettings("beerSize", ["0.3L", "0.5L"]), + + + "Sex": stringInSettings("sex", ["male", "female"]), + + 'Weight': { + value: 80|settings.weight, + min: 40, max: 500, + onchange: v => { + settings.weight = v; + writeSettings(); + } + }, + "Weight unit": stringInSettings("weightUnit", ["Kilo", "US Pounds"]) + + + }; + + E.showMenu(mainmenu); + +}) diff --git a/apps/dsky_clock/ChangeLog b/apps/dsky_clock/ChangeLog new file mode 100644 index 000000000..707d483e1 --- /dev/null +++ b/apps/dsky_clock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial commit +0.02: Added Alt modes +0.03: Fix display misalignment \ No newline at end of file diff --git a/apps/dsky_clock/README.md b/apps/dsky_clock/README.md new file mode 100644 index 000000000..d85f826ea --- /dev/null +++ b/apps/dsky_clock/README.md @@ -0,0 +1,38 @@ +# DSKY Clock + +This is a clockface inspired by the Apollo DSKY interface. + +## Features +- battery level indicator (PROG indicator) +- month (VERB indicator) +- date (NOUN indicator) +- Current local time (DATA 1) +- UTC time (DATA 2) +- Step counter (DATA 3) + +Indicator lights are: +- COMP ACTY (GPS/HRM active) +- MSG (Messages waiting) +- LOCK (Screen is locked) +- BT (Bluetooth is disconnected) +- BATT (Low battery or battery charging) +- ALARM (Alarm is set) +- STEP (Reached STEP goal)) + +---- +## Alt Modes +Swipe left/right to switch between different PROGRAM modes. + +- HRM (BPM/Confidence) +- GPS (Latitude/Longitude/Speed) +- Weather (Humidity/Rain/Temperature/Wind/Condition Code) +- Accelerometer (X/Y/Z) +- Compass (HEADING) + +In the interests of usability these change the "LIGHT" on the left side to show the current mode. + +---- +This is my first watch face, may add features and customization later. + +## Creator +Written by Carl Chan | [github](https://github.com/carlchan) diff --git a/apps/dsky_clock/app-icon.js b/apps/dsky_clock/app-icon.js new file mode 100644 index 000000000..12249608f --- /dev/null +++ b/apps/dsky_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkIkQAFkIEDiQMGABUAiQtIu1nu9wgETC40QDIIAHu4ABC4MCkURiVCAAMimQXJswABAgMDC4IADLIQLBBAYcHF4QXSg4XDiEBgIXCkIXCAQgADgwvFBwIXFF4sGuwXHI4cxCIQ4CF5gXCR4ZfJF5SPXI4S4EFgQGBsAvMdAikCAwJ5BF5YXJu7vMI4RABI4gvCmQXJDIQEIL5YRBgxHDAgR3D7vd6gCB6QXEHwIXDCoQGBC4YACC4sGs5fDAgQvE6lEpvd6YXECwkGAgQvFrovDU4QXJL4pHIRQtnR4oXGkIlCAAK6BDwgvPgwEDSIgXLgynBAYhHMC4UHOQIWBAIMHBAIXCmXdqioBC4UyIoYXBeQNmCoJOCF4VVCwJHGH4UHGAQXEF4JHFF4ZHEAYJHEU5RCBI4SoBC4JHFpoBCC4jWDdoaPFiIADiS/FABIXIL4QvCRwK/IisAiEAgIXBL4ZzCPIZfDCAIVBAAIXCU4gXEX4xfJBwNmXoS/GC40yd4hcCIoa/EF40xdYS9BMQNnC5ovER4JFDU6K/NgJHHXoQBBSQbzCI5C/Cg9mAAQaBAYSnNNwJcBMANwAIQXKlS/EbIIBDC4UDa5gvBMAYvFAAsQI4bsCPQYFBC5MAC4RcCCgZiCBQNTXAIXEiMikJOCiBUEBAcTA=")) \ No newline at end of file diff --git a/apps/dsky_clock/app-icon.png b/apps/dsky_clock/app-icon.png new file mode 100644 index 000000000..be7195743 Binary files /dev/null and b/apps/dsky_clock/app-icon.png differ diff --git a/apps/dsky_clock/app.js b/apps/dsky_clock/app.js new file mode 100644 index 000000000..3dc3d2ad5 --- /dev/null +++ b/apps/dsky_clock/app.js @@ -0,0 +1,381 @@ +//Init +var Layout = require("Layout"); +require("Font7x11Numeric7Seg").add(Graphics); +require("FontTeletext5x9Ascii").add(Graphics); + +const Light_on='#fff'; +const Light_off='#554'; +const Light_warn='#f90'; +const Light_COMPACTY='#0F0'; +const Light_width=43; +const Light_height=25; +const EL7_height=30; +const LightFont='Teletext5x9Ascii'; +const DataFont='7x11Numeric7Seg:2'; +var mode = 0; + +if (global.WIDGETS) {require("widget_utils").swipeOn();} // If `dsky_clock` was fast loaded into we seemingly need to hide the widgets before setting the layout so elements are not moved down. + +var layout = new Layout( + {type:"h", c:[ + {type:"",width:6}, + { type:"v", c: [ + {type:"txt", font:LightFont, col:"#000", bgCol:"#555", id:'L1', label:"UPLINK\nACTY", width:Light_width, height:Light_height}, + {type:"txt", font:LightFont, col:"#000", bgCol:"#555", id:'L2', label:"TEMP", width:Light_width, height:Light_height }, + {type:"txt", font:LightFont, col:"#000", bgCol:"#555", id:'L3', label:"GIMBAL\nLOCK", width:Light_width, height:Light_height }, + {type:"txt", font:LightFont, col:"#000", bgCol:"#555", id:'L4', label:"STBY", width:Light_width, height:Light_height }, + {type:"txt", font:LightFont, col:"#000", bgCol:"#555", id:'L5', label:"PROG", width:Light_width, height:Light_height }, + {type:"txt", font:LightFont, col:"#000", bgCol:"#eee", id:'L6', label:"OPR ERR", width:Light_width, height:Light_height }, + ]}, + { type:"", width:25}, + { type:"v", c: [ + {type:"",height:2}, + {type:"h", c: [ + {type:"", width:50},{type:"txt", font:"6x8", col:"#000",bgCol:"#0F0", label:"PROG", width:25, height:10}, + ]}, + {type:"h", c: [ + {type:"",width:10}, + {type:"txt", font:"6x8", col:"#000", bgCol:"#000", id:"COMPACTY", label:"COMP\nACTY", width:26, height:26 }, + {type:"",width:17}, + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000",label:"00", id:"PROG", fillx:1, height:EL7_height }, + ]}, + {type:"",height:1}, + {type:"h", c: [ + {type:"txt", font:"6x8", col:"#000", bgCol:"#0F0",label:"VERB", width:25, height:10}, + {type:"",width:30}, + {type:"txt", font:"6x8", col:"#000",bgCol:"#0F0",label:"NOUN", width:25, height:10}, + ]}, + {type:"h", c: [ + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000", label:"00", id:"VERB", fillx:1, height:EL7_height}, + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000", label:"00", id:"NOUN", fillx:1, height:EL7_height}, + ]}, + { type:"",bgCol:'#070', width:80, height:2 }, + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000", label:"00000", id:"R1", halign:1, fillx:1, height:EL7_height}, + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000", label:"00000", id:"R2", halign:1, fillx:1, height:EL7_height}, + {type:"txt", font:DataFont, col:"#0F0", bgCol:"#000", label:"00000", id:"R3", halign:1, fillx:1, height:EL7_height}, + ]}, + {type:"",width:5}, + ], + lazy:true}, + {btns:[ + {label:"", cb: Bangle.showLauncher} + ], lazy:true}); +layout.update(); + +//support functioe_ns + +function getWeather() { + var weather = {}; + try { + weather = require("Storage").readJSON('weather.json', 1).weather; + } catch(e) { + return {}; + } + return weather; +} + +function getdatetime(){ + var datetime = []; + var d = new Date(); +// var offsets = require("Storage").readJSON("worldclock.settings.json") || []; +// var meridian = require("locale").meridian(d); + datetime.clock = require("locale").time(d, 1); + datetime.month = d.getMonth()+1; + datetime.day = d.getDate(); + datetime.localtime=String(d.getHours()).padStart(2,'0')+String(d.getMinutes()).padStart(2,'0'); + let utchour=((d.getHours()+(Math.round(d.getTimezoneOffset()/60))) % 24); + datetime.utctime=String(utchour).padStart(2,'0')+String(d.getMinutes()).padStart(2,'0'); + return datetime; +} + +function getSteps(){ + let steps=Bangle.getHealthStatus("day").steps; + steps = typeof steps !== 'undefined' ? steps:0; + return steps; +} + +function getStepGoal(){ + let stepGoal = (require("Storage").readJSON("health.json",1)||10000).stepGoal; + stepGoal = typeof stepGoal !== 'undefined' ? stepGoal:10000; + return stepGoal; +} + +function isAlarmSet(){ + let alarmStatus = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on); + return alarmStatus; +} + +function isMessagesNotify(){ + if (require("Storage").read("messages.json")!==undefined) { + return true; + } else { + return false; + } +} + +function getHRM(){ + let hrm=Bangle.getHealthStatus('last'); + hrm = typeof hrm !== 'undefined' ? hrm:0; + return hrm; +} + +function isBTConnected(){ + return NRF.getSecurityStatus().connected; +} + +function getBattery(){ + let battlevel = E.getBattery(); + if (Bangle.isCharging()) { + battlevel = -1; + } else if (battlevel >= 100) { + battlevel = 99; + } + battlevel=String(battlevel); + return battlevel; +} + +function isActive(){ + if (Bangle.isCompassOn() || Bangle.isGPSOn() || Bangle.isHRMOn() || Bangle.isBarometerOn() ) { + return true; + } else { + return false; + } +} + +function setLight(id,label,check,onColour,offColour){ + // print('setlight:',id); //debug + layout.clear(layout[id]); + onColour = typeof onColour !== 'undefined' ? onColour:Light_on; + offColour = typeof offColour !== 'undefined' ? offColour:Light_off; + if (label !== '') { + layout[id].label=label; + } + if (check) { + layout[id].bgCol=onColour; + } else { + layout[id].bgCol=offColour; + } + layout.render(layout[id]); +} + +function setDATA(id,label) { + layout.clear(layout[id]); + let data='-----'; + let sign=''; + try { + if (!isNaN(label)) { + if (label < 0) { + label=Math.abs(label); + sign='-'; + } + data=String(String(label).toString(16)).toUpperCase().padStart(5,'0').substring(0,5); + data=sign+data; + } + } catch(e) { + data='-----'; + } + layout[id].label=data; + layout.render(layout[id]); +} + +function setWORD(id,label){ + layout.clear(layout[id]); + let data='--'; + if (!isNaN(label)) { + data=String(String(label).toString(16)).toUpperCase().padStart(2,'0').substring(0,2); + } + // print(id, data); //debug + layout[id].label=data; + layout.render(layout[id]); +} + +function draw_bg(){ + g.setColor('#666'); + g.fillRect(0,0,176,176); + g.setColor('#000'); + g.fillRect(69,2,172,174); + g.fillCircle(59,10,5); + g.fillCircle(59,166,5); +} + +// actual display +function drawMain(){ + let datetime=getdatetime(); + + setDATA('R1',datetime.localtime); + setDATA('R2',datetime.utctime); + setDATA('R3',getSteps()); + + setWORD('PROG',getBattery()); + setWORD('VERB',datetime.month); + setWORD('NOUN',datetime.day); + + setLight('COMPACTY','',isActive(),Light_COMPACTY); + setLight('L1','MSG',isMessagesNotify()); + setLight('L2','LOCK',Bangle.isLocked()); + setLight('L3','BT',!isBTConnected(),Light_warn); + setLight('L4','BATT',(getBattery()<=20),Light_warn); + setLight('L5','ALARM',isAlarmSet(),Light_warn); + setLight('L6','STEP',(getSteps()>=getStepGoal()),'#0a0'); + +// layout.setUI(); + layout.forgetLazyState(); + layout.render(); + queueDraw(); +} + +////// ALT modes ///// +var AltDrawTimer; +function drawAlt(mode) { + if (AltDrawTimer) clearTimeout(AltDrawTimer); + mode = typeof mode !== 'undefined' ? mode:0; + mode=Math.abs(mode); + // print('drawAlt: ', mode); // debug + // Show mode in PROG + setWORD('PROG',mode); + setWORD('NOUN',''); + setWORD('VERB',''); + // Disable Battery warning light in to show PROG no longer shows battery level + setLight('L4','BATT',false); + setDATA('R1'); + setDATA('R2'); + setDATA('R3'); + switch (mode) { + case 1: + setLight('L6','HRM',true); + mode_HRM(); + break; + case 2: + setLight('L6','TEMP',true); + mode_weather(); + break; + case 3: + setLight('L6','GPS',true); + mode_GPS(); + break; + case 4: + setLight('L6','ACCEL',true); + mode_accel(); + break; + case 5: + setLight('L6','HDG',true); + mode_compass(); + break; + default: + drawMain(); + } + layout.render(); +} + +function mode_HRM() { + setLight('COMPACTY','',true,Light_COMPACTY); + AltDrawTimer = setTimeout( function() { + Bangle.setHRMPower(true, 'dsky_clock'); + let hrm=getHRM(); + setDATA('R1',hrm.bpm); + setDATA('R2',hrm.bpmConfidence); + setDATA('R3',getSteps()); + mode_HRM(); + }, 5000); + Bangle.setHRMPower(false); +} + +function mode_weather() { + let weather=getWeather(); + try { + weather.temp = Math.round(weather.temp-273.15); + setDATA('R1',weather.temp); + setDATA('R2',weather.hum); + setDATA('R3',weather.code); + setWORD('NOUN',weather.hum); + setWORD('VERB',weather.rain); + } catch(e) { + setDATA('R1','-----'); + setDATA('R2','-----'); + setDATA('R3','-----'); + setDATA('R1','--'); + setDATA('R1','--'); + } +} + +function mode_compass() { + AltDrawTimer = setTimeout ( function() { + setLight('COMPACTY','',true,Light_COMPACTY); //isCompassOn seems to be incorrect? + Bangle.setCompassPower(1); + let compass=Bangle.getCompass(); + setDATA('R1',compass.heading); + setDATA('R2'); + setDATA('R3'); + mode_compass(); + }, 200); + Bangle.setCompassPower(0); +} + +function mode_GPS() { + setLight('COMPACTY','',true,Light_COMPACTY); + AltDrawTimer = setTimeout( function() { + Bangle.setGPSPower(1,'dsky_clock'); + let gps=Bangle.getGPSFix(); + setWORD('NOUN',gps.fix); + setWORD('VERB',gps.satellites); + setDATA('R1',gps.lat); + setDATA('R2',gps.lon); + setDATA('R3',gps.speed); + mode_GPS(); + }, 5000); + Bangle.setGPSPower(0); +} + +function mode_accel() { + AltDrawTimer = setTimeout( function() { + setLight('COMPACTY','',isActive(),Light_COMPACTY); + let accel=Bangle.getAccel(); + setDATA('R1',accel.x); + setDATA('R2',accel.y); + setDATA('R3',accel.z); + mode_accel(); + }, 100); +} + +//////////// Main + +var drawTimeout; +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + mode = 0; + if (AltDrawTimer) clearTimeout(AltDrawTimer); + drawMain(); + }, 60000 - (Date.now() % 60000)); +} + +Bangle.on('lock',on=>{ + mode = 0; + drawMain(); // draw immediately + }); +Bangle.on("message",function() { setLight('COMPACTY','',isActive(),Light_COMPACTY);}); +Bangle.on('charging',drawMain); +NRF.on('connect',function() { setLight('L3','BT',!isBTConnected(),Light_warn); }); +NRF.on('disconnect',function() { setLight('L3','BT',!isBTConnected(),Light_warn); }); + +Bangle.on('swipe', function(directionLR) { + if (directionLR == 1) { + mode=mode-1; + } + if (directionLR == -1) { + mode=mode+1; + } + if (mode < 0 ) { mode=5; } + mode=(mode % 6); + drawAlt(mode); +}); + +g.clear(); +draw_bg(); +drawMain(); + +Bangle.CLOCK = 1; +Bangle.loadWidgets(); // loading widgets after drawing the layout in `drawMain()` to display the app UI ASAP. +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe diff --git a/apps/dsky_clock/metadata.json b/apps/dsky_clock/metadata.json new file mode 100644 index 000000000..564f54d45 --- /dev/null +++ b/apps/dsky_clock/metadata.json @@ -0,0 +1,16 @@ +{ "id": "dsky_clock", + "name": "DSKY Clock", + "icon": "app-icon.png", + "screenshots": [{"url":"screenshot1.png"}], + "version": "0.03", + "description": "A clockface inspired by the Apollo DSKY interface.", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"dsky_clock.app.js","url":"app.js"}, + {"name":"dsky_clock.img","url":"app-icon.js","evaluate":true} + ] + } diff --git a/apps/dsky_clock/screenshot1.png b/apps/dsky_clock/screenshot1.png new file mode 100644 index 000000000..949199a69 Binary files /dev/null and b/apps/dsky_clock/screenshot1.png differ diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 09804b82e..aac1c30bd 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -11,4 +11,24 @@ 0.11: Fix bangle.js 1 white icons not displaying 0.12: On Bangle 2 change to swiping up/down to move between pages as to match page indicator. Swiping from left to right now loads the clock. 0.13: Added swipeExit setting so that left-right to exit is an option -0.14: Don't move pages when doing exit swipe. +0.14: Don't move pages when doing exit swipe - Bangle 2. +0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2. +0.16: Use default Bangle formatter for booleans +0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to +clock face by timeout. +0.18: Bangle 2: Move interactions inside setUI. Replace "one click exit" with +back-functionality through setUI, adding the red back button as well. Hardware +button to exit is no longer an option. +0.19: Bangle 2: Utilize new Bangle.load(), Bangle.showClock() functions to +facilitate 'fast switching' of apps where available. +0.20: Bangle 2: Revert use of Bangle.load() to classic load() calls since +widgets would still be loaded when they weren't supposed to. +0.21: Bangle 2: Call Bangle.drawWidgets() early on so that the widget field +immediately follows the correct theme. +0.22: Bangle 2: Change to not automatically marking the first app on a page +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 +0.26: Bangle 2: Postpone loading icons that are not needed initially. +0.27: Bangle 2: Add setting to remember and present the last open page between instances of dtlaunch. diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md index 55c9f53b8..276e62358 100644 --- a/apps/dtlaunch/README.md +++ b/apps/dtlaunch/README.md @@ -6,6 +6,10 @@ Bangle 1: In the picture above, the Settings app is selected. + +![](sshot_e1.png) + + Bangle 2: ![shot1](https://user-images.githubusercontent.com/89286474/146471756-ec6d16de-6916-4fde-b991-ba88c2c8fa1a.png) @@ -13,7 +17,9 @@ Bangle 2: ![shot3](https://user-images.githubusercontent.com/89286474/146471760-5497fd1b-8e82-4fd5-a4e3-4734701a7dbd.png) -## Controls- Bangle +## Controls + +### Bangle 1 **BTN1** - move backward through app icons on a page @@ -25,10 +31,34 @@ Bangle 2: **Swipe Right** - move to previous page of app icons -## Controls- Bangle 2 +**Touch Left(1) area** - "Back" to Clock -**Touch** - icon to select, scond touch launches app +**Touch Right(2) area** - move forward through app icons + +**Touch Middle(1+2) area** - run the selected app + +### Bangle 2 + +**Touch** - icon to select, second touch launches app **Swipe Left/Up** - move to next page of app icons **Swipe Right/Down** - move to previous page of app icons + +## Settings + +**Show clocks** + +**Show launchers** + +### Only Bangle 2 + +**Direct launch** - launch on first touch. + +**Swipe Exit** - Swipe left to exit. + +**Time Out** - Return to clock after a short while. + +**Interaction buzz** + +**Remember Page** - Remember page when leaving and coming back to the launcher. diff --git a/apps/dtlaunch/app-b1.js b/apps/dtlaunch/app-b1.js index ed9cc778e..c7e78d671 100644 --- a/apps/dtlaunch/app-b1.js +++ b/apps/dtlaunch/app-b1.js @@ -13,13 +13,13 @@ function wdog(handle,timeout){ wdog.timeout = timeout; } if(wdog.timer){ - clearTimeout(wdog.timer) + clearTimeout(wdog.timer); } - wdog.timer = setTimeout(wdog.handle,wdog.timeout) + wdog.timer = setTimeout(wdog.handle,wdog.timeout); } // reset after two minutes of inactivity -wdog(load,120000) +wdog(load,120000); var s = require("Storage"); var apps = s.list(/\.info$/).map(app=>{ @@ -49,7 +49,13 @@ function draw_icon(p,n,selected) { var y = n>2?130:40; (selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89); g.setColor(g.theme.fg); - g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); + //bad g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); + if ((apps[p*6+n].icon)){ + if (s.read(apps[p*6+n].icon)) //ensure that graph exist + g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); + else console.log("icon file NOT exist :"+apps[p*6+n].icon); + } + else console.log("icon property NOT exist"); g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1); var txt = apps[p*6+n].name.split(" "); for (var i = 0; i < txt.length; i++) { @@ -65,10 +71,31 @@ function drawPage(p){ if (!apps[p*6+i]) return i; draw_icon(p,i,selected==i); } -} + } + + // case was not working +Bangle.on("touch", function(tzone){ + //(tzone)=>{ + //console.log("tzone"+tzone); + switch(tzone){ + case 1: //left managed by + console.log("1, left or back to clock?"); + load();//clock + //nextapp(-1); + break; + case 2: // right + nextapp(1); + break; + case 3: //center 1+2 no for emul + doselect(); + break; + default: + console.log("no match"); + } + }); Bangle.on("swipe",(dir)=>{ - wdog() + wdog(); selected = 0; oldselected=-1; if (dir<0){ diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index 46194ec5d..ea163c57e 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -1,61 +1,75 @@ -/* Desktop launcher -* -*/ +{ // must be inside our own scope here so that when we are unloaded everything disappears -var settings = Object.assign({ - showClocks: true, - showLaunchers: true, - direct: false, - oneClickExit:false, - swipeExit: false -}, require('Storage').readJSON("dtlaunch.json", true) || {}); + /* Desktop launcher + * + */ -if( settings.oneClickExit) - setWatch(_=> load(), BTN1); - -var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{ - var a=s.readJSON(app,1); - return a && { - name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src - };}).filter( - app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type)); + let settings = Object.assign({ + showClocks: true, + showLaunchers: true, + direct: false, + swipeExit: false, + timeOut: "Off", + interactionBuzz: false, + rememberPage: false, + }, require('Storage').readJSON("dtlaunch.json", true) || {}); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -apps.forEach(app=>{ - if (app.icon) - app.icon = s.read(app.icon); // should just be a link to a memory area - }); + let s = require("Storage"); + // Borrowed caching from Icon Launcher, code by halemmerich. + let launchCache = s.readJSON("launch.cache.json", true)||{}; + let launchHash = require("Storage").hash(/\.info/); + if (launchCache.hash!=launchHash) { + launchCache = { + hash : launchHash, + apps : s.list(/\.info$/) + .map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) + .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) + .sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; + }) }; + s.writeJSON("launch.cache.json", launchCache); + } + let apps = launchCache.apps; + let page = 0; + let initPageAppZeroth = 0; + let initPageAppLast = 3; + if (settings.rememberPage) { + page = (global.dtlaunch&&global.dtlaunch.handlePagePersist()) ?? + (parseInt(s.read("dtlaunch.page")) ?? 0); + initPageAppZeroth = page*4; + initPageAppLast = Math.min(page*4+3, apps.length-1); + } -var Napps = apps.length; -var Npages = Math.ceil(Napps/4); -var maxPage = Npages-1; -var selected = -1; -var oldselected = -1; -var page = 0; -const XOFF = 24; -const YOFF = 30; + for (let i = initPageAppZeroth; i <= initPageAppLast; i++) { // Initially only load icons for the current page. + if (apps[i].icon) + apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area + } -function draw_icon(p,n,selected) { - var x = (n%2)*72+XOFF; - var y = n>1?72+YOFF:YOFF; + let Napps = apps.length; + let Npages = Math.ceil(Napps/4); + let maxPage = Npages-1; + let selected = -1; + //let oldselected = -1; + const XOFF = 24; + const YOFF = 30; + + let drawIcon= function(p,n,selected) { + let x = (n%2)*72+XOFF; + let y = n>1?72+YOFF:YOFF; (selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52); g.clearRect(x+12,y+4,x+59,y+51); g.setColor(g.theme.fg); try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){} g.setFontAlign(0,-1,0).setFont("6x8",1); - var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" "); - var lineY = 0; - var line = ""; - while (txt.length > 0){ - var c = txt.shift(); - + let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" "); + let lineY = 0; + let line = ""; + while (txt.length > 0){ + let c = txt.shift(); if (c.length + 1 + line.length > 13){ if (line.length > 0){ g.drawString(line.trim(),x+36,y+54+lineY*8); @@ -67,76 +81,132 @@ function draw_icon(p,n,selected) { } } g.drawString(line.trim(),x+36,y+54+lineY*8); -} + }; -function drawPage(p){ + let drawPage = function(p){ g.reset(); g.clearRect(0,24,175,175); - var O = 88+YOFF/2-12*(Npages/2); - for (var j=0;j{ - selected = 0; - oldselected=-1; - if(settings.swipeExit && dirLeftRight==1) showClock(); + let buzzShort = function() { + if (settings.interactionBuzz) Bangle.buzz(20); + }; + let buzzLong = function() { + if (settings.interactionBuzz) Bangle.buzz(100); + }; + + Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme. + Bangle.loadWidgets(); + drawPage(page); + + for (let i = 0; i < apps.length; i++) { // Load the rest of the app icons that were not initially. + if (i >= initPageAppZeroth && i <= initPageAppLast) continue; + if (apps[i].icon) + apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area + } + + if (!global.dtlaunch) { + global.dtlaunch = {}; + global.dtlaunch.handlePagePersist = function(page) { + // Function for persisting the active page when leaving dtlaunch. + if (page===undefined) {return this.page||0;} + + if (!this.killHandler) { // Only register kill listener once. + this.killHandler = () => { + s.write("dtlaunch.page", this.page.toString()); + }; + E.on("kill", this.killHandler); // This is intentionally left around after fastloading into other apps. I.e. not removed in uiRemove. + } + + this.page = page; + }; + global.dtlaunch.handlePagePersist(page); + } + + let swipeListenerDt = function(dirLeftRight, dirUpDown){ + updateTimeoutToClock(); + selected = -1; + //oldselected=-1; + if(settings.swipeExit && dirLeftRight==1) Bangle.showClock(); if (dirUpDown==-1||dirLeftRight==-1){ - ++page; if (page>maxPage) page=0; - drawPage(page); + ++page; if (page>maxPage) page=0; + buzzShort(); + drawPage(page); } else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){ - --page; if (page<0) page=maxPage; - drawPage(page); + --page; if (page<0) page=maxPage; + buzzShort(); + drawPage(page); } -}); + }; -function showClock(){ - var app = require("Storage").readJSON('setting.json', 1).clock; - if (app) load(app); - else E.showMessage("clock\nnot found"); -} - -function isTouched(p,n){ + let isTouched = function(p,n){ if (n<0 || n>3) return false; - var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; - var x2 = x1+71; var y2 = y1+81; + let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF; + let x2 = x1+71; let y2 = y1+81; return (p.x>x1 && p.y>y1 && p.x{ - var i; + let touchListenerDt = function(_,p){ + updateTimeoutToClock(); + let i; for (i=0;i<4;i++){ - if((page*4+i)=0 || settings.direct) { - if (selected!=i && !settings.direct){ - draw_icon(page,selected,false); - } else { - load(apps[page*4+i].src); - } - } - selected=i; - break; + if((page*4+i)=0 || settings.direct) { + if (selected!=i && !settings.direct){ + buzzShort(); + drawIcon(page,selected,false); + } else { + buzzLong(); + global.dtlaunch.handlePagePersist(page); + load(apps[page*4+i].src); } + } + selected=i; + break; } + } } if ((i==4 || (page*4+i)>Napps) && selected>=0) { - draw_icon(page,selected,false); - selected=-1; + buzzShort(); + drawIcon(page,selected,false); + selected=-1; } -}); + }; -Bangle.loadWidgets(); -g.clear(); -Bangle.drawWidgets(); -drawPage(0); + Bangle.setUI({ + mode : 'custom', + back : Bangle.showClock, + swipe : swipeListenerDt, + touch : touchListenerDt, + remove : ()=>{ + if (timeoutToClock) {clearTimeout(timeoutToClock);} + global.dtlaunch.handlePagePersist(page); + } + }); + + // taken from Icon Launcher with minor alterations + let timeoutToClock; + const updateTimeoutToClock = function(){ + if (settings.timeOut!="Off"){ + let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt + if (timeoutToClock) clearTimeout(timeoutToClock); + timeoutToClock = setTimeout(Bangle.showClock,time*1000); + } + }; + updateTimeoutToClock(); + +} // end of app scope diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 4a0b8067c..1ff75b953 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.14", + "version": "0.27", "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/dtlaunch/settings-b1.js b/apps/dtlaunch/settings-b1.js index f3101da16..fe5546edb 100644 --- a/apps/dtlaunch/settings-b1.js +++ b/apps/dtlaunch/settings-b1.js @@ -15,7 +15,6 @@ "< Back" : () => back(), 'Show clocks': { value: settings.showClocks, - format: v => v?"On":"Off", onchange: v => { settings.showClocks = v; writeSettings(); @@ -23,7 +22,6 @@ }, 'Show launchers': { value: settings.showLaunchers, - format: v => v?"On":"Off", onchange: v => { settings.showLaunchers = v; writeSettings(); diff --git a/apps/dtlaunch/settings-b2.js b/apps/dtlaunch/settings-b2.js index 7ead63be0..dcad03a65 100644 --- a/apps/dtlaunch/settings-b2.js +++ b/apps/dtlaunch/settings-b2.js @@ -5,56 +5,72 @@ showClocks: true, showLaunchers: true, direct: false, - oneClickExit:false, - swipeExit: false + swipeExit: false, + timeOut: "Off", + interactionBuzz: false, + rememberPage: false, }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { require('Storage').writeJSON(FILE, settings); } + const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; + E.showMenu({ "" : { "title" : "Desktop launcher" }, - "< Back" : () => back(), - 'Show clocks': { + /*LANG*/"< Back" : () => back(), + /*LANG*/'Show clocks': { value: settings.showClocks, - format: v => v?"On":"Off", onchange: v => { settings.showClocks = v; writeSettings(); } }, - 'Show launchers': { + /*LANG*/'Show launchers': { value: settings.showLaunchers, - format: v => v?"On":"Off", onchange: v => { settings.showLaunchers = v; writeSettings(); } }, - 'Direct launch': { + /*LANG*/'Direct launch': { value: settings.direct, - format: v => v?"On":"Off", onchange: v => { settings.direct = v; writeSettings(); } }, - 'Swipe Exit': { + /*LANG*/'Swipe Exit': { value: settings.swipeExit, - format: v => v?"On":"Off", onchange: v => { settings.swipeExit = v; writeSettings(); } }, - 'One click exit': { - value: settings.oneClickExit, - format: v => v?"On":"Off", + /*LANG*/'Time Out': { // Adapted from Icon Launcher + value: timeOutChoices.indexOf(settings.timeOut), + min: 0, + max: timeOutChoices.length-1, + format: v => timeOutChoices[v], onchange: v => { - settings.oneClickExit = v; + settings.timeOut = timeOutChoices[v]; writeSettings(); } - } + }, + /*LANG*/'Interaction buzz': { + value: settings.interactionBuzz, + onchange: v => { + settings.interactionBuzz = v; + writeSettings(); + } + }, + /*LANG*/'Remember Page': { + value: settings.rememberPage, + onchange: v => { + settings.rememberPage = v; + writeSettings(); + } + }, }); }) diff --git a/apps/dtlaunch/sshot_e1.png b/apps/dtlaunch/sshot_e1.png new file mode 100644 index 000000000..69f708c33 Binary files /dev/null and b/apps/dtlaunch/sshot_e1.png differ diff --git a/apps/dutchclock/ChangeLog b/apps/dutchclock/ChangeLog new file mode 100644 index 000000000..8efcb9edb --- /dev/null +++ b/apps/dutchclock/ChangeLog @@ -0,0 +1 @@ +0.20: First release \ No newline at end of file diff --git a/apps/dutchclock/README.md b/apps/dutchclock/README.md new file mode 100644 index 000000000..787bcce1b --- /dev/null +++ b/apps/dutchclock/README.md @@ -0,0 +1,22 @@ +# Dutch Clock +This clock shows the time, in words, the way a Dutch person might respond when asked what time it is. Useful when learning Dutch and/or pretending to know Dutch. + +Dedicated to my wife, who will sometimes insist I tell her exactly what time it says on the watch and not just an approximation. + +## Options +- Three modes: + - exact time ("zeven voor half zes / twee voor tien") + - approximate time, rounded to the nearest 5-minute mark ("bijna vijf voor half zes / tegen tienen") (the default) + - hybrid mode, rounded when close to the quarter marks and exact otherwise ("zeven voor half zes / tegen tienen") +- Option to turn top widgets on/off (on by default) +- Option to show digital time at the bottom (off by default) +- Option to show the date at the bottom (on by default) + +The app respects top and bottom widgets, but it gets a bit crowded when you add the time/date and you also have bottom widgets turned on. + +When you turn widgets off, you can still see the top widgets by swiping down from the top. + +## Screenshots +![](screenshotbangle1-2.png) +![](screenshotbangle2.png) +![](screenshotbangle1.png) \ No newline at end of file diff --git a/apps/dutchclock/app-icon.js b/apps/dutchclock/app-icon.js new file mode 100644 index 000000000..7d6e655e8 --- /dev/null +++ b/apps/dutchclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AE0/Ao/4sccAoX79NtAofttIFD8dsAof3t1/GZ397oGE/YLE6IFDloFE1vbAoeNAondAon/z4FE356U/nNxhZC/drlpLDscNAoX4ue9C4f3L4oAKt4FEQ4qxE/0skIGDtg7DAoNtAocsAogAX94POA")) \ No newline at end of file diff --git a/apps/dutchclock/app.js b/apps/dutchclock/app.js new file mode 100644 index 000000000..588692a2b --- /dev/null +++ b/apps/dutchclock/app.js @@ -0,0 +1,260 @@ +// Load libraries +const storage = require("Storage"); +const locale = require('locale'); +const widget_utils = require('widget_utils'); + +// Define constants +const DATETIME_SPACING_HEIGHT = 5; +const TIME_HEIGHT = 8; +const DATE_HEIGHT = 8; +const BOTTOM_SPACING = 2; + +const MINS_IN_HOUR = 60; +const MINS_IN_DAY = 24 * MINS_IN_HOUR; + +const VARIANT_EXACT = 'exact'; +const VARIANT_APPROXIMATE = 'approximate'; +const VARIANT_HYBRID = 'hybrid'; + +const DEFAULTS_FILE = "dutchclock.default.json"; +const SETTINGS_FILE = "dutchclock.json"; + +// Load settings +const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} +); + +// Define global variables +const textBox = {}; +let date, mins; + +// Define functions +function initialize() { + // Reset the state of the graphics library + g.clear(true); + + // Tell Bangle this is a clock + Bangle.setUI("clock"); + + // Load widgets + Bangle.loadWidgets(); + + // Show widgets, or not + if (settings.showWidgets) { + Bangle.drawWidgets(); + } else { + widget_utils.swipeOn(); + } + + const dateTimeHeight = (settings.showDate || settings.showTime ? DATETIME_SPACING_HEIGHT : 0) + + (settings.showDate ? DATE_HEIGHT : 0) + + (settings.showTime ? TIME_HEIGHT : 0); + + Object.assign(textBox, { + x: Bangle.appRect.x + Bangle.appRect.w / 2, + y: Bangle.appRect.y + (Bangle.appRect.h - dateTimeHeight) / 2, + w: Bangle.appRect.w - 2, + h: Bangle.appRect.h - dateTimeHeight + }); + + // draw immediately at first + tick(); + + // now check every second + let secondInterval = setInterval(tick, 1000); + + // Stop updates when LCD is off, restart when on + Bangle.on('lcdPower',on=>{ + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(tick, 1000); + draw(); // draw immediately + } + }); +} + +function tick() { + date = new Date(); + const m = (date.getHours() * MINS_IN_HOUR + date.getMinutes()) % MINS_IN_DAY; + + if (m !== mins) { + mins = m; + draw(); + } +} + +function draw() { + // work out how to display the current time + const timeLines = getTimeLines(mins); + const bottomLines = getBottomLines(); + + g.reset().clearRect(Bangle.appRect); + + // draw the current time (4x size 7 segment) + setFont(timeLines); + + g.setFontAlign(0,0); // align center top + g.drawString(timeLines.join("\n"), textBox.x, textBox.y, false); + + if (bottomLines.length) { + // draw the time and/or date, in a normal font + g.setFont("6x8"); + g.setFontAlign(0,1); // align center bottom + // pad the date - this clears the background if the date were to change length + g.drawString(bottomLines.join('\n'), Bangle.appRect.w / 2, Bangle.appRect.y2 - BOTTOM_SPACING, false); + } +} + +function setFont(timeLines) { + const size = textBox.h / timeLines.length; + + g.setFont("Vector", size); + + let width = g.stringWidth(timeLines.join('\n')); + + if (width > textBox.w) { + g.setFont("Vector", Math.floor(size * (textBox.w / width))); + } +} + +function getBottomLines() { + const lines = []; + + if (settings.showTime) { + lines.push(locale.time(date, 1)); + } + + if (settings.showDate) { + lines.push(locale.date(date)); + } + + return lines; + } + +function getTimeLines(m) { + switch (settings.variant) { + case VARIANT_EXACT: + return getExactTimeLines(m); + case VARIANT_APPROXIMATE: + return getApproximateTimeLines(m); + case VARIANT_HYBRID: + return distanceFromNearest(15)(m) < 3 + ? getApproximateTimeLines(m) + : getExactTimeLines(m); + default: + console.warn(`Error in settings: unknown variant "${settings.variant}"`); + return getExactTimeLines(m); + } +} + +function getExactTimeLines(m) { + if (m === 0) { + return ['middernacht']; + } + + const hour = getHour(m); + const minutes = getMinutes(hour.offset); + + const lines = minutes.concat(hour.lines); + if (lines.length === 1) { + lines.push('uur'); + } + + return lines; +} + +function getApproximateTimeLines(m) { + const roundMinutes = getRoundMinutes(m); + + const lines = getExactTimeLines(roundMinutes.minutes); + + return addApproximateDescription(lines, roundMinutes.offset); +} + +function getHour(minutes) { + const hours = ['twaalf', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf']; + + const h = Math.floor(minutes / MINS_IN_HOUR), m = minutes % MINS_IN_HOUR; + + if (m <= 15) { + return {lines: [hours[h % 12]], offset: m}; + } + + if (m > 15 && m < 45) { + return { + lines: ['half', hours[(h + 1) % 12]], + offset: m - (MINS_IN_HOUR / 2) + }; + } + + return {lines: [hours[(h + 1) % 12]], offset: m - MINS_IN_HOUR}; +} + +function getMinutes(m) { + const minutes = ['', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf', 'twaalf', 'dertien', 'veertien', 'kwart']; + + if (m === 0) { + return []; + } + + return [minutes[Math.abs(m)], m > 0 ? 'over' : 'voor']; +} + +function getRoundMinutes(m) { + const nearest = roundTo(5)(m); + + return { + minutes: nearest % MINS_IN_DAY, + offset: m - nearest + }; +} + +function addApproximateDescription(lines, offset) { + if (offset === 0) { + return lines; + } + + if (lines.length === 1 || lines[1] === 'uur') { + const singular = lines[0]; + const plural = getPlural(singular); + return { + '-2': ['tegen', plural], + '-1': ['iets voor', singular], + '1': ['iets na', plural], + '2': ['even na', plural] + }[`${offset}`]; + } + + return { + '-2': ['bijna'].concat(lines), + '-1': ['rond'].concat(lines), + '1': ['iets na'].concat(lines), + '2': lines.concat(['geweest']) + }[`${offset}`]; +} + +function getPlural(h) { + return { + middernacht: 'middernacht', + een: 'enen', + twee: 'tweeën', + drie: 'drieën', + vijf: 'vijven', + zes: 'zessen', + elf: 'elven', + twaalf: 'twaalven' + }[h] || `${h}en`; +} + +function distanceFromNearest(x) { + return n => Math.abs(n - roundTo(x)(n)); +} + +function roundTo(x) { + return n => Math.round(n / x) * x; +} + +// Let's go +initialize(); \ No newline at end of file diff --git a/apps/dutchclock/app.png b/apps/dutchclock/app.png new file mode 100644 index 000000000..94d35b0c5 Binary files /dev/null and b/apps/dutchclock/app.png differ diff --git a/apps/dutchclock/default.json b/apps/dutchclock/default.json new file mode 100644 index 000000000..cfe5d34a4 --- /dev/null +++ b/apps/dutchclock/default.json @@ -0,0 +1,6 @@ +{ + "variant": "approximate", + "showWidgets": true, + "showTime": false, + "showDate": true +} \ No newline at end of file diff --git a/apps/dutchclock/metadata.json b/apps/dutchclock/metadata.json new file mode 100644 index 000000000..d336023f8 --- /dev/null +++ b/apps/dutchclock/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "dutchclock", + "name": "Dutch Clock", + "shortName":"Dutch Clock", + "icon": "app.png", + "version":"0.20", + "description": "A clock that displays the time the way a Dutch person would respond when asked what time it is.", + "type": "clock", + "tags": "clock,dutch,text", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "screenshots": [ + {"url":"screenshotbangle1-2.png"}, + {"url":"screenshotbangle2.png"}, + {"url":"screenshotbangle1.png"} + ], + "storage": [ + {"name":"dutchclock.app.js","url":"app.js"}, + {"name":"dutchclock.settings.js","url":"settings.js"}, + {"name":"dutchclock.default.json","url":"default.json"}, + {"name":"dutchclock.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"dutchclock.json"} + ], + "readme":"README.md" +} + \ No newline at end of file diff --git a/apps/dutchclock/screenshotbangle1-2.png b/apps/dutchclock/screenshotbangle1-2.png new file mode 100644 index 000000000..08bf31939 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1-2.png differ diff --git a/apps/dutchclock/screenshotbangle1.png b/apps/dutchclock/screenshotbangle1.png new file mode 100644 index 000000000..49ba895f4 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1.png differ diff --git a/apps/dutchclock/screenshotbangle2.png b/apps/dutchclock/screenshotbangle2.png new file mode 100644 index 000000000..48b3fd501 Binary files /dev/null and b/apps/dutchclock/screenshotbangle2.png differ diff --git a/apps/dutchclock/settings.js b/apps/dutchclock/settings.js new file mode 100644 index 000000000..146df5395 --- /dev/null +++ b/apps/dutchclock/settings.js @@ -0,0 +1,73 @@ +(function(back) { + const storage = require("Storage"); + + const VARIANT_EXACT = 'exact'; + const VARIANT_APPROXIMATE = 'approximate'; + const VARIANT_HYBRID = 'hybrid'; + + const DEFAULTS_FILE = "dutchclock.default.json"; + const SETTINGS_FILE = "dutchclock.json"; + + // Load settings + const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function writeSettings() { + require('Storage').writeJSON(SETTINGS_FILE, settings); + } + + function writeSetting(setting, value) { + settings[setting] = value; + writeSettings(); + } + + function writeOption(setting, value) { + writeSetting(setting, value); + showMainMenu(); + } + + function getOption(label, setting, value) { + return { + title: label, + value: settings[setting] === value, + onchange: () => { + writeOption(setting, value); + } + }; + } + + // Show the menu + function showMainMenu() { + const mainMenu = [ + getOption('Exact', 'variant', VARIANT_EXACT), + getOption('Approximate', 'variant', VARIANT_APPROXIMATE), + getOption('Hybrid', 'variant', VARIANT_HYBRID), + { + title: 'Show widgets?', + value: settings.showWidgets, + onchange: v => writeSetting('showWidgets', v) + }, + { + title: 'Show time?', + value: settings.showTime, + onchange: v => writeSetting('showTime', v) + }, + { + title: 'Show date?', + value: settings.showDate, + onchange: v => writeSetting('showDate', v) + } + ]; + + mainMenu[""] = { + title : "Dutch Clock", + back: back + }; + + E.showMenu(mainMenu); + } + + showMainMenu(); + }) \ No newline at end of file diff --git a/apps/dvdbounce/ChangeLog b/apps/dvdbounce/ChangeLog new file mode 100644 index 000000000..6d1dc4ce4 --- /dev/null +++ b/apps/dvdbounce/ChangeLog @@ -0,0 +1 @@ +0.01: Created the app. The logo bounces and buzz when it hits the angles. diff --git a/apps/dvdbounce/README.md b/apps/dvdbounce/README.md new file mode 100644 index 000000000..50f3ef0e9 --- /dev/null +++ b/apps/dvdbounce/README.md @@ -0,0 +1,9 @@ +# Bouncing DVD logo + +Have you ever wanted to admire the bouncing DVD logo on your watch? Now you can! Let's hope it touches an angle. + +![Gif of the DVD logo bouncing around](screenshot.gif) + +## Creator + +I'm [TrinTragula](https://github.com/TrinTragula) on Github. Feel free to reach me. \ No newline at end of file diff --git a/apps/dvdbounce/dvdbounce-icon.js b/apps/dvdbounce/dvdbounce-icon.js new file mode 100644 index 000000000..0625c2394 --- /dev/null +++ b/apps/dvdbounce/dvdbounce-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4A/ACWIAA4MJwAXPhALKC5AlCBZYADE4gXELQ4SDC4gCBC4IDCAwYTEBAghCBYYCEC5YUFAooOIBBAKDMwwIFCwQIDNIZtGTRANEEIiyMVYrYHLIq0GQA4OGABIPPAA77GC8CDGABAfFBAbpCAIIfCAQqmJhAXDhB4CCIMIEgIYETAoaCC5zYHIIRQDDATAKPJasWAH4A/AH4A/AE4")) \ No newline at end of file diff --git a/apps/dvdbounce/dvdbounce.app.js b/apps/dvdbounce/dvdbounce.app.js new file mode 100644 index 000000000..39037df34 --- /dev/null +++ b/apps/dvdbounce/dvdbounce.app.js @@ -0,0 +1,108 @@ +// The DVD logo +var dvdLogo = require("heatshrink").decompress(atob("3dTwIFC/4AG/ALCgYJEwAcDj4XHBgYJF4AJCg4WH8AMCn4KFF4YWH/wLCh4KJIpA7CgIKG+BnHOg1/BYxGCCw4LDHQ/8IpQ6CQBBRBIpBpDBY4uCKA6jDUQy8FTIn3EQYIBGYSQDBYTVF/x0DKITMGFwh4D96jDKILuDDoSvDEAn9AQIeDB4glCwA5ELwW/FIRNB/x2BVQSvDHIgIB+ZvCNwWPOwZ7DAYKEFDwJoBAYPhHgYeC8ADCP4QEB85YCEQRFELoIqBBA5oFL4SMEwBFEBAWfDwQFB4K1EJoPwIooIB+InCHIQ8EOgYIHA4PAIQP8IogqCBYQIFw4TBAoRICIoQiB/AaDBAfwJAOAUwTrEFQZFHAQPwCYPwCIIxBWIZFIwICBSIIKBIo4IHFYXgSwRFKBARFCFYLlC8DDCRZTaDOIPPHgngaIhFFBoQfB/YhCIojRDBAg/B/gaCGYRFEHgaUED4IEBD4JBDIAIDBLgQEBIoYbC4AMCHAQcBG4IbCAgJFDCQRlBJIIzCIogJCBAgWCD4IICHARFDCwQIEIAY7DGYRmBEAIWCBAJOCNwYfBPAQSBEIWDMog8DAAQfBUYYzEAATkCBAq+CGgQzEYQgIGD4Q3CGYJnDKYhFGCwLtEVAjQDIow2BawY8HNQQIFCwR0CfYgWFBAoWDEAJ5CIogWCIooHCEAR5CJQQWFIoYPCOgg0BHgiJBIooHDUYaCCIoRLCABl/CB4AFg5MFACBNGAB8fQYYARgL/CACc/CysHLisAuAWVhgWVgL/BCyn/WyX/AAx4Mv4VHAARLJFZAAEbA5VBABwWFh4WPJAs/CZoODOA3A/8H/k//BNBK4N/AQQPB/wWF/kf4P/w4jBg4+BKIMAgYuC/BEF4P4n/w//gEIPwCYJYBCAYLBT4f4n0P/0f/k+g/+FoPwn4uDHQYACwZFB4ZHB8A2BKAQYBCAXwW4nwuF//BCBFgJ3CwZ/BCAR1BUIkEJYJHCFgINB+F/GgIAC4BLENgXhI4J5B8BfCI4KMEFwY0BKoK6BvwSDYoIoDCAIuE8APBXQMPwJaC/kPDALqEGgf8NAK6NAoLpDTYS6CCAKCCAoK+BCwhYBMYQiBXQOALQQ2BAQQWFXggAMGoIAECx6gBAArVEABJDDAAhQDABCTBABIYJ4AVKAAbCDChYA=")); + +// Screen width +const WIDTH = g.getWidth(); +// Screen height +const HEIGHT = g.getHeight(); +// dvd logo image width +const IMG_WIDTH = 94; +/// dvd logo image height +const IMG_HEIGHT = 42; + +// Assign a random X and Y initial speed between 1.5 and 1 +var speedX = 1.5 - Math.random() / 2; +var speedY = 1.5 - Math.random() / 2; +// The logo X and Y position +var posX = 0; +var posY = 0; + +// The current logo color +var currentColor = "#ff00ff"; + +// Get a random value between "ff" and "00" +function getHexColor() { + if (Math.round(Math.random())) { + return "ff"; + } else { + return "00"; + } +} + +// Get a new 8 bit color +function getNewColor() { + return "#" + getHexColor() + getHexColor() + getHexColor(); +} + +// Change the dvd logo color on impact +// Only allow colors different from the current one +// and different from the bg +function changeColor() { + var newColor = getNewColor(); + while (newColor == currentColor || newColor == "#000000") { + newColor = getNewColor(); + } + currentColor = newColor; + g.setColor(newColor); +} + +// Draw the logo +function draw() { + // Move it + posX += speedX; + posY += speedY; + + var collisions = 0; + // Collision detection + if (posX <= 0) { + speedX = -speedX; + posX = 0; + collisions++; + } + if (posY <= 0) { + speedY = -speedY; + posY = 0; + collisions++; + } + if (posX >= (WIDTH - IMG_WIDTH)) { + speedX = -speedX; + posX = WIDTH - IMG_WIDTH; + collisions++; + } + if (posY >= (HEIGHT - IMG_HEIGHT)) { + speedY = -speedY; + posY = HEIGHT - IMG_HEIGHT; + collisions++; + } + + // If we detected 2 collisions, we touched an angle, HURRAY! + if (collisions > 1) { + Bangle.buzz(); + } + + // Change logo color on collision + if (collisions > 0) { + changeColor(); + } + + // Actually draw the logo + g.clear(); + g.drawImage(dvdLogo, posX, posY, { + scale: 0.5 + }); + setTimeout(function () { + draw(); + }, 15); +} + +// Set the background to black +g.setBgColor(0, 0, 0); +// Start from purple +g.setColor(currentColor); +// Clear the screen +g.clear(); +// Start drawing +draw(); + +// Exit on button press +setWatch(Bangle.showLauncher, BTN, { repeat: false, edge: "falling" }); diff --git a/apps/dvdbounce/dvdbounce.png b/apps/dvdbounce/dvdbounce.png new file mode 100644 index 000000000..a44e7a4ba Binary files /dev/null and b/apps/dvdbounce/dvdbounce.png differ diff --git a/apps/dvdbounce/metadata.json b/apps/dvdbounce/metadata.json new file mode 100644 index 000000000..f1c3e8343 --- /dev/null +++ b/apps/dvdbounce/metadata.json @@ -0,0 +1,31 @@ +{ + "id": "dvdbounce", + "name": "Bouncing DVD logo", + "shortName": "Bouncing DVD", + "version": "0.01", + "description": "Have you ever wanted to admire the bouncing DVD logo on your watch? Now you can! Let's hope it touches an angle.", + "icon": "dvdbounce.png", + "tags": "game", + "supports": [ + "BANGLEJS", + "BANGLEJS2" + ], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { + "name": "dvdbounce.app.js", + "url": "dvdbounce.app.js" + }, + { + "name": "dvdbounce.img", + "url": "dvdbounce-icon.js", + "evaluate": true + } + ], + "screenshots": [ + { + "url": "screenshot.gif" + } + ] +} \ No newline at end of file diff --git a/apps/dvdbounce/screenshot.gif b/apps/dvdbounce/screenshot.gif new file mode 100644 index 000000000..6c82438bc Binary files /dev/null and b/apps/dvdbounce/screenshot.gif differ diff --git a/apps/dwm-clock/ChangeLog b/apps/dwm-clock/ChangeLog new file mode 100644 index 000000000..7727f3cc4 --- /dev/null +++ b/apps/dwm-clock/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor code improvements diff --git a/apps/dwm-clock/README.md b/apps/dwm-clock/README.md new file mode 100644 index 000000000..619cdccd0 --- /dev/null +++ b/apps/dwm-clock/README.md @@ -0,0 +1,7 @@ +A clock with a daylight world map + +The function for the daylight graph is a crude approximation for an equirectangular projection of a circle on a sphere. + +You can change the longitudinal map offset by swiping the map sideways. For saving the changes to the file dwm-clock.json press the top left quarter of the screen. To discard changes press the top right quarter. + +If you are interested in changing the vector font to another one, please do. diff --git a/apps/dwm-clock/app-icon.js b/apps/dwm-clock/app-icon.js new file mode 100644 index 000000000..d3e9182e2 --- /dev/null +++ b/apps/dwm-clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/AoPg/8fAYM5sGt4FakFjsFKgHGv9rsEpoHOkGl0ExsHvj0AkQAugHyAYMv/4ECkX/BwfwAogACgQDCl8PBQnwBQZ3BC4opDAAg0BGIP/+CVCAoPykAiBiIABjOZBwIMB/8PUYQAJ4AYCiuZzLLQCwUYgEEgGZuAXQ5bEBqMAgeTChaHCiMc2cRmFTBQMJ0AXMl8Rj0QqALEyAwL+QXBGAIKFhJHOjcMglz4EMjew4IwJWILVBiMWPQNOstABgMBsxFJkAvCiNsmc+AgPqSIJIIe4KPEBAMG9FstQFCJA/y/4WBC4PNBo0IhQXICwUijy3FglggtKjIXMBYsKwAaBjPwRIICBIgYXJgGFgHBiMvZ4QtCgQXLhXOSgOfQ4TTCAoR3CuAYGhgXBySiBCgIvB+AXEisA8AXEgIXBiUvkUvLwP/AoIXDAATtCAAPhC4QOBL4Mvh//SAYXHhwHCjIPC+Hwh8CkCTCC4cWFwoXEJAIUCPgPyC4/RBAeSLIZiCAYIvIgO0C45LCI5WLR4RIEAAMgIwLaBC4cbCwMIigXI/4lCbQIXDiLVCjdhMA7NEO4kRmEACogXF+AXDgQOEiEGCwrBFC5MeXoYXHl7uBDQJHFABKoFYQQXOYIzXFABUSC4rXFABTZFC6JIHC6EZDAwXPJI4PIilBAgdEAAOZz//UwIXCj3r3YABxGwcgVLwDqDgF5zOQgEPj3QBYgANhOZzMwCyQA/ABYA=")) diff --git a/apps/dwm-clock/app.js b/apps/dwm-clock/app.js new file mode 100644 index 000000000..6d9bd3767 --- /dev/null +++ b/apps/dwm-clock/app.js @@ -0,0 +1,224 @@ +// daylight world map clock +// equirectangular projected map and approximated daylight graph + +// load font for timezone, weekday and day in month +require("FontDennis8").add(Graphics); + +const W = g.getWidth(); +const H = g.getHeight(); + +const TZOFFSET = new Date().getTimezoneOffset(); + +const UTCSTRING = ((TZOFFSET > 0 ? "-" : "+") + + ("0" + Math.floor(Math.abs(TZOFFSET) / 60)).slice(-2)) + + (TZOFFSET % 60 ? Math.abs(TZOFFSET) % 60 : ""); + +function getMap() { + return { + width: 176, height: 88, bpp: 1, + transparent: 1, + buffer: require("heatshrink").decompress(atob("/4A/AA0Av+Ag4UQwBhDn//1//8///AUI3MAhAUBgIQBh4LC/kfCg34rmAngVD/1/CYICBA4IAF8EOwF/+AVCAAXj//AA4PjDQIVDgkQj/4gBtEx+EGgXwCoJ8Bv+8geQgIVE4P/553Egf/nwFCgUE4H8gBqB/0AhLxHggFE+E8gJoBDIIAI5wFE4F8h/4v5FBABA2BAAUf7n+VYXgoAVNn/Dv+fCoPACo8MEQPHHAUf4DuB//58FgCgsHeoWfMgUDConw4AVFh/wXIRDDwBWC8jfBFY3xaAa5DYYXkKw8D+YVDHAcXAwKuIgIUDSIIJCsYVKeAIVHj5fGNogVHgN/AwPyEgPhCokZCo40D8E0wcwTYhsECoY0D8H2hEACocBCoqnCKwQVB/nICokJ+4VL/RGBQQkdw4VESQTwCDgIVBNgkeEQaSEQQReC4QrEhwUECoUECooAFVwoABgF+CoY+DAYZAFAAOgv4VGoFgCpXwGIoABkEHDQUvCo9zD4YVE4EIgIUGCoNnZwYVCiEP8E8hYVH/kHII0Qj/wvkP94WH4IVGhE/MQMH54VH+IVGKYIJBgfnCo/98IVFcYP5/9HMYbdGn7FFv/4/9vCpH/4DmC4AVCD4P/n4VKUoXgCwQ2Cz42CCpX//BtCCoMeCpJTBZgcAgYFCjElCpA7BEIQVBZoeYp4sICoIQCIIJzC/+Mp+DCpJSC/kAj4KC5/f4GfK5AVIeYPgNpIVEIIf/6f/v6ZHPwYVG//7V5BtDCoMOEof+jYVH8AVFhgLD/EZCo6UBCokYBYa2BCp04G4oVJNAX+gF4XYqDHCoKqCCoIrDAoL9DCowfCB4N9CorMDCooPEfowVMB4IVPeAQABwIVPeAQABw4LEg/ANo/wTAQAI8E//YVS+F//IIGGg4AFCo7OHAAf+v/jCowqM//HAwvhCpuPOwwVNAAwrOAA3xCqhtOAH4AfW4wAN/0/A4sP//AgFygYVH/V/AwlwgE8gAACDYIAF9ArC+uACAUgCocAHIn8k/gj4FBCgYAGBoXwgEYDof+ChMAJ4PmAwcBDgIUKgANBJIkZ/0cCpYrBIAIADzkwChQ5B/tgBAh7FNpANMAGg=")) + }; +} + +const YOFFSET = H - getMap().height; + +// map offset in degree +// -180 to 180 / default: 0 +function getLongitudeOffset() { + return require("Storage").readJSON("dwm-clock.json", 1) || {"lon": 0}; +} + +function drawMap() { + g.setBgColor(0, 0, 0); + + // does not flip on it's own, but there is a draw function after that does + g.drawImages([{ + x: -lonOffset * W / 360, + y: YOFFSET, + image: getMap(), + scale: 1, + rotate: 0, + center: false, + repeat: true, + nobounds: false + }], { + x: 0, + y: YOFFSET, + width: getMap().width, + height: getMap().height + }); +} + +function drawDaylightMap() { + // number of xy points, < 40 looks very skewed around solstice + const STEPS = 40; + const YFACTOR = getMap().height / 2; + const YOFF = H / 2 + YFACTOR; + var graph = []; + + // progress of day, float 0 to 1 + var dayOffset = (now.getHours() + (now.getMinutes() + TZOFFSET) / 60) / 24; + + // sun position modifier + var sunPosMod; + + var solarNoon = require("suncalc").getTimes(now, 0, 0, 0).solarNoon; + + var altitude = require("suncalc").getPosition(solarNoon, 0, 0).altitude; + + // this is trial and error. no thought went into this + sunPosMod = Math.pow(altitude - 0.08, 8); + + // switch sign on equinox + // this is an approximation + if (require("suncalc").getPosition(solarNoon, 0, 0).azimuth < -1) { + sunPosMod = -sunPosMod; + } + + for (var x = 0; x < (STEPS + 1) / STEPS; x += 1 / STEPS) { + // this is an approximation instead of projecting a circle onto a sphere + // y = arctan(sin(x) * n) + var y = Math.atan(Math.sin(2 * Math.PI * x + dayOffset * 2 * Math.PI + // user defined map offset fixed offset + // v v + + 2 * Math.PI * lonOffset / 360 - Math.PI / 2) * sunPosMod) + * (2 / Math.PI); + // ^ + // factor keeps y <= 1 + + graph.push(x * W, y * YFACTOR + YOFF); + } + + // day area, yellow + g.setColor(0.8, 0.8, 0.3); + g.fillRect(0, YOFFSET, W, H); + + // night area, blue + g.setColor(0, 0, 0.5); + // switch on equinox + if (sunPosMod < 0) { + g.fillPoly([0, H - 1].concat(graph, W - 1, H - 1)); + } else { + g.fillPoly([0, YOFFSET].concat(graph, W, YOFFSET)); + } + + drawMap(); + + // day-night line, white + g.setColor(1, 1, 1); + g.drawPoly(graph, false); +} + +function drawClock() { + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // clock text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Vector", 58); + // with the vector font this leaves 26px above the text + g.drawString(require("locale").time(now, 1), W / 2, 24 - 2); + + + // timezone text + g.setFontAlign(-1, 1); + g.setFont("6x8", 2); + g.drawString("UTC" + UTCSTRING, 3, YOFFSET); + + + // day text + g.setFontAlign(1, 1); + g.setFont("Dennis8", 2); + g.drawString(require("locale").dow(now, 1) + " " + now.getDate(), + W - 1, YOFFSET); +} + +function renderScreen() { + now = new Date(); + + drawClock(); + drawDaylightMap(); +} + +function renderAndQueue() { + /*timeoutID =*/ setTimeout(renderAndQueue, 60000 - (Date.now() % 60000)); + renderScreen(); +} + +g.reset().clearRect(Bangle.appRect); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.setBgColor(0, 0, 0); + +var now = new Date(); + +// map offsets +var defLonOffset = getLongitudeOffset().lon; +var lonOffset = defLonOffset; + +//var timeoutID; +var timeoutIDTouch; + +Bangle.on('drag', function(touch) { + + if (timeoutIDTouch) { + clearTimeout(timeoutIDTouch); + } + + // return after not touching for 5 seconds + timeoutIDTouch = setTimeout(renderAndQueue, 5 * 1000); + + // touch map + if (touch.y >= YOFFSET) { + lonOffset -= touch.dx * 360 / W; + + // wrap map offset + if (lonOffset < -180) { + lonOffset += 360; + } else if (lonOffset >= 180) { + lonOffset -= 360; + } + + // snap to 0° longitude + if (lonOffset > -5 && lonOffset < 5) { + lonOffset = 0; + } + + lonOffset = Math.round(lonOffset); + + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Dennis8", 2); + // could not get ° (degree sign) to render + g.drawString("select lon offset\n< tap: save\nreset: tap >\n" + + lonOffset + " degree", W / 2, 24); + + drawDaylightMap(); + + // touch clock, left side, save offset + } else if (touch.x < W / 2) { + if (defLonOffset != lonOffset) { + require("Storage").writeJSON("dwm-clock.json", {"lon": lonOffset}); + defLonOffset = lonOffset; + } + + renderScreen(); + + // touch clock, right side, reset offset + } else { + lonOffset = defLonOffset; + renderScreen(); + } +}); + +renderAndQueue(); diff --git a/apps/dwm-clock/app.png b/apps/dwm-clock/app.png new file mode 100644 index 000000000..cf9a16fbf Binary files /dev/null and b/apps/dwm-clock/app.png differ diff --git a/apps/dwm-clock/metadata.json b/apps/dwm-clock/metadata.json new file mode 100644 index 000000000..2a03c396c --- /dev/null +++ b/apps/dwm-clock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "dwm-clock", + "name": "Daylight World Map Clock", + "shortName": "DWM Clock", + "version": "0.02", + "description": "A clock with a daylight world map", + "readme":"README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"dwm-clock.app.js","url":"app.js"}, + {"name":"dwm-clock.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"dwm-clock.json"}] +} diff --git a/apps/dwm-clock/screenshot.png b/apps/dwm-clock/screenshot.png new file mode 100644 index 000000000..a153c655d Binary files /dev/null and b/apps/dwm-clock/screenshot.png differ diff --git a/apps/edgeclk/ChangeLog b/apps/edgeclk/ChangeLog new file mode 100644 index 000000000..72bb39ab1 --- /dev/null +++ b/apps/edgeclk/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial release. +0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps. +0.03: Added option to display weather. +0.04: Added option to display live updates of step count. diff --git a/apps/edgeclk/README.md b/apps/edgeclk/README.md new file mode 100644 index 000000000..51747780f --- /dev/null +++ b/apps/edgeclk/README.md @@ -0,0 +1,32 @@ +# Edge Clock + +![Screenshot](screenshot.png) +![Screenshot](screenshot2.png) +![Screenshot](screenshot3.png) +![Screenshot](screenshot4.png) + +Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability. +It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging. +There are three progress bars that indicate day of the week, time of the day, and daily step goal. +The watch face is monochrome and allows for applying your favorite color scheme. + +The appearance is highly configurable. In the settings menu you can: +- De-/activate a buzz when the charger is connected while the watch face is active. +- Decide if month or day should be displayed first. +- Switch between 24h and 12h clock. +- Hide or display seconds.* +- Show AM/PM in place of the seconds. +- Show weather temperature and icon in place of the seconds. +- Set the daily step goal. +- En- or disable the individual progress bars. +- Set if your week should start with Monday or Sunday (for week progress bar). +- Toggle live step count updates.* + +*) Hiding seconds and leaving live steps off should further reduce power consumption as the draw interval is prolonged as well. + +The clock implements Fast Loading for faster switching to and fro. + +## Contributors + - [tinxx](https://github.com/tinxx) + - [peerdavid](https://github.com/peerdavid) + diff --git a/apps/edgeclk/app-icon.js b/apps/edgeclk/app-icon.js new file mode 100644 index 000000000..b81918b73 --- /dev/null +++ b/apps/edgeclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE")) diff --git a/apps/edgeclk/app.js b/apps/edgeclk/app.js new file mode 100644 index 000000000..79310c3da --- /dev/null +++ b/apps/edgeclk/app.js @@ -0,0 +1,350 @@ +{ + /* Configuration + ------------------------------------------------------------------------------*/ + + const settings = Object.assign({ + buzzOnCharge: true, + monthFirst: true, + twentyFourH: true, + showAmPm: false, + showSeconds: true, + showWeather: false, + stepGoal: 10000, + stepBar: true, + weekBar: true, + mondayFirst: true, + dayBar: true, + liveSteps: false, + }, require('Storage').readJSON('edgeclk.settings.json', true) || {}); + + /* Runtime Variables + ------------------------------------------------------------------------------*/ + + let startTimeout; + let drawInterval; + + let lcdPower = true; + let charging = Bangle.isCharging(); + + const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA'); + + const iconSize = [19, 26]; + const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA='); + const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA='); + + + /* Draw Functions + ------------------------------------------------------------------------------*/ + + const drawAll = function () { + const date = new Date(); + + drawDate(date); + if (settings.showSeconds) drawSecs(date); + drawTime(date); + drawLower(); + }; + + const drawLower = function (stepsOnlyCount) { + if (charging) { + drawCharge(); + } else { + drawSteps(stepsOnlyCount); + } + + drawWeather(); + }; + + const drawWeather = function () { + if (!settings.showWeather){ + return; + } + + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + + try{ + const weather = require('weather'); + const w = weather.get(); + let temp = parseInt(w.temp-273.15); + temp = temp < 0 ? '\\' + String(temp*-1) : String(temp); + + g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true); + + // clear icon area in case weather condition changed + g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight()); + weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14); + + } catch(e) { + g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true); + } + }; + + const drawDate = function (date) { + const top = 30; + g.reset(); + + // weekday + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, -1); // left top + g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true); + + // date + g.setFontAlign(1, -1); // right top + // Note: to save space first and last two lines of ASCII are left out. + // That is why '-' is assigned to '\' and ' ' (space) to '_'. + if (settings.monthFirst) { + g.drawString((date.getMonth()+1).toString().padStart(2, '_') + + '\\' + + date.getDate().toString().padStart(2, 0), + g.getWidth(), top + 12, true); + } else { + g.drawString('_' + + date.getDate().toString().padStart(2, 0) + + '\\' + + (date.getMonth()+1).toString(), + g.getWidth(), top + 12, true); + } + + // line/progress bar + if (settings.weekBar) { + let weekday = date.getDay(); + if (settings.mondayFirst) { + if (weekday === 0) { weekday = 7; } + } else { + weekday += 1; + } + drawBar(top, weekday/7); + } else { + drawLine(top); + } + }; + + const drawTime = function (date) { + const top = 72; + g.reset(); + + const h = date.getHours(); + g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9) + g.setFontAlign(-1, -1); // left top + g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0), + 0, top+12, true); + g.setFontAlign(0, -1); // center top + g.drawString(':', g.getWidth()/2, top+12, false); + const m = date.getMinutes(); + g.setFontAlign(1, -1); // right top + g.drawString(m.toString().padStart(2, 0), + g.getWidth(), top+12, true); + + if (settings.showAmPm) { + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true); + } + + if (settings.dayBar) { + drawBar(top, (h*60+m)/1440); + } else { + drawLine(top); + } + }; + + const drawSecs = function (date) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(1, 1); // right bottom + g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true); + }; + + const drawSteps = function (onlyCount) { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const steps = Bangle.getHealthStatus('day').steps; + const toKSteps = settings.showWeather ? 1000 : 100000; + g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'), + iconSize[0] + 6, g.getHeight() - 1, true); + + if (onlyCount === true) { + return; + } + + const progress = steps / settings.stepGoal; + if (settings.stepBar) { + drawBar(g.getHeight() - 38, progress); + } else { + drawLine(g.getHeight() - 38); + } + + // icon + if (progress < 1) { + g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]); + } else { + g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]); + } + }; + + const drawCharge = function () { + g.reset(); + g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9) + g.setFontAlign(-1, 1); // left bottom + + const charge = E.getBattery(); + g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true); + + drawBar(g.getHeight() - 38, charge / 100); + g.drawImage(plugIcon, 0, g.getHeight() - 26); + }; + + const drawBar = function (top, progress) { + // draw frame + g.drawRect(0, top, g.getWidth() - 1, top + 5); + g.drawRect(1, top + 1, g.getWidth() - 2, top + 4); + // clear bar area + g.clearRect(2, top + 2, g.getWidth() - 3, top + 3); + // draw bar + const barLen = progress >= 1 ? g.getWidth() : (g.getWidth() - 4) * progress; + if (barLen < 1) return; + g.drawLine(2, top + 2, barLen + 2, top + 2); + g.drawLine(2, top + 3, barLen + 2, top + 3); + }; + + const drawLine = function (top) { + const width = g.getWidth(); + g.drawLine(0, top + 2, width, top + 2); + g.drawLine(0, top + 3, width, top + 3); + }; + + + /* Event Handlers + ------------------------------------------------------------------------------*/ + + const onSecondInterval = function () { + const date = new Date(); + drawSecs(date); + if (date.getSeconds() === 0) { + onMinuteInterval(); + } + }; + + const onMinuteInterval = function () { + const date = new Date(); + drawTime(date); + drawLower(true); + }; + + const onMinuteIntervalStarter = function () { + drawInterval = setInterval(onMinuteInterval, 60000); + startTimeout = null; + onMinuteInterval(); + }; + + const onLcdPower = function (on) { + lcdPower = on; + if (on) { + drawAll(); + startTimers(); + } else { + stopTimers(); + } + }; + + const onMidnight = function () { + if (!lcdPower) return; + drawDate(new Date()); + // Lower part (steps/charge) will be updated every minute. + // However, to save power while on battery only step count will get updated. + // This will update icon and progress bar as well: + if (!charging) drawSteps(); + drawWeather(); + }; + + const onHealth = function () { + if (!lcdPower || charging) return; + // This will update progress bar and icon: + drawSteps(); + drawWeather(); + }; + + const onLock = function (locked) { + if (locked) return; + drawLower(); + }; + + const onCharging = function (isCharging) { + charging = isCharging; + if (isCharging && settings.buzzOnCharge) Bangle.buzz(); + if (!lcdPower) return; + drawLower(); + }; + + const onStep = function () { + drawSteps(); + } + + /* Lifecycle Functions + ------------------------------------------------------------------------------*/ + + const registerEvents = function () { + // This is for original Bangle.js; version two has always-on display: + Bangle.on('lcdPower', onLcdPower); + + // Midnight event is triggered when health data is reset and a new day begins: + Bangle.on('midnight', onMidnight); + + // Health data is published via 10 mins interval: + Bangle.on('health', onHealth); + + // Lock event signals screen (un)lock: + Bangle.on('lock', onLock); + + // Charging event signals when charging status changes: + Bangle.on('charging', onCharging); + + // Continously update step count when they happen: + if (settings.redrawOnStep) Bangle.on('step', onStep); + }; + + const deregisterEvents = function () { + Bangle.removeListener('lcdPower', onLcdPower); + Bangle.removeListener('midnight', onMidnight); + Bangle.removeListener('health', onHealth); + Bangle.removeListener('lock', onLock); + Bangle.removeListener('charging', onCharging); + if (settings.redrawOnStep) Bangle.removeListener('step', onStep); + }; + + const startTimers = function () { + if (drawInterval) return; + if (settings.showSeconds) { + drawInterval = setInterval( onSecondInterval, 1000); + } else { + startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000); + } + }; + + const stopTimers = function () { + if (startTimeout) clearTimeout(startTimeout); + if (!drawInterval) return; + clearInterval(drawInterval); + drawInterval = null; + }; + + + /* Startup Process + ------------------------------------------------------------------------------*/ + + g.clear(); + drawAll(); + startTimers(); + registerEvents(); + + Bangle.setUI({mode: 'clock', remove: function() { + stopTimers(); + deregisterEvents(); + }}); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} diff --git a/apps/edgeclk/app.png b/apps/edgeclk/app.png new file mode 100644 index 000000000..3a0bbe130 Binary files /dev/null and b/apps/edgeclk/app.png differ diff --git a/apps/edgeclk/metadata.json b/apps/edgeclk/metadata.json new file mode 100644 index 000000000..ef97b314b --- /dev/null +++ b/apps/edgeclk/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "edgeclk", + "name": "Edge Clock", + "shortName": "Edge Clock", + "version": "0.04", + "description": "Crisp clock with perfect readability.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"edgeclk.app.js", "url": "app.js"}, + {"name":"edgeclk.settings.js", "url": "settings.js"}, + {"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true} + ], + "data": [{"name":"edgeclk.settings.json"}] +} diff --git a/apps/edgeclk/screenshot.png b/apps/edgeclk/screenshot.png new file mode 100644 index 000000000..758dca96b Binary files /dev/null and b/apps/edgeclk/screenshot.png differ diff --git a/apps/edgeclk/screenshot2.png b/apps/edgeclk/screenshot2.png new file mode 100644 index 000000000..febac2d2c Binary files /dev/null and b/apps/edgeclk/screenshot2.png differ diff --git a/apps/edgeclk/screenshot3.png b/apps/edgeclk/screenshot3.png new file mode 100644 index 000000000..bdad9e1d5 Binary files /dev/null and b/apps/edgeclk/screenshot3.png differ diff --git a/apps/edgeclk/screenshot4.png b/apps/edgeclk/screenshot4.png new file mode 100644 index 000000000..66ec85c89 Binary files /dev/null and b/apps/edgeclk/screenshot4.png differ diff --git a/apps/edgeclk/settings.js b/apps/edgeclk/settings.js new file mode 100644 index 000000000..81a7acc5b --- /dev/null +++ b/apps/edgeclk/settings.js @@ -0,0 +1,133 @@ +(function(back) { + const SETTINGS_FILE = 'edgeclk.settings.json'; + const storage = require('Storage'); + + const settings = { + buzzOnCharge: true, + monthFirst: true, + twentyFourH: true, + showAmPm: false, + showSeconds: true, + showWeather: false, + stepGoal: 10000, + stepBar: true, + weekBar: true, + mondayFirst: true, + dayBar: true, + redrawOnStep: false, + }; + + const saved_settings = storage.readJSON(SETTINGS_FILE, true); + if (saved_settings) { + for (const key in saved_settings) { + if (!settings.hasOwnProperty(key)) continue; + settings[key] = saved_settings[key]; + } + } + + let save = function() { + storage.write(SETTINGS_FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Edge Clock' }, + '< Back': back, + 'Charge Buzz': { + value: settings.buzzOnCharge, + onchange: () => { + settings.buzzOnCharge = !settings.buzzOnCharge; + save(); + }, + }, + 'Month First': { + value: settings.monthFirst, + onchange: () => { + settings.monthFirst = !settings.monthFirst; + save(); + }, + }, + '24h Clock': { + value: settings.twentyFourH, + onchange: () => { + settings.twentyFourH = !settings.twentyFourH; + save(); + }, + }, + 'Show AM/PM': { + value: settings.showAmPm, + onchange: () => { + settings.showAmPm = !settings.showAmPm; + // TODO can this be visually changed? + if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false; + if (settings.showAmPm && settings.showWeather) settings.showWeather = false; + save(); + }, + }, + 'Show Seconds': { + value: settings.showSeconds, + onchange: () => { + settings.showSeconds = !settings.showSeconds; + // TODO can this be visually changed? + if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false; + if (settings.showSeconds && settings.showWeather) settings.showWeather = false; + save(); + }, + }, + 'Show Weather': { + value: settings.showWeather, + onchange: () => { + settings.showWeather = !settings.showWeather; + // TODO can this be visually changed? + if (settings.showWeather && settings.showAmPm) settings.showAmPm = false; + if (settings.showWeather && settings.showSeconds) settings.showSeconds = false; + save(); + }, + }, + 'Step Goal': { + value: settings.stepGoal, + min: 250, + max: 50000, + step: 250, + onchange: v => { + settings.stepGoal = v; + save(); + } + }, + 'Step Progress': { + value: settings.stepBar, + onchange: () => { + settings.stepBar = !settings.stepBar; + save(); + } + }, + 'Week Progress': { + value: settings.weekBar, + onchange: () => { + settings.weekBar = !settings.weekBar; + save(); + }, + }, + 'Week Start': { + value: settings.mondayFirst, + format: () => settings.mondayFirst ? 'Monday' : 'Sunday', + onchange: () => { + settings.mondayFirst = !settings.mondayFirst; + save(); + }, + }, + 'Day Progress': { + value: settings.dayBar, + onchange: () => { + settings.dayBar = !settings.dayBar; + save(); + }, + }, + 'Live steps': { + value: settings.redrawOnStep, + onchange: () => { + settings.redrawOnStep = !settings.redrawOnStep; + save(); + }, + }, + }); +}) 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..26fbf5ff0 --- /dev/null +++ b/apps/elapsed_t/ChangeLog @@ -0,0 +1,4 @@ +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 +0.04: Use new pickers from the more_pickers library, add settings to display seconds never/unlocked/always diff --git a/apps/elapsed_t/README.md b/apps/elapsed_t/README.md new file mode 100644 index 000000000..9e361be59 --- /dev/null +++ b/apps/elapsed_t/README.md @@ -0,0 +1,30 @@ +# 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. The seconds can be shown: +- always +- when the watch is unlocked +- never. + +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..910ff85f3 --- /dev/null +++ b/apps/elapsed_t/app.js @@ -0,0 +1,537 @@ +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: 1, + displayMonthsYears: true, + dateFormat: 0, + time24: true +}, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + +function writeSettings() { + require('Storage').writeJSON(APP_NAME + ".settings.json", settings); +} + +if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); +} + +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); +} + +let inMenu = false; + +Bangle.on('touch', function (zone, e) { + if (!inMenu && e.y > 24) { + if (drawTimeout) clearTimeout(drawTimeout); + showMainMenu(); + 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}\n${ampm}`; +} + +function getDatePickerObject() { + switch (settings.dateFormat) { + case 0: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: data.target.D, + min_1: 1, max_1: 31, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.D = v_1; data.target.M = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 1: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: data.target.M, + min_1: 1, max_1: 12, step_1: 1, wrap_1: true, + + value_2: data.target.D, + min_2: 1, max_2: 31, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.M = v_1; data.target.D = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 2: + return { + back: showMainMenu, + title: "Date", + separator_1: "-", + separator_2: "-", + + value_1: data.target.Y, + min_1: 1900, max_1: 2100, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.D, + min_3: 1, max_3: 31, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.Y = v_1; data.target.M = v_2; data.target.D = v_3; setTarget(true); } + }; + } +} + +function getTimePickerObject() { + var timePickerObject = { + back: showMainMenu, + title: "Time", + separator_1: ":", + separator_2: ":", + + value_1: data.target.h, + min_1: 0, max_1: 23, step_1: 1, wrap_1: true, + + value_2: data.target.m, + min_2: 0, max_2: 59, step_2: 1, wrap_2: true, + + value_3: data.target.s, + min_3: 0, max_3: 59, step_3: 1, wrap_3: true, + + format_2: function (v_2) { return (pad2(v_2)); }, + format_3: function (v_3) { return (pad2(v_3)); }, + onchange: function (v_1, v_2, v_3) { data.target.h = v_1; data.target.m = v_2; data.target.s = v_3; setTarget(true); }, + }; + + if (settings.time24) { + timePickerObject.format_1 = function (v_1) { return (pad2(v_1)); }; + } else { + timePickerObject.format_1 = function (v_1) { return (formatHourToAMPM(v_1)); }; + } + + return timePickerObject; +} + +function showMainMenu() { + E.showMenu({ + "": { + "title": "Set target", + back: function () { + E.showMenu(); + Bangle.setUI("clock"); + inMenu = false; + draw(); + } + }, + 'Date': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).date, + onchange: function () { require("more_pickers").triplePicker(getDatePickerObject()); } + }, + 'Time': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).time, + onchange: function () { require("more_pickers").triplePicker(getTimePickerObject()); } + }, + 'Reset': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(false); + 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(); + target.setSeconds(0); + Object.assign( + data, + { + target: { + isSet: false, + Y: target.getFullYear(), + M: target.getMonth() + 1, // Month is zero-based, so add 1 + D: target.getDate(), + h: target.getHours(), + m: target.getMinutes(), + s: 0 + } + } + ); + } + + writeData(); +} + +var target; +setTarget(data.target.isSet); + +var drawTimeout; +var temp_displaySeconds; +var queueMillis; + +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 updateQueueMillisAndDraw(displaySeconds) { + temp_displaySeconds = displaySeconds; + if (displaySeconds) { + queueMillis = 1000; + } else { + queueMillis = 60000; + } + draw(); +} + +Bangle.on('lock', function (on, reason) { + if (inMenu || settings.displaySeconds == 0 || settings.displaySeconds == 2) { // if already in a menu, or always/never show seconds, nothing to do + return; + } + + if (on) { // screen is locked + updateQueueMillisAndDraw(false); + } else { // screen is unlocked + updateQueueMillisAndDraw(true); + } +}); + +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 = new Date(target.getTime()); + end = new Date(now.getTime()); + } else { + start = new Date(now.getTime()); + end = new Date(target.getTime()); + } + + // Adjust for DST + end.setMinutes(end.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset()); + + 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(); + 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"); + +switch (settings.displaySeconds) { + case 0: // never + updateQueueMillisAndDraw(false); + break; + case 1: // unlocked + updateQueueMillisAndDraw(Bangle.isBacklightOn()); + break; + case 2: // always + updateQueueMillisAndDraw(true); + break; +} 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..2515e0e79 --- /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.04", + "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..4726516d5 --- /dev/null +++ b/apps/elapsed_t/settings.js @@ -0,0 +1,63 @@ +(function(back) { + var APP_NAME = "elapsed_t"; + var FILE = APP_NAME + ".settings.json"; + // Load settings + var settings = Object.assign({ + // default values + displaySeconds: 1, + displayMonthsYears: true, + dateFormat: 0, + time24: true + }, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); + } + + var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]; + var displaySecondsFormats = ["Never", "Unlocked", "Always"]; + + // Show the menu + E.showMenu({ + "" : { "title" : "Elapsed Time" }, + "< Back" : () => back(), + 'Show\nseconds': { + value: settings.displaySeconds, + min: 0, max: 2, wrap: true, + onchange: v => { + settings.displaySeconds = v; + writeSettings(); + }, + format: function (v) {return displaySecondsFormats[v];} + }, + '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 new file mode 100644 index 000000000..9cb404008 --- /dev/null +++ b/apps/encourageclk/ChangeLog @@ -0,0 +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/README.md b/apps/encourageclk/README.md new file mode 100644 index 000000000..420eddbcc --- /dev/null +++ b/apps/encourageclk/README.md @@ -0,0 +1,18 @@ +# Encouragement & Positivity Clock + +Tap on the watch for a note of encouragement + +## Features + +Pretty backgrounds + +Screenshot 2023-03-28-2 +Screenshot 2023-03-28-1 + +## Requests + +If you have any issues or would like to suggest an encouraging note, please tweet me! + +## Creator + +[Eleanor Tayam](http://twitter.com/elykittytee) diff --git a/apps/encourageclk/app-icon.js b/apps/encourageclk/app-icon.js new file mode 100644 index 000000000..56a3c6b6f --- /dev/null +++ b/apps/encourageclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwMB/4AFgYCB4H//kAAoMAn/w+IFBx8P8fjAoPH4/n4/gg/j8/Px4rB+Pz58ch/wnHzz0wv/+hl5zlhDoOGnOY44FB8cZyOP/1/+OJwcfAoP44OGn4FB/lh5giBAIMz7n/AoP/nf4Aocf/IFDz5YBAoWP+YFD54FFMgIFD84FD84FM/0AApKfDApiaCAAJBCApKyCWgRlBAAWfOIIACj/8Aoc//g/BJ4KTBn4FBBIUfAoIbCx4CBFoUHAQPgDIMhAoOEV4NwVgMOn/4/jdBn8fDILpBUIfwh5TBIAYABA=")) diff --git a/apps/encourageclk/app.js b/apps/encourageclk/app.js new file mode 100644 index 000000000..1f19cc314 --- /dev/null +++ b/apps/encourageclk/app.js @@ -0,0 +1,72 @@ +//230404 + +require("FontHaxorNarrow7x17").add(Graphics); +require("FontDylex7x13").add(Graphics); + +const locale = require("locale"); +const dateutil = require("date_utils"); +const currentFont=g.getFont(); + +const width = 175; +const height = 175; +const offset = 25; + +var d = new Date(); +var nowDate = d.getDate(); //today's date +var drawTimeout; +var encourage = ["you\'re doing\ngreat!","pas de deux it!","you\'re amazing~","you got dis","keep going","you\'re one\nin a melon!","we\'re rooting\nfor you!","believe in\nyourself","dance like\nno one\'s\nwatching"]; + +var bgimg = { + width : 175, height : 175, bpp : 4, + transparent : -1, + palette : new Uint16Array([46452,38393,32024,42585,52984,46739,59130,21489,46543,21493,25358,52696,48564,61210,29944,46322]), + buffer : require("heatshrink").decompress(atob("1UiAAUZ1QGEAAUhzWvBI0izIAY7tm7vWswAGtvTCo+Q7e23e263dDgNtDgVq0RCD9Wq1xMGl2qCAgJDKzGbHAI/BKw9mlIWHle7AAIXDAQJ1CU42quKvH1UhK8GS7u3lqXCWYnWDBcG3e9te77vb3oZBVwoABy6lIvRhHl3uABXpHpXiSgOy83bTYRaC6YYB2ZqEpe7A4OQKgIAE6Wd7quGkN68RNGiWqiQJGABceKxXhtZQBi9NHwVtK4QOBzvZCgc9BQIHBVwIAE216tvaUw/q17+HvXhK6UiVAspAYSABjbmBo3d3dm3tpBoPp9s5Ngud63Ws1iAwO2swAB7d3AAKuGJoWqySZG04IGABkuz3u93pKIPp9IcC8Wz2dtIoOxkWyJ4dCAQMhA4crXYXpzOSgMRiMQk5WBvSuHkPq1SvGkOqiSvTMZfuyZECUwoACyz/BAAXtCIPSCA3n06uJl2a1VxBI16yJXeEQMTiMTjy/BAAniYoJJBylEzIWCNJGXVxClBl+qcoIAEiRrIWDQABBZBHCz3d7xODzwWC9JXEIRUhMYL/FMAJXgABChIAAndAAPWBAmaVxB3C1WnWAsq1xWol2eKpWe3pXCtORiMSkPpTJchK4KvEW4OnV9MeK5VtswAB62RLYXbzWulyiFPYni1UhBAnq0JXpl3uAAUuVonW6xWBttr3e72xYBJAwAGjOpMguqyRXpAAkpKwc9KwSuBK4dm7Whmcx9weJkOq+USAwfq8JWt9xXD7pVCV4XdK4IAB1UTiIABaZV604HEzWiKtnjom72OR2ytDAAPdWAdq0MxiMzmQhJj2q1y2F8SusUYPdt1tKwoAB6yuHmIhK9WqiQGEuMuXoZdk91tJQIBEAApjBlOZzSuCiMTJIhXHu5LEu9yCgcRK0fhnr5BfofdKIQJCAwPR9OZVyEijOpKAUukd6iZeBl0TmKvil1NVAtt6xWCK4Nt7e7syuFiTxEA4JDB9xkCiOquITCmMq0YLDiIaDAAhgEAgZpQ9ZWEKIO27tr3e7AgmwgCuEDokhmamCAgIABm+qAoUxmeqiIFBiMTmSuhkW9KwZbBte2sxRB3a0BAgVmtSuDAB0x1WjCgUTlUjBgnuAAPiL4IICUwMu8QsDmSvO8KhBfYICBUoQAD7q3CAASuEABsTj2q1xICmN3uZdDCxEuf4INEAYJWNl0bUwO2VgRUDWoKtBtpWD7SuSV4YFCmZyFmYACKwsuiYIEicyVxxPDVYu9KAJZBAAiuSdIeq+ZuClSvDACMzKxoAB2xUFKoJTGAAVqVyY5BjWqkIXBV4IECCZK1FAAUSKxyvDAAe27vWK5CuVic61V2KQMT9UjDacukXiAQIAL9xYE7pVKVyw7C1QYCmMq0YdSmXukKxM9xlBjarCtpXBJ4QEBLoIDDVypXBmGqVQUz1QaTiUhmczjyvMVwdtKYICCAAa1D7SuVAAU60czIQMquwcVmPiVpNN6avB2xYCWIKvB7pgDAYauWVQWayQ9BmOn8J2UiauJ8M27s+AgJXFtvWtYGCAYdqVzCqB16vBi16OyquJl297quBkXi6yvFKQYAEVzA6BjWq8avBlWjO6auKkQNBAgUhiQDBjZXBKxG6VzERmfq1RbBmYDCOaZWJ9wABAovuKY6udSYM/1UjAgMqAYIdViT8BkUuKgoGCBgvhte2j3u9a0BtfqVzKTCu9zmcxAYIhWK4XincznceVYUbmexLAZZBkIVBBAIbCjyuaSIMzDoQDDDqpQBl0t7vd7ZPC8YGB3xVCWIjDFlWjVzI6ClRUClSvXDwMu7u73vWiUj61tswCBpxRCAAXr3a/CAAOqc4IpLmczHBkx1WpWYUhK6I1DFYUWte7s3Umdt7tmsy2BkMSWAk72K/CkSuBK5gqBK5qvCCIPqkZWQinTAYXdsVN3YAC7pVBAQJWBBAKvEly1FGwREJKoS9NV4MivVymJ7BV6CuFGARNBVgPd6xeDAAL/EAAquJEYJWEAoJjMAQOq0UT1UhK6AjFAQRWBswABtZWE2b/DAAyuJKggADKxKUE1WqscauxWUE4ipFAAViz3uVxhQJLBJVFMQky093kWn8J8HACEbKguzXQIiBiSvJVwJWRUgJWHfoczsWq+2qkavYAA0u93k7e2jyuLADYyEiGq3+v/5pBMYYAYmPujtr3cykZYBl0hBwceVwJWgie61cK1WiVrsTiXh7e7mMiAIIABdQcxVzo0GmfP1QAC0IOHLCoNNVzFGsgLJnn6kUhid6uVxNJIAfjSuXmndsYMJ5+joczpywBgbfCK8yuWinWtvW6cTmMTVw4ECmMzmWq16wnVy00tvds3dskzoilBVwwGEj8H0UQgCu7mhVBAANt6M96yuKAAU2lWq1WhV8kaEwMzs1jKx8xKAJXC61GLoKvFVwwXBkIxBkUhBARNWiYlEVwk0maUHVx9m7vWtvUFIiuHC4ICBn+q+cxH4RNQCgIDBKwoGCVwOm7sxToXdH4oAHVogADttBGAVNmKuHAAdC1QAB19DIoKkPIwLuKVwNt6zpBdwPdslEChFEo0ztpWGswcCBwPWpiuHAAcXkH/vWqkY/HdKAAFnWm63WTodt7tjCQ9NXYM2Vw9m7szm3dEIMPVxUzoc82mq05uBAAiqGACKuBTQQDBV4JJBsJeBEoa/BCIPdKQYUBBIIICAQNtsH6igGBJIwADhWjlWvJ6wAFifa1fdHwaYD7s07tkRYKtBXYQQEAYRSCN4SuC0NNs3UGpMw/9Bv8hiJXbpuq2zxCtvb3e7KwIAC6kd6JTCA4INBK4QUB3vW3YNBDISuCBgI1Kh9zmcx1WjKzXdtWrIoXdtZWBBwk0AQMbB4INCKAIWCtZyB3YGBOQSuB1YEB3e2ZQKuH+4DBmMR/WhCBAAOHwSuEHwKMJ3amCAANmV4SuCJYKuCAgKuBCQRdBVxYABn+qkcTKys7RoPa0lkogALtpUDAAVtNQQAJ37yNVwZYEuywOmKaGHwOqoxWMo1mpe72lEAYKvBKxe7/Q+NVwgABiWii8jA4c0HASmEnZmDnezGAVqVxwAFK4QAE2ZHG56uUmc2hWqKwhCCskTUwe9oJWCHIiuOABjjFRIU8/Q1DVxqjDom61VNFY1mohOE2iRG3SuUAAxDEmhdCVyaPDs36/gqGo3UKApdFVzpdIVwNGoawDUQjyCg/0Rg1K/v6U4IATpSubABG/EptAv4JH1/a1YxVV0TiBpiuBCBiuBSo/91ULS6iul56uYsnK/qu4oiuZ+3U1/UV2PWswACtohBVwPdVy1NpUGVytNHQYABBIIGEMgYEDoxQEm8iAAdw7quBkV0SoYqFs3Ug/9RgtGtX9/WgVyQiBtWkVrVgu4ADvV0VwOnu8EC5Wwu6uI7Wqho3RozmB1Q6EAAMN6gGEund7vTu99AgPSCwwAFvf6AYN2CgPQBw1+3+nhvdo3dWQPQvX91SuGBwIFD3YAGte6RAIAFgEnAwkggAICAYMAKxl3I4IgCChJmCgUGmUjs1Au+vm6uHqEWAgVLKou02UGVw4AcVwZmO0/e1Ut6env4IBVwlmo1Y062FAAlku/a0FVAENQ34lNgH3AYNUqsFC4NV19VhWq4B2Bg93xGI+4RBABWqBpgAWrf6EpsHutFA4lFrX1qv60H61X/u94K4N3EJda0pWiqu/EptQIQ1UxWnw93VIOq1WshnQ1AJBVxhWjVwIPNg5mHK4KmBx96v+q4HEh9F+91V36uHAAN6fwdw5WsgHw5vYVxtUZ4iusrkPfQJOCAgYFBvWmt/MVwMAgG8Bod1oodBAQSuC1FYB4eIV1nAKg+Ix/3VgNG5///hWBh/4x4TBw+FJQ+qBgIACvV1V1kcxGHKogGBHQOr7cL1W7KwMA1QLCvADCu+oV4da04cBvCrdVwgrDbAN3cwVFxGFwCmBvWqTIWHv8P1Wq1+q/WqVwMLt64BK4RKDqiuEvAdBwpWfVwMFAwadCA4RFBU4X3J4IEBLgOvBgIAD1/Ah9N572DJRCuDXYYAERgQAOqgbFn+nAYL1Dv6xBWQOq06UBVwWq5nLKgWm7nMoGv3nAgH07AeCw6jBJQ57Dw4yDAAV3bIQAOrF4LAIFBxn3TYX/xH/+5NB14DCu/61HyBAJUBAIPw/W2hnP+/8gAAB4gsCx5PCLA2KVwQAIW5AAJQAJ0DQ4QvBMISkCXAL3D//85nPAwW97vA+HA1W8KwUIKw4AGPYI7DACwaBJQIcCxRMDAQJVDAAWs5gABf4QAB/vdhgKC3+g4BVBhkK/AvFfQ2PvQuCTphZPv57DPgICCAAJWEVQIACA4ev7fPBIXw1XwVoS7BdwoDBIIuHFwYAKCgpfGPQoABUwoAE/gKJABGsVwX604mBwrEBRxGKVyQEDZ4QNBVQf3f4JWD14DDTgL+D1+wgH3WQYAF/QVB4BXBs3//AqBDgOoKw6uNYQzQFB4uHKgL9D17nBAAPMAAKeD3vd6nL3e73nL5m7CAWw1+8KwUG7//JYauIx96VwZOFNRAEDVQZpDdZH7IQKrD03W6nwBIJIBAA8LhWsAoUM1XP1CJBKoP3JJCuMAApvEK4iqBAIIADdIIFDKwP3+H/s1m3hUJKAX/v6tCgHP1n6/4tB/GIvGPAYIAE/SuDABpyFAwZUB06rF5nLVQb/Dh//+BWLgHK1hWDK4PPFQN3rA2DKwv3VyBvGagKtDVAPKVYRTC19r3e7h7/CIgiuL1UMAwZtBVoLfCAA2HKgN6Vx4dHVwP3u6hC14BB5//A4UN7vd7cMh5UOVwegNIfLK4ItBSAwACKgSuMUwhZCEYN4xRyCfIev5jgBKoXWfhquO/fbVwPP094RYLlDxGFHYN/xSuK/5wDKQJWC1WnOQIDBKweqKof7he72BWVVw291X//n6JgP3AIOq1GPu/3VwN4VxRuBAAYbBOYJJBvADCAAfM5gEC03d36tWVw0L7WvVwKHBIYSNBWYIABVxisDAAJtBA4NaKYoAB//LKomwSQauah6uB5n//4pB095AYRWDUIKuJVoptCx/4Kw+s/TbB0H/s1sVi6uHhlg/RWBV4JTBVoJPDAYOIVyAJDKo385nKAgOv7vQKrKuGV4PW1W/LAivBy4+DVwb5DVgZWEBQSrH1/8Kodr3e7KrSuGgENtSEB//8bYKwCIASqCvWnJwWHA4N/VYitDZIRWF5//AgP65vW/5WbVw0As3616uBF4SlBAIQCBAARPGAAeHxBTHKoXM34FC3vb+BVcVw8A5sK1W8Q4YAEVwV3fggAF+93+5WJ54ECgELs3PKzquH/nbFwX856zBLA6tLMgKuD1/aVogEC1vd56teVxNs/SKC/nMAoIAEu9/VpZYCCQOsDQR2BKwWmsnMRQiujgEE6CME/4ADLAf4VpV4V4hxD5nPAYOg7vQVj6uJh+9oGvRwf854CB5RAC1BPBx93KQSuEKoYADOIP7VoXfKkCuKgEN77hC1WrSAI8BLYIIBv6kBx5SBJ4IDBUwZWHDAX73ZWjVwPMBI27tj/F/hXCAoOnUYKpCw4IBrADCAAwZBDAWg7nwV0oIGh/N2HM349DV4ShCKwN4KwOP1Wo+93WQKtIaAWtsBVjAAPP1/ABI0E7tP//HH4ev5nM/5OBu/4//4xWqx4HBVpLOC/9rVskAhkK/ivH21r3e8/SpCK4I9B5n4KAKnBWAOIVwl5zRWE/YEB03QK0quKLAO963w5hXDIYXIKIIFB1AEBVoJWC1WnAYSsB54EB0m8KsquB/SuHBYW821m7brDAARRBxQEDw5WE1QECVoP/OQOv7itmVxavB/nd7ts3dPJAapCxF/KoRWD36uBAoPP//LBIOt6BVmVwX0B5n7LIO/+HKegSqC/GIKwgAE1//54EBuFLK08A4H/+CvJAAXE3YAB236//3vCuFKw5XB5gDB1nQ+BXogn6/gPM///3vd7fM5aoBAAWqKxAVBKwWm3gjG5gAi3+g3nMLBkMCYNNs3XKwd605ZBAAv8//PWIXbVoziBAEPwRQTcP/fWsytE1WnVw3M//7AgOt7iJG5f6NowAfhhXOAAI5CVoIbEWQpJJKc45CVx4ACJ45eIAGX7KyMHJgpTBLo4ALFyIATEyhWFKg76D/4JE1//AAP85gAj5Ws4BWRhREE1KuBAoXL5nNAoWt7m7AAYxDVsnA1W8YaauFKwd3u/PAoX2EqYAbh/6/iuXK4JWEv/6AoOr6HwK92/1/AVy4ADKwN3AoX9p5VugEMVzRVCK40APaQAd56ubKYRXFPaSu01hWGK4d6BIWsV+HK0AyR1Wv/RWD05TCAYJWD1XwK9/M1UMVyOsKwOaVwZSCKwqvBV36uEKwN5JogAH+H/V18M1XMVyKlIAAZjB1Wv3hWvgHP14TRu+q05WC1/M+4IBAAO9KN4AF4GqRSEHKgJQC1//04FC08LpkM5iswAAMMhWsVyWXVwXM/WnVoUN56u/VxN3Von/KoWq/fb+BW0VwOrK6D+BVoRWB/RWCvlGDqCum1/wHJ8K05WB1/MK4PPVoXUVuoABhf6/gSP1V3KAOv1///gFC5tvK20A5+vVyZSC//KAgP86hW3hiuSAAn//QDBgncK26uC2CuPKwvPAYOstpW4VwP/VyWs////iuD6BX53Wg4CuO15UBAARVBA4Ku5K4OqhiuP/n8KwXKK4MLglgK/PKVyv7VwW936u75gROKwn/KwX77fwK/PP//8CBsPCIIAB/hWC2HN2BW5gGw1XACBv8K4e/K4P8/iu7hn6Vx5VCAAKuC5m2V3fP16uOKIKvBh4EB1nA6yt6VwX7VyX6K4MMhtgK3UA4H/0CvN56tBAAJWBCoNmK3cAgn6/iuQ/nKVwUGV3kA4+g2APMKwX//SuD6xW8hn6//AVx6tC1UAhqu9hnK1iuP/hVB18A228K3kA5mqIBiuGhf06HwV3sK0CuP/auC5+94Cu9gHP1+wVxxWB1X8/fcK30MJIMMVxn8/Su/K4u60BCLA==")) +}; +//TAP ALL THE THINGS +Bangle.on('touch', (n, e) => { + // <88, top + if (e.x < width && e.y > offset) { + g.setColor(0,0,0); + g.setFont("Dylex7x13",2).setFontAlign(0,0).drawString(getEncour(), width/2, height/2); + setInterval(draw,3000); + } +}); + +//getters +function getRandomInt(max) { + return Math.floor(Math.random() * max); +} +function getEncour(){ //return string + let rando = getRandomInt(encourage.length); + return encourage[rando]; +} +//everymine +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function draw() { + var time = locale.time(d, 1); + //var date = locale.date(d); + var mo = dateutil.month(d.getMonth() + 1, 1); + + g.drawImage(bgimg,0,offset); //bg + g.setFont("HaxorNarrow7x17").setColor(0,0,0); + g.setFontAlign(0, 0).setFont(currentFont, 7).drawString(time, width/2, 100); + g.setFontAlign(0,0).setFont(currentFont, 3).drawString(mo + " " + nowDate, width/2, 130); + + queueDraw(); +} + +//ready set go! +g.clear(); + +Bangle.setUI("clock"); //button +Bangle.drawWidgets(); //widgets +Bangle.loadWidgets(); + +draw(); //draw all the things diff --git a/apps/encourageclk/app.png b/apps/encourageclk/app.png new file mode 100644 index 000000000..6dc6b9270 Binary files /dev/null and b/apps/encourageclk/app.png differ diff --git a/apps/encourageclk/metadata.json b/apps/encourageclk/metadata.json new file mode 100644 index 000000000..f3816c9de --- /dev/null +++ b/apps/encourageclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "encourageclk", + "name": "Encouragement & Positivity Clock", + "shortName":"Encouragement Clock", + "version": "0.04", + "description": "Tap on the watch for a note of encouragement", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"encourageclk.app.js","url":"app.js"}, + {"name":"encourageclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/encourageclk/screenshot.png b/apps/encourageclk/screenshot.png new file mode 100644 index 000000000..2569b6291 Binary files /dev/null and b/apps/encourageclk/screenshot.png differ diff --git a/apps/entonclk/ChangeLog b/apps/entonclk/ChangeLog new file mode 100644 index 000000000..8c737cb68 --- /dev/null +++ b/apps/entonclk/ChangeLog @@ -0,0 +1,3 @@ +0.1: New App! +0.2: Now with timer function +0.3: Fix HRM diff --git a/apps/entonclk/README.md b/apps/entonclk/README.md new file mode 100644 index 000000000..c67cc19c8 --- /dev/null +++ b/apps/entonclk/README.md @@ -0,0 +1,21 @@ +Enton - Enhanced Anton Clock + +This clock face is based on the 'Anton Clock'. + +Things I changed: + +- The main font for the time is now Audiowide +- Removed the written out day name and replaced it with steps and bpm +- Changed the date string to a (for me) more readable string + +Timer function: +- Touch the right side, to start the timer +- Initial timer timeout is 300s/5min +- Right touch again, add 300s/5min to timeout +- Left touch, decrease timeout by 60s/1min +- So it is easy, to add timeouts like 7min/3min or 12min +- Special thanks to the maintainer of the a_clock_timer app from which I borrowed the code. + +Todo: +- Make displayed information configurable, after https://github.com/espruino/BangleApps/issues/2226 +- Clean up code diff --git a/apps/entonclk/app-icon.js b/apps/entonclk/app-icon.js new file mode 100644 index 000000000..9993b0871 --- /dev/null +++ b/apps/entonclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA==")) \ No newline at end of file diff --git a/apps/entonclk/app.js b/apps/entonclk/app.js new file mode 100644 index 000000000..d3e18ae91 --- /dev/null +++ b/apps/entonclk/app.js @@ -0,0 +1,127 @@ +// Fonts +Graphics.prototype.setFontAudiowide = function() { + var widths = atob("CiAsESQjJSQkHyQkDA=="); + var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA"); + var scale = 1; // size multiplier for this font + g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16)); +}; + +// Globals variables +var timervalue = 0; +var istimeron = false; +var timertick; + +// Functions +function getSteps() { + var steps = 0; + try{ + if (WIDGETS.wpedom !== undefined) { + steps = WIDGETS.wpedom.getSteps(); + } else if (WIDGETS.activepedom !== undefined) { + steps = WIDGETS.activepedom.getSteps(); + } else { + steps = Bangle.getHealthStatus("day").steps; + } + } catch(ex) { + // In case we failed, we can only show 0 steps. + return "?"; + } + + return Math.round(steps); +} + +function timeToString(duration) { + var hrs = ~~(duration / 3600); + var mins = ~~((duration % 3600) / 60); + var secs = ~~duration % 60; + var ret = ""; + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + } + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; +} + +function countDown() { + timervalue--; + + g.reset().clearRect(0, 40, 44+99, g.getHeight()/2-25); + + g.setFontAlign(0, -1, 0); + g.setFont("6x8", 2).drawString(timeToString(timervalue), 95, g.getHeight()/2-50); + + if (timervalue <= 0) { + istimeron = false; + clearInterval(timertick); + + Bangle.buzz().then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 500)); + }).then(()=>{ + return Bangle.buzz(1000); + }); + } + else + if ((timervalue <= 30) && (timervalue % 10 == 0)) { Bangle.buzz(); } +} + +// Touch +Bangle.on('touch',t => { + if (t == 1) { + // Touch on the left, reduce timervalue about 60s + Bangle.buzz(30); + if (timervalue < 60) { timervalue = 1 ; } + else { timervalue -= 60; } + } + // Touch on the right, raise timervaule about 300s + else if (t == 2) { + Bangle.buzz(30); + if (!istimeron) { + istimeron = true; + timertick = setInterval(countDown, 1000); + } + timervalue += 60*5; + } +}); + +{ // 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 + let drawTimeout; + + + // Actually draw the watch face + let draw = function() { + var x = g.getWidth() / 2; + var y = g.getHeight() / 2; + g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets) + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y); + var dateStr = require("locale").date(date, 1).toUpperCase(); + g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28); + g.setFontAlign(0, 0).setFont("6x8", 2); + g.drawString(getSteps(), 50, y+70); + g.drawString(Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + // Show launcher when middle button pressed + Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFontAnton; + }}); + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} diff --git a/apps/entonclk/app.png b/apps/entonclk/app.png new file mode 100644 index 000000000..5b634de5a Binary files /dev/null and b/apps/entonclk/app.png differ diff --git a/apps/entonclk/metadata.json b/apps/entonclk/metadata.json new file mode 100644 index 000000000..159f0c605 --- /dev/null +++ b/apps/entonclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "entonclk", + "name": "Enton Clock", + "version": "0.3", + "description": "A simple clock using the Audiowide font with timer. ", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"entonclk.app.js","url":"app.js"}, + {"name":"entonclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/entonclk/screenshot.png b/apps/entonclk/screenshot.png new file mode 100644 index 000000000..0905c6fc8 Binary files /dev/null and b/apps/entonclk/screenshot.png differ diff --git a/apps/espruinoctrl/ChangeLog b/apps/espruinoctrl/ChangeLog index 5560f00bc..522cba63e 100644 --- a/apps/espruinoctrl/ChangeLog +++ b/apps/espruinoctrl/ChangeLog @@ -1 +1,3 @@ 0.01: New App! +0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power +0.03: Now use BTN2 on Bangle.js 1, and on Bangle.js 2 use the middle button to return to the menu \ No newline at end of file diff --git a/apps/espruinoctrl/README.md b/apps/espruinoctrl/README.md index a7bca662c..59c96b0de 100644 --- a/apps/espruinoctrl/README.md +++ b/apps/espruinoctrl/README.md @@ -14,10 +14,11 @@ with 4 options: with this address will be connected to directly. If not specified a menu showing available Espruino devices is popped up. * **RX** - If checked, the app will display any data received from the -device being connected to. Use this if you want to print data - eg: `print(E.getBattery())` +device being connected to (waiting 500ms after the last data before disconnecting). +Use this if you want to print data - eg: `print(E.getBattery())` When done, click 'Upload'. Your changes will be saved to local storage -so they'll be remembered next time you upload from the same device.s +so they'll be remembered next time you upload from the same device. ## Usage @@ -25,4 +26,9 @@ Simply load the app and you'll see a menu with the menu items you defined. Select one and you'll be able to connect to the device and send the command. -If a command should wait for a response then +The Bangle will connect to the device, send the command, and if: + +* `RX` isn't set it will disconnect immediately and return to the menu +* `RX` is set it will listen for a response and write it to the screen, before +disconnecting after 500ms of inactivity. To return to the menu after this, press the button. + diff --git a/apps/espruinoctrl/app-icon.js b/apps/espruinoctrl/app-icon.js index 70d2dd062..3f9572f72 100644 --- a/apps/espruinoctrl/app-icon.js +++ b/apps/espruinoctrl/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AH4AFwIuuAAIllAAYIGF041IF34AKqwuuAANXF9QuCAANdGHqQgGBwvdGCIud5mjGB4udAAIwPFz3MSR61VFxQwNci4vGeh4uXGAguHGBK3WGA4AIegtXc69dGBxoBGAouWO4IwNe4gwZa4YwLFwikEFzAwLFwwwCFzQwKFw68YGB4AdF5AwmF5IwlF5QwkF5Yw/F8IwEL9WBB4IuuADwuzGxAugFAgliGBYutAH4A/AH4A/ADA=")) +require("heatshrink").decompress(atob("mEw4UA///muVt9TgH+Jf4AQgILKgtABI9VqkVqAgHqoABC48FBYQKGhEVBQNUBY0qyoLJ1WlEZMq1ILJhWqBZMC1QwCBY0PGAYLGn/qGAQLG/4wDBIkggf8GARfF1ED+BhCTQgTBgfAMISaF1WAAYM61SBG0ADB/wLFgNq1EAHoIcDXYVaCYMP+EqC4kVqwTBn/AhDqFqowBn72HqowCBZAwCBZAwCBZAwCBZIwIiowKBYVWC5VUkAvJXYiaDBYS7FTQVUgr2HC4IgHAAYgHAH4AJA==")) diff --git a/apps/espruinoctrl/custom.html b/apps/espruinoctrl/custom.html index f8e7e38b9..27ef1eb53 100644 --- a/apps/espruinoctrl/custom.html +++ b/apps/espruinoctrl/custom.html @@ -164,6 +164,7 @@ function cmd(cmd,mac,rx) { if (mac) { E.showMessage("Connecting\\n"+mac); if (mac.length==17) mac+=" random"; + NRF.setTxPower(process.env.HWVERSION == 2 ? 8 : 4); NRF.connect(mac).then(dev=>send(dev,cmd,onDone)).catch(err); } else { E.showMessage("Scanning..."); @@ -197,9 +198,9 @@ function sendCommandRX(device, text, callback) { setWatch(function() { if (callback) callback(); resolve(); - }, BTN3); + }, (process.env.HWVERSION==2) ? BTN1 : BTN2); g.reset().setFont("6x8",2).setFontAlign(0,0,1); - g.drawString("Back", g.getWidth()-10, g.getHeight()-50); + g.drawString("Back", g.getWidth()-10, g.getHeight()/2); }, 200); } device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) { diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json index 5798c7842..4f5fa01c8 100644 --- a/apps/espruinoctrl/metadata.json +++ b/apps/espruinoctrl/metadata.json @@ -2,11 +2,11 @@ "id": "espruinoctrl", "name": "Espruino Control", "shortName": "Espruino Ctrl", - "version": "0.01", + "version": "0.03", "description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!", "icon": "app.png", - "tags": "", - "supports": ["BANGLEJS"], + "tags": "tool,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "custom": "custom.html", "storage": [ diff --git a/apps/espruinoprog/ChangeLog b/apps/espruinoprog/ChangeLog new file mode 100644 index 000000000..6fdcad1d6 --- /dev/null +++ b/apps/espruinoprog/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add 'pre' code that can erase the device + Wait more between sending code snippets + Now force use of 'Storage' (assume 2v00 or later) diff --git a/apps/espruinoprog/README.md b/apps/espruinoprog/README.md new file mode 100644 index 000000000..aef4cccad --- /dev/null +++ b/apps/espruinoprog/README.md @@ -0,0 +1,43 @@ +# Espruino Programmer + +Finds Bluetooth devices with a specific name (eg `Puck.js`), connects and uploads code. Great for programming many devices at once! + +**WARNING:** This will reprogram **any matching Espruino device within range** while +the app is running. Unless you are careful to remove other devices from the area or +turn them off, you could find some of your devices unexpectedly get programmed! + +## Customising + +Click on the Customise button in the app loader to set up the programmer. + +* First you need to choose the kind of devices you want to upload to. This is +the text that should match the Bluetooth advertising name. So `Puck.js` for Puck.js +devices, or `Bangle.js` for Bangles. +* In the next box, you have code to run before the upload of the main code. By default +the code `require("Storage").list().forEach(f=>require("Storage").erase(f));reset();` will +erase all files on the device and reset it. +* Now paste in the code you want to write to the device. This is automatically +written to flash (`.bootcde`). See https://www.espruino.com/Saving#save-on-send-to-flash- +for more information. +* Now enter the code that should be sent **after** programming. This code +should make the device so it doesn't advertise on Bluetooth with the Bluetooth +name you entered for the first item. It may also help if it indicates to you that +the device is programmed properly. + * You could turn advertising off with `NRF.sleep()` + * You could change the advertising name with `NRF.setAdvertising({},{name:"Ok"});` + * On a Bangle, you could turn it off with `Bangle.off()` +* Finally scroll down and click `Upload` +* Now you can run the new `Programmer` app on the Bangle. + +## Usage + +Just run the app, and as soon as it starts it'll start scanning for +devices to upload to! + +To stop scanning, long-press the button to return to the clock. + +## Notes + +* This assumes the device being written to is at least version 2v00 of Espruino +* Currently, code is not minified before upload (so you need to supply pre-minified + code if you want that) diff --git a/apps/espruinoprog/app-icon.js b/apps/espruinoprog/app-icon.js new file mode 100644 index 000000000..532c60eea --- /dev/null +++ b/apps/espruinoprog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA/4AB7wJB8/5uX+7uUgH41lSKf4AKpMkyQCCggEDAQVtCAMCCNWUx9JufSrmkCJeKqsiytICJtFkWRCJWAEaARCI5BkEoAGBymJ9eSvXkCJZ9JCLI1DyM9uQRLNYWRpRZMR5ARWAwSPCuWR9MuCJZZIgARGPouTCIcSA4OQAoMW7dt2wCEEZECCI1oCJAADrZyBAAcDuQRByOABQkKDAvbtwRBxu24AMFAAcGCIY/B7AQIhpOC3MjKIVsCJe3jYRCwiPEkARBQg227ieDAQO0CJPhCKHJCK1N0ARI28JCIjUDEY4OBzWRfAoRG3ARBygRH3oPBswRB4QjFfAYgCt9pYoJoEkmbCJONCI1ACJGSiQRE7TXDCIuQEYkmCIhpDEYSCFCIj2DCIOTrYRE6ARDAH4AHA")) diff --git a/apps/espruinoprog/app.js b/apps/espruinoprog/app.js new file mode 100644 index 000000000..58fac4a0b --- /dev/null +++ b/apps/espruinoprog/app.js @@ -0,0 +1,100 @@ +var uart; // require("ble_uart") +var device; // BluetoothDevice +var uploadTimeout; // a timeout used during upload - if we disconnect, kill this +Bangle.loadWidgets(); + +var json = require("Storage").readJSON("espruinoprog.json",1); +/*var json = { // for example + namePrefix : "Puck.js ", + code : "E.setBootCode('digitalPulse(LED2,1,100);')", + post : "LED.set();NRF.sleep()", +};*/ + +if ("object" != typeof json) { + E.showAlert("JSON not found","Programmer").then(() => load()); + throw new Error("JSON not found"); + // stops execution +} + +// Set up terminal +var R = Bangle.appRect; +var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); +termg.setFont("6x8"); +var term; + +function showTerminal() { + E.showMenu(); // clear anything that was drawn + if (term) term.print(""); // redraw terminal +} + +function scanAndConnect() { + termg.clear(); + term = require("VT100").connect(termg, { + charWidth : 6, + charHeight : 8 + }); + term.print = str => { + for (var i of str) term.char(i); + g.reset().drawImage(termg,R.x,R.y); + }; + term.print(`\r\nScanning...\r\n`); + NRF.requestDevice({ filters: [{ namePrefix: json.namePrefix }] }).then(function(dev) { + term.print(`Found ${dev.name||dev.id.substr(0,17)}\r\n`); + device = dev; + + term.print(`Connect to ${dev.name||dev.id.substr(0,17)}...\r\n`); + device.removeAllListeners(); + device.on('gattserverdisconnected', function(reason) { + if (!uart) return; + term.print(`\r\nDISCONNECTED (${reason})\r\n`); + uart = undefined; + device = undefined; + if (uploadTimeout) clearTimeout(uploadTimeout); + uploadTimeout = undefined; + setTimeout(scanAndConnect, 1000); + }); + require("ble_uart").connect(device).then(function(u) { + uart = u; + term.print("Connected...\r\n"); + uart.removeAllListeners(); + uart.on('data', function(d) { term.print(d); }); + term.print("Upload initial...\r\n"); + uart.write((json.pre||"")+"\n").then(() => { + term.print("\r\Done.\r\n"); + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\nUpload Code...\r\n"); + uart.write((json.code||"")+"\n").then(() => { + term.print("\r\Done.\r\n"); + // main upload completed - wait a bit + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\Upload final...\r\n"); + // now upload the code to run after... + uart.write((json.post||"")+"\n").then(() => { + term.print("\r\nDone.\r\n"); + // now wait and disconnect (if not already done!) + uploadTimeout = setTimeout(function() { + uploadTimeout = undefined; + term.print("\r\nDisconnecting...\r\n"); + if (uart) uart.disconnect(); + }, 500); + }); + }, 2000); + }); + }, 2000); + }); + }); + }).catch(err => { + if (err.toString().startsWith("No device found")) { + // expected - try again + scanAndConnect(); + } else + term.print(`\r\ERROR ${err.toString()}\r\n`); + }); +} + +// now start +Bangle.drawWidgets(); +showTerminal(); +scanAndConnect(); diff --git a/apps/espruinoprog/app.png b/apps/espruinoprog/app.png new file mode 100644 index 000000000..b2b435f04 Binary files /dev/null and b/apps/espruinoprog/app.png differ diff --git a/apps/espruinoprog/custom.html b/apps/espruinoprog/custom.html new file mode 100644 index 000000000..c6c51ca3e --- /dev/null +++ b/apps/espruinoprog/custom.html @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + +

Upload code to devices with names starting with:

+

+

Enter the code to send before upload here:

+

+

Enter your program to upload here:

+

+

Enter the code to send after upload here:

+

+

Then click  

+

Click here to reset to defaults.

+ + + + diff --git a/apps/espruinoprog/metadata.json b/apps/espruinoprog/metadata.json new file mode 100644 index 000000000..7371e005d --- /dev/null +++ b/apps/espruinoprog/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "espruinoprog", + "name": "Espruino Programmer", + "shortName": "Programmer", + "version": "0.02", + "description": "Finds Bluetooth devices with a specific name (eg 'Puck.js'), connects and uploads code. Great for programming many devices at once!", + "icon": "app.png", + "tags": "tool,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"espruinoprog.app.js","url":"app.js"}, + {"name":"espruinoprog.img","url":"app-icon.js","evaluate":true} + ], "data": [ + {"name":"espruinoprog.json"} + ] +} diff --git a/apps/espruinoterm/ChangeLog b/apps/espruinoterm/ChangeLog new file mode 100644 index 000000000..7727f3cc4 --- /dev/null +++ b/apps/espruinoterm/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Minor code improvements diff --git a/apps/espruinoterm/README.md b/apps/espruinoterm/README.md new file mode 100644 index 000000000..df26d59a0 --- /dev/null +++ b/apps/espruinoterm/README.md @@ -0,0 +1,22 @@ +# Espruino Terminal + +Send commands to other Espruino devices via the Bluetooth UART interface and +see the result on a terminal. + +## Customising + +Once installed and you're connected to the Bangle you can click the button next to the app in the app loader +to change the commands (they will be read from the device). + +When done, click `Save to Bangle.js` and your changes will be saved to the same device. + +## Usage + +* Load the app and after a few seconds you'll see a menu with Espruino devices +in the vicinity. +* Tap on the device you want to connect to +* A terminal will pop up showing `Connecting...` and then `Connected` +* Now tap on the right (or press the button) to bring up a menu with options for commands, or the option to disconnect. + +You can also choose `Custom` in which case a keyboard (using the currently installed text input method) will +be displayed and you can enter the command you would like to send. diff --git a/apps/espruinoterm/app-icon.js b/apps/espruinoterm/app-icon.js new file mode 100644 index 000000000..f566aedf7 --- /dev/null +++ b/apps/espruinoterm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCpMkyQC/AVW//4AK/oR/COD8LCP4R/CK8DCKNsCKFt2BHPhu2CJ8BCKAjQI4OQNaIUB23bsCPMCJzp/CP4Rf/4AKCKwC/AVIA==")) diff --git a/apps/espruinoterm/app.js b/apps/espruinoterm/app.js new file mode 100644 index 000000000..1253b253a --- /dev/null +++ b/apps/espruinoterm/app.js @@ -0,0 +1,101 @@ +var uart; // require("ble_uart") +var device; // BluetoothDevice +var customCommand = ""; +// Set up terminal +Bangle.loadWidgets(); +var R = Bangle.appRect; +var termg = Graphics.createArrayBuffer(R.w, R.h, 1, {msb:true}); +var termVisible = false; +termg.setFont("6x8"); +let term = require("VT100").connect(termg, { + charWidth : 6, + charHeight : 8 +}); +term.print = str => { + for (var i of str) term.char(i); + if (termVisible) g.reset().drawImage(termg,R.x,R.y).setFont("6x8").setFontAlign(0,-1,1).drawString("MORE",R.w-1,(R.y+R.y2)/2); +}; + +function showConnectMenu() { + termVisible = false; + var m = { "" : {title:"Devices"} }; + E.showMessage("Scanning..."); + NRF.findDevices(devices => { + devices.forEach(dev=>{ + m[dev.name||dev.id.substr(0,17)] = ()=>{ + connectTo(dev); + }; + }); + m["< Back"] = () => showConnectMenu(); + E.showMenu(m); + },{filters:[ + { namePrefix: 'Puck.js' }, + { namePrefix: 'Pixl.js' }, + { namePrefix: 'MDBT42Q' }, + { namePrefix: 'Bangle.js' }, + { namePrefix: 'Espruino' }, + { services: [ "6e400001-b5a3-f393-e0a9-e50e24dcca9e" ] } + ],active:true,timeout:4000}); +} + +function showOptionsMenu() { + if (!uart) showConnectMenu(); + termVisible = false; + var menu = {"":{title:/*LANG*/"Options"}, + "< Back" : () => showTerminal(), + }; + var json = require("Storage").readJSON("espruinoterm.json",1); + if (Array.isArray(json)) { + json.forEach(j => { menu[j.title] = () => sendCommand(j.cmd); }); + } else { + Object.assign(menu,{ + "Version" : () => sendCommand("process.env.VERSION"), + "Battery" : () => sendCommand("E.getBattery()"), + "Flash LED" : () => sendCommand("LED.set();setTimeout(()=>LED.reset(),1000);") + }); + } + menu[/*LANG*/"Custom"] = () => { require("textinput").input({text:customCommand}).then(result => { + customCommand = result; + sendCommand(customCommand); + })}; + menu[/*LANG*/"Disconnect"] = () => { showTerminal(); term.print("\r\nDisconnecting...\r\n"); uart.disconnect(); } + + E.showMenu(menu); +} + +function showTerminal() { + E.showMenu(); + Bangle.setUI({ + mode : "custom", + btn : n=> { showOptionsMenu(); }, + touch : (n,e) => { if (n==2) showOptionsMenu(); } + }); + termVisible = true; + term.print(""); // redraw terminal +} + +function sendCommand(cmd) { + showTerminal(); + uart.write(cmd+"\n"); +} + +function connectTo(dev) { + device = dev; + showTerminal(); + term.print(`\r\nConnect to ${dev.name||dev.id.substr(0,17)}...\r\n`); + device.on('gattserverdisconnected', function(reason) { + term.print(`\r\nDISCONNECTED (${reason})\r\n`); + uart = undefined; + device = undefined; + setTimeout(showConnectMenu, 1000); + }); + require("ble_uart").connect(device).then(function(u) { + uart = u; + term.print("Connected...\r\n"); + uart.on('data', function(d) { term.print(d); }); + }); +} + +// now start +Bangle.drawWidgets(); +showConnectMenu(); diff --git a/apps/espruinoterm/app.json b/apps/espruinoterm/app.json new file mode 100644 index 000000000..72a12e635 --- /dev/null +++ b/apps/espruinoterm/app.json @@ -0,0 +1,5 @@ +[ + {"title":"Version", "cmd":"process.env.VERSION"}, + {"title":"Battery", "cmd":"E.getBattery()"}, + {"title":"Flash LED", "cmd":"LED.set();setTimeout(()=>LED.reset(),1000);"} +] diff --git a/apps/espruinoterm/app.png b/apps/espruinoterm/app.png new file mode 100644 index 000000000..e9a8c3758 Binary files /dev/null and b/apps/espruinoterm/app.png differ diff --git a/apps/espruinoterm/interface.html b/apps/espruinoterm/interface.html new file mode 100644 index 000000000..6ff7b7da5 --- /dev/null +++ b/apps/espruinoterm/interface.html @@ -0,0 +1,101 @@ + + + + + + + + +

Enter the menu items you'd like to see appear in the app below. When finished, click `Save to Bangle.js` to save the JavaScript back.

+
+ + + + + + + + + + +
TitleCommand
+
+

+ + + + + + diff --git a/apps/espruinoterm/metadata.json b/apps/espruinoterm/metadata.json new file mode 100644 index 000000000..d967e0e1a --- /dev/null +++ b/apps/espruinoterm/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "espruinoterm", + "name": "Espruino Terminal", + "shortName": "Espruino Term", + "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"}], + "tags": "tool,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "dependencies": {"textinput":"type"}, + "storage": [ + {"name":"espruinoterm.app.js","url":"app.js"}, + {"name":"espruinoterm.img","url":"app-icon.js","evaluate":true} + ],"data": [ + {"name":"espruinoterm.json","url":"app.json"} + ] +} diff --git a/apps/espruinoterm/screenshot.png b/apps/espruinoterm/screenshot.png new file mode 100644 index 000000000..cce881a37 Binary files /dev/null and b/apps/espruinoterm/screenshot.png differ diff --git a/apps/exactwords/0100.png b/apps/exactwords/0100.png new file mode 100644 index 000000000..dc7ffb279 Binary files /dev/null and b/apps/exactwords/0100.png differ diff --git a/apps/exactwords/0634.png b/apps/exactwords/0634.png new file mode 100644 index 000000000..e04ffa9ce Binary files /dev/null and b/apps/exactwords/0634.png differ diff --git a/apps/exactwords/1200.png b/apps/exactwords/1200.png new file mode 100644 index 000000000..a7f67a1b6 Binary files /dev/null and b/apps/exactwords/1200.png differ diff --git a/apps/exactwords/1517.png b/apps/exactwords/1517.png new file mode 100644 index 000000000..357e0a144 Binary files /dev/null and b/apps/exactwords/1517.png differ diff --git a/apps/exactwords/1616.png b/apps/exactwords/1616.png new file mode 100644 index 000000000..8f9d301b8 Binary files /dev/null and b/apps/exactwords/1616.png differ diff --git a/apps/exactwords/2020.png b/apps/exactwords/2020.png new file mode 100644 index 000000000..70296db5c Binary files /dev/null and b/apps/exactwords/2020.png differ diff --git a/apps/exactwords/2358.png b/apps/exactwords/2358.png new file mode 100644 index 000000000..8163f59b2 Binary files /dev/null and b/apps/exactwords/2358.png differ diff --git a/apps/exactwords/ChangeLog b/apps/exactwords/ChangeLog new file mode 100644 index 000000000..189c6233b --- /dev/null +++ b/apps/exactwords/ChangeLog @@ -0,0 +1 @@ +0.1: New App! Need to work out locale settings diff --git a/apps/exactwords/README.md b/apps/exactwords/README.md new file mode 100644 index 000000000..e9b360df6 --- /dev/null +++ b/apps/exactwords/README.md @@ -0,0 +1,38 @@ +# Exact Words + +This is a clock for expressing the time in exact words. Each minute of +the day has a different phrase. + +Ranging from "Twelve" to "Coming up to midnight" to "A little after +twenty-five past four in the early hours" + +Screenshots best demonstrate + +![1200.png](1200.png) +![2358.png](2358.png) +![1616.png](1616.png) +![0634.png](0634.png) +![1517.png](1517.png) +![2020.png](2020.png) + + +"just gone " - as in Just gone quarter past four is 16:16 + +"a little after ", as in A little after quarter past three is 15:17 + +"coming up to ", as in Coming up to midnight is 23:58 + +"almost " as in Almost twenty-five to seven is 06:34 + +## To Do + +Add localisation. + +## Requests + +Written by: [Brendan Sleight](https://github.com/bmsleight/) For support and discussion please post in the Bangle JS Forum + + +## Creator + +[Brendan Sleight](https://github.com/bmsleight/) diff --git a/apps/exactwords/app-icon.js b/apps/exactwords/app-icon.js new file mode 100644 index 000000000..a6e8dee5a --- /dev/null +++ b/apps/exactwords/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AAMEjtogFQgEMjIFB6EAh0d/eH7dwAoNrx/X4Eaju7g/boAoKkACFh9f23BzswAoO38P/0EP78/wN/0EO70WwPf2EGDYKNCguAgFAlAFDgAFBg/d3F4v3ggvz/F8lXgg/x3F8nXAPBkHEgQABn+Xs1MAoN9393/wFBqu+r++AoN021W9ytYQIQACv3/j/bz+cv+/j/0/8cgECI4MQPYWUqoYCgP//4fDiAQCAAoA=")) diff --git a/apps/exactwords/app.js b/apps/exactwords/app.js new file mode 100644 index 000000000..e3547aa1d --- /dev/null +++ b/apps/exactwords/app.js @@ -0,0 +1,225 @@ +// timeout used to update every minute +var drawTimeout; + +// https://www.espruino.com/Bangle.js+Locale + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function wordsFromTime(h, m) +{ + + // Tests +/* +// Example 12:00 = Twelve +// h = 12; +// m = 0; + // Example 23:58 = Coming up to midnight +// h = 23; +// m = 58; + // Example 12:15 = Quarter past twelve +// h = 12; +// m = 15; + // Example 04:16 = Just gone quarter past four +// h = 16; +// m = 16; + // Example 01:00 = One at night +// h = 1; +// m = 0; + // Example 17:01 = Just gone five in the afternoon +// h = 17; +// m = 1; + // Example 05:25 = Twenty-five past five in the early hours +// h = 23; +// m = 33; + // Example 22:33 = coming up to eleven at night + // max + //words = "a little after twenty-five past four in the early hours"; +// h = 04; +// m = 27; +*/ + + + const HOUR_WORD_ARRAY = [ + "midnight", "one", "two", "three", "four", "five", "six", "seven", + "eight", "nine", "ten", "eleven", "twelve", "one", "two", "three", + "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", + "midnight"]; + const PART_DAY_WORD_ARRAY = ["", + " at night", + " in the early hours", + " in the early hours", + " in the early hours", + " in the early hours", + " in the morning", + " in the morning", + " in the morning", + " in the morning", + " in the morning", + " in the morning", + "", + " in the afternoon", + " in the afternoon", + " in the afternoon", + " in the afternoon", + " in the afternoon", + " in the evening", + " in the evening", + " in the evening", + " in the evening", + " at night", + " at night", + ""]; + const MINUTES_ROUGH_ARRAY = ["", + "five past ", + "ten past ", + "quarter past ", + "twenty past ", + "twenty-five past ", + "half past ", + "twenty-five to ", + "twenty to ", + "quarter to ", + "ten to ", + "five to ", + ""]; + const MINUTES_ACCURATE_ARRAY = ["", "just gone ", "a little after ", "coming up to ", "almost "]; + + var hourAdjusted = h; + var words = " ", hourWord = " ", partDayWord = " ", minutesRough = " ", minutesAccurate = " "; + + // At 33 past the hours we start referign to the next hour + if (m > 32) { + hourAdjusted = (h+ 1) % 24; + } else { + hourAdjusted = h; + } + + hourWord = HOUR_WORD_ARRAY[hourAdjusted]; + partDayWord = PART_DAY_WORD_ARRAY[Math.round(hourAdjusted)]; + minutesRough = MINUTES_ROUGH_ARRAY[Math.round((m + 0 ) / 5)]; + minutesAccurate = MINUTES_ACCURATE_ARRAY[m % 5]; + + words = minutesAccurate + minutesRough + hourWord + partDayWord; + words = words.charAt(0).toUpperCase() + words.slice(1); + return words; +} + +function wordsFromDayMonth(day, date, month) +{ + // Tests + +// Example 12:00 = Twelve +// New Year's Day +// date = 1; +// month = 0; +// on the Ides of March +// date = 15; +// month = 2; +// , ERROR C Nonsense in BASIC +// date = 1; +// month = 3; +// - O'Canada +// date = 1; +// month = 6; +// - on Halloween +// date = 31; +// month = 9; +// - Christmas Eve +// date = 24; +// month = 11; +// - Christmas Day +// date = 25; +// month = 11; +// - Boxing day +// date = 26; +// month = 11; +// New Year's eve +// date = 31; +// month = 11; +// longest +// date = 29; +// month = 10; + + + const DAY_WORD_ARRAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + const DATE_WORD_ARRAY = ["zero", "first", "second", "third", "fourth", "fifth", "sixth", "seventh","eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth","sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth", "twenty-first", "twenty-second", "twenty-third","twenty-fourth", "twenty-fifth", "twenty-sixth", "twenty-seventh", "twenty-eighth", "twenty-ninth", "thirtieth", "thirty-first"]; + const MONTH_WORD_ARRAY = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + var words = " "; + words = DAY_WORD_ARRAY[day] + ", " + DATE_WORD_ARRAY[date] + " of " + MONTH_WORD_ARRAY[month]; + if ((date == 1) && (month == 0)) { + words = "New Year's Day"; + } else if ((date == 15) && (month == 2)) { + words = DAY_WORD_ARRAY[day] + " on the Ides of March"; + } else if ((date == 1) && (month == 3)) { + words = DAY_WORD_ARRAY[day] + ", ERROR C Nonsense in BASIC"; + } else if ((date == 1) && (month == 6)) { + words = DAY_WORD_ARRAY[day] + " - O'Canada"; + } else if ((date == 31) && (month == 9)) { + words = DAY_WORD_ARRAY[day] + " - on Halloween"; + } else if ((date == 24) && (month == 11)) { + words = "Christmas Eve"; + } else if ((date == 25) && (month == 11)) { + words = "Christmas Day"; + } else if ((date == 26) && (month == 11)) { + words = "Boxing Day"; + } else if ((date == 31) && (month == 11)) { + words = "New Year's eve"; + } + return words; +} + +function draw() { + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + + var d = new Date(); + var h = d.getHours(); + var m = d.getMinutes(); + var day = d.getDay(); + var date = d.getDate(); + var month = d.getMonth(); + + var timeStr = wordsFromTime(h,m); + var dateStr = wordsFromDayMonth(day, date, month); + + // draw time + g.setBgColor(g.theme.bg); + g.setColor(g.theme.fg); + g.clear(); + g.setFontAlign(0,0).setFont("Vector",24); + g.drawString(g.wrapString(timeStr, g.getWidth()).join("\n"),x,y-24*0); + // draw date + + g.setFontAlign(0,0).setFont("Vector",12); + g.drawString(g.wrapString(dateStr, g.getWidth()).join("\n"),x,y+12*6); + // queue draw in one minute + queueDraw(); +} + +// Clear the screen once/, at startup +g.clear(); +// draw immediately at first, queue update +draw(); +// 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; + } +}); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/exactwords/app.png b/apps/exactwords/app.png new file mode 100644 index 000000000..24c06208c Binary files /dev/null and b/apps/exactwords/app.png differ diff --git a/apps/exactwords/metadata.json b/apps/exactwords/metadata.json new file mode 100644 index 000000000..428572632 --- /dev/null +++ b/apps/exactwords/metadata.json @@ -0,0 +1,22 @@ +{ "id": "exactwords", + "name": "Exact Words Clock", + "shortName":"Exact Words", + "version":"0.1", + "description": "Each minute of the day has a different phrase. ", + "icon": "app.png", + "screenshots" : [ { "url":"1517.png" }, + { "url":"0634.png" }, + { "url":"1200.png" }, + { "url":"1517.png" }, + { "url":"1616.png" }, + { "url":"2020.png" }, + { "url":"2358.png" } ], + "tags": "clock", + "type": "clock", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"exactwords.app.js","url":"app.js"}, + {"name":"exactwords.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog index 5560f00bc..8aed5d989 100644 --- a/apps/f9lander/ChangeLog +++ b/apps/f9lander/ChangeLog @@ -1 +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 7e52104c0..d195a7c67 100644 --- a/apps/f9lander/app.js +++ b/apps/f9lander/app.js @@ -45,7 +45,10 @@ 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) || {}; const gravity = 4; const dt = 0.1; @@ -61,18 +64,40 @@ function flameImageGen (throttle) { function drawFalcon(x, y, throttle, angle) { g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle}); - if (throttle>0) { + if (throttle>0 || lightning>0) { var flameImg = flameImageGen(throttle); var r = falcon9.height/2 + flameImg.height/2-1; var xoffs = -Math.sin(angle)*r; var yoffs = Math.cos(angle)*r; if (Math.random()>0.7) g.setColor(1, 0.5, 0); else g.setColor(1, 1, 0); - g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle}); + if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle}); + if (lightning>1 && lightning<30) { + for (var i=0; i<6; ++i) { + var r = Math.random()*6; + var x = Math.random()*5 - xoffs; + var y = Math.random()*5 - yoffs; + g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r); + } + } } } +function drawLightning() { + var c = {x:cloudOffs+50, y:30}; + var dx = c.x-booster.x; + var dy = c.y-booster.y; + var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10}; + var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10}; + g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y); +} + function drawBG() { + if (lightning==1) { + g.setBgColor(1, 1, 1).clear(); + Bangle.buzz(200); + return; + } g.setBgColor(0.2, 0.2, 1).clear(); g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1); g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10); @@ -88,6 +113,7 @@ function renderScreen(input) { drawBG(); showFuel(); drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle); + if (lightning>1 && lightning<6) drawLightning(); } function getInputs() { @@ -97,6 +123,7 @@ function getInputs() { if (t > 1) t = 1; if (t < 0) t = 0; if (booster.fuel<=0) t = 0; + if (lightning>0 && lightning<20) t = 0; return {throttle: t, angle: a}; } @@ -121,7 +148,6 @@ function gameStep() { else { var input = getInputs(); if (booster.y >= targetY) { -// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle); if (Math.abs(booster.x-droneX-droneShip.width/2)40) && Math.random()>0.98) lightning = 1; booster.x += booster.vx*dt; booster.y += booster.vy*dt; booster.vy += gravity*dt; diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index 75c6a0164..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.01", + "version": "0.04", "description": "Land a rocket booster", "icon": "f9lander.png", "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], @@ -10,6 +10,8 @@ "supports" : ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"f9lander.app.js","url":"app.js"}, - {"name":"f9lander.img","url":"app-icon.js","evaluate":true} - ] + {"name":"f9lander.img","url":"app-icon.js","evaluate":true}, + {"name":"f9lander.settings.js", "url":"settings.js"} + ], + "data":[{"name":"f9settings.json"}] } diff --git a/apps/f9lander/settings.js b/apps/f9lander/settings.js new file mode 100644 index 000000000..9d85da394 --- /dev/null +++ b/apps/f9lander/settings.js @@ -0,0 +1,34 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'f9settings.json' + // initialize with default settings... + let settings = { + 'lightning': false, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + settings[key] = saved[key]; + } + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + } + const menu = { + '': { 'title': 'OpenWind' }, + '< Back': back, + 'Lightning': { + value: !!settings.lightning, + onchange: save('lightning'), + } + } + E.showMenu(menu); +}) diff --git a/apps/factclock/ChangeLog b/apps/factclock/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/factclock/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/factclock/README.md b/apps/factclock/README.md new file mode 100644 index 000000000..83b590c0b --- /dev/null +++ b/apps/factclock/README.md @@ -0,0 +1,5 @@ +# Fact Clock + +A clock that displays a random fact alongside the time. + +This uses `text_facts` for the list of facts, but you can implement new apps that provide the `textsource` module (see the readme for the `text_facts` app) to make this clock display different information. \ No newline at end of file diff --git a/apps/factclock/app-icon.js b/apps/factclock/app-icon.js new file mode 100644 index 000000000..6eb4bb2a5 --- /dev/null +++ b/apps/factclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcF23btoCV7QfBrYRM2kAhMkgEtCJegiwDBgBBLtAMCoA3BCwQRHoADCNIUCKxMDAYPAPgZcICIWwRwgRI6ARBwAREhYRIjYRBgVggEDCJOACIO07dsgETtsBCJcTsEGBIORCJRHCmwJB2LRHg0DGQIFC7ZKBNwnRkmSm0ACIsAgO2yRcCwIRDhgRILgWJCIMWgENR4fbhuBCINBMgMJCJNt2YLBoEtCIcGwEN2wRE3+kCI1C4DpDR4WdGoIRFpHYCIUBR4XtCJFYCIacBtgRIpKECCIdnpJZHpMhZIRcB7c7AwIRHkktEYNN23ZaYQRIwDhDrwRLyUCBoOXCoYRJpMgIgIIECJMkEAKwBCJ41OCMoFECJlpNaJZ0I/0BCKEACIsAn//AAX0yUQCIQAGFQ2QCJMACIuAlu2wASIAAkBJYPQBIsF0AHFhZgEARoA==")) \ No newline at end of file diff --git a/apps/factclock/app.js b/apps/factclock/app.js new file mode 100644 index 000000000..6ae457d4e --- /dev/null +++ b/apps/factclock/app.js @@ -0,0 +1,110 @@ +Graphics.prototype.setFontBebasNeue = function() { + // Actual height 31 (32 - 2) + // 1 BPP + return this.setFontCustom( + atob('AAAAAAAAAAAAAAAAAAAPgAAAAAD4AAAAAA+AAAAAAPgAAAAAD4AAAAAAAAAAAAAAgAAAAAA4AAAAAB+AAAAAD/gAAAAD/4AAAAH/4AAAAH/4AAAAP/wAAAAP/wAAAAf/gAAAAf/gAAAA//AAAAA/+AAAAAP+AAAAAD8AAAAAA8AAAAAAIAAAAAAAAAAAAAAAAAAAAAAf//8AAAf///wAAP///+AAH////wAD////+AA/////gAPgAAD4AD4AAA+AA+AAAPgAPgAAD4AD////+AAf////AAH////wAA////4AAH///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAADwAAAAAA8AAAAAAfAAAAAAHwAAAAAD////4AB////+AA/////gAP////4AD////+AA/////gAAAAAAAAAAAAAAAAAcAAHgAB/gA/4AA/4A/+AAf+Af/gAP/gP/4AD/wH/+AA+AD+PgAPgD+D4AD4B/A+AA+B/gPgAP//wD4AD//4A+AAf/8APgAD/8AD4AAf8AAeAAAAAAAAAAAAAAAAADgAfAAAH4AH8AAD+AB/wAB/gAf8AA/4AH/gAPwPgH4AD4D4A+AA+A+APgAPgPwD4AD8H+B+AA/////gAH////wAB//f/8AAP/j/+AAA/gf+AAAAAAAAAAAAAHgAAAAAP8AAAAAP/AAAAAP/wAAAAf/8AAAAf//AAAA//HwAAA/+B8AAA/+AfAAA/+AHwAAP////4AD////+AA/////gAP////4AD////+AAAAAHwAAAAAB8AAAAAAAAAAAAAAAAAAAAh4AAD//4fwAA//+H+AAP//h/wAD//4f+AA//+B/gAPgeAD4AD4PAA+AA+DwAPgAPh+AD4AD4f//+AA+D///gAPg///wAD4H//4AA8A//8AAAAAAAAAAAAAAAAAA///gAAB////AAA////4AAf////AAP////4AD////+AA+A8APgAPgfAD4AD4HwA+AA+B8APgAP8f//4AD/H//+AAfx///AAD8P//gAAfB//wAAAAB/AAAAAAAAAAAAAAAAAA8AAAAAAPgAAAAAD4AAACAA+AAAHgAPgAAf4AD4AA/+AA+AD//gAPgH//4AD4P//wAA+///gAAP//+AAAD//8AAAA//wAAAAP/AAAAAD+AAAAAAAAAAAAAAAAAAAAAH4D/gAAH/j/+AAD/9//wAB////8AA/////gAP9/4H4AD4D8A+AA+A+APgAPgPgD4AD4D8A+AA/////gAP////4AB////8AAP/3/+AAB/4f/AAAAAA+AAAAAAAAAAAAAAAAAAH/wHAAAH//B8AAH//4fwAB///H8AA///x/gAPwH8H4AD4AfA+AA+AHwPgAPgB4D4AD4A+B+AA/////gAH////wAB////8AAP///+AAA///+AAAAAAAAAAAAAAAAAAAAAAAAAAPgA+AAAD4APgAAA+AD4AAAPgA+AAAD4APgAAAAAAAAA'), + 46, + atob("CBIRDxEREhESERIRCA=="), + 44|65536 + ); +}; + +{ + // the font we're using + const factFont = "6x15"; + // swap every 10 minutes + const minsPerFact = 5; + // timeout used to update every minute + let drawTimeout; + // the fact we're going to display (pre-rendered with a border) + let factGfx; + // how long until the next fact? + let factCounter = minsPerFact; + // the gfx we use for the time so we can gat a shadow on it + let timeGfx = Graphics.createArrayBuffer(g.getWidth()>>1, 48, 2, {msb:true}); + timeGfx.transparent = 0; + timeGfx.palette = new Uint16Array([ + 0, g.toColor(g.theme.bg), 0, g.toColor(g.theme.fg) + ]); + + + let getNewFact = () => { + let fact = require("textsource").getRandomText(); + // wrap to fit the screen + let lines = g.setFont(factFont).wrapString(fact.txt, g.getWidth()-10); + let txt = lines.join("\n"); + // allocate a gfx for this + factGfx = Graphics.createArrayBuffer(g.getWidth(), g.stringMetrics(txt).height+4, 2, {msb:true}); + factGfx.transparent = 0; + factGfx.setFont(factFont).setFontAlign(0,-1).setColor(3).drawString(txt, factGfx.getWidth()/2, 2); + if (factGfx.filter) factGfx.filter([ // add shadow behind text + 0,1,1,1,0, + 1,1,1,1,1, + 1,1,1,1,1, + 1,1,1,1,1, + 0,1,1,1,0, + ], { w:5, h:5, div:1, max:1, filter:"max" }); + factGfx.palette = new Uint16Array([ + 0, g.toColor(g.theme.bg), 0, g.toColor(g.theme.fg) + ]); + }; + getNewFact(); + + // schedule a draw for the next minute + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function() { + // queue next draw in one minute + queueDraw(); + // new fact? + if (--factCounter < 0) { + factCounter = minsPerFact; + getNewFact(); + } + // Work out where to draw... + g.reset(); + require("clockbg").fillRect(Bangle.appRect); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date,1); + // draw time to buffer + timeGfx.clear(1); + timeGfx.setFontAlign(0,-1).setFont("BebasNeue"); + timeGfx.drawString(timeStr,timeGfx.getWidth()/2,2); + timeGfx.setFontAlign(0,1).setFont("6x8"); + timeGfx.drawString(dateStr,timeGfx.getWidth()/2,timeGfx.getHeight()-2); + // add shadow to buffer and render + if (timeGfx.filter) timeGfx.filter([ // add shadow behind text + 0,1,1,1,0, + 1,1,1,1,1, + 1,1,1,1,1, + 1,1,1,1,1, + 0,1,1,1,0, + ], { w:5, h:5, div:1, max:1, filter:"max" }); + var y = (Bangle.appRect.y+g.getHeight()-(factGfx.getHeight()+timeGfx.getHeight()*2))>>1; + g.drawImage(timeGfx,0, y, {scale:2}); + // draw the fact + g.drawImage(factGfx,0, g.getHeight()-factGfx.getHeight()); + }; + + // Show launcher when middle button pressed + Bangle.setUI({mode:"clock", remove:function() { + // free any memory we allocated to allow fast loading + delete Graphics.prototype.setFontBebasNeue; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + require('widget_utils').show(); // re-show widgets + }}); + // Load widgets + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); + // draw immediately at first, queue update + draw(); +} \ No newline at end of file diff --git a/apps/factclock/icon.png b/apps/factclock/icon.png new file mode 100644 index 000000000..3186d0a96 Binary files /dev/null and b/apps/factclock/icon.png differ diff --git a/apps/factclock/metadata.json b/apps/factclock/metadata.json new file mode 100644 index 000000000..35b13f393 --- /dev/null +++ b/apps/factclock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "factclock", + "name": "Fact Clock", + "shortName":"Facts", + "version":"0.01", + "description": "A clock that displays a random fact alongside the time", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "dependencies" : { "textsource":"module", "clockbg":"module" }, + "readme": "README.md", + "storage": [ + {"name":"factclock.app.js","url":"app.js"}, + {"name":"factclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/factclock/screenshot.png b/apps/factclock/screenshot.png new file mode 100644 index 000000000..e52590491 Binary files /dev/null and b/apps/factclock/screenshot.png differ 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/ChangeLog b/apps/fastload/ChangeLog new file mode 100644 index 000000000..6581e5188 --- /dev/null +++ b/apps/fastload/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: Allow redirection of loads to the launcher +0.03: Allow hiding the fastloading info screen +0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app). +0.05: Check for changes in setting.js and force real reload if needed +0.06: Fix caching whether an app is fastloadable diff --git a/apps/fastload/README.md b/apps/fastload/README.md new file mode 100644 index 000000000..f7fab4933 --- /dev/null +++ b/apps/fastload/README.md @@ -0,0 +1,38 @@ +#### ⚠️EXPERIMENTAL⚠️ + +# Fastload Utils + +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 +* If Quick Launch is installed it can be excluded from app history +* Allows to redirect all loads usually loading the clock to the launcher instead +* The "Fastloading..." screen can be switched off +* Enable checking `setting.json` and force a complete load on changes + +## App history + +* Long press of hardware button clears the app history and loads the clock face +* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button just a little longer than a click. Useful if there are many apps in the history and the user want to access the clock quickly. + +## Technical infos + +This is still experimental but it uses the same mechanism as `.bootcde` does. +It checks the app to be loaded for widget use and stores the result of that and a hash of the js in a cache. + +# Creator + +[halemmerich](https://github.com/halemmerich) + +# Contributors +[thyttan](https://github.com/thyttan) diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js new file mode 100644 index 000000000..57bc8ea94 --- /dev/null +++ b/apps/fastload/boot.js @@ -0,0 +1,99 @@ +{ +const s = require("Storage"); +const SETTINGS = s.readJSON("fastload.json") || {}; + +let loadingScreen = function(){ + g.reset(); + + let x = g.getWidth()/2; + let y = g.getHeight()/2; + g.setColor(g.theme.bg); + g.fillRect(x-49, y-19, x+49, y+19); + g.setColor(g.theme.fg); + g.drawRect(x-50, y-20, x+50, y+20); + g.setFont("6x8"); + g.setFontAlign(0,0); + g.drawString("Fastloading...", x, y); + g.flip(true); +}; + +let cache = s.readJSON("fastload.cache") || {}; + +const SYS_SETTINGS="setting.json"; + +let appFastloadPossible = function(n){ + if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){ + cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS); + s.writeJSON("fastload.cache", cache); + return false; + } + + // no widgets, no problem + if (!global.WIDGETS) return true; + let hash = s.hash(n); + if (cache[n] && hash == cache[n].hash) + return cache[n].fast; + let app = s.read(n); + cache[n] = {}; + cache[n].fast = app.includes("Bangle.loadWidgets"); + cache[n].hash = hash; + s.writeJSON("fastload.cache", cache); + return cache[n].fast; +}; + +global._load = load; + +let slowload = function(n){ + global._load(n); +}; + +let fastload = function(n){ + if (!n || appFastloadPossible(n)){ + // Bangle.load can call load, to prevent recursion this must be the system load + global.load = slowload; + Bangle.load(n); + // if fastloading worked, we need to set load back to this method + global.load = fastload; + } + else + slowload(n); +}; +global.load = fastload; + +let appHistory, resetHistory, recordHistory; +if (SETTINGS.useAppHistory){ + appHistory = s.readJSON("fastload.history.json",true)||[]; + resetHistory = ()=>{appHistory=[];s.writeJSON("fastload.history.json",appHistory);}; + recordHistory = ()=>{s.writeJSON("fastload.history.json",appHistory);}; +} + +Bangle.load = (o => (name) => { + if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen(); + if (SETTINGS.useAppHistory){ + if (name && name!=".bootcde" && !(name=="quicklaunch.app.js" && SETTINGS.disregardQuicklaunch)) { + // store the name of the app to launch + appHistory.push(name); + } else if (name==".bootcde") { // when Bangle.showClock is called + resetHistory(); + } else if (name=="quicklaunch.app.js" && SETTINGS.disregardQuicklaunch) { + // do nothing with history + } else { + // go back in history + appHistory.pop(); + name = appHistory[appHistory.length-1]; + } + } + if (SETTINGS.autoloadLauncher && !name){ + let orig = Bangle.load; + Bangle.load = (n)=>{ + Bangle.load = orig; + fastload(n); + }; + Bangle.showLauncher(); + Bangle.load = orig; + } else + o(name); +})(Bangle.load); + +if (SETTINGS.useAppHistory) E.on('kill', ()=>{if (!BTN.read()) recordHistory(); else resetHistory();}); // Usually record history, but reset it if long press of HW button was used. +} diff --git a/apps/fastload/icon.png b/apps/fastload/icon.png new file mode 100644 index 000000000..7fe9afe6e Binary files /dev/null and b/apps/fastload/icon.png differ diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json new file mode 100644 index 000000000..8edd1f95b --- /dev/null +++ b/apps/fastload/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fastload", + "name": "Fastload Utils", + "shortName" : "Fastload Utils", + "version": "0.06", + "icon": "icon.png", + "description": "Enable experimental fastloading for more apps", + "type":"bootloader", + "tags": "system", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fastload.5.boot.js","url":"boot.js"}, + {"name":"fastload.settings.js","url":"settings.js"} + ], + "data": [{"name":"fastload.json"}] +} diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js new file mode 100644 index 000000000..15c135fe4 --- /dev/null +++ b/apps/fastload/settings.js @@ -0,0 +1,73 @@ +(function(back) { + var FILE="fastload.json"; + var settings; + var isQuicklaunchPresent = !!require('Storage').read("quicklaunch.app.js", 0, 1); + + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = require('Storage').readJSON(FILE, true) || {}; + } + + readSettings(); + + function buildMainMenu(){ + var mainmenu = {}; + + mainmenu[''] = { 'title': 'Fastload', back: back }; + + mainmenu['Activate app history'] = { + value: !!settings.useAppHistory, + onchange: v => { + writeSettings("useAppHistory",v); + if (v && settings.autoloadLauncher) { + writeSettings("autoloadLauncher",!v); // Don't use app history and load to launcher together. + setTimeout(()=>E.showMenu(buildMainMenu()), 0); // Update the menu so it can be seen if a value was automatically set to false (app history vs load launcher). + } + } + }; + + if (isQuicklaunchPresent) { + mainmenu['Exclude Quick Launch from history'] = { + value: !!settings.disregardQuicklaunch, + onchange: v => { + writeSettings("disregardQuicklaunch",v); + } + }; + } + + mainmenu['Force load to launcher'] = { + value: !!settings.autoloadLauncher, + onchange: v => { + writeSettings("autoloadLauncher",v); + if (v && settings.useAppHistory) { + writeSettings("useAppHistory",!v); + setTimeout(()=>E.showMenu(buildMainMenu()), 0); // Update the menu so it can be seen if a value was automatically set to false (app history vs load launcher). + } // Don't use app history and load to launcher together. + } + }; + + mainmenu['Hide "Fastloading..."'] = { + value: !!settings.hideLoading, + onchange: v => { + writeSettings("hideLoading",v); + } + }; + + mainmenu['Detect settings changes'] = { + value: !!settings.detectSettingsChange, + onchange: v => { + writeSettings("detectSettingsChange",v); + } + }; + + return mainmenu; + } + + E.showMenu(buildMainMenu()); +}) diff --git a/apps/fastreset/ChangeLog b/apps/fastreset/ChangeLog new file mode 100644 index 000000000..eec108328 --- /dev/null +++ b/apps/fastreset/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! +0.02: Shorten the timeout before executing to 250 ms. +0.03: Add inner timeout of 150 ms so user has more time to release the button + before clock ui is initialized and adds it's button watch for going to + launcher. diff --git a/apps/fastreset/README.md b/apps/fastreset/README.md new file mode 100644 index 000000000..b23023f4a --- /dev/null +++ b/apps/fastreset/README.md @@ -0,0 +1,32 @@ +# Fast Reset + +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. + +## Usage + +Just install and it will run as boot code. + +## Features + +If 'Fastload Utils' is installed fastloading will be used when possible. Otherwise a standard `load(.bootcde)` is used. + +If the hardware button is held for longer the standard reset functionality of the firmware is executed as well. And eventually the watchdog will be kicked. + +## Controls + +Press the hardware button just a little longer than a click to feel the buzz, loading the clock face. + +## Requests + +Mention @[thyttan](https://github.com/thyttan) in an issue to the official [BangleApps repository](https://github.com/espruino/BangleApps/issues) for feature requests and bug reports. + +## Acknowledgements + +Rewind icon by Icons8 + +## Creator + +[thyttan](https://github.com/thyttan) + diff --git a/apps/fastreset/app.png b/apps/fastreset/app.png new file mode 100644 index 000000000..79e0f310e Binary files /dev/null and b/apps/fastreset/app.png differ diff --git a/apps/fastreset/boot.js b/apps/fastreset/boot.js new file mode 100644 index 000000000..5d1fd50b1 --- /dev/null +++ b/apps/fastreset/boot.js @@ -0,0 +1,5 @@ +{let buzzTimeout; +setWatch((e)=>{ + if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);setTimeout(Bangle.showClock,150);}, 250); + if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);}, +BTN,{repeat:true,edge:'both'});} diff --git a/apps/fastreset/metadata.json b/apps/fastreset/metadata.json new file mode 100644 index 000000000..ccd5e1ce4 --- /dev/null +++ b/apps/fastreset/metadata.json @@ -0,0 +1,14 @@ +{ "id": "fastreset", + "name": "Fast Reset", + "shortName":"Fast Reset", + "version":"0.03", + "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", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fastreset.boot.js","url":"boot.js"} + ] +} diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index 30e049f69..35fa366a4 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1,2 +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 afa0c5e2d..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,16 +163,17 @@ 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); } }; +// Show launcher when button pressed +Bangle.setUI("clock"); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -198,6 +199,3 @@ Bangle.on('HRM', function (d) { // draw now drawClock(); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/fclock/metadata.json b/apps/fclock/metadata.json index da553e110..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.02", + "version": "0.05", "description": "Simple design of a digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog index cb520193b..6d2f50119 100644 --- a/apps/ffcniftya/ChangeLog +++ b/apps/ffcniftya/ChangeLog @@ -2,3 +2,5 @@ 0.02: Shows the current week number (ISO8601), can be disabled via settings 0.03: Call setUI before loading widgets Improve settings page +0.04: Use ClockFace library + diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js index 4000a1578..2c1a54f6e 100644 --- a/apps/ffcniftya/app.js +++ b/apps/ffcniftya/app.js @@ -1,22 +1,3 @@ -const locale = require("locale"); -const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"]; -const showWeekNum = Object.assign({ showWeekNum: true }, require('Storage').readJSON("ffcniftya.json", true))["showWeekNum"]; - -/* Clock *********************************************/ -const scale = g.getWidth() / 176; - -const widget = 24; - -const viewport = { - width: g.getWidth(), - height: g.getHeight(), -} - -const center = { - x: viewport.width / 2, - y: Math.round(((viewport.height - widget) / 2) + widget), -} - // copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 function ISO8601_week_no(date) { var tdt = new Date(date.valueOf()); @@ -30,77 +11,49 @@ function ISO8601_week_no(date) { return 1 + Math.ceil((firstThursday - tdt) / 604800000); } -function d02(value) { - return ('0' + value).substr(-2); +function format(value) { + return ("0" + value).substr(-2); } -function draw() { - g.reset(); - g.clearRect(0, widget, viewport.width, viewport.height); - const now = new Date(); +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + init: function () { + const appRect = Bangle.appRect; - const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); - const minutes = d02(now.getMinutes()); - const day = d02(now.getDate()); - const month = d02(now.getMonth() + 1); - const year = now.getFullYear(now); - const weekNum = d02(ISO8601_week_no(now)); - const monthName = locale.month(now, 3); - const dayName = locale.dow(now, 3); + this.viewport = { + width: appRect.w, + height: appRect.h + }; - const centerTimeScaleX = center.x + 32 * scale; - g.setFontAlign(1, 0).setFont("Vector", 90 * scale); - g.drawString(hour, centerTimeScaleX, center.y - 31 * scale); - g.drawString(minutes, centerTimeScaleX, center.y + 46 * scale); + this.center = { + x: this.viewport.width / 2, + y: Math.round((this.viewport.height / 2) + appRect.y) + }; - g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); + 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); - const centerDatesScaleX = center.x + 40 * scale; - g.setFontAlign(-1, 0).setFont("Vector", 16 * scale); - g.drawString(year, centerDatesScaleX, center.y - 62 * scale); - g.drawString(month, centerDatesScaleX, center.y - 44 * scale); - g.drawString(day, centerDatesScaleX, center.y - 26 * scale); - if (showWeekNum) g.drawString(weekNum, centerDatesScaleX, center.y + 15 * scale); - g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale); - g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale); -} + 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); -/* Minute Ticker *************************************/ - -let tickTimer; - -function clearTickTimer() { - if (tickTimer) { - clearTimeout(tickTimer); - tickTimer = undefined; - } -} - -function queueNextTick() { - clearTickTimer(); - tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000)); -} - -function tick() { - draw(); - queueNextTick(); -} - -/* Init **********************************************/ - -// Clear the screen once, at startup -g.clear(); -tick(); - -Bangle.on('lcdPower', (on) => { - if (on) { - tick(); - } else { - clearTickTimer(); - } + g.setFontAlign(-1, 0).setFont("Vector", 16 * this.scale); + g.drawString(date.getFullYear(date), this.centerDatesScaleX, this.center.y - 62 * this.scale); + g.drawString(format(month), this.centerDatesScaleX, this.center.y - 44 * this.scale); + g.drawString(format(date.getDate()), this.centerDatesScaleX, this.center.y - 26 * this.scale); + if (this.showWeekNum) g.drawString(format(ISO8601_week_no(date)), this.centerDatesScaleX, this.center.y + 15 * this.scale); + g.drawString(monthName, this.centerDatesScaleX, this.center.y + 48 * this.scale); + g.drawString(dayName, this.centerDatesScaleX, this.center.y + 66 * this.scale); + }, + settingsFile: "ffcniftya.json" }); - -Bangle.setUI("clock"); -Bangle.loadWidgets(); -Bangle.drawWidgets(); +clock.start(); \ No newline at end of file diff --git a/apps/ffcniftya/metadata.json b/apps/ffcniftya/metadata.json index 91b426cd0..015c56119 100644 --- a/apps/ffcniftya/metadata.json +++ b/apps/ffcniftya/metadata.json @@ -1,7 +1,7 @@ { "id": "ffcniftya", "name": "Nifty-A Clock", - "version": "0.03", + "version": "0.04", "description": "A nifty clock with time and date", "icon": "app.png", "screenshots": [{"url":"screenshot_nifty.png"}], diff --git a/apps/ffcniftyapp/ChangeLog b/apps/ffcniftyapp/ChangeLog new file mode 100644 index 000000000..ef797827e --- /dev/null +++ b/apps/ffcniftyapp/ChangeLog @@ -0,0 +1,4 @@ +0.01: New Clock Nifty A ++ >> adding more information on the right side of the clock +0.02: Fix weather icon for languages other than English + + 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..5ca48c2f1 --- /dev/null +++ b/apps/ffcniftyapp/app.js @@ -0,0 +1,210 @@ +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; +} + +/* +* Choose weather icon to display based on weather conditition code +* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 +*/ +function chooseIconByCode(code) { + const codeGroup = Math.round(code / 100); + switch (codeGroup) { + case 2: return getStorm; + case 3: return getRain; + case 5: + switch (code) { + case 511: return getSnow; + default: return getRain; + } + case 6: return getSnow; + case 7: return getPartSun; + case 8: + switch (code) { + case 800: return getSun; + case 804: return getCloud; + default: return getPartSun; + } + default: return getCloud; + } +} + +/*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 = getErr; + if (locale.name === "en" || locale.name === "en_GB" || locale.name === "en_US") { + w_icon = chooseIcon(curr.txt === undefined ? "no data" : curr.txt); + } else { + // cannot use condition string to determine icon if language is not English; use weather code instead + const code = curr.code || -1; + if (code > 0) { + w_icon = chooseIconByCode(curr.code); + } + } + + 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..6f368160b --- /dev/null +++ b/apps/ffcniftyapp/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "ffcniftyapp", + "name": "Nifty-A Clock ++", + "version": "0.02", + "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/ffcniftyb/ChangeLog b/apps/ffcniftyb/ChangeLog index 9fc7e3c5c..83b11eb78 100644 --- a/apps/ffcniftyb/ChangeLog +++ b/apps/ffcniftyb/ChangeLog @@ -3,3 +3,4 @@ 0.03: Call setUI before loading widgets Fix bug with black being unselectable Improve settings page +0.04: Use ClockFace library diff --git a/apps/ffcniftyb/app.js b/apps/ffcniftyb/app.js index 65c74dbd7..540924fa5 100644 --- a/apps/ffcniftyb/app.js +++ b/apps/ffcniftyb/app.js @@ -1,20 +1,10 @@ -const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"]; -const color = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true)).color; // Default to RED +var scale; +var screen; +var center; +var buf; +var img; -/* Clock *********************************************/ -const scale = g.getWidth() / 176; - -const screen = { - width: g.getWidth(), - height: g.getHeight() - 24, -}; - -const center = { - x: screen.width / 2, - y: screen.height / 2, -}; - -function d02(value) { +function format(value) { return ("0" + value).substr(-2); } @@ -22,91 +12,69 @@ function renderEllipse(g) { g.fillEllipse(center.x - 5 * scale, center.y - 70 * scale, center.x + 160 * scale, center.y + 90 * scale); } -function renderText(g) { - const now = new Date(); +function renderText(g, date) { + const hour = date.getHours() - (this.is12Hour && date.getHours() > 12 ? 12 : 0); + const month = date.getMonth() + 1; - const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); - const minutes = d02(now.getMinutes()); - const day = d02(now.getDate()); - const month = d02(now.getMonth() + 1); - const year = now.getFullYear(); - - const month2 = require("locale").month(now, 3); - const day2 = require("locale").dow(now, 3); + const monthName = require("date_utils").month(month, 1); + const dayName = require("date_utils").dow(date.getDay(), 1); g.setFontAlign(1, 0).setFont("Vector", 90 * scale); - g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale); - g.drawString(minutes, center.x + 32 * scale, center.y + 46 * scale); + g.drawString(format(hour), center.x + 32 * scale, center.y - 31 * scale); + g.drawString(format(date.getMinutes()), center.x + 32 * scale, center.y + 46 * scale); g.setFontAlign(1, 0).setFont("Vector", 16 * scale); - g.drawString(year, center.x + 80 * scale, center.y - 42 * scale); - g.drawString(month, center.x + 80 * scale, center.y - 26 * scale); - g.drawString(day, center.x + 80 * scale, center.y - 10 * scale); - g.drawString(month2, center.x + 80 * scale, center.y + 44 * scale); - g.drawString(day2, center.x + 80 * scale, center.y + 60 * scale); + g.drawString(date.getFullYear(), center.x + 80 * scale, center.y - 42 * scale); + g.drawString(format(month), center.x + 80 * scale, center.y - 26 * scale); + g.drawString(format(date.getDate()), center.x + 80 * scale, center.y - 10 * scale); + g.drawString(monthName, center.x + 80 * scale, center.y + 44 * scale); + g.drawString(dayName, center.x + 80 * scale, center.y + 60 * scale); } -const buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, { - msb: true +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + init: function () { + const appRect = Bangle.appRect; + + screen = { + width: appRect.w, + height: appRect.h + }; + + center = { + x: screen.width / 2, + y: screen.height / 2 + }; + + buf = Graphics.createArrayBuffer(screen.width, screen.height, 1, { msb: true }); + + scale = g.getWidth() / screen.width; + + img = { + width: screen.width, + height: screen.height, + transparent: 0, + bpp: 1, + buffer: buf.buffer + }; + + // default to RED (see settings.js) + // don't use || to default because 0 is a valid color + this.color = this.color === undefined ? 63488 : this.color; + }, + draw: function (date) { + // render outside text with ellipse + buf.clear(); + renderText(buf.setColor(1), date); + renderEllipse(buf.setColor(0)); + g.setColor(this.color).drawImage(img, 0, 24); + + // render ellipse with inside text + buf.clear(); + renderEllipse(buf.setColor(1)); + renderText(buf.setColor(0), date); + g.setColor(this.color).drawImage(img, 0, 24); + }, + settingsFile: "ffcniftyb.json" }); - -function draw() { - - const img = { - width: screen.width, - height: screen.height, - transparent: 0, - bpp: 1, - buffer: buf.buffer - }; - - // cleat screen area - g.clearRect(0, 24, g.getWidth(), g.getHeight()); - - // render outside text with ellipse - buf.clear(); - renderText(buf.setColor(1)); - renderEllipse(buf.setColor(0)); - g.setColor(color).drawImage(img, 0, 24); - - // render ellipse with inside text - buf.clear(); - renderEllipse(buf.setColor(1)); - renderText(buf.setColor(0)); - g.setColor(color).drawImage(img, 0, 24); -} - - -/* Minute Ticker *************************************/ - -let ticker; - -function stopTick() { - if (ticker) { - clearTimeout(ticker); - ticker = undefined; - } -} - -function startTick(run) { - stopTick(); - run(); - ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000)); -} - -/* Init **********************************************/ - -g.clear(); -startTick(draw); - -Bangle.on("lcdPower", (on) => { - if (on) { - startTick(draw); - } else { - stopTick(); - } -}); - -Bangle.setUI("clock"); -Bangle.loadWidgets(); -Bangle.drawWidgets(); +clock.start(); \ No newline at end of file diff --git a/apps/ffcniftyb/metadata.json b/apps/ffcniftyb/metadata.json index 3d26c27ea..019ae6eb3 100644 --- a/apps/ffcniftyb/metadata.json +++ b/apps/ffcniftyb/metadata.json @@ -1,7 +1,7 @@ { "id": "ffcniftyb", "name": "Nifty-B Clock", - "version": "0.03", + "version": "0.04", "description": "A nifty clock (series B) with time, date and colour configuration", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/ffcniftyb/settings.js b/apps/ffcniftyb/settings.js index da350edd8..8c3bb6e4d 100644 --- a/apps/ffcniftyb/settings.js +++ b/apps/ffcniftyb/settings.js @@ -28,4 +28,4 @@ }); E.showMenu(menu); -}); +}) 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/fileman.app.js b/apps/fileman/fileman.app.js index 6a3c5598d..5baae298b 100644 --- a/apps/fileman/fileman.app.js +++ b/apps/fileman/fileman.app.js @@ -7,7 +7,7 @@ var m; var files; function delete_file(fn) { - E.showPrompt("Delete\n"+fn+"?", {buttons: {"No":false, "Yes":true}}).then(function(v) { + E.showPrompt(/*LANG*/"Delete\n"+fn+"?", {buttons: {/*LANG*/"No":false, /*LANG*/"Yes":true}}).then(function(v) { if (v) { if (fn.charCodeAt(fn.length-1)==1) { var fh = STOR.open(fn.substr(0, fn.length-1), "r"); 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/files/ChangeLog b/apps/files/ChangeLog index 1908f7e5c..4622e6f0f 100644 --- a/apps/files/ChangeLog +++ b/apps/files/ChangeLog @@ -3,4 +3,5 @@ 0.04: Add functionality to sort apps manually or alphabetically ascending/descending. 0.05: Tweaks to help with memory usage 0.06: Reduce memory usage -0.07: Allow negative numbers when manual-sorting \ No newline at end of file +0.07: Allow negative numbers when manual-sorting +0.08: Automatic translation of strings. diff --git a/apps/files/files.js b/apps/files/files.js index e7b42c101..2f7b5c9a1 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -1,24 +1,22 @@ const store = require('Storage'); -const boolFormat = (v) => v ? "On" : "Off"; - function showMainMenu() { const mainmenu = { '': { - 'title': 'App Manager', + 'title': /*LANG*/'App Manager', }, '< Back': ()=> {load();}, - 'Sort Apps': () => showSortAppsMenu(), - 'Manage Apps': ()=> showApps(), - 'Compact': () => { - E.showMessage('Compacting...'); + /*LANG*/'Sort Apps': () => showSortAppsMenu(), + /*LANG*/'Manage Apps': ()=> showApps(), + /*LANG*/'Compact': () => { + E.showMessage(/*LANG*/'Compacting...'); try { store.compact(); } catch (e) { } showMainMenu(); }, - 'Free': { + /*LANG*/'Free': { value: undefined, format: (v) => { return store.getFree(); @@ -67,13 +65,13 @@ function eraseData(info) { }); } function eraseApp(app, files,data) { - E.showMessage('Erasing\n' + app.name + '...'); + E.showMessage(/*LANG*/'Erasing\n' + app.name + '...'); var info = store.readJSON(app.id + ".info", 1)||{}; if (files) eraseFiles(info); if (data) eraseData(info); } function eraseOne(app, files,data){ - E.showPrompt('Erase\n'+app.name+'?').then((v) => { + E.showPrompt(/*LANG*/'Erase\n'+app.name+'?').then((v) => { if (v) { Bangle.buzz(100, 1); eraseApp(app, files, data); @@ -84,7 +82,7 @@ function eraseOne(app, files,data){ }); } function eraseAll(apps, files,data) { - E.showPrompt('Erase all?').then((v) => { + E.showPrompt(/*LANG*/'Erase all?').then((v) => { if (v) { Bangle.buzz(100, 1); apps.forEach(app => eraseApp(app, files, data)); @@ -101,11 +99,11 @@ function showAppMenu(app) { '< Back': () => showApps(), }; if (app.hasData) { - appmenu['Erase Completely'] = () => eraseOne(app, true, true); - appmenu['Erase App,Keep Data'] = () => eraseOne(app, true, false); - appmenu['Only Erase Data'] = () => eraseOne(app, false, true); + appmenu[/*LANG*/'Erase Completely'] = () => eraseOne(app, true, true); + appmenu[/*LANG*/'Erase App,Keep Data'] = () => eraseOne(app, true, false); + appmenu[/*LANG*/'Only Erase Data'] = () => eraseOne(app, false, true); } else { - appmenu['Erase'] = () => eraseOne(app, true, false); + appmenu[/*LANG*/'Erase'] = () => eraseOne(app, true, false); } E.showMenu(appmenu); } @@ -113,7 +111,7 @@ function showAppMenu(app) { function showApps() { const appsmenu = { '': { - 'title': 'Apps', + 'title': /*LANG*/'Apps', }, '< Back': () => showMainMenu(), }; @@ -130,17 +128,17 @@ function showApps() { menu[app.name] = () => showAppMenu(app); return menu; }, appsmenu); - appsmenu['Erase All'] = () => { + appsmenu[/*LANG*/'Erase All'] = () => { E.showMenu({ - '': {'title': 'Erase All'}, - 'Erase Everything': () => eraseAll(list, true, true), - 'Erase Apps,Keep Data': () => eraseAll(list, true, false), - 'Only Erase Data': () => eraseAll(list, false, true), + '': {'title': /*LANG*/'Erase All'}, + /*LANG*/'Erase Everything': () => eraseAll(list, true, true), + /*LANG*/'Erase Apps,Keep Data': () => eraseAll(list, true, false), + /*LANG*/'Only Erase Data': () => eraseAll(list, false, true), '< Back': () => showApps(), }); }; } else { - appsmenu['...No Apps...'] = { + appsmenu[/*LANG*/'...No Apps...'] = { value: undefined, format: ()=> '', onchange: ()=> {} @@ -152,16 +150,16 @@ function showApps() { function showSortAppsMenu() { const sorterMenu = { '': { - 'title': 'App Sorter', + 'title': /*LANG*/'App Sorter', }, '< Back': () => showMainMenu(), - 'Sort: manually': ()=> showSortAppsManually(), - 'Sort: alph. ASC': () => { - E.showMessage('Sorting:\nAlphabetically\nascending ...'); + /*LANG*/'Sort: manually': ()=> showSortAppsManually(), + /*LANG*/'Sort: alph. ASC': () => { + E.showMessage(/*LANG*/'Sorting:\nAlphabetically\nascending ...'); sortAlphabet(false); }, 'Sort: alph. DESC': () => { - E.showMessage('Sorting:\nAlphabetically\ndescending ...'); + E.showMessage(/*LANG*/'Sorting:\nAlphabetically\ndescending ...'); sortAlphabet(true); } }; @@ -171,7 +169,7 @@ function showSortAppsMenu() { function showSortAppsManually() { const appsSorterMenu = { '': { - 'title': 'Sort: manually', + 'title': /*LANG*/'Sort: manually', }, '< Back': () => showSortAppsMenu(), }; @@ -188,7 +186,7 @@ function showSortAppsManually() { return menu; }, appsSorterMenu); } else { - appsSorterMenu['...No Apps...'] = { + appsSorterMenu[/*LANG*/'...No Apps...'] = { value: undefined, format: ()=> '', onchange: ()=> {} diff --git a/apps/files/metadata.json b/apps/files/metadata.json index ac73a7717..a53f914e6 100644 --- a/apps/files/metadata.json +++ b/apps/files/metadata.json @@ -1,7 +1,7 @@ { "id": "files", "name": "App Manager", - "version": "0.07", + "version": "0.08", "description": "Show currently installed apps, free space, and allow their deletion from the watch", "icon": "files.png", "tags": "tool,system,files", diff --git a/apps/findphone/ChangeLog b/apps/findphone/ChangeLog index 29100f3c1..27c7460ab 100644 --- a/apps/findphone/ChangeLog +++ b/apps/findphone/ChangeLog @@ -1,3 +1,4 @@ 0.01: First Version 0.02: Remove HID requirement, update screen 0.03: Fix for Bangle 2, toggle find with top half of screen, exit touch bottom half of screen +0.04: Issue newline before GB commands (solves issue with console.log and ignored commands) \ No newline at end of file diff --git a/apps/findphone/app.js b/apps/findphone/app.js index e5e32739a..6a2681031 100644 --- a/apps/findphone/app.js +++ b/apps/findphone/app.js @@ -5,11 +5,11 @@ var finding = false; function draw() { // show message - g.clear(g.theme.bg); + g.clear(g.theme.bg); g.setColor(g.theme.fg); g.setFont("Vector", fontSize); g.setFontAlign(0,0); - + if (finding) { g.drawString("Finding...", g.getWidth()/2, (g.getHeight()/2)-20); g.drawString("Click to stop", g.getWidth()/2, (g.getHeight()/2)+20); @@ -20,6 +20,7 @@ function draw() { } function findPhone(v) { + Bluetooth.println(""); Bluetooth.println(JSON.stringify({t:"findPhone", n:v})); } @@ -43,7 +44,7 @@ if (process.env.HWVERSION == 1) { if (process.env.HWVERSION == 2) { Bangle.on('touch', function(button, xy) { - + // click top part of the screen to stop start if (xy.y < g.getHeight() / 2) { find(); diff --git a/apps/findphone/metadata.json b/apps/findphone/metadata.json index d67c6ec93..4ce72982e 100644 --- a/apps/findphone/metadata.json +++ b/apps/findphone/metadata.json @@ -2,7 +2,7 @@ "id": "findphone", "name": "Find Phone", "shortName": "Find Phone", - "version": "0.03", + "version": "0.04", "description": "Find your phone via Gadgetbridge. Click any button to let your phone ring. 📳 Note: The functionality is available even without this app, just go to Settings, App Settings, Gadgetbridge, Find Phone.", "icon": "app.png", "tags": "tool,android", diff --git a/apps/flappy/ChangeLog b/apps/flappy/ChangeLog index 349cb9d07..d660f85aa 100644 --- a/apps/flappy/ChangeLog +++ b/apps/flappy/ChangeLog @@ -2,3 +2,4 @@ 0.03: A few tweaks to improve rendering speed 0.04: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.05: Don't use Bangle.setLCDMode, just use offscreen buffer (allows widgets) +0.06: Bangle.js 2 enhancements - remove offscreen buffer and render direct diff --git a/apps/flappy/app.js b/apps/flappy/app.js index e9ca31fa5..70553fe97 100644 --- a/apps/flappy/app.js +++ b/apps/flappy/app.js @@ -1,19 +1,20 @@ -b = Graphics.createArrayBuffer(120,120,8); -var gimg = { - width:120, - height:104, - bpp:8, - buffer:b.buffer - }; - +var Y; if (process.env.HWVERSION==2) { - b.flip = function() { - g.drawImage(gimg,28,50); - }; + // we have offscreen graphics, so just go direct + b = g; + Y = 24; // widgets } else { + b = Graphics.createArrayBuffer(120,120,8); + var gimg = { + width:120, + height:104, + bpp:8, + buffer:b.buffer + }; b.flip = function() { g.drawImage(gimg,0,24,{scale:2}); }; + Y = 0; // we offset for widgets anyway } var BIRDIMG = E.toArrayBuffer(atob("EQyI/v7+/v7+/gAAAAAAAP7+/v7+/v7+/gYG0tLS0gDXAP7+/v7+/v4A0tLS0tIA19fXAP7+/v4AAAAA0tLS0gDX1wDXAP7+ANfX19cA0tLSANfXANcA/v4A19fX19cA0tLSANfX1wD+/gDS19fX0gDS0tLSAAAAAAD+/gDS0tIA0tLS0gDAwMDAwAD+/gAAAM3Nzc0AwAAAAAAA/v7+/v4Azc3Nzc0AwMDAwAD+/v7+/v4AAM3Nzc0AAAAAAP7+/v7+/v7+AAAAAP7+/v7+/g==")) @@ -30,14 +31,14 @@ function newBarrier(x) { barriers.push({ x1 : x-7, x2 : x+7, - y : 20+Math.random()*38, + y : Y+20+Math.random()*38, gap : 12+Math.random()*15 }); } function gameStart() { running = true; - birdy = 48/2; + birdy = Y + 48/2; birdvy = 0; barriers = []; for (var i=38;ibbot)) gameStop(); }); diff --git a/apps/flappy/metadata.json b/apps/flappy/metadata.json index 910797066..cb50f0094 100644 --- a/apps/flappy/metadata.json +++ b/apps/flappy/metadata.json @@ -1,7 +1,7 @@ { "id": "flappy", "name": "Flappy Bird", - "version": "0.05", + "version": "0.06", "description": "A Flappy Bird game clone", "icon": "app.png", "screenshots": [{"url":"screenshot1_flappy.png"},{"url":"screenshot2_flappy.png"}], diff --git a/apps/flashcards/ChangeLog b/apps/flashcards/ChangeLog new file mode 100644 index 000000000..4c0434f5b --- /dev/null +++ b/apps/flashcards/ChangeLog @@ -0,0 +1,6 @@ +1.00: Local cards data +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 +1.32: Minor code improvements diff --git a/apps/flashcards/README.md b/apps/flashcards/README.md new file mode 100644 index 000000000..484d1102f --- /dev/null +++ b/apps/flashcards/README.md @@ -0,0 +1,19 @@ +A simple flash cards application based on Trello public board. + +Configuration: + +1. Create public Trello board +2. Create new Trello list +3. Add Trello cards: +- card name will be flash card front text +- card description will be flash card back text +4. Add ".json" to the end of the Trello board URL and refresh page +5. Find your list ID +6. Save list ID to the "flashcards.settings.json" file on your watch, e.g.: +{"listId":"65942f7b27z68000996ddc00","fontSize":1,"cardWidth":9,"swipeGesture":1} +7. Connect phone with Gadgetbridge to the watch +8. Enable "Allow Internet Access" in Gadgetbridge +9. On the watch go to Settings -> Apps -> Flash Cards -> Get from Trello +10. Start Flash Cards as watch app or set it as watch clock face +11. Swipe left/right to change card +12. Tap to switch card front/back text \ No newline at end of file diff --git a/apps/flashcards/app-icon.js b/apps/flashcards/app-icon.js new file mode 100644 index 000000000..72371cac4 --- /dev/null +++ b/apps/flashcards/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH0iABQWKgQXLkAXhCxZIKFxgwKC68zABwWGgYXPmAX/C/4X/C/4X/C9ndACQX/C6dEAAQXS6gXDpovpR/4X/C8ENCyPQC4YA/AGo")) diff --git a/apps/flashcards/app.js b/apps/flashcards/app.js new file mode 100644 index 000000000..43dde213e --- /dev/null +++ b/apps/flashcards/app.js @@ -0,0 +1,186 @@ +/** + * Copyright 2023 Crisp Advice + * We believe in Finnish + */ +{ + // Modules + let Layout = require("Layout"); + let locale = require("locale"); + let storage = require("Storage"); + + // Global variables + const SWAP_SIDE_BUZZ_MILLISECONDS = 50; + const CARD_DATA_FILE = "flashcards.data.json"; + const CARD_SETTINGS_FILE = "flashcards.settings.json"; + const CARD_EMPTY = "no cards found"; + + let cards = []; + let cardIndex = 0; + let backSide = false; + let drawTimeout; + let fontSizes = ["15%","20%","25%"]; + let lastDragX = 0; + let lastDragY = 0; + + let settings = Object.assign({ + listId: "", + fontSize: 1, + cardWidth: 9, + swipeGesture: 1 + }, storage.readJSON(CARD_SETTINGS_FILE, true) || {}); + + // Cards data + let wordWrap = function (textStr, maxLength) { + if (maxLength == undefined) { + maxLength = settings.cardWidth; + } + let res = ''; + let str = textStr.trim(); + while (str.length > maxLength) { + let found = false; + // Inserts new line at first whitespace of the line + 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); + found = true; + break; + } + } + // Inserts new line at MAX_LENGTH position, the word is too long to wrap + if (!found) { + res += [str.slice(0, maxLength), "\n"].join(''); + str = str.slice(maxLength); + } + } + return res + str; + } + + let loadLocalCards = function() { + var cardsJSON = ""; + if (storage.read(CARD_DATA_FILE)) + { + cardsJSON = storage.readJSON(CARD_DATA_FILE, 1) || {}; + } + refreshCards(cardsJSON,false); + } + + let refreshCards = function(cardsJSON,showMsg) + { + cardIndex = 0; + backSide = false; + cards = []; + + if (cardsJSON && cardsJSON.length) { + cardsJSON.forEach(card => { + cards.push([ wordWrap(card.name), wordWrap(card.desc) ]); + }); + } + + if (!cards.length) { + cards.push([ wordWrap(CARD_EMPTY), wordWrap(CARD_EMPTY) ]); + drawMessage("e: cards not found"); + } else if (showMsg) { + drawMessage("i: cards refreshed"); + } + } + + // Drawing a card + let queueDraw = function() { + let timeout = 60000; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, timeout - (Date.now() % timeout)); + }; + + let cardLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:3", label:"", id:"widgets", fillx:1 }, + {type:"txt", font:fontSizes[settings.fontSize], label:"ABCDEFGHIJ KLMNOPQRST UVWXYZÅÖÄ", filly:1, fillx:1, id:"card" }, + {type:"txt", font:"6x8:2", label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg } + ] + }, {lazy:true}); + + let drawCard = function() { + cardLayout.card.label = cards[cardIndex][backSide ? 1 : 0]; + cardLayout.clock.label = locale.time(new Date(),1); + cardLayout.render(); + } + + let drawMessage = function(msg) { + cardLayout.card.label = wordWrap(msg); + cardLayout.render(); + console.log(msg); + } + + let draw = function() { + drawCard(); + Bangle.drawWidgets(); + queueDraw(); + } + + let swipeCard = function(forward) + { + if(forward) { + cardIndex = (cardIndex + 1) % cards.length; + } + else if(--cardIndex < 0) { + cardIndex = cards.length - 1; + } + drawCard(); + } + + // Handle a touch: swap card side + let handleTouch = function(zone, event) { + backSide = !backSide; + drawCard(); + Bangle.buzz(SWAP_SIDE_BUZZ_MILLISECONDS); + } + + // Handle a stroke event: cycle cards + let handleStroke = function(event) { + let first_x = event.xy[0]; + let last_x = event.xy[event.xy.length - 2]; + swipeCard((last_x - first_x) > 0); + } + + // Handle a drag event: cycle cards + let handleDrag = function(event) { + let isFingerReleased = (event.b === 0); + if(isFingerReleased) { + let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) && + (lastDragX !== 0); + if(isHorizontalDrag) { + swipeCard(lastDragX > 0); + } + } + else { + lastDragX = event.dx; + lastDragY = event.dy; + } + } + + // Ensure pressing the button goes to the launcher (by making this seem like a clock?) + Bangle.setUI({mode:"clock", remove:function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + Bangle.removeListener("touch", handleTouch); + if (settings.swipeGesture) { Bangle.removeListener("drag", handleDrag);} else { Bangle.removeListener("stroke", handleStroke); } + }}); + + // initialize + cardLayout.update(); + Bangle.loadWidgets(); + loadLocalCards(); + + Bangle.on("touch", handleTouch); + if (settings.swipeGesture) { Bangle.on("drag", handleDrag); } else { Bangle.on("stroke", handleStroke); } + + // On start: display the first card + g.clear(); + draw(); +} + + diff --git a/apps/flashcards/app.png b/apps/flashcards/app.png new file mode 100644 index 000000000..e16b8a263 Binary files /dev/null and b/apps/flashcards/app.png differ diff --git a/apps/flashcards/flashcards.data.json b/apps/flashcards/flashcards.data.json new file mode 100644 index 000000000..2bda9fd54 --- /dev/null +++ b/apps/flashcards/flashcards.data.json @@ -0,0 +1 @@ +[{"id":"634fb1548fcbf401dcb49cf4","name":"monikeitin","desc":"multicooker"},{"id":"634fb18855a957017a5d03f7","name":"lihamylly","desc":"meat grinder"},{"id":"634fb2b50f1f1101890aa7f7","name":"uuni","desc":"oven"},{"id":"633988016d09b300c3770b6d","name":"riittää","desc":"is enough"},{"id":"634fb26ea0aef000ec78e481","name":"kahvinkeitin","desc":"coffee maker"},{"id":"634fb1f8e1378600b51bb317","name":"mehulinko","desc":"juicer"},{"id":"634fb307df637101f7f36b2d","name":"palovaroitin","desc":"smoke \nsensor"},{"id":"634fb29699f51701d79a2cbb","name":"tiskikone","desc":"dishwashing \nmachine"},{"id":"634fb2bbe01e1c0446179a40","name":"liesi","desc":"kitchen\nstove"},{"id":"63419634ce475100b444d577","name":"kohtalainen","desc":"moderate"}] diff --git a/apps/flashcards/flashcards.settings.json b/apps/flashcards/flashcards.settings.json new file mode 100644 index 000000000..7c8ef545b --- /dev/null +++ b/apps/flashcards/flashcards.settings.json @@ -0,0 +1 @@ +{"listId":"","fontSize":1,"cardWidth":9,"swipeGesture":1} diff --git a/apps/flashcards/metadata.json b/apps/flashcards/metadata.json new file mode 100644 index 000000000..dee6a9e3a --- /dev/null +++ b/apps/flashcards/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "flashcards", + "name": "Flash Cards", + "shortName": "Flash Cards", + "version": "1.32", + "description": "Flash cards based on public Trello board", + "readme":"README.md", + "screenshots" : [ { "url":"screenshot.png" }], + "icon": "app.png", + "tags": "flash cards", + "type": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"flashcards.app.js","url":"app.js"}, + {"name":"flashcards.settings.js","url":"settings.js"}, + {"name":"flashcards.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"flashcards.data.json"}, + {"name":"flashcards.settings.json"} + ] +} diff --git a/apps/flashcards/screenshot.png b/apps/flashcards/screenshot.png new file mode 100644 index 000000000..f9c5bcb63 Binary files /dev/null and b/apps/flashcards/screenshot.png differ diff --git a/apps/flashcards/settings.js b/apps/flashcards/settings.js new file mode 100644 index 000000000..cc03349b4 --- /dev/null +++ b/apps/flashcards/settings.js @@ -0,0 +1,79 @@ +(function(back) { + var storage = require("Storage"); + + var settingsFile = "flashcards.settings.json"; + var dataFile = "flashcards.data.json"; + var trelloTimeout = 3000; + var trelloURL = "https://api.trello.com/1/lists/$cardsListId/cards/?fields=name%2Cdesc%2Clist"; + + var settings = Object.assign({ + listId: "", + fontSize: 1, + cardWidth: 9, + swipeGesture: 1 + }, storage.readJSON(settingsFile, true) || {}); + + function writeSettings() { + storage.writeJSON(settingsFile, settings); + } + + const fontSizes = [/*LANG*/"Small",/*LANG*/"Medium",/*LANG*/"Large"]; + const swipeGestures = [/*LANG*/"Stroke",/*LANG*/"Drag"]; + var settingsMenu = { + "" : { "title" : "Flash Cards" }, + "< Back" : () => back(), + /*LANG*/"Get from Trello": () => { + if (!storage.read(settingsFile)) { writeSettings();} + E.showPrompt(/*LANG*/"Download cards?").then((v) => { + let delay = 500; + if (v) { + if (Bangle.http) + { + if (settings.listId) + { + delay = delay + trelloTimeout; + E.showMessage(/*LANG*/"Downloading"); + Bangle.http(trelloURL.replace("$cardsListId", settings.listId), + { + timeout : trelloTimeout, + method: "GET", + headers: { "Content-Type": "application/json" } + }).then(data=>{ + var cardsJSON = JSON.parse(data.resp); + storage.write(dataFile, JSON.stringify(cardsJSON)); + E.showMessage(/*LANG*/"Downloaded"); + }) + .catch((e) => { + E.showMessage(/*LANG*/"Error:" + e); + }); + } else { + E.showMessage(/*LANG*/"List Id not found"); + } + } else { + E.showMessage(/*LANG*/"Gadgetbridge not found"); + } + } + setTimeout(() => E.showMenu(settingsMenu), delay); + }); + }, + /*LANG*/"Font size": { + value: settings.fontSize, + min: 0, max: 2, wrap: true, + format: v => fontSizes[v], + onchange: v => { settings.fontSize = v; writeSettings(); } + }, + /*LANG*/"Card width": { + value: settings.cardWidth, + min: 6, max: 12, + onchange: v => { settings.cardWidth = v; writeSettings(); } + }, + /*LANG*/"Swipe gesture": { + value: settings.swipeGesture, + min: 0, max: 1, wrap: true, + format: v => swipeGestures[v], + onchange: v => { settings.swipeGesture = v; writeSettings(); } + } + } + // Show the menu + E.showMenu(settingsMenu); +})//(load) \ No newline at end of file diff --git a/apps/flashcount/ChangeLog b/apps/flashcount/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/flashcount/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/flashcount/app-icon.js b/apps/flashcount/app-icon.js new file mode 100644 index 000000000..e1cf5fb54 --- /dev/null +++ b/apps/flashcount/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///x3ygHo8H1Jf8AgILLoALVgoLHggLCqAgJioLIqgLDGQsBqtAioOBqoYFqtUAIMVBY9VqwCBDIIAECYILCHowLBrWVBZFlrWWyptGgtq1WqJYI7GrQLCFxGrBYJHEBQNV1Wv9IEBEocFKIOq//qJAIZEAoNq3/+1QMBHoYYBrQLB1J4GitaEYZfGtfvBYJ3HtWr9WlNY0V1Nr1WlC4xIBrWmBZWVrJGFcYILBZY4LBoILIgoNBEILvHDIQ5BBY4IBBYMBMAwLBBA4LPBRMAKAoLRiALWAGw=")) \ No newline at end of file diff --git a/apps/flashcount/app.js b/apps/flashcount/app.js new file mode 100644 index 000000000..e3f925b27 --- /dev/null +++ b/apps/flashcount/app.js @@ -0,0 +1,59 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); +E.showMessage("Loading..."); +Bangle.setOptions({hrmPollInterval:5}); +Bangle.setHRMPower(1); + +function drawCounter() { + g.reset().clearRect(0,24,175,90); + //g.drawRect(0,24,175,90); + g.setFontAlign(0,0).setFontVector(60); + g.drawString(count, 88, 60); +} + +function hadPulse() { + count++; + drawCounter(); + g.setColor("#f00").fillCircle(156,156,20); + setTimeout(function() { + g.setColor(g.theme.bg).fillCircle(156,156,20); + }, 500); +} + +if (parseFloat(process.env.VERSION.replace("v","0"))<2019) { + E.showMessage("You need at least firmware 2v19","Error"); +} else if (Bangle.hrmRd(0)!=33) { // wrong sensor - probably VC31 from original bangle.js 2 + E.showMessage("This Bangle.js doesn't have a VC31B HRM sensor","Error"); +} else { + Bangle.setOptions({hrmGreenAdjust:false, hrmWearDetect:false, hrmPushEnv:true}); + Bangle.hrmWr(0x10, 197&0xF8 | 4); // just SLOT2 + Bangle.hrmWr(0x16, 0); // force env to be used as fast as possible + + var samples = 0, samplesHi = 0; + var count = 0; + { + let last = 0; + Bangle.on('HRM-env',v => { + if (v) { + if (!last) hadPulse(); + samplesHi++; + } + last = v; + samples++; + }); + } + + drawCounter(); + setInterval(function() { + g.reset().clearRect(0,90,175,130); + g.setFontAlign(0,0).setFont("6x8:2"); + g.drawString(samples+" sps", 88, 100); + if (samplesHi*5 > samples) { + g.setBgColor("#f00").setColor("#fff"); + g.clearRect(0,110,175,130).drawString("TOO LIGHT",88,120); + } + samples=0; + samplesHi=0; + Bangle.setLCDPower(1); // force LCD on! + }, 1000); +} \ No newline at end of file diff --git a/apps/flashcount/app.png b/apps/flashcount/app.png new file mode 100644 index 000000000..379b9d381 Binary files /dev/null and b/apps/flashcount/app.png differ diff --git a/apps/flashcount/metadata.json b/apps/flashcount/metadata.json new file mode 100644 index 000000000..1c0f785fd --- /dev/null +++ b/apps/flashcount/metadata.json @@ -0,0 +1,13 @@ +{ "id": "flashcount", + "name": "Flash Counter", + "shortName":"FlashCount", + "version":"0.01", + "description": "Count flashes/pulses of light using the heart rate monitor. Requires a VC31B HRM sensor, which should be in most watches except those produced for the original KickStarter campaign.", + "icon": "app.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"flashcount.app.js","url":"app.js"}, + {"name":"flashcount.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/flightdash/ChangeLog b/apps/flightdash/ChangeLog new file mode 100644 index 000000000..b0e36941a --- /dev/null +++ b/apps/flightdash/ChangeLog @@ -0,0 +1,2 @@ +1.00: initial release +1.01: Minor code improvements diff --git a/apps/flightdash/README.md b/apps/flightdash/README.md new file mode 100644 index 000000000..07b753178 --- /dev/null +++ b/apps/flightdash/README.md @@ -0,0 +1,76 @@ +# Flight Dashboard + +Shows basic flight and navigation instruments. + +![](screenshot.png) + +Basic flight data includes: + +- Ground speed +- Track +- Altimeter +- VSI +- Local time + +You can also set a destination to get nav guidance: + +- Distance from destination +- Bearing to destination +- Estimated Time En-route (minutes and seconds) +- Estimated Time of Arrival (in UTC) + +The speed/distance and altitude units are configurable. + +Altitude data can be derived from GPS or the Bangle's barometer. + + +## DISCLAIMER + +Remember to Aviate - Navigate - Communicate! Do NOT get distracted by your +gadgets, keep your eyes looking outside and do NOT rely on this app for actual +navigation! + + +## Usage + +After installing the app, use the "interface" page (floppy disk icon) in the +App Loader to filter and upload a list of airports (to be used as navigation +destinations). Due to memory constraints, only up to about 500 airports can be +stored on the Bangle itself (recommended is around 100 - 150 airports max.). + +Then, on the Bangle, access the Flight-Dash settings, either through the +Settings app (Settings -> Apps -> Flight-Dash) or a tap anywhere in the +Flight-Dash app itself. The following settings are available: + +- **Nav Dest.**: Choose the navigation destination: + - Nearest airports (from the uploaded list) + - Search the uploaded list of airports + - User waypoints (which can be set/edited through the settings) + - Nearest airports (queried online through AVWX - requires Internet connection at the time) +- **Speed** and **Altitude**: Set the preferred units of measurements. +- **Use Baro**: If enabled, altitude information is derived from the Bangle's barometer (instead of using GPS altitude). + +If the barometer is used for altitude information, the current QNH value is +also displayed. It can be adjusted by swiping up/down in the app. + +To query the nearest airports online through AVWX, you have to install - and +configure - the [avwx](?id=avwx) module. + +The app requires a text input method (to set user waypoint names, and search +for airports), and if not already installed will automatically install the +default "textinput" app as a dependency. + + +## Hint + +Under the bearing "band", the current nav destination is displayed. Next to +that, you'll also find the cardinal direction you are approaching **from**. +This can be useful for inbound radio calls. Together with the distance, the +current altitude and the ETA, you have all the information required to make +radio calls like a pro! + + +## Author + +Flaparoo [github](https://github.com/flaparoo) + diff --git a/apps/flightdash/flightdash-icon.js b/apps/flightdash/flightdash-icon.js new file mode 100644 index 000000000..3a2e2757c --- /dev/null +++ b/apps/flightdash/flightdash-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4")) diff --git a/apps/flightdash/flightdash.app.js b/apps/flightdash/flightdash.app.js new file mode 100644 index 000000000..ac0146210 --- /dev/null +++ b/apps/flightdash/flightdash.app.js @@ -0,0 +1,527 @@ +/* + * Flight Dashboard - Bangle.js + */ + +const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0) +const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1) +const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0) +const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1) +const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1) +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 dataFontHeight = 22; +const secondaryFontHeight = 18; +const labelFontHeight = 12; + + +//globals +var settings = {}; + +//var updateInterval; + +var speed = '-'; var speedPrev = -1; +var track = '-'; var trackPrev = -1; +var lat = 0; var lon = 0; +var distance = '-'; var distancePrev = -1; +var bearing = '-'; var bearingPrev = -1; +var relativeBearing = 0; var relativeBearingPrev = -1; +var fromCardinal = '-'; +var ETAdate = new Date(); +var ETA = '-'; var ETAPrev = ''; + +var QNH = Math.round(Bangle.getOptions().seaLevelPressure); var QNHPrev = -1; + +var altitude = '-'; var altitudePrev = -1; + +var VSI = '-'; var VSIPrev = -1; +var VSIraw = 0; +var VSIprevTimestamp = Date.now(); +var VSIprevAltitude; +var VSIsamples = 0; var VSIsamplesCount = 0; + +var speedUnit = 'N/A'; +var distanceUnit = 'N/A'; +var altUnit = 'N/A'; + + +// date object to time string in format (HH:MM[:SS]) +function timeStr(date, seconds) { + let timeStr = date.getHours().toString(); + if (timeStr.length == 1) timeStr = '0' + timeStr; + let minutes = date.getMinutes().toString(); + if (minutes.length == 1) minutes = '0' + minutes; + timeStr += ':' + minutes; + if (seconds) { + let seconds = date.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + timeStr += ':' + seconds; + } + return timeStr; +} + +// add thousands separator to number +function addThousandSeparator(n) { + let s = n.toString(); + if (s.length > 3) { + return s.substr(0, s.length - 3) + ',' + s.substr(s.length - 3, 3); + } else { + return s; + } +} + + +// update VSI +function updateVSI(alt) { + VSIsamples += alt; VSIsamplesCount += 1; + let VSInewTimestamp = Date.now(); + if (VSIprevTimestamp + 1000 <= VSInewTimestamp) { // update VSI every 1 second + let VSInewAltitude = VSIsamples / VSIsamplesCount; + if (VSIprevAltitude) { + let VSIinterval = (VSInewTimestamp - VSIprevTimestamp) / 1000; + VSIraw = (VSInewAltitude - VSIprevAltitude) * 60 / VSIinterval; // extrapolate to change / minute + } + VSIprevTimestamp = VSInewTimestamp; + VSIprevAltitude = VSInewAltitude; + VSIsamples = 0; VSIsamplesCount = 0; + } + + VSI = Math.floor(VSIraw / 10) * 10; // "smooth" VSI value + if (settings.altimeterUnits == 0) { // Feet + VSI = Math.round(VSI * 3.28084); + } // nothing else required since VSI is already in meters ("smoothed") + + if (VSI > 9999) VSI = 9999; + else if (VSI < -9999) VSI = -9999; +} + +// update GPS-derived information +function updateGPS(fix) { + if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return; + + speed = 'N/A'; + if (settings.speedUnits == 0) { // Knots + speed = Math.round(fix.speed * 0.539957); + } else if (settings.speedUnits == 1) { // km/h + speed = Math.round(fix.speed); + } else if (settings.speedUnits == 2) { // MPH + speed = Math.round(fix.speed * 0.621371); + } + if (speed > 9999) speed = 9999; + + if (! settings.useBaro) { // use GPS altitude + altitude = 'N/A'; + if (settings.altimeterUnits == 0) { // Feet + altitude = Math.round(fix.alt * 3.28084); + } else if (settings.altimeterUnits == 1) { // Meters + altitude = Math.round(fix.alt); + } + if (altitude > 99999) altitude = 99999; + + updateVSI(fix.alt); + } + + track = Math.round(fix.course); + if (isNaN(track)) track = '-'; + else if (track < 10) track = '00'+track; + else if (track < 100) track = '0'+track; + + lat = fix.lat; + lon = fix.lon; + + // calculation from https://www.movable-type.co.uk/scripts/latlong.html + const latRad1 = lat * Math.PI/180; + const latRad2 = settings.destLat * Math.PI/180; + const lonRad1 = lon * Math.PI/180; + const lonRad2 = settings.destLon * Math.PI/180; + + // distance (using "Equirectangular approximation") + let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2); + let y = (latRad2 - latRad1); + let distanceNumber = Math.sqrt(x*x + y*y) * 6371; // in km - 6371 = mean Earth radius + if (settings.speedUnits == 0) { // NM + distanceNumber = distanceNumber * 0.539957; + } else if (settings.speedUnits == 2) { // miles + distanceNumber = distanceNumber * 0.621371; + } + if (distanceNumber > 99.9) { + distance = '>100'; + } else { + distance = (Math.round(distanceNumber * 10) / 10).toString(); + if (! distance.includes('.')) + distance += '.0'; + } + + // bearing + y = Math.sin(lonRad2 - lonRad1) * Math.cos(latRad2); + x = Math.cos(latRad1) * Math.sin(latRad2) - + Math.sin(latRad1) * Math.cos(latRad2) * Math.cos(lonRad2 - lonRad1); + let nonNormalisedBearing = Math.atan2(y, x); + bearing = Math.round((nonNormalisedBearing * 180 / Math.PI + 360) % 360); + + if (bearing > 337 || bearing < 23) { + fromCardinal = 'S'; + } else if (bearing < 68) { + fromCardinal = 'SW'; + } else if (bearing < 113) { + fromCardinal = 'W'; + } else if (bearing < 158) { + fromCardinal = 'NW'; + } else if (bearing < 203) { + fromCardinal = 'N'; + } else if (bearing < 248) { + fromCardinal = 'NE'; + } else if (bearing < 293) { + fromCardinal = 'E'; + } else{ + fromCardinal = 'SE'; + } + + if (bearing < 10) bearing = '00'+bearing; + else if (bearing < 100) bearing = '0'+bearing; + + relativeBearing = parseInt(bearing) - parseInt(track); + if (isNaN(relativeBearing)) relativeBearing = 0; + if (relativeBearing > 180) relativeBearing -= 360; + else if (relativeBearing < -180) relativeBearing += 360; + + // ETA + if (speed) { + let ETE = distanceNumber * 3600 / speed; + let now = new Date(); + ETAdate = new Date(now + (now.getTimezoneOffset() * 1000 * 60) + ETE*1000); + if (ETE < 86400) { + ETA = timeStr(ETAdate, false); + } else { + ETA = '>24h'; + } + } else { + ETAdate = new Date(); + ETA = '-'; + } +} + + +// update barometric information +function updatePressure(e) { + altitude = 'N/A'; + if (settings.altimeterUnits == 0) { // Feet + altitude = Math.round(e.altitude * 3.28084); + } else if (settings.altimeterUnits == 1) { // Meters + altitude = Math.round(e.altitude); // altitude is given in meters + } + if (altitude > 99999) altitude = 99999; + + updateVSI(e.altitude); +} + + +// (re-)draw all read-outs +function draw(initial) { + + g.setBgColor(COLOUR_BLACK); + + // speed + if (speed != speedPrev || initial) { + g.setFontAlign(-1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_GREEN); + g.clearRect(0, 0, 55, dataFontHeight); + g.drawString(speed.toString(), 0, 0, false); + if (initial) { + g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(speedUnit, 0, dataFontHeight, false); + } + speedPrev = speed; + } + + + // distance + if (distance != distancePrev || initial) { + g.setFontAlign(1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.clearRect(g.getWidth() - 58, 0, g.getWidth(), dataFontHeight); + g.drawString(distance, g.getWidth(), 0, false); + if (initial) { + g.setFontAlign(1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(distanceUnit, g.getWidth(), dataFontHeight, false); + } + distancePrev = distance; + } + + + // track (+ static track/bearing content) + let trackY = 18; + let destInfoY = trackY + 53; + if (track != trackPrev || initial) { + g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.clearRect(horizontalCenter - 29, trackY, horizontalCenter + 28, trackY + dataFontHeight); + g.drawString(track.toString() + "\xB0", horizontalCenter + 3, trackY, false); + if (initial) { + let y = trackY + dataFontHeight + 1; + g.setColor(COLOUR_YELLOW); + g.drawRect(horizontalCenter - 30, trackY - 3, horizontalCenter + 29, y); + g.drawLine(0, y, g.getWidth(), y); + y += dataFontHeight + 5; + g.drawLine(0, y, g.getWidth(), y); + + g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(settings.destID, horizontalCenter, destInfoY, false); + } + trackPrev = track; + } + + + // bearing + if (bearing != bearingPrev || relativeBearing != relativeBearingPrev || initial) { + let bearingY = trackY + 27; + + g.clearRect(0, bearingY, g.getWidth(), bearingY + dataFontHeight); + + g.setColor(COLOUR_YELLOW); + for (let i = Math.floor(relativeBearing * 2.5) % 25; i <= g.getWidth(); i += 25) { + g.drawLine(i, bearingY + 3, i, bearingY + 16); + } + + let bearingX = horizontalCenter + relativeBearing * 2.5; + if (bearingX > g.getWidth() - 26) bearingX = g.getWidth() - 26; + else if (bearingX < 26) bearingX = 26; + g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(bearing.toString() + "\xB0", bearingX + 3, bearingY, false); + + g.clearRect(horizontalCenter + 42, destInfoY, horizontalCenter + 69, destInfoY + secondaryFontHeight); + g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA); + g.drawString(fromCardinal, horizontalCenter + 42, destInfoY, false); + if (initial) { + g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(' from', horizontalCenter, destInfoY, false); + } + + bearingPrev = bearing; + relativeBearingPrev = relativeBearing; + } + + + let row3y = g.getHeight() - 48; + + // QNH + if (settings.useBaro) { + if (QNH != QNHPrev || initial) { + let QNHy = row3y - secondaryFontHeight - 2; + g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(horizontalCenter - 29, QNHy - secondaryFontHeight, horizontalCenter + 22, QNHy); + g.drawString(QNH.toString(), horizontalCenter - 3, QNHy, false); + if (initial) { + g.setFontAlign(0, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('QNH', horizontalCenter - 3, QNHy, false); + } + QNHPrev = QNH; + } + } + + + // VSI + if (VSI != VSIPrev || initial) { + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(0, row3y - secondaryFontHeight, 51, row3y); + g.drawString(VSI.toString(), 0, row3y, false); + if (initial) { + g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(altUnit + '/min', 0, row3y - secondaryFontHeight, false); + } + + let VSIarrowX = 6; + let VSIarrowY = row3y - 42; + g.clearRect(VSIarrowX - 7, VSIarrowY - 10, VSIarrowX + 6, VSIarrowY + 10); + g.setColor(COLOUR_WHITE); + if (VSIraw > 30) { // climbing + g.fillRect(VSIarrowX - 1, VSIarrowY, VSIarrowX + 1, VSIarrowY + 10); + g.fillPoly([ VSIarrowX , VSIarrowY - 11, + VSIarrowX + 7, VSIarrowY, + VSIarrowX - 7, VSIarrowY]); + } else if (VSIraw < -30) { // descending + g.fillRect(VSIarrowX - 1, VSIarrowY - 10, VSIarrowX + 1, VSIarrowY); + g.fillPoly([ VSIarrowX , VSIarrowY + 11, + VSIarrowX + 7, VSIarrowY, + VSIarrowX - 7, VSIarrowY ]); + } + } + + + // altitude + if (altitude != altitudePrev || initial) { + g.setFontAlign(1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.clearRect(g.getWidth() - 65, row3y - secondaryFontHeight, g.getWidth(), row3y); + g.drawString(addThousandSeparator(altitude), g.getWidth(), row3y, false); + if (initial) { + g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString(altUnit, g.getWidth(), row3y - secondaryFontHeight, false); + } + altitudePrev = altitude; + } + + + // time + let now = new Date(); + let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60)); + g.setFontAlign(-1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_LIGHT_BLUE); + let timeStrMetrics = g.stringMetrics(timeStr(now, false)); + g.drawString(timeStr(now, false), 0, g.getHeight(), true); + + let seconds = now.getSeconds().toString(); + if (seconds.length == 1) seconds = '0' + seconds; + g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight); + g.drawString(seconds, timeStrMetrics.width + 2, g.getHeight() - 1, true); + + if (initial) { + g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('LOCAL', 0, g.getHeight() - dataFontHeight, false); + } + + + // ETE + let ETEy = g.getHeight() - dataFontHeight; + let ETE = '-'; + if (ETA != '-') { + let ETEseconds = Math.floor((ETAdate - nowUTC) / 1000); + if (ETEseconds < 0) ETEseconds = 0; + ETE = ETEseconds % 60; + if (ETE < 10) ETE = '0' + ETE; + ETE = Math.floor(ETEseconds / 60) + ':' + ETE; + if (ETE.length > 6) ETE = '>999m'; + } + g.clearRect(horizontalCenter - 35, ETEy - secondaryFontHeight, horizontalCenter + 29, ETEy); + g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE); + g.drawString(ETE, horizontalCenter - 3, ETEy, false); + if (initial) { + g.setFontAlign(0, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('ETE', horizontalCenter - 3, ETEy - secondaryFontHeight, false); + } + + + // ETA + if (ETA != ETAPrev || initial) { + g.clearRect(g.getWidth() - 63, g.getHeight() - dataFontHeight, g.getWidth(), g.getHeight()); + g.setFontAlign(1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE); + g.drawString(ETA, g.getWidth(), g.getHeight(), false); + if (initial) { + g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN); + g.drawString('UTC ETA', g.getWidth(), g.getHeight() - dataFontHeight, false); + } + ETAPrev = ETA; + } +} + + +function handleSwipes(directionLR, directionUD) { + if (directionUD == -1) { // up -> increase QNH + QNH = Math.round(Bangle.getOptions().seaLevelPressure); + QNH++; + Bangle.setOptions({'seaLevelPressure': QNH}); + } else if (directionUD == 1) { // down -> decrease QNH + QNH = Math.round(Bangle.getOptions().seaLevelPressure); + QNH--; + Bangle.setOptions({'seaLevelPressure': QNH}); + } +} + +function handleTouch(button, xy) { + if ('handled' in xy && xy.handled) return; + Bangle.removeListener('touch', handleTouch); + if (settings.useBaro) { + Bangle.removeListener('swipe', handleSwipes); + } + + // any touch -> show settings + clearInterval(updateTimeInterval); + Bangle.setGPSPower(false, APP_NAME); + if (settings.useBaro) + Bangle.setBarometerPower(false, APP_NAME); + + eval(require("Storage").read(APP_NAME+'.settings.js'))( () => { + E.showMenu(); + // "clear" values potentially affected by a settings change + speed = '-'; distance = '-'; + altitude = '-'; VSI = '-'; + // re-launch + start(); + }); +} + + +/* + * main + */ +function start() { + + // read in the settings + settings = Object.assign({ + useBaro: false, + speedUnits: 0, // KTS + altimeterUnits: 0, // FT + destID: 'KOSH', + destLat: 43.9844, + destLon: -88.5570, + }, require('Storage').readJSON(APP_NAME+'.json', true) || {}); + + // set units + if (settings.speedUnits == 0) { // Knots + speedUnit = 'KTS'; + distanceUnit = 'NM'; + } else if (settings.speedUnits == 1) { // km/h + speedUnit = 'KPH'; + distanceUnit = 'KM'; + } else if (settings.speedUnits == 2) { // MPH + speedUnit = 'MPH'; + distanceUnit = 'SM'; + } + + if (settings.altimeterUnits == 0) { // Feet + altUnit = 'FT'; + } else if (settings.altimeterUnits == 1) { // Meters + altUnit = 'M'; + } + + // initialise + g.reset(); + g.setBgColor(COLOUR_BLACK); + g.clear(); + + // draw incl. static components + draw(true); + + // enable timeout/interval and sensors + setTimeout(function() { + draw(); + updateTimeInterval = setInterval(draw, 1000); + }, 1000 - (Date.now() % 1000)); + + Bangle.setGPSPower(true, APP_NAME); + Bangle.on('GPS', updateGPS); + + if (settings.useBaro) { + Bangle.setBarometerPower(true, APP_NAME); + Bangle.on('pressure', updatePressure); + } + + // handle interaction + if (settings.useBaro) { + Bangle.on('swipe', handleSwipes); + } + Bangle.on('touch', handleTouch); + setWatch(e => { Bangle.showClock(); }, BTN1); // exit on button press +} + +start(); + + +/* +// TMP for testing: +//settings.speedUnits = 1; +//settings.altimeterUnits = 1; +QNH = 1013; +updateGPS({"fix":1,"speed":228,"alt":3763,"course":329,"lat":36.0182,"lon":-75.6713}); +updatePressure({"altitude":3700}); +*/ diff --git a/apps/flightdash/flightdash.png b/apps/flightdash/flightdash.png new file mode 100644 index 000000000..8230bc0c1 Binary files /dev/null and b/apps/flightdash/flightdash.png differ diff --git a/apps/flightdash/flightdash.settings.js b/apps/flightdash/flightdash.settings.js new file mode 100644 index 000000000..cd1ecdac6 --- /dev/null +++ b/apps/flightdash/flightdash.settings.js @@ -0,0 +1,327 @@ +(function(back) { + const APP_NAME = 'flightdash'; + const FILE = APP_NAME+'.json'; + + // if the avwx module is available, include an extra menu item to query nearest airports via AVWX + var avwx; + try { + avwx = require('avwx'); + } catch (error) { + // avwx module not installed + } + + // Load settings + var settings = Object.assign({ + useBaro: false, + speedUnits: 0, // KTS + altimeterUnits: 0, // FT + destID: 'KOSH', + destLat: 43.9844, + destLon: -88.5570, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // update the nav destination + function updateNavDest(destID, destLat, destLon) { + settings.destID = destID.replace(/[\W]+/g, '').slice(0, 7); + settings.destLat = parseFloat(destLat); + settings.destLon = parseFloat(destLon); + writeSettings(); + createDestMainMenu(); + } + + var airports; // cache list of airports + function readAirportsList(empty_cb) { + if (airports) { // airport list has already been read in + return true; + } + airports = require('Storage').readJSON(APP_NAME+'.airports.json', true); + if (! airports) { + E.showPrompt('No airports stored - download from the Bangle Apps Loader!', + {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + empty_cb(); + }); + return false; + } + return true; + } + + // use GPS fix + var afterGPSfixMenu = 'destNearest'; + function getLatLon(fix) { + if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return; + Bangle.setGPSPower(false, APP_NAME+'-settings'); + Bangle.removeListener('GPS', getLatLon); + switch (afterGPSfixMenu) { + case 'destNearest': + loadNearest(fix.lat, fix.lon); + break; + case 'createUserWaypoint': + { + if (!('userWaypoints' in settings)) + settings.userWaypoints = []; + let newIdx = settings.userWaypoints.length; + settings.userWaypoints[newIdx] = { + 'ID': 'USER'+(newIdx + 1), + 'lat': fix.lat, + 'lon': fix.lon, + }; + writeSettings(); + showUserWaypoints(); + break; + } + case 'destAVWX': + // the free ("hobby") account of AVWX is limited to 10 nearest stations + avwx.request('station/near/'+fix.lat+','+fix.lon, 'n=10&airport=true&reporting=false', data => { + loadAVWX(data); + }, error => { + console.log(error); + E.showPrompt('AVWX query failed: '+error, {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + createDestMainMenu(); + }); + }); + break; + default: + back(); + } + } + + // find nearest airports + function loadNearest(lat, lon) { + if (! readAirportsList(createDestMainMenu)) + return; + + const latRad1 = lat * Math.PI/180; + const lonRad1 = lon * Math.PI/180; + for (let i = 0; i < airports.length; i++) { + const latRad2 = airports[i].la * Math.PI/180; + const lonRad2 = airports[i].lo * Math.PI/180; + let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2); + let y = (latRad2 - latRad1); + airports[i].distance = Math.sqrt(x*x + y*y) * 6371; + } + let nearest = airports.sort((a, b) => a.distance - b.distance).slice(0, 14); + + let destNearest = { + '' : { 'title' : 'Nearest' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in nearest) { + let airport = nearest[i]; + destNearest[airport.i+' - '+airport.n] = + () => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo); + } + + E.showMenu(destNearest); + } + + // process the data returned by AVWX + function loadAVWX(data) { + let AVWXairports = JSON.parse(data.resp); + + let destAVWX = { + '' : { 'title' : 'Nearest (AVWX)' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in AVWXairports) { + let airport = AVWXairports[i].station; + let airport_id = ( airport.icao ? airport.icao : airport.gps ); + destAVWX[airport_id+' - '+airport.name] = + () => setTimeout(updateNavDest, 10, airport_id, airport.latitude, airport.longitude); + } + + E.showMenu(destAVWX); + } + + // individual user waypoint menu + function showUserWaypoint(idx) { + let wayptID = settings.userWaypoints[idx].ID; + let wayptLat = settings.userWaypoints[idx].lat; + let wayptLon = settings.userWaypoints[idx].lon; + let destUser = { + '' : { 'title' : wayptID }, + '< Back' : () => showUserWaypoints(), + }; + destUser['Set as Dest.'] = + () => setTimeout(updateNavDest, 10, wayptID, wayptLat, wayptLon); + destUser['Edit ID'] = function() { + require('textinput').input({text: wayptID}).then(result => { + if (result) { + if (result.length > 7) { + console.log('test'); + E.showPrompt('ID is too long!\n(max. 7 chars)', + {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + showUserWaypoint(idx); + }); + } else { + settings.userWaypoints[idx].ID = result; + writeSettings(); + showUserWaypoint(idx); + } + } else { + showUserWaypoint(idx); + } + }); + }; + destUser['Delete'] = function() { + E.showPrompt('Delete user waypoint '+wayptID+'?', + {'title': 'Flight-Dash'}).then((v) => { + if (v) { + settings.userWaypoints.splice(idx, 1); + writeSettings(); + showUserWaypoints(); + } else { + showUserWaypoint(idx); + } + }); + }; + + E.showMenu(destUser); + } + + // user waypoints menu + function showUserWaypoints() { + let destUser = { + '' : { 'title' : 'User Waypoints' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in settings.userWaypoints) { + let waypt = settings.userWaypoints[i]; + let idx = i; + destUser[waypt.ID] = + () => setTimeout(showUserWaypoint, 10, idx); + } + destUser['Create New'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'createUserWaypoint'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + + E.showMenu(destUser); + } + + // destination main menu + function createDestMainMenu() { + let destMainMenu = { + '' : { 'title' : 'Nav Dest.' }, + '< Back' : () => E.showMenu(mainMenu), + }; + destMainMenu['Is: '+settings.destID] = {}; + destMainMenu['Nearest'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'destNearest'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + destMainMenu['Search'] = function() { + require('textinput').input({text: ''}).then(result => { + if (result) { + if (! readAirportsList(createDestMainMenu)) + return; + + result = result.toUpperCase(); + let matches = []; + let tooManyFound = false; + for (let i in airports) { + if (airports[i].i.toUpperCase().includes(result) || + airports[i].n.toUpperCase().includes(result)) { + matches.push(airports[i]); + if (matches.length >= 15) { + tooManyFound = true; + break; + } + } + } + if (! matches.length) { + E.showPrompt('No airports found!', {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => { + createDestMainMenu(); + }); + return; + } + + let destSearch = { + '' : { 'title' : 'Search Results' }, + '< Back' : () => createDestMainMenu(), + }; + for (let i in matches) { + let airport = matches[i]; + destSearch[airport.i+' - '+airport.n] = + () => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo); + } + if (tooManyFound) { + destSearch['More than 15 airports found!'] = {}; + } + + E.showMenu(destSearch); + } else { + createDestMainMenu(); + } + }); + }; + destMainMenu['User waypts'] = function() { showUserWaypoints(); }; + if (avwx) { + destMainMenu['Nearest (AVWX)'] = function() { + E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'}); + afterGPSfixMenu = 'destAVWX'; + Bangle.setGPSPower(true, APP_NAME+'-settings'); + Bangle.on('GPS', getLatLon); + }; + } + E.showMenu(destMainMenu); + } + + // main menu + mainMenu = { + '' : { 'title' : 'Flight-Dash' }, + '< Back' : () => { + Bangle.setGPSPower(false, APP_NAME+'-settings'); + Bangle.removeListener('GPS', getLatLon); + back(); + }, + 'Nav Dest.': () => createDestMainMenu(), + 'Speed': { + value: parseInt(settings.speedUnits) || 0, + min: 0, + max: 2, + format: v => { + switch (v) { + case 0: return 'Knots'; + case 1: return 'km/h'; + case 2: return 'MPH'; + } + }, + onchange: v => { + settings.speedUnits = v; + writeSettings(); + } + }, + 'Altitude': { + value: parseInt(settings.altimeterUnits) || 0, + min: 0, + max: 1, + format: v => { + switch (v) { + case 0: return 'Feet'; + case 1: return 'Meters'; + } + }, + onchange: v => { + settings.altimeterUnits = v; + writeSettings(); + } + }, + 'Use Baro': { + value: !!settings.useBaro, // !! converts undefined to false + onchange: v => { + settings.useBaro = v; + writeSettings(); + } + }, + }; + + E.showMenu(mainMenu); +}) diff --git a/apps/flightdash/interface.html b/apps/flightdash/interface.html new file mode 100644 index 000000000..d0f57f316 --- /dev/null +++ b/apps/flightdash/interface.html @@ -0,0 +1,186 @@ + + + + + + + + +

You can upload a list of airports, which can then be used as the + navigation destinations in the Flight-Dash. It is recommended to only + upload up to 100 - 150 airports max. Due to memory contraints on the + Bangle, no more than 500 airports can be uploaded.

+ +

The database of airports is based on OurAirports. + +

Filter Airports

+
+ + nm of + + / + + +
+ This is using a simple lat/lon "block" - and + not within a proper radius around the given lat/lon position. An easy + way to find a lat/lon pair is to search for an airport based on ident + or name, and then use the found coordinates. +
+
+

- or -

+

+ + +

+

- or -

+

+ + +

+

Only 1 of the above filters is applied, with higher up in the list taking precedence.

+
+ + +
+ Use the + ISO-3166 2-letter code, + eg. "AU" +
+
+ +

+ + +

+ +
+ +

Results:

+

+
+ + + + + + + diff --git a/apps/flightdash/jquery-csv.min.js b/apps/flightdash/jquery-csv.min.js new file mode 100644 index 000000000..cbaefa6b8 --- /dev/null +++ b/apps/flightdash/jquery-csv.min.js @@ -0,0 +1 @@ +RegExp.escape=function(r){return r.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")},function(){"use strict";var p;(p="undefined"!=typeof jQuery&&jQuery?jQuery:{}).csv={defaults:{separator:",",delimiter:'"',headers:!0},hooks:{castToScalar:function(r,e){if(isNaN(r))return r;if(/\./.test(r))return parseFloat(r);var a=parseInt(r);return isNaN(a)?null:a}},parsers:{parse:function(r,e){var a=e.separator,t=e.delimiter;e.state.rowNum||(e.state.rowNum=1),e.state.colNum||(e.state.colNum=1);var o=[],s=[],n=0,i="",l=!1;function u(){if(n=0,i="",e.start&&e.state.rowNum=e.end&&(l=!0),e.state.rowNum++,e.state.colNum=1}function c(){if(void 0===e.onParseValue)s.push(i);else if(e.headers&&1===e.state.rowNum)s.push(i);else{var r=e.onParseValue(i,e.state);!1!==r&&s.push(r)}i="",n=0,e.state.colNum++}var f=RegExp.escape(a),d=RegExp.escape(t),m=/(D|S|\r\n|\n|\r|[^DS\r\n]+)/,p=m.source;return p=(p=p.replace(/S/g,f)).replace(/D/g,d),m=new RegExp(p,"gm"),r.replace(m,function(r){if(!l)switch(n){case 0:if(r===a){i+="",c();break}if(r===t){n=1;break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}i+=r,n=3;break;case 1:if(r===t){n=2;break}i+=r,n=1;break;case 2:if(r===t){i+=r,n=1;break}if(r===a){c();break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}throw Error("CSVDataError: Illegal State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");case 3:if(r===a){c();break}if(/^(\r\n|\n|\r)$/.test(r)){c(),u();break}if(r===t)throw Error("CSVDataError: Illegal Quote [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");throw Error("CSVDataError: Illegal Data [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");default:throw Error("CSVDataError: Unknown State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]")}}),0!==s.length&&(c(),u()),o},splitLines:function(r,a){if(r){var t=(a=a||{}).separator||p.csv.defaults.separator,o=a.delimiter||p.csv.defaults.delimiter;a.state=a.state||{},a.state.rowNum||(a.state.rowNum=1);var e=[],s=0,n="",i=!1,l=RegExp.escape(t),u=RegExp.escape(o),c=/(D|S|\n|\r|[^DS\r\n]+)/,f=c.source;return f=(f=f.replace(/S/g,l)).replace(/D/g,u),c=new RegExp(f,"gm"),r.replace(c,function(r){if(!i)switch(s){case 0:if(r===t){n+=r,s=0;break}if(r===o){n+=r,s=1;break}if("\n"===r){d();break}if(/^\r$/.test(r))break;n+=r,s=3;break;case 1:if(r===o){n+=r,s=2;break}n+=r,s=1;break;case 2:var e=n.substr(n.length-1);if(r===o&&e===o){n+=r,s=1;break}if(r===t){n+=r,s=0;break}if("\n"===r){d();break}if("\r"===r)break;throw Error("CSVDataError: Illegal state [Row:"+a.state.rowNum+"]");case 3:if(r===t){n+=r,s=0;break}if("\n"===r){d();break}if("\r"===r)break;if(r===o)throw Error("CSVDataError: Illegal quote [Row:"+a.state.rowNum+"]");throw Error("CSVDataError: Illegal state [Row:"+a.state.rowNum+"]");default:throw Error("CSVDataError: Unknown state [Row:"+a.state.rowNum+"]")}}),""!==n&&d(),e}function d(){if(s=0,a.start&&a.state.rowNum=a.end&&(i=!0),a.state.rowNum++}},parseEntry:function(r,e){var a=e.separator,t=e.delimiter;e.state.rowNum||(e.state.rowNum=1),e.state.colNum||(e.state.colNum=1);var o=[],s=0,n="";function i(){if(void 0===e.onParseValue)o.push(n);else{var r=e.onParseValue(n,e.state);!1!==r&&o.push(r)}n="",s=0,e.state.colNum++}if(!e.match){var l=RegExp.escape(a),u=RegExp.escape(t),c=/(D|S|\n|\r|[^DS\r\n]+)/.source;c=(c=c.replace(/S/g,l)).replace(/D/g,u),e.match=new RegExp(c,"gm")}return r.replace(e.match,function(r){switch(s){case 0:if(r===a){n+="",i();break}if(r===t){s=1;break}if("\n"===r||"\r"===r)break;n+=r,s=3;break;case 1:if(r===t){s=2;break}n+=r,s=1;break;case 2:if(r===t){n+=r,s=1;break}if(r===a){i();break}if("\n"===r||"\r"===r)break;throw Error("CSVDataError: Illegal State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");case 3:if(r===a){i();break}if("\n"===r||"\r"===r)break;if(r===t)throw Error("CSVDataError: Illegal Quote [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");throw Error("CSVDataError: Illegal Data [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]");default:throw Error("CSVDataError: Unknown State [Row:"+e.state.rowNum+"][Col:"+e.state.colNum+"]")}}),i(),o}},helpers:{collectPropertyNames:function(r){var e=[],a=[],t=[];for(e in r)for(a in r[e])r[e].hasOwnProperty(a)&&t.indexOf(a)<0&&"function"!=typeof r[e][a]&&t.push(a);return t}},toArray:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter;var o=void 0!==e.state?e.state:{};e={delimiter:t.delimiter,separator:t.separator,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,state:o};var s=p.csv.parsers.parseEntry(r,e);if(!t.callback)return s;t.callback("",s)},toArrays:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter;var o=[];if(void 0!==(e={delimiter:t.delimiter,separator:t.separator,onPreParse:e.onPreParse,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,onPostParse:e.onPostParse,start:e.start,end:e.end,state:{rowNum:1,colNum:1}}).onPreParse&&(r=e.onPreParse(r,e.state)),o=p.csv.parsers.parse(r,e),void 0!==e.onPostParse&&(o=e.onPostParse(o,e.state)),!t.callback)return o;t.callback("",o)},toObjects:function(r,e,a){if(void 0!==e&&"function"==typeof e){if(void 0!==a)return console.error("You cannot 3 arguments with the 2nd argument being a function");a=e,e={}}e=void 0!==e?e:{};var t={};t.callback=void 0!==a&&"function"==typeof a&&a,t.separator="separator"in e?e.separator:p.csv.defaults.separator,t.delimiter="delimiter"in e?e.delimiter:p.csv.defaults.delimiter,t.headers="headers"in e?e.headers:p.csv.defaults.headers,e.start="start"in e?e.start:1,t.headers&&e.start++,e.end&&t.headers&&e.end++;var o,s=[];e={delimiter:t.delimiter,separator:t.separator,onPreParse:e.onPreParse,onParseEntry:e.onParseEntry,onParseValue:e.onParseValue,onPostParse:e.onPostParse,start:e.start,end:e.end,state:{rowNum:1,colNum:1},match:!1,transform:e.transform};var n={delimiter:t.delimiter,separator:t.separator,start:1,end:1,state:{rowNum:1,colNum:1},headers:!0};void 0!==e.onPreParse&&(r=e.onPreParse(r,e.state));var i=p.csv.parsers.splitLines(r,n),l=p.csv.toArray(i[0],n);o=p.csv.parsers.splitLines(r,e),e.state.colNum=1,e.state.rowNum=l?2:1;for(var u=0,c=o.length;u