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/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1eb009153..7c0cfca3a 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 16.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: 16.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 fce2efb1a..f4588ac6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .htaccess node_modules -package-lock.json .DS_Store *.js.bak appdates.csv @@ -11,3 +10,7 @@ tests/Layout/bin/tmp.* tests/Layout/testresult.bmp apps.local.json _site +.jekyll-cache +.owncloudsync.log +Desktop.ini +.sync_*.db* diff --git a/README.md b/README.md index 78dd1b492..d2f7022e9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ 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/) @@ -186,12 +186,12 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget Widgets are just small bits of code that run whenever an app that supports them calls `Bangle.loadWidgets()`. If they want to display something in the 24px high -widget bar at the top of the screen they can add themselves to the global +widget bar at the top of the screen they can add themselves to the global `WIDGETS` array with: ``` 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 @@ -226,10 +226,8 @@ and which gives information about the app for the Launcher. "name":"Short Name", // for Bangle.js menu "icon":"*myappid", // for Bangle.js menu "src":"-myappid", // source file - "type":"widget/clock/app/bootloader", // optional, default "app" - // if this is 'widget' then it's not displayed in the menu - // if it's 'clock' then it'll be loaded by default at boot time - // if this is 'bootloader' then it's code that is run at boot time, but is not in a menu + "type":"widget/clock/app/bootloader/...", // optional, default "app" + // see 'type' in 'metadata.json format' below for more options/info "version":"1.23", // added by BangleApps loader on upload based on metadata.json "files:"file1,file2,file3", @@ -252,17 +250,24 @@ and which gives information about the app for the Launcher. "version": "0v01", // the version of this app "description": "...", // long description (can contain markdown) "icon": "icon.png", // icon in apps/ - "screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app + "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app "type":"...", // optional(if app) - // 'app' - an application // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget - // 'launch' - replacement launcher app - // 'bootloader' - code that runs at startup only + // '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' // '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 + // 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers + // (currently only 'sched' app) + // '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 "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 - "dependencies" : { "notify":"type" } // optional, app 'types' we depend on + "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' "readme": "README.md", // if supplied, a link to a markdown-style text file @@ -319,7 +324,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 @@ -415,7 +420,7 @@ Example `settings.js` // make sure to enclose the function in parentheses (function(back) { let settings = require('Storage').readJSON('myappid.json',1)||{}; - if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value + if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value function save(key, value) { settings[key] = value; require('Storage').write('myappid.json', settings); diff --git a/_config.yml b/_config.yml index 2f7efbeab..c74188174 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-minimal \ No newline at end of file +theme: jekyll-theme-slate \ No newline at end of file diff --git a/android.html b/android.html new file mode 100644 index 000000000..93999008f --- /dev/null +++ b/android.html @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + Bangle.js App Loader + + + + + + +
+ +
+ + + + +
+
+ +
+ + +
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/2ofthemclk/ChangeLog b/apps/2ofthemclk/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/2ofthemclk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file 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..78415fba7 --- /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 +var batteryInterval = 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..fa02b3e2f --- /dev/null +++ b/apps/2ofthemclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "2ofthemclk", + "name": "two of them clock", + "version": "0.01", + "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/7x7dotsclock/7x7dotsclock.app.js b/apps/7x7dotsclock/7x7dotsclock.app.js index aa174b2d2..aa6672a4f 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); @@ -289,27 +289,27 @@ function drawWidgeds() { 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) { diff --git a/apps/7x7dotsclock/ChangeLog b/apps/7x7dotsclock/ChangeLog index d2c98a472..5e8e48b0b 100644 --- a/apps/7x7dotsclock/ChangeLog +++ b/apps/7x7dotsclock/ChangeLog @@ -1,2 +1,3 @@ 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 \ No newline at end of file diff --git a/apps/7x7dotsclock/metadata.json b/apps/7x7dotsclock/metadata.json index 41f0836d3..ba1996544 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.03", "description": "A clock with a big 7x7 dots Font", "icon": "dotsfontclock.png", "tags": "clock", diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog index feb008f5f..057d6ff73 100644 --- a/apps/90sclk/ChangeLog +++ b/apps/90sclk/ChangeLog @@ -1,2 +1,3 @@ 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. diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js index 6babbfec2..351c235e0 100644 --- a/apps/90sclk/app.js +++ b/apps/90sclk/app.js @@ -115,6 +115,9 @@ function draw() { } } +// Show launcher when middle button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); // Clear the screen once, at startup @@ -140,5 +143,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..59b627427 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.03", "description": "A 90s style watch-face", "readme": "README.md", "icon": "app.png", diff --git a/apps/UI4swatch/Changelog b/apps/UI4swatch/ChangeLog similarity index 100% rename from apps/UI4swatch/Changelog rename to apps/UI4swatch/ChangeLog diff --git a/apps/_example_widget/widget.js b/apps/_example_widget/widget.js index f7aed6991..226aea589 100644 --- a/apps/_example_widget/widget.js +++ b/apps/_example_widget/widget.js @@ -9,7 +9,7 @@ currently-running apps */ // 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 }; diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog index 53e29a66d..c913481ff 100644 --- a/apps/activityreminder/ChangeLog +++ b/apps/activityreminder/ChangeLog @@ -1,2 +1,9 @@ 0.01: New App! 0.02: Fix the settings bug and some tweaking +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 \ No newline at end of file diff --git a/apps/activityreminder/README.md b/apps/activityreminder/README.md index 1e643fb54..0c79b4141 100644 --- a/apps/activityreminder/README.md +++ b/apps/activityreminder/README.md @@ -1,13 +1,15 @@ # Activity reminder A reminder to take short walks for the ones with a sedentary lifestyle. -The alert will popup only if you didn't take your short walk yet +The alert will popup only if you didn't take your short walk yet. Different settings can be personalized: - Enable : Enable/Disable the app - Start hour: Hour to start the reminder - End hour: Hour to end the reminder -- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 60 min -- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min +- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 120 min +- 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..96a9b76c4 --- /dev/null +++ b/apps/activityreminder/alert.js @@ -0,0 +1,37 @@ +(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); + } + setTimeout(load, 20000); + } + + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + run(); + +})(); \ No newline at end of file diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js index 310dc10b0..97f03ce97 100644 --- a/apps/activityreminder/app.js +++ b/apps/activityreminder/app.js @@ -1,37 +1,54 @@ -function drawAlert(){ - E.showPrompt("Inactivity detected",{ - title:"Activity reminder", - buttons : {"Ok": true,"Dismiss": false} - }).then(function(v) { - if(v == true){ - stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - 3); - require("activityreminder").saveStepsArray(stepsArray); - } - if(v == false){ - stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - activityreminder.dismissDelayMin); - require("activityreminder").saveStepsArray(stepsArray); - } - 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(); + + function getHoursMins(date){ + var h = date.getHours(); + var m = date.getMinutes(); + return (""+h).substr(-2) + ":" + ("0"+m).substr(-2); + } + + function drawData(name, value, y){ + g.drawString(name, 10, y); + g.drawString(value, 100, y); + } - Bangle.buzz(400); - setTimeout(load, 20000); -} + function drawInfo() { + var h=18, y = h; + g.setColor(g.theme.fg); + g.setFont("Vector",h).setFontAlign(-1,-1); -function run(){ - if(stepsArray.length == activityreminder.maxInnactivityMin){ - if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){ - drawAlert(); - } - }else{ - eval(require("Storage").read("activityreminder.settings.js"))(()=>load()); - } -} + // 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); + /* + 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); + */ + } -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -activityreminder = require("activityreminder").loadSettings(); -stepsArray = require("activityreminder").loadStepsArray(); -run(); + function getCurrentSteps(){ + let health = Bangle.getHealthStatus("day"); + return health.steps - activityreminder_data.stepsOnDate; + } + + function run() { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + drawInfo(); + } + + run(); + +})(); \ No newline at end of file diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js index 0f89bf543..5a11d73b8 100644 --- a/apps/activityreminder/boot.js +++ b/apps/activityreminder/boot.js @@ -1,29 +1,81 @@ -function run(){ - var now = new Date(); - var h = now.getHours(); - if(h >= activityreminder.startHour && h < activityreminder.endHour){ - var health = Bangle.getHealthStatus("day"); - stepsArray.unshift(health.steps); - stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin); - require("activityreminder").saveStepsArray(stepsArray); +(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(); + + 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'); + } } - else{ - if(stepsArray != []){ - stepsArray = []; - require("activityreminder").saveStepsArray(stepsArray); - } + + } + + 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); } - if(stepsArray.length >= activityreminder.maxInnactivityMin){ - if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){ - load('activityreminder.app.js'); - } + } + + 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); + } + }); -activityreminder = require("activityreminder").loadSettings(); -if(activityreminder.enabled) { - stepsArray = require("activityreminder").loadStepsArray(); + if (activityreminder_settings.enabled) { setInterval(run, 60000); -} - + /* todo in a futur release + increase setInterval time to something that is still sensible (5 mins ?) + when we added a settimer + */ + } +})(); diff --git a/apps/activityreminder/lib.js b/apps/activityreminder/lib.js index 712842fba..a5c35190c 100644 --- a/apps/activityreminder/lib.js +++ b/apps/activityreminder/lib.js @@ -1,22 +1,44 @@ -exports.loadSettings = function() { - return Object.assign({ - enabled: true, - startHour: 9, - endHour: 20, - maxInnactivityMin: 30, - dismissDelayMin: 15, - minSteps: 50 - }, require("Storage").readJSON("activityreminder.s.json", true) || {}); +exports.loadSettings = function () { + 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){ - require("Storage").writeJSON("activityreminder.s.json", settings); +exports.writeSettings = function (settings) { + require("Storage").writeJSON("activityreminder.s.json", settings); }; -exports.saveStepsArray = function(stepsArray) { - require("Storage").writeJSON("activityreminder.sa.json", stepsArray); +exports.saveData = function (data) { + require("Storage").writeJSON("activityreminder.data.json", data); }; -exports.loadStepsArray = function(){ - return require("Storage").readJSON("activityreminder.sa.json") || []; -}; \ No newline at end of file +exports.loadData = function () { + 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); + + return data; +}; diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json index eba5de105..9e8c552b1 100644 --- a/apps/activityreminder/metadata.json +++ b/apps/activityreminder/metadata.json @@ -3,7 +3,7 @@ "name": "Activity Reminder", "shortName":"Activity Reminder", "description": "A reminder to take short walks for the ones with a sedentary lifestyle", - "version":"0.02", + "version":"0.09", "icon": "app.png", "type": "app", "tags": "tool,activity", @@ -13,11 +13,12 @@ {"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.sa.json"} + {"name": "activityreminder.data.json", "storageFile": true} ] } diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js index 9b9a0ecd8..051c0dcd8 100644 --- a/apps/activityreminder/settings.js +++ b/apps/activityreminder/settings.js @@ -1,17 +1,17 @@ -(function(back) { +(function (back) { // Load settings - var settings = require("activityreminder").loadSettings(); + const activityreminder = require("activityreminder"); + let settings = activityreminder.loadSettings(); - // Show the menu - E.showMenu({ - "" : { "title" : "Activity Reminder" }, - "< Back" : () => back(), + function getMainMenu(){ + var mainMenu = { + "": { "title": "Activity Reminder" }, + "< Back": () => back(), 'Enable': { value: settings.enabled, - format: v => v?"Yes":"No", onchange: v => { settings.enabled = v; - require("activityreminder").writeSettings(settings); + activityreminder.writeSettings(settings); } }, 'Start hour': { @@ -19,46 +19,68 @@ min: 0, max: 24, onchange: v => { settings.startHour = v; - require("activityreminder").writeSettings(settings); + activityreminder.writeSettings(settings); } - }, - 'End hour': { + }, + 'End hour': { value: settings.endHour, min: 0, max: 24, onchange: v => { settings.endHour = v; - require("activityreminder").writeSettings(settings); + activityreminder.writeSettings(settings); } - }, - 'Max inactivity': { + }, + 'Max inactivity': { value: settings.maxInnactivityMin, min: 15, max: 120, onchange: v => { settings.maxInnactivityMin = v; - require("activityreminder").writeSettings(settings); + activityreminder.writeSettings(settings); }, - format: x => { - return x + " min"; - } - }, - 'Dismiss delay': { + format: x => x + "m" + }, + 'Dismiss delay': { value: settings.dismissDelayMin, - min: 5, max: 15, + min: 5, max: 60, onchange: v => { settings.dismissDelayMin = v; - require("activityreminder").writeSettings(settings); + 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 + " min"; + return x + "m"; } - }, - 'Min steps': { + }, + 'Min steps': { value: settings.minSteps, - min: 10, max: 500, + min: 10, max: 500, step: 10, onchange: v => { settings.minSteps = v; - require("activityreminder").writeSettings(settings); + 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); + } + } + }; + + return mainMenu; + } + + // Show the menu + E.showMenu(getMainMenu()); }) diff --git a/apps/advcasio/ChangeLog b/apps/advcasio/ChangeLog new file mode 100644 index 000000000..a1b528cf6 --- /dev/null +++ b/apps/advcasio/ChangeLog @@ -0,0 +1,3 @@ +0.01: AdvCasio first version +0.02: Remove un-needed fonts to improve memory usage +0.03: Tell clock widgets to hide. diff --git a/apps/advcasio/README.md b/apps/advcasio/README.md new file mode 100644 index 000000000..3ce771497 --- /dev/null +++ b/apps/advcasio/README.md @@ -0,0 +1,62 @@ +# Adv Casio Clock + + + +An over-engineered clock inspired by Casio watches.
+It has a dedicated timer, a scratchpad and can display the weather condition 4 days ahead.
+It uses a custom web app to update its content.
+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 +- Weather forecast (7 days) +- Scratchpad + +## Screenshots +Clock:
+ + + + +Web interface to update weather & scratchpad
+https://dotgreg.github.io/advCasioBangleClock + + + +## Usage +### How to update the tasks list / weather +- you will need a free openweathermap.org api key. +- go to https://dotgreg.github.io/advCasioBangleClock/ + - Alternatively you can install it on your own server/heroku/service/github pages, the web-app code is here +- fill the location and the api key (it will be saved on your browser, no need to do it each time) +- edit the scratchpad with what you want +- click on sync +- reload your clock! + +### 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..8cb904f90 --- /dev/null +++ b/apps/advcasio/app.js @@ -0,0 +1,304 @@ +const storage = require('Storage'); + +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 getClockBg() { + return require("heatshrink").decompress(atob("icVgf/ABv8v4DBx4CB+PH8F+nAGB48fwEHBwXjxwqBuPH//+nAGBBwIjCAwI2D/wGBgIyDI4QGDwAGBHYX/4AGBn4UFEYQpCEYYpCAAMfMhP4FIgABwJ8OEBIA==")); +} + + +// sun, cloud, rain, thunder +var iconsWeather = [ + require("heatshrink").decompress(atob("i8Ugf/ACcfA434BA/AAwsAv0/8F/BAcDwEHHIpECFI3wn4GC/gOC+PAGoXggEH/+ODQgXBGQv/wAbBBAnguEACIn4gfxI4JXFwJmG/kPBA3jSynw")), require("heatshrink").decompress(atob("i0Ugf/AEXggIGE/0A/kPBAmBCIN/A4Y8CgAICwEHBYoUE/ACCj4sDn4CBC4YyDwBrDCgYA3A")), require("heatshrink").decompress(atob("h8Rgf/AAuBAgf8h4FDCwM/AgPA/gFC/0HgEBBQPwnEfDoWAg4jC/gOCAoQmBAQXjFIV//8f//4IQP4j/+gAIB4EcHII4CAoI+DLQJXF/AA==")), require("heatshrink").decompress(atob("h0Pgf/AA8fAYX+g4EC8EBAgXADAeAgAECgAOC/wrCDQIOBBYfwgAaC/kAn4EB/EAv4aDHAeBIg38")) +]; + + +function getBackgroundImage() { + return require("heatshrink").decompress(atob("2GwghC/AH4A/AH4AMl////wAwURiQECgUzmcxBQQCBiYUBBARW+LAcCAgcPBYgFBkAIFG7kQiAKIiIKBgISOAAJBD//zKQfxK4vyAoMQCgn/ERBhBBYR5BAwR1DB4Y2DgYPCGIQRCCQcP+EfGJI0FEgRSCGAQCCX4JXCkAhDn4lI+HyK4YWBFIPzJYJXHAIMSK4cwJ4I3CAYMzA4cfcRMBdwytBK4i6FK4IUCMgYAEGIITBK4cCaAPwgJXB+fzK4sAgYtCK5EfA4pXR+AmBaIZYCK6KcCAwSjDEYXx/8vK5QRCK4kPK6cDkJREBIMBfgIrDK5svUAIQBAwIaCK4w+DK4YGBK7IaBboIuCK4gFCJwYBBiBCCCgQhHHYgGDgArBK5IGDAYMgJ4Xwn53BGgLVDmBXKAAinDLpJXCAAYhHR4YODn/wJIPyTYZXDE4RXD+ECNILIDAIPwj4xIAAYNCR4fyVIYLFA4KEBBAglKAGUCmcykEAiMQBIURBYM/BgIUEgcz+bTKAH4A/AH4A/AHP/AGY1d+BWCh5X/LCpW1K74fgG/5X/AH5X/K9Bg/K63wK/5XWgBX/K6pWBK/5XU+BWBh5J/K6auCK/5XTVwRfFAH5XOKwRX/K6auDh5I/K6SuDWP5XSVwYADWX6vXK/5XQWQpW/K6auDJP5XWV35XT+Cu/K7Ku/K65H/K6hW/K7EPI35XWIv5XWAH5X/K/4A/K/5X/K/4A/K9cAAH4A/AFzz/AHRX/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/40VAH4A/AFzLb+EPDm4AdK/5X/K+PwgEAHy5X9HgMAK/5XXH6xX/H65X/K/5X/K98AK7sAgBX3DjBWFO644DSTHwGzJXED4RXaDoLqcK7weWDIQcXK8I6YK77KXK4o8DPbY6ZK7qvDDy6vdR7JXDh60EDyw5BAIRXYSwjMbAgIhUDwJZCHwJX0GwjRWNwIAEHSwBCDSpXFH4pXzDS5XIEARXVSYbQEDaYzCK+6vcKaxXNDypX9HwQkbHS40COSpXKK2A6CHgRXcPIhX0SwpXYVuQ6EgBX/K644YODBXkSDJX/K/5X/DtRX6gA3YOkRWbLDZX4KwYA/AG8F5vdABncKH4AGhpRJAYXNAgPAKP4AF5vMJwoDBAQIKE6BR/AAvc5vO9wAB7oCB9veAoPcAoPcK+kwh8AgcA98An//gH/+sD//wCISgBJ4IABAYpaC9vdK4UP/9AAQNQr/zgHwEYNQFYQAh+EP+FegH+A4QBCMQIKBAAPNK4yxBA4RXCV4YZBE4IjChwCDmApCK8VdmHggHgFYf0SQJXE5nMK4anCAoYHC5pXCaQJXBop+BqAGEK7f/AAQeEKwQrBqCtDAILjBCQfNK4JTCAYZXF7qvD//gV4S2DgEFFIYAECgIACMC8PKoIBB8n1K4ivF5vc5xOCWYZbBAYavHU4RXCr4pEAEMDfoNQGoMEgEwYQPwAoIBBAAPM5ipC7oDCVIIAE7hXCD4SdBiEP+gGBgihCFYIAz5pXBAAnN7oIB7nc5gOBK4QA/K4pNCWgSpCBInNK/4AGhncKIStC7gCBA4QAC4BR/AAysCABZW/AHwA=")); +} + + + +// 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; +} + +//////////////////////////////////////////// +// TIMER FUNC +// +var timer_time = 0; +var alreadyListenTouch = false; +function initTouchTimer () { + if (alreadyListenTouch) return; + alreadyListenTouch = true; + + Bangle.on('swipe', function(dirX,dirY) { + if (canTouch === false) return; + var njson = getDataJson(); + if (!njson) return; + + if (dirX === -1) { + timer_time = 0; + delete njson.timer; + setDataJson(njson); + } + else if (dirX === 1) { + var now = new Date().getTime(); + njson.timer = now + (timer_time * 1000 * 60); + Bangle.setLocked(true); + setDataJson(njson); + Bangle.buzz(200, 0); + timer_time = 0; + } + else if (dirY === -1) { + if (canTouch === false || njson.timer) return; + timer_time = timer_time + 5; + } + else if (dirY === 1) { + if (canTouch === false || njson.timer) return; + timer_time = timer_time - 5; + } + draw(); + }); +} +setTimeout(() => { + initTouchTimer (); +}); + +function getTimerTime() { + // if timer_time !== -1, take it + if (timer_time !== 0) { + return timer_time + "m"; + } else { + // else, show diff between njsontime and now + var njson = getDataJson(); + if (!njson) return false; + var now = new Date().getTime(); + var diff = Math.round((njson.timer - now) / (1000 * 60)); + //console.log(123, njson, diff, now, njson.timer - now); + if (diff > 0) return diff + "m"; + else if (njson.timer) { + Bangle.buzz(1000, 1); + console.log("END OF TIMER"); + delete njson.timer; + setDataJson(njson); + return false; + } else { + return false; + } + // if diff is <0, delete timer from json + } +} +function drawTimer() { + //g.drawString(getTimerTime(), 100, 100); + g.setFont("8x12", 2); + var t = 97; + var l = 105; + var time = getTimerTime(); + if (time || timer_time !== 0) g.drawString(time, l+5, t+0); + if (time && timer_time === 0) g.drawImage(getClockBg(), l-20, t+2, { scale: 1 }); +} + + +//////////////////////////////////////////// +// DATA READING +// +function getDataJson(){ + var res = {"tasks":"", "weather":[]}; + try { + res = storage.readJSON('advcasio.data.json'); + } catch(ex) { + return res; + } + return res; +} +function setDataJson(resJson){ + try { + res = storage.writeJSON('advcasio.data.json', resJson); + } catch(ex) { + return res; + } + return res; +} +var dataJson = getDataJson(); + +//////////////////////////////////////////// +// WEATHER! +// +function drawWeather(arr) { + g.setFont("6x8", 1); + var p = {l: 8, tText: 40, tIcon:20, decal:25}; + var today = new Date().getTime(); + var yesterday = today - (1000 * 60 * 60 * 24); + var testday = today + (1000 * 60 * 60 * 24 * 2); + //12h auj > 12h hier qui est sup a 0h auj + //23h59 hier est sup a 0h auj + var j = 0; + for(var i = 0; i yesterday && j < 4) { + g.drawString(arr[i][0], p.l + p.decal*j + 4, p.tText); + g.drawImage(iconsWeather[arr[i][1]], p.l + p.decal*j, p.tIcon, { scale: 1 }); + j++ + } + } +} + + +//////////////////////////////////////////// +// DRAWING FUNCS +// +function drawTasks(str) { + g.setFont("6x8", 1); + var t = 57; + var l = 0; + g.drawString(str, l+5, t+0); +} + +function drawSteps() { + g.setFont("8x12", 2); + var t = 132; + var l = 150; + g.drawString(getSteps(), l+5, t+0); +} + + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.clearRect(80, 57, 170, 96); + g.setColor(255, 255, 255); + var l = 77; + var t = 57; + var w = 170; + var h = 116; + g.drawRect(l, t, w, h); + g.fillRect(l, t, w, h); + g.setColor(0, 0, 0); + g.drawString(require("locale").time(new Date(), 1), 76, 60); + + // day + //g.setFont("8x12", 1); + //g.setFont("9x18", 1); + //g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 25, 136); + g.setFont("8x12", 2); + g.drawString(require("locale").dow(new Date(), 2), 18, 130); + + // month + g.setFont("8x12"); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127); + + // day nb + g.setFont("8x12", 2); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 78, 137); +} + +function drawBattery() { + bigThenSmall(E.getBattery(), "%", 140, 23); +} + + +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 "? k"; + } + + steps = Math.round(steps/1000); + return steps + "k"; +} + + + +function draw() { + + queueDraw(); + + g.reset(); + g.clear(); + g.setColor(255, 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); + if(dataJson && dataJson.weather) drawWeather(dataJson.weather); + if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks); + + + g.setFontAlign(0,-1); + g.setFont("8x12", 2); + + drawSteps(); + g.setFontAlign(-1,-1); + drawClock(); + drawBattery(); + drawTimer(); + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +// save batt power, does not seem to work although... +var canTouch = true; +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + canTouch = false; + clearIntervals(); + } +}); + + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); + if (!locked) { + canTouch = true; + } else { + canTouch = false; + } +}); + + +Bangle.setUI("clock"); + +// Load widgets, but don't show them +Bangle.loadWidgets(); + +g.reset(); +g.clear(); +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..152f47132 --- /dev/null +++ b/apps/advcasio/metadata.json @@ -0,0 +1,25 @@ +{ "id": "advcasio", + "name": "Advanced Casio Clock", + "shortName":"advcasio", + "version":"0.03", + "description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, 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} + ], + "data": [ + { "name": "advcasio.data.json", "url": "data.json", "storageFile": 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..2c59c3cc2 --- /dev/null +++ b/apps/agenda/ChangeLog @@ -0,0 +1,5 @@ +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 diff --git a/apps/agenda/README.md b/apps/agenda/README.md new file mode 100644 index 000000000..7063a70a2 --- /dev/null +++ b/apps/agenda/README.md @@ -0,0 +1,20 @@ +# 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 + +### 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). + +### Known Problems + +Any all-day event lasts just one day: that is a GB limitation that we will likely fix in the future. 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.js b/apps/agenda/agenda.js new file mode 100644 index 000000000..9cffe0265 --- /dev/null +++ b/apps/agenda/agenda.js @@ -0,0 +1,138 @@ +/* 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) { + return new Date(timestamp*1000); +} +function formatDateLong(date, includeDay, allDay) { + let shortTime = Locale.time(date,1)+Locale.meridian(date); + if(allDay) shortTime = ""; + if(includeDay || allDay) + return Locale.date(date)+" "+shortTime; + return shortTime; +} +function formatDateShort(date, allDay) { + return Locale.date(date).replace(/\d\d\d\d/,"")+(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); + var end = getDate((+ev.timestamp) + (+ev.durationInSeconds)); + 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) { + 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(Locale.date(start), 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) + 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("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)+"\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..982870905 --- /dev/null +++ b/apps/agenda/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "agenda", + "name": "Agenda", + "version": "0.05", + "description": "Simple agenda", + "icon": "agenda.png", + "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], + "tags": "agenda", + "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.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..4220fcb63 --- /dev/null +++ b/apps/agenda/settings.js @@ -0,0 +1,48 @@ +(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(); + } + }, + }; + E.showMenu(mainmenu); +}) diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog new file mode 100644 index 000000000..ae26512de --- /dev/null +++ b/apps/agpsdata/ChangeLog @@ -0,0 +1,2 @@ +0.01: First, proof of concept +0.02: Load AGPS data on app start and automatically in background 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..647723bb4 --- /dev/null +++ b/apps/agpsdata/app.js @@ -0,0 +1,54 @@ +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() { + g.reset(); + g.clear(); + waiting = false; + display("Retry?", "touch to retry"); + Bangle.on("touch", () => { updateAgps(); }); +} + +function updateAgps() { + g.reset(); + g.clear(); + if (!waiting) { + waiting = true; + display("Updating A-GPS..."); + 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(); }); + }); + } else { + display("Waiting..."); + } +} +updateAgps(); diff --git a/apps/agpsdata/boot.js b/apps/agpsdata/boot.js new file mode 100644 index 000000000..6415f0b52 --- /dev/null +++ b/apps/agpsdata/boot.js @@ -0,0 +1,33 @@ +(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) { + let lastUpdate = settings.lastUpdate; + if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){ + if (!waiting){ + waiting = true; + require("agpsdata").pull(successCallback, errorCallback); + } + } + 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..7d9758c0a --- /dev/null +++ b/apps/agpsdata/lib.js @@ -0,0 +1,75 @@ +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(data) { + var js = jsFromBase64(data); + try { + eval(js); + return true; + } + catch(e) { + console.log("error:", e); + } + return false; +} + +function jsFromBase64(b64) { + var bin = atob(b64); + var chunkSize = 128; + var js = "Bangle.setGPSPower(1);\n"; // turn GPS on + var gnsstype = settings.gnsstype || 1; // default GPS + js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode + // What about: + // NAV-TIMEUTC (0x01 0x10) + // NAV-PV (0x01 0x03) + // or AGPS.zip uses AID-INI (0x0B 0x01) + + for (var i=0;i { + let result = setAGPS(event.resp); + if (result) { + updateLastUpdate(); + if (successCallback) successCallback(); + } else { + console.log("error applying AGPS data"); + if (failureCallback) failureCallback("Error applying AGPS data"); + } + }).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..d3863be52 --- /dev/null +++ b/apps/agpsdata/metadata.json @@ -0,0 +1,24 @@ +{ "id": "agpsdata", + "name": "A-GPS Data", + "shortName":"A-GPS Data", + "icon": "agpsdata.png", + "version":"0.02", + "description": "Download assisted GPS (A-GPS) data directly to your Bangle.js **using Gadgetbridge**", + "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..80a2f3956 --- /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 : 1, + 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/alarm/ChangeLog b/apps/alarm/ChangeLog index 41dd93081..52ee8bf9c 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -24,3 +24,14 @@ 0.23: Fix regression with Days of Week (#1735) 0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow Add "Enable All", "Disable All" and "Remove All" actions +0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu +0.26: Add support for Monday as first day of the week (#1780) +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 + diff --git a/apps/alarm/README.md b/apps/alarm/README.md index e979dbaf1..741946b0c 100644 --- a/apps/alarm/README.md +++ b/apps/alarm/README.md @@ -1,7 +1,31 @@ -Alarms & Timers -=============== +# Alarms & Timers This app allows you to add/modify any alarms and timers. -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. +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 + - `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 +- `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 + +## Creator + +- [Gordon Williams](https://github.com/gfwilliams) + +## Main Contributors + +- [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 + +## Attributions + +All icons used in this app are from [icons8](https://icons8.com). diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 3b3421115..ed5aa608a 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -1,240 +1,386 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); +// 0 = Sunday (default), 1 = Monday +const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0; +const WORKDAYS = 62 +const WEEKEND = firstDayOfWeek ? 192 : 65; +const EVERY_DAY = firstDayOfWeek ? 254 : 127; + +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 + ? atob("GBjBAP////8AAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg=") + : atob("GBjBAP//AAAAAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg=")); + +const iconTimerOn = "\0" + (g.theme.dark + ? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA=") + : atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA=")); +const iconTimerOff = "\0" + (g.theme.dark + ? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg=") + : atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg=")); + // An array of alarm objects (see sched/README.md) -let alarms = require("sched").getAlarms(); +var alarms = require("sched").getAlarms(); -function getCurrentTime() { - let time = new Date(); - return ( - time.getHours() * 3600000 + - time.getMinutes() * 60000 + - time.getSeconds() * 1000 - ); +function handleFirstDayOfWeek(dow) { + if (firstDayOfWeek == 1) { + if ((dow & 1) == 1) { + // In the scheduler API Sunday is 1. + // Here the week starts on Monday and Sunday is ON so + // when I read the dow I need to move Sunday to 128... + dow += 127; + } else if ((dow & 128) == 128) { + // ... and then when I write the dow I need to move Sunday back to 1. + dow -= 127; + } + } + return dow; } -function saveAndReload() { - require("sched").setAlarms(alarms); - require("sched").reload(); -} +// 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() { - // Timer img "\0"+atob("DhKBAP////MDDAwwMGGBzgPwB4AeAPwHOBhgwMMzDez////w") - // Alarm img "\0"+atob("FBSBAABgA4YcMPDGP8Zn/mx/48//PP/zD/8A//AP/wD/8A//AP/wH/+D//w//8AAAADwAAYA") const menu = { - '': { 'title': /*LANG*/'Alarms&Timers' }, - /*LANG*/'< Back' : ()=>{load();}, - /*LANG*/'New Alarm': ()=>editAlarm(-1), - /*LANG*/'New Timer': ()=>editTimer(-1) + "": { "title": /*LANG*/"Alarms & Timers" }, + "< Back": () => load(), + /*LANG*/"New...": () => showNewMenu() }; - alarms.forEach((alarm,idx)=>{ - var type,txt; // a leading space is currently required (JS error in Espruino 2v12) - if (alarm.timer) { - type = /*LANG*/"Timer"; - txt = " "+require("sched").formatTime(alarm.timer); - } else { - type = /*LANG*/"Alarm"; - txt = " "+require("sched").formatTime(alarm.t); - } - if (alarm.rp) txt += "\0"+atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA="); - // rename duplicate alarms - if (menu[type+txt]) { - var n = 2; - while (menu[type+" "+n+txt]) n++; - txt = type+" "+n+txt; - } else txt = type+txt; - // add to menu - menu[txt] = { - value : "\0"+atob(alarm.on?"EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g":"EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"), - onchange : function() { - if (alarm.timer) editTimer(idx, alarm); - else editAlarm(idx, alarm); - } + + 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) }; }); - if (alarms.some(e => !e.on)) { - menu[/*LANG*/"Enable All"] = () => enableAll(true); - } - if (alarms.some(e => e.on)) { - menu[/*LANG*/"Disable All"] = () => enableAll(false); - } - if (alarms.length > 0) { - menu[/*LANG*/"Delete All"] = () => deleteAll(); - } + menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); - if (WIDGETS["alarm"]) WIDGETS["alarm"].reload(); - return E.showMenu(menu); -} - -function editDOW(dow, onchange) { - const menu = { - '': { 'title': /*LANG*/'Days of Week' }, - /*LANG*/'< Back' : () => onchange(dow) - }; - for (let i = 0; i < 7; i++) (i => { - let dayOfWeek = require("locale").dow({ getDay: () => i }); - menu[dayOfWeek] = { - value: !!(dow&(1< v ? /*LANG*/"Yes" : /*LANG*/"No", - onchange: v => v ? dow |= 1< showMainMenu(), + /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined), + /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined) + }); +} + +function showEditAlarmMenu(selectedAlarm, alarmIndex) { + var isNew = alarmIndex === undefined; + + var alarm = require("sched").newDefaultAlarm(); + alarm.dow = handleFirstDayOfWeek(alarm.dow); + + if (selectedAlarm) { + Object.assign(alarm, selectedAlarm); + } + + var time = require("time_utils").decodeTime(alarm.t); const menu = { - '': { 'title': /*LANG*/'Alarm' }, - /*LANG*/'< Back': () => { - saveAlarm(newAlarm, alarmIndex, a, t); + "": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" }, + "< Back": () => { + prepareAlarmForSave(alarm, alarmIndex, time); + saveAndReload(); showMainMenu(); }, - /*LANG*/'Hours': { - value: t.hrs, min : 0, max : 23, wrap : true, - onchange: v => t.hrs=v + /*LANG*/"Hour": { + value: time.h, + format: v => ("0" + v).substr(-2), + min: 0, + max: 23, + wrap: true, + onchange: v => time.h = v }, - /*LANG*/'Minutes': { - value: t.mins, min : 0, max : 59, wrap : true, - onchange: v => t.mins=v + /*LANG*/"Minute": { + value: time.m, + format: v => ("0" + v).substr(-2), + min: 0, + max: 59, + wrap: true, + onchange: v => time.m = v }, - /*LANG*/'Enabled': { - value: a.on, - format: v => v ? /*LANG*/"On" : /*LANG*/"Off", - onchange: v=>a.on=v + /*LANG*/"Enabled": { + value: alarm.on, + onchange: v => alarm.on = v }, - /*LANG*/'Repeat': { - value: a.rp, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", - onchange: v => a.rp = v - }, - /*LANG*/'Days': { - value: "SMTWTFS".split("").map((d,n)=>a.dow&(1< editDOW(a.dow, d => { - a.dow = d; - a.t = require("sched").encodeTime(t); - editAlarm(alarmIndex, a); + /*LANG*/"Repeat": { + value: decodeDOW(alarm), + onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => { + alarm.rp = repeat; + alarm.dow = dow; + alarm.t = require("time_utils").encodeTime(time); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); }) }, - /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), - /*LANG*/'Auto Snooze': { - value: a.as, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", - onchange: v => a.as = v + /*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v), + /*LANG*/"Auto Snooze": { + value: alarm.as, + onchange: v => alarm.as = v + }, + /*LANG*/"Hidden": { + value: alarm.hidden || false, + onchange: v => alarm.hidden = v + }, + /*LANG*/"Cancel": () => showMainMenu() + }; + + if (!isNew) { + menu[/*LANG*/"Delete"] = () => { + E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { + if (confirm) { + alarms.splice(alarmIndex, 1); + saveAndReload(); + showMainMenu(); + } else { + alarm.t = require("time_utils").encodeTime(time); + setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); + } + }); + }; + } + + E.showMenu(menu); +} + +function prepareAlarmForSave(alarm, alarmIndex, time) { + alarm.t = require("time_utils").encodeTime(time); + alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0; + + if (alarmIndex === undefined) { + alarms.push(alarm); + } else { + alarms[alarmIndex] = alarm; + } +} + +function saveAndReload() { + // Before saving revert the dow to the standard format (alarms only!) + alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); + + require("sched").setAlarms(alarms); + require("sched").reload(); + + // Fix after save + alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); +} + +function decodeDOW(alarm) { + return alarm.rp + ? require("date_utils") + .dows(firstDayOfWeek, 2) + .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") + .join("") + .toLowerCase() + : "Once" +} + +function showEditRepeatMenu(repeat, dow, dowChangeCallback) { + var originalRepeat = repeat; + var originalDow = dow; + var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY; + + const menu = { + "": { "title": /*LANG*/"Repeat Alarm" }, + "< Back": () => dowChangeCallback(repeat, dow), + /*LANG*/"Once": { + // 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) } }; - menu[/*LANG*/"Cancel"] = () => showMainMenu(); - - if (!newAlarm) { - menu[/*LANG*/"Delete"] = function () { - alarms.splice(alarmIndex, 1); - saveAndReload(); - showMainMenu(); - }; - } - - return E.showMenu(menu); + E.showMenu(menu); } -function saveAlarm(newAlarm, alarmIndex, a, t) { - a.t = require("sched").encodeTime(t); - a.last = (a.t < getCurrentTime()) ? (new Date()).getDate() : 0; - - if (newAlarm) { - alarms.push(a); - } else { - alarms[alarmIndex] = a; - } - - saveAndReload(); -} - -function editTimer(alarmIndex, alarm) { - let newAlarm = alarmIndex < 0; - let a = require("sched").newDefaultTimer(); - if (!newAlarm) Object.assign(a, alarms[alarmIndex]); - if (alarm) Object.assign(a,alarm); - let t = require("sched").decodeTime(a.timer); - +function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) { const menu = { - '': { 'title': /*LANG*/'Timer' }, - /*LANG*/'< Back': () => { - saveTimer(newAlarm, alarmIndex, a, t); - showMainMenu(); - }, - /*LANG*/'Hours': { - value: t.hrs, min : 0, max : 23, wrap : true, - onchange: v => t.hrs=v - }, - /*LANG*/'Minutes': { - value: t.mins, min : 0, max : 59, wrap : true, - onchange: v => t.mins=v - }, - /*LANG*/'Enabled': { - value: a.on, - format: v => v ? /*LANG*/"On" : /*LANG*/"Off", - onchange: v => a.on = v - }, - /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), + "": { "title": /*LANG*/"Custom Days" }, + "< Back": () => { + // 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) + } }; - menu[/*LANG*/"Cancel"] = () => showMainMenu(); - - if (!newAlarm) { - menu[/*LANG*/"Delete"] = function() { - alarms.splice(alarmIndex,1); - saveAndReload(); - showMainMenu(); + require("date_utils").dows(firstDayOfWeek).forEach((day, i) => { + menu[day] = { + value: !!(dow & (1 << (i + firstDayOfWeek))), + onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek))) }; - } - return E.showMenu(menu); + }); + + menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback) + + E.showMenu(menu); } -function saveTimer(newAlarm, alarmIndex, a, t) { - a.timer = require("sched").encodeTime(t); - a.t = getCurrentTime() + a.timer; - a.last = 0; +function showEditTimerMenu(selectedTimer, timerIndex) { + var isNew = timerIndex === undefined; - if (newAlarm) { - alarms.push(a); - } else { - alarms[alarmIndex] = a; + var timer = require("sched").newDefaultTimer(); + + if (selectedTimer) { + Object.assign(timer, selectedTimer); } - saveAndReload(); + var time = require("time_utils").decodeTime(timer.timer); + + const menu = { + "": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" }, + "< Back": () => { + prepareTimerForSave(timer, timerIndex, time); + saveAndReload(); + showMainMenu(); + }, + /*LANG*/"Hours": { + value: time.h, + min: 0, + max: 23, + wrap: true, + onchange: v => time.h = v + }, + /*LANG*/"Minutes": { + value: time.m, + min: 0, + max: 59, + 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*/"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() + }; + + if (!isNew) { + menu[/*LANG*/"Delete"] = () => { + E.showPrompt(/*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) + } + }); + }; + } + + E.showMenu(menu); +} + +function prepareTimerForSave(timer, timerIndex, time) { + timer.timer = require("time_utils").encodeTime(time); + timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer; + timer.last = 0; + + if (timerIndex === undefined) { + alarms.push(timer); + } else { + alarms[timerIndex] = timer; + } +} + +function showAdvancedMenu() { + E.showMenu({ + "": { "title": /*LANG*/"Advanced" }, + "< Back": () => showMainMenu(), + /*LANG*/"Scheduler Settings": () => eval(require("Storage").read("sched.settings.js"))(() => showAdvancedMenu()), + /*LANG*/"Enable All": () => enableAll(true), + /*LANG*/"Disable All": () => enableAll(false), + /*LANG*/"Delete All": () => deleteAll() + }); } function enableAll(on) { - E.showPrompt(/*LANG*/"Are you sure?", { - title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" - }).then((confirm) => { - if (confirm) { - alarms.forEach(alarm => alarm.on = on); - saveAndReload(); - } - - showMainMenu(); - }); + if (alarms.filter(e => e.on == !on).length == 0) { + E.showAlert( + on ? /*LANG*/"Nothing to Enable" : /*LANG*/"Nothing to Disable", + on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" + ).then(() => showAdvancedMenu()); + } else { + E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" }).then((confirm) => { + if (confirm) { + 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 { + showAdvancedMenu(); + } + }); + } } function deleteAll() { - E.showPrompt(/*LANG*/"Are you sure?", { - title: /*LANG*/"Delete All" - }).then((confirm) => { - if (confirm) { - alarms = []; - saveAndReload(); - } - - showMainMenu(); - }); + if (alarms.length == 0) { + E.showAlert(/*LANG*/"Nothing to delete", /*LANG*/"Delete All").then(() => showAdvancedMenu()); + } else { + E.showPrompt(/*LANG*/"Are you sure?", { + title: /*LANG*/"Delete All" + }).then((confirm) => { + if (confirm) { + alarms = []; + saveAndReload(); + showMainMenu(); + } else { + showAdvancedMenu(); + } + }); + } } showMainMenu(); diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 2084c2a30..31dd58ece 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,16 +2,29 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.24", + "version": "0.33", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm,widget", - "supports": ["BANGLEJS","BANGLEJS2"], + "supports": [ "BANGLEJS", "BANGLEJS2" ], "readme": "README.md", - "dependencies": {"scheduler":"type"}, + "dependencies": { "scheduler":"type" }, "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.app.js", "url": "app.js" }, + { "name": "alarm.img", "url": "app-icon.js", "evaluate": true }, + { "name": "alarm.wid.js", "url": "widget.js" } + ], + "screenshots": [ + { "url": "screenshot-1.png" }, + { "url": "screenshot-2.png" }, + { "url": "screenshot-3.png" }, + { "url": "screenshot-4.png" }, + { "url": "screenshot-5.png" }, + { "url": "screenshot-6.png" }, + { "url": "screenshot-7.png" }, + { "url": "screenshot-8.png" }, + { "url": "screenshot-9.png" }, + { "url": "screenshot-10.png" }, + { "url": "screenshot-11.png" } ] } diff --git a/apps/alarm/screenshot-1.png b/apps/alarm/screenshot-1.png new file mode 100644 index 000000000..d2bd3a409 Binary files /dev/null and b/apps/alarm/screenshot-1.png differ diff --git a/apps/alarm/screenshot-10.png b/apps/alarm/screenshot-10.png new file mode 100644 index 000000000..1e6e516c3 Binary files /dev/null and b/apps/alarm/screenshot-10.png differ diff --git a/apps/alarm/screenshot-11.png b/apps/alarm/screenshot-11.png new file mode 100644 index 000000000..197c84194 Binary files /dev/null and b/apps/alarm/screenshot-11.png differ diff --git a/apps/alarm/screenshot-2.png b/apps/alarm/screenshot-2.png new file mode 100644 index 000000000..1cbc255a9 Binary files /dev/null and b/apps/alarm/screenshot-2.png differ diff --git a/apps/alarm/screenshot-3.png b/apps/alarm/screenshot-3.png new file mode 100644 index 000000000..a165d3594 Binary files /dev/null and b/apps/alarm/screenshot-3.png differ diff --git a/apps/alarm/screenshot-4.png b/apps/alarm/screenshot-4.png new file mode 100644 index 000000000..7fd7e99b6 Binary files /dev/null and b/apps/alarm/screenshot-4.png differ diff --git a/apps/alarm/screenshot-5.png b/apps/alarm/screenshot-5.png new file mode 100644 index 000000000..4174c5670 Binary files /dev/null and b/apps/alarm/screenshot-5.png differ diff --git a/apps/alarm/screenshot-6.png b/apps/alarm/screenshot-6.png new file mode 100644 index 000000000..dc579ca5c Binary files /dev/null and b/apps/alarm/screenshot-6.png differ diff --git a/apps/alarm/screenshot-7.png b/apps/alarm/screenshot-7.png new file mode 100644 index 000000000..49da44710 Binary files /dev/null and b/apps/alarm/screenshot-7.png differ diff --git a/apps/alarm/screenshot-8.png b/apps/alarm/screenshot-8.png new file mode 100644 index 000000000..86d69cd93 Binary files /dev/null and b/apps/alarm/screenshot-8.png differ diff --git a/apps/alarm/screenshot-9.png b/apps/alarm/screenshot-9.png new file mode 100644 index 000000000..2d8c7fc83 Binary files /dev/null and b/apps/alarm/screenshot-9.png differ diff --git a/apps/alarm/widget.js b/apps/alarm/widget.js index 052ac9ebd..964176fc7 100644 --- a/apps/alarm/widget.js +++ b/apps/alarm/widget.js @@ -2,7 +2,7 @@ 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"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==true)) ? 24 : 0; } }; WIDGETS["alarm"].reload(); diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 96b50c3a0..0cc7aedd4 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -7,3 +7,9 @@ 0.06: Option to keep messages after a disconnect (default false) (fix #1186) 0.07: Include charging state in battery updates to phone 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) diff --git a/apps/android/README.md b/apps/android/README.md index 580eeec9a..f9ab73699 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -21,7 +21,6 @@ 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? * `Messages` - launches the messages app, showing a list of messages -* `Alarms` - opens a submenu where you can set default settings for alarms such as vibration pattern, repeat, and auto snooze ## How it works @@ -33,6 +32,25 @@ 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) + +eg: + +``` +Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{ + console.log("Got ",data); +}); +``` + ## 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 9e24c9893..bc8e3032d 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -3,6 +3,7 @@ Bluetooth.println(""); Bluetooth.println(JSON.stringify(message)); } + var lastMsg; var settings = require("Storage").readJSON("android.settings.json",1)||{}; //default alarm settings @@ -18,7 +19,17 @@ /* 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});require("messages").pushMessage(event); }, + "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 @@ -67,26 +78,93 @@ 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 = { - id : "gb"+j, - appid : "gbalarms", - on : true, - t : event.d[j].h * 3600000 + event.d[j].m * 60000, - dow : ((dow&63)<<1) | (dow>>6), // Gadgetbridge sends DOW in a different format - last : last, - rp : settings.rp, - as : settings.as, - vibrate : settings.vibrate - }; + 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(); }, + //TODO perhaps move those in a library (like messages), used also for viewing events? + //simple package with events all together + "calendarevents" : function() { + require("Storage").writeJSON("android.calendar.json", event.events); + }, + //add and remove events based on activity on phone (pebble-like) + "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); + }, + "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)) return; + cal = cal.filter(e=>e.id!=event.id); + require("Storage").writeJSON("android.calendar.json", cal); + }, + //triggered by GB, send all ids + "force_calendar_sync_start" : function() { + var cal = require("Storage").readJSON("android.calendar.json",true); + if (!cal || !Array.isArray(cal)) cal = []; + gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.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 + } }; var h = HANDLERS[event.t]; if (h) h(); else console.log("GB Unknown",event); }; + // HTTP request handling - see the readme + // options = {id,timeout,xpath} + Bangle.http = (url,options)=>{ + options = options||{}; + 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.method) req.method = options.method; + if (options.body) req.body = options.body; + if (options.headers) req.headers = options.headers; + 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; + } // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 203cd18b1..5d1b2f561 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.08", + "version": "0.15", "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", @@ -15,6 +15,6 @@ {"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.boot.js","url":"boot.js"} ], - "data": [{"name":"android.settings.json"}], + "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}], "sortorder": -8 } diff --git a/apps/android/settings.js b/apps/android/settings.js index 9f72947ab..c7c34a76f 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -18,34 +18,12 @@ }), /*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*/"Alarms" : () => E.showMenu({ - "" : { "title" : /*LANG*/"Alarms" }, - "< Back" : ()=>E.showMenu(mainmenu), - /*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => {settings.vibrate = v; updateSettings();}), - /*LANG*/"Repeat": { - value: settings.rp, - format : v=>v?/*LANG*/"Yes":/*LANG*/"No", - onchange: v => { - settings.rp = v; - updateSettings(); - } - }, - /*LANG*/"Auto snooze": { - value: settings.as, - format : v=>v?/*LANG*/"Yes":/*LANG*/"No", - onchange: v => { - settings.as = v; - updateSettings(); - } - }, - }) }; E.showMenu(mainmenu); }) diff --git a/apps/animclk/ChangeLog b/apps/animclk/ChangeLog index 348448c34..76d15bdb1 100644 --- a/apps/animclk/ChangeLog +++ b/apps/animclk/ChangeLog @@ -1,3 +1,5 @@ 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. diff --git a/apps/animclk/app.js b/apps/animclk/app.js index 4bf63daf6..bdc399fbe 100644 --- a/apps/animclk/app.js +++ b/apps/animclk/app.js @@ -87,7 +87,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 +103,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..0b426a37d 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.04", "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..f7e95b5fa 100644 --- a/apps/antonclk/ChangeLog +++ b/apps/antonclk/ChangeLog @@ -9,4 +9,5 @@ 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 \ No newline at end of file diff --git a/apps/antonclk/metadata.json b/apps/antonclk/metadata.json index c58ee2a1b..16bdf3aa8 100644 --- a/apps/antonclk/metadata.json +++ b/apps/antonclk/metadata.json @@ -1,7 +1,7 @@ { "id": "antonclk", "name": "Anton Clock", - "version": "0.08", + "version": "0.09", "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "readme":"README.md", "icon": "app.png", diff --git a/apps/antonclk/settings.js b/apps/antonclk/settings.js index 6882cbd0f..4448c00ed 100644 --- a/apps/antonclk/settings.js +++ b/apps/antonclk/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/astral/ChangeLog b/apps/astral/ChangeLog index a51c96760..c93c2b6c2 100644 --- a/apps/astral/ChangeLog +++ b/apps/astral/ChangeLog @@ -1,3 +1,4 @@ 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. diff --git a/apps/astral/app.js b/apps/astral/app.js index c445463f2..c4339bc09 100644 --- a/apps/astral/app.js +++ b/apps/astral/app.js @@ -767,6 +767,24 @@ function draw() { g.clear(); current_moonphase = getMoonPhase(); +Bangle.setUI("clockupdown", btn => { + if (btn==0) { + if (!processing) { + if (!modeswitch) { + modeswitch = true; + if (mode == "planetary") mode = "extras"; + else mode = "planetary"; + } + else + modeswitch = false; + } + } else { + if (!processing) + ready_to_compute = true; + } +}); + + // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -799,23 +817,6 @@ Bangle.setGPSPower(1); // Show launcher when button pressed Bangle.setClockMode(); -Bangle.setUI("clockupdown", btn => { - if (btn==0) { - if (!processing) { - if (!modeswitch) { - modeswitch = true; - if (mode == "planetary") mode = "extras"; - else mode = "planetary"; - } - else - modeswitch = false; - } - } else { - if (!processing) - ready_to_compute = true; - } -}); - setWatch(function () { if (!astral_settings.astral_default) { colours_switched = true; diff --git a/apps/astral/metadata.json b/apps/astral/metadata.json index 3317092db..d7120878b 100644 --- a/apps/astral/metadata.json +++ b/apps/astral/metadata.json @@ -1,7 +1,7 @@ { "id": "astral", "name": "Astral Clock", - "version": "0.03", + "version": "0.04", "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", diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index 316660fc6..a00ae9325 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -7,3 +7,9 @@ 0.07: Update to use Bangle.setUI instead of setWatch 0.08: Use theme colors, Layout library 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 \ No newline at end of file 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 5d46a1cb4..5a7dfc8c0 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -3,7 +3,6 @@ * A simple digital clock showing seconds as a bar **/ // Check settings for what type our clock should be -const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; let locale = require("locale"); { // add some more info to locale let date = new Date(); @@ -11,51 +10,25 @@ let locale = require("locale"); date.setMonth(1, 3); // februari: months are zero-indexed const localized = locale.date(date, true); locale.dayFirst = /3.*2/.test(localized); - - locale.hasMeridian = false; - if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed - locale.hasMeridian = (locale.meridian(date)!==""); - } + locale.hasMeridian = (locale.meridian(date)!==""); } -Bangle.loadWidgets(); + +let barW = 0, prevX = 0; function renderBar(l) { - if (!this.fraction) { - // zero-size fillRect stills draws one line of pixels, we don't want that - return; - } - const width = this.fraction*l.w; - g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); + "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 (x2 - ) / (5chars * 6px) - thickness = Math.floor((g.getWidth()-24)/(5*6)); -} else { - layout.ampm.label = ""; - thickness = Math.floor(g.getWidth()/(5*6)); -} -layout.bar.height = thickness+1; -layout.time.font = "6x8:"+thickness; -layout.update(); - function timeText(date) { - if (!is12Hour) { + if (!clock.is12Hour) { return locale.time(date, true); } const date12 = new Date(date.getTime()); @@ -68,7 +41,7 @@ function timeText(date) { return locale.time(date12, true); } function ampmText(date) { - return (is12Hour && locale.hasMeridian)? locale.meridian(date) : ""; + return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; } function dateText(date) { const dayName = locale.dow(date, true), @@ -78,31 +51,74 @@ function dateText(date) { return `${dayName} ${dayMonth}`; } -draw = function draw(force) { - if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled - const date = new Date(); - layout.time.label = timeText(date); - layout.ampm.label = ampmText(date); - layout.date.label = dateText(date); - const SECONDS_PER_MINUTE = 60; - layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; - if (force) { - Bangle.drawWidgets(); - layout.forgetLazyState(); - } - layout.render(); - // schedule update at start of next second - const millis = date.getMilliseconds(); - setTimeout(draw, 1000-millis); -}; +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(); + }, + }); -// Show launcher when button pressed -Bangle.setUI("clock"); -Bangle.on("lcdPower", function(on) { - if (on) { - draw(true); - } -}); -g.reset().clear(); -Bangle.drawWidgets(); -draw(); +// power saving: only update once a minute while locked, hide bar +if (clock.powerSave) { + Bangle.on("lock", lock => { + clock.precision = lock ? 60 : 1; + clock.tick(); + renderBar(); // hide/redraw bar right away + }); +} + +clock.start(); diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json index 2b7be355f..5b783dbda 100644 --- a/apps/barclock/metadata.json +++ b/apps/barclock/metadata.json @@ -1,7 +1,7 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.09", + "version": "0.15", "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..7b88b7021 --- /dev/null +++ b/apps/barclock/settings.js @@ -0,0 +1,30 @@ +(function(back) { + let s = require("Storage").readJSON("barclock.settings.json", true) || {}; + + 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, + loadWidgets: s.loadWidgets, + }; + // 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..7ab5d8587 --- /dev/null +++ b/apps/barcode/ChangeLog @@ -0,0 +1,10 @@ +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. 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..0d9df78d5 --- /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..3f6bf06e6 --- /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.10", + "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/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/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/bigdclock/ChangeLog b/apps/bigdclock/ChangeLog new file mode 100644 index 000000000..b373f1876 --- /dev/null +++ b/apps/bigdclock/ChangeLog @@ -0,0 +1,6 @@ +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. 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..3adf96984 --- /dev/null +++ b/apps/bigdclock/bigdclock.app.js @@ -0,0 +1,92 @@ +// + +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; + +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(), + w = date.getDay(); // d=1..31; w=0..6 + const level = E.getBattery(); + const width = level + (level/2); + var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + var dows = require("date_utils").dows(0,1); + + 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(dows[w].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); + } else if (level > 40) { + g.setColor(0,1,0); + } else { + g.setColor(1,0,0); + } + g.fillRect(12,162,12+width,168); + if (level < 100) { + g.setColor(g.theme.bg); + g.fillRect(12+width+1,162,162,168); + } + + 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..ce91d921e --- /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.06", + "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..8a12b266e Binary files /dev/null and b/apps/bigdclock/screenshot.png differ diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog index 2a3023750..10752ee2b 100644 --- a/apps/bikespeedo/ChangeLog +++ b/apps/bikespeedo/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Barometer altitude adjustment setting +0.03: Use default Bangle formatter for booleans diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json index c3de0487c..80b91427c 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.03", "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..f41524263 100644 --- a/apps/bikespeedo/settings.js +++ b/apps/bikespeedo/settings.js @@ -33,12 +33,10 @@ '< 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(); } } }; 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..e355155b3 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -2,3 +2,5 @@ 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. diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js index 28d7a06a5..153bebb32 100644 --- a/apps/binwatch/app.js +++ b/apps/binwatch/app.js @@ -334,6 +334,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 +372,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..0b4dbc697 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.05", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator":true, 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 e3f492d3b..a43ecf86e 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -51,3 +51,4 @@ 0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk) 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) diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 119cd2c2c..4cb3c52e4 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -197,8 +197,18 @@ bootFiles.forEach(bootFile=>{ require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset); fileOffset+=2+bootFile.length+1; var bf = require('Storage').read(bootFile); - require('Storage').write('.boot0',bf,fileOffset); - fileOffset+=bf.length; + // 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; }); diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index d1bf2edde..62adc4db1 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.47", + "version": "0.48", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/bordle/ChangeLog b/apps/bordle/ChangeLog index f45509a34..ddbd6239c 100644 --- a/apps/bordle/ChangeLog +++ b/apps/bordle/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App 0.02: app keeps track of statistics now +0.03: Fix bug in valid word detection diff --git a/apps/bordle/bordle.app.js b/apps/bordle/bordle.app.js index 20aa02bc2..07e954a6d 100644 --- a/apps/bordle/bordle.app.js +++ b/apps/bordle/bordle.app.js @@ -110,7 +110,12 @@ class Wordle { } } addGuess(w) { - if ((this.words.indexOf(w.toLowerCase())%5)!=0) { + let idx = -1; + do{ + idx = this.words.indexOf(w.toLowerCase(), idx+1); + } + while(idx !== -1 && idx%5 !== 0); + if(idx%5 !== 0) { E.showAlert(w+"\nis not a word", "Invalid word").then(function() { layout = getKeyLayout(""); wordle.render(true); diff --git a/apps/bordle/metadata.json b/apps/bordle/metadata.json index 37ef5c855..f6011f798 100644 --- a/apps/bordle/metadata.json +++ b/apps/bordle/metadata.json @@ -2,7 +2,7 @@ "name": "Bordle", "shortName":"Bordle", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Bangle version of a popular word search game", "supports" : ["BANGLEJS2"], "readme": "README.md", diff --git a/apps/bowserWF/ChangeLog b/apps/bowserWF/ChangeLog new file mode 100644 index 000000000..dd2b05fb3 --- /dev/null +++ b/apps/bowserWF/ChangeLog @@ -0,0 +1,3 @@ +... +0.02: First update with ChangeLog Added +0.03: updated watch face to use the ClockFace library diff --git a/apps/bowserWF/app.js b/apps/bowserWF/app.js index e53d945cc..956c43602 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 + 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 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}, +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 22df2dea4..bba15e5df 100644 --- a/apps/bowserWF/metadata.json +++ b/apps/bowserWF/metadata.json @@ -1,14 +1,18 @@ -{ "id": "bowserWF", +{ + "id": "bowserWF", "name": "Bowser Watchface", - "shortName":"Bowser Watchface", - "version":"0.01", + "shortName": "Bowser Watchface", + "version": "0.03", "description": "Let bowser show you the time", "icon": "app.png", - "tags": "", - "supports" : ["BANGLEJS2"], + "type": "clock", + "tags": "clock", + "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" }] } diff --git a/apps/bradbury/app-icon.js b/apps/bradbury/app-icon.js new file mode 100644 index 000000000..07c4f5582 --- /dev/null +++ b/apps/bradbury/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcCkGSpEgwQCChICFkgCBgkQoMEyFJAoICByVBkgLBkkSpIaDEwWShEkFgcIBAIdCEYQCBAoQdBAoYsBC4Q7BpICBEYQCDF4Q7CEYYCCEYUSKYYUDyRlCJQQIBNYYvBMoQCBkgjBFgxxCL4REDFgaPEHYgmCIgosCNYZEEDoZ0CNwY7CIIYgDEYtB9+e/dg/4AB2EJkYEB/mC/fn33Ivvz598v4MB/0BgoRCyVHvmW7Mg2EA8uD/EAh/IkGP/8AgVLtkA5El+FJvoRBgmf4Mkh0HkEQo9kyEfkeQofsgf4kmPCIP+h/gwULkkCncEu/ZsmRI4cEv0H8ESpdgEwMjwXI9kTCIOANYkSEYOCncF+UAjuR/ED+FBg/3/f8RgNgiVPkYdBtkT/Egv0Il+AoMfI4PgyX7vkW799F4Nl//4//woH/+0Ztvx7Fs335sk//5EB/IRBhACB77CBpEkgEIgGQoDRBgEggVBgDdBgGAgPv317ku+5cj334t+OSoI+B8gCBtlx7dkuFfgvx4N8yPbvgOB8ACBR4MA9mf4Egz3IgeChEDwDOBx/AjuCoN8y/JgkX4ME2FBjuQn65BgMtwELkGOEYOO4Mh2EJh+Sh/jOIMd+3fskRcwMTEwOWo98gCSBwFJkm2pfgx3II4PBk++/aABhEfwEInpZBvkX7MkJQMl2FHfANBjgCBlmQhHsgwjB33IkeyBAOChMcEwM9+/ZsBHBboMJtv2hd9+FHZANBVoM7kGC/fv2FJ9+GEYOAh//+UIaIMBkkQpEAHwIIBoMgiFJBANJEAMIkGShEkwQIChIIBhIIBhIaCkmQpIFCgmSEwYpDEYwCCpAICBwUEiQdFEwIICyAIDHwQ7CEYYpCEYWSpA7FDocSEwojBCgIaDIgYCBNwR0BNYYjFEwZTDLgQjGOgYvBEYQ7ENYlJFgQCCDohuGTYpBFkhoCSoQICEYIA=")) diff --git a/apps/bradbury/app.js b/apps/bradbury/app.js new file mode 100644 index 000000000..147242689 --- /dev/null +++ b/apps/bradbury/app.js @@ -0,0 +1,115 @@ +require("Font7x11Numeric7Seg").add(Graphics); +require("Font5x9Numeric7Seg").add(Graphics); +require("Font8x12").add(Graphics); +require("FontDylex7x13").add(Graphics); +const X = 98, Y = 46; +var wizible = 0; + +function getImg() { + return require("heatshrink").decompress(atob("2GwwcCAoNBgmQpMkiACCoMkyALBAoMEyQCDkkSAoICCCIIXCCgQaCAQNJDQYUBDQIIBDQkgIwsShEkwUJkGSpACBBAQFCAQOCBAgFCyQXBDQQIDCIUIEYgOCpICBFgYXCII2W7ft237AQPbt++7fvBAIFBAQgRCAoNtCgIaDC4QaD7dtBAgUBDQYXC9+z5cEIIv279t+/fvoFBvoFC+/bBAe3CIQIBBwW2CIgCCCIYgFAQgjEAoN5sGAIAcF33JaIT4DAoTyDcYL1CAQgXBpIUDfYQCCC4T+CDoYgDDQQFBocMyBBDkeyagb4EBArpCpD1DfAoIDfAQLCDQwIGDQklwRBDLIJfEiffjv//H/AAPkgUB//+oEk+Pf/+B5/8+VHn/wh9/+ff/vx4f/+0f/+yFIIsCPoSMCWYSVDoBBCL48A8kQvEn+VA/i2Bn/yoMgh0BkvwhMFx0Bg6nBhkQh8Ej14h4XB9kSU4iSFQwMkGoWWhCDDagRQCiUPgED5Ej+EI/kAhF8+0BkH+LIOyCIOT4EAF4M8gVfgGG5EhRgNggFJGoIpBJQKJDAQQLBRgSDEKYRZCoNnOIXypaJBgPkjzFBn1AkeQv8/FgMcwFBnkArNsv/Bkl+v4vBVQQCBPQKzBRgoCCjEgIILOCKwQFBo8gwf4kf//50Bp/kz/IgEyhEj+UPskPPQOAoE8yE7GQInBx/4R4R6DfwQCGBYQCBIITOCagJNBo/gyEZg/wAoMCv5GB/MnR4KDBvEEi0IhkCQYMQp8Aj0BFgPBgECOgT+BQwJ9EAoQCEwEAJobOBQwM/kFx5E/+UH8mCv8gyf5kHygHlwVLklz5EjSQM8wP/BwPJkGX7IsBVQL7CO4SJFAoK5BBAUAI4SDDwfP9+eoH//0P/+f//gj//8OOvcsgV5/6JBgm+nNkgPH8kev8ETQMANALOBO4QFBPoa2CR4qDByBKCagTPBwAOBgEIVQJTBTAIIDDQIRBgMggQIBiQaGfAwCLRgWQoDUEpCeDgCSDAQLyCoEAAQIdCHIIjEAoS/BDQbsCyQCNX4dIJQaGCDRwCnRIbUDyTRBIOy8BQYTLDBYL7BAGUEIgI+CQYeCpMgIGYABiSDByUIkkSI4JKBgEB/4AW4/j+PHILECXgKDDpCDCgF+QehBBgDCBkkQI4VIgeAY2q/ByCDBySDCIPKDCgkQoKDCjg4tgcMmHDgACCQYuChMkQYJBujFhwwCBwQICpEEySDDAoKD1oaDDiSDDAQNIDI1z588+YCiQYoCBkCDCwSDCI4KDHgeevPnAUeAQYkQgaDDyFBkjIBkiDHjiAjAQXwQYwxBHAI+CiFBZYKDGg+f/4Ak+CDEAQSDCHwMgZAKDKIMyDEwCDDgg+BIgUkQYJBFQd0DQY9IAQSD1sCDDAQKDCySDvgKDEAQKDCySDChKDygKDCAQMgQYcIgkSI4MSQd8DQYkDQYcSYQJEBAQKDwkCDHgA+CiDLCQeCADmFDQYsgQAICCQd8hw0AsOCgFgQZESQeEMQZbCBQeR6BjFhw0YQYlIgmQoKDFgOwQdnBQYPDQYY+BkmChICBIIcP+yDqQAQCBgEgQYMEYQKDDAoKDxQAKDFiFBkCDBAQJBDAASDpgOCQwdgQYMAwUIkhEBkkSIIccuHAQd1DQY5EBQYnjx04QdMAgUAsOCgCDDyUIIgUEQYtxQdSABAQiDGhICBQeEhw0YsOAhCDCgmSAQOQoMkQeMMmHDQYICBQYQ+BQZUYQdAuCAAo4BHYMkZAKDGmKDriCDHiQ+ByUIQYvggU4QdEYsOGAQdgQYhEBI4SDEgKDrQASDFYQWCQY8AQejCBkmQoMEySD8kBEBQY0GjCD4kkSIIcEuKD0ySDJ8OOQdkIQYw7BZAUEQYkcgKDsoaDGYQKABQY3jwSDqgEGQwOAQYcEQYTIBAQKDyoKDGYQKABpKDF8EOhCDpsOGgACCQYkIkkQpMkiSDwgiACQYuQoMkyUIQY0AjCDnwCDCAQOAhCDCgCDEgiDFuPAgaDl/kAgcMmFDQY0QoKABhKDFsOOAgQAmQYsAQYUEyQCBIgKDFAFcDQAKDC4CDDyCDCpKDFaAIHBAT+Dx048YCDhCDBQAICCQYQ7BQYaJBIAIHDAQM/AokEj3x/mf/IIDz3JgEQv8/+VxtmX+MEj/BnkAuPAglx4cMQYYxBmAFBQYQkBkmSLoSDIn+DxBrDHwP48R0E+0OAoP6k+D/N/33YlAUCQYMIgEOgHgh0YsEGjCGBAoMArA2BQYMSI4KDJnnz4IIDjlx4mwqIID//P4EQp8Hifx4/y56DB6N8QYQaBAQNwQYUBAQPDQY8JkiDKwSDEyV5tGX5AIEh9gwX+QYPJ8+f/CYB5MgQYM48ICB8eOgEBw0AQYMIQYR9BhBBBQZYCRgAOLQYPHQYICBQYMMmFDQY0SoJpBpACCQYQAsQAMYsACCQYMAQYWQI4VJIN4AGgQ7ByCDFIPHIgA+BgmSoMkiFJkBB1iVAgkQQYTIByVJkmAIGcEy1IHYKDBIgKGDIgQCxkuSQYMSQYOShCGBJQQ1m5cs2QCKwUBkjCCiFJQwYC1gBBBAoJWBQYQCBf9gAIgVIYQRECJoKeBJQQFCyALBAQMkiSVBboICDNAgUDDQQOCBAQXFyQRDBAICBDQdBQAMJZYICCQwOSpCPEpICBLIIFBCIIFCDRwCFDQp6BCI5HEOgiJDBAJ0ETAaPFCIgaGSo4IDGpKDCOIZTDBAJWCOgRxCboQCBCgQCCBwICDOgqPCBAqeDCgoODdhDdBBYqPIBwSPDDQgCDfAQCCSQgyDEASJDQYLODOgZfBOIRWFL4TjERIYdDTAYOEBAICEC4o4FBwLaGAXNAgBuFAXMAAH4A/AAcB23btoC84ENIP/Yj/+7Ml/4AG9u27//+3/CgIJB/3bt4EBEAe3DAn2BAO/DoQJCGof/HYgXD/3JlpBDpdsz5CHAF/yrdk//4hvy/9kwf7v5lCTA9vQAJxB/YLFPoRxETYTCSt+WTAOeQYOX7cki1/QWv833bsmRQYOW7Mg31f9rvB/pWCCoR6HNBF9NYQXGRJAOCCQXbtm+5MkgX4jgFBgvy5//wEAAB8PQcPl2VIgG2vEM21Il+W5/8ICAABNYKYEcIf+SoKABBYI4Gtu/CgaMCsmSgNv+VYjmyoN83xBTgKDh8mArf8y14huShfl21bthBS/ZlCt59Bt7vBtowFBYIRCtoFBv4FBBYSGC9kW/8n2XYj+Qr8t+1/Qev83/Jtuz/EN+X5v+z/d8IKqGCAQO///9PQW274FE//tAoYCEDoNvyV/vueQYP+pdsz5NBQen/+Vbsn/QYO27MlcAWAIKEGdgP2dgSGCBAIABPQoOBAoO3SoftAQKbE5Mt2yDBNUQAc/BBBMoXfBALXCAQLyF27sIEIYRCAgJ3CEwQLDv6wBRIIADEAYFDQf6DChpuGAQ++BZP/C5VvOggFCCgV/Rgi5GQf6DDIIP7gAAUgz+C//t2APIn/tO4WABxEbPoKPDtu/IIX4IKsBMImDNQ/+o4EC/ixI/0HQZENZAJBVgBfBcwNsgVbfwQCD2VAAoVgiQLEBwcAAoX9BYfYQbv8CBQOC8AONQYxBXQYRiBthHB8FwBQNwg4CBgF/IJtvBwPtQccB4EcIIcA8eAQbEf+3YILXsIIkDx0AjgQBOIVgD5R9B//9AQKDE/5BVh6DFYoZBFQa4oCd4TjCKAPf9u3AoTaC74ZD+3bYorCCMoKJBGQP9DoJBLGQQjCtrFCJY4AUIId//EcZYSDZhrLDOgP+PQSJDBAtt34IBAoX/7dsGRZxBsAOKEwaVBAoPYQbx0NQakfZYRNBt4LDAon+R4qDDBwPbvkSpMkyQCEOgSYBIJanDHYZBBA4IWKABUPQYk/RhKDYAQJBVgHbv6DBtiDHkB0EsCDLUgNtH4IFB7BBYgJ6GOhaDW7BBXMoVsGRe275BLQYgCBQf6DDh6DXgHf/qDNtu3IJiDCt/27f//yD/QYUNZAKDW7d9MoJBLQaP/2xBDQf5BCZAJBW/Z0C9gQK/p0BsAOKt49C+3bRIPYQf4+BhpEBIKoiBOgVsB5ZxBQZYdCUgTFDQf/4jqDYMQP+QZjyBQZlt34+C/aGBQYX/ICsPQakJkmSpICEoCDIhrOCJQQFB/4ICAQ9/DAIFFIKATCAAnyQYIjC+3fGoPYQYQAaQbGQBwaDFIIKADt//AQQIDboYODIKQdB75BBBxW/F4iDwBxiDHO4T7EKAYCEQwgICQaH/sAFByUAYIMBkgOFSoRBE/4lKABUPQauAoEggEIAoKDM/BBVgCGCQZpBGkmAIIJQDUgSqDILMBQarFBQYYOFQYsNIgJBYMQNsQZaSBYpd//6AB/wCBYrSDWYoWQgMkQZcf/3YIKsAcYSDM/oOBsBQL+3bv6DC23YQb0CrZHCAQeyoCDDXoSSKQYsN3//IKsGHAd/wYoH/1HQYVsiVJkmSAQsDto4BLgiDCADnwKJE/BwaDJBwiDFAYJcCvoCBa4PfbQRrBO4IFBL4P7XgwdBt6ADBAQLDCgwCB9oFDF4KDjAEKDCKwwCFCIIFDSoQOD2//NYJoBAoYRHQwaeB3//96PGDoP/Qf6DCh50DMoOAgAAPgz7E356Dt4oCSQynE+wLCRIP//YXDQYfZkoGB/hAQgEBP8X+5MvQYMf/1Ltme7dsIKhxENAJuBPoKMFAQe/RIdv/wIDtvyrdk+yDB+X/t+zQe+X/ckj/4huXLIVbQaUAfAv//r+ER4r7BO4O2SQJ9B94IDXIO+7Mg2f4juSpMkyVfQevs23Jgvy/EcwAtChZBSQYR3FQAT1CBY6YE7f9BYll+1Il+WrEcQYdP/5HDABsPQcPl2VBvm2vEcBQfLMRO/cwZrFdgPbt/2CgXfOIttE4Pt/4dBSQv/74NBQYOShfl+1YjqDDr5vhACfsyFfluy/EfyxQB+1/bQRiCO4Vt3x9DMoVvBwW2eQRrBOIZ6BOIIXCBwYpBv4aBSoIIDtmC/Nv2XYgfy/d/2aC1AAOX5f9z/AgO+pdszzmEAQT1BeQZrDO4qGCDQ4CJCoILI+Vbsn/4EAv/ZkqC3cAPJl/+gEAh5oH350DeobjD76GCBYgFB/4FCDQIdCBYNvTAQFBv4RDAQ3/+BBBgE/QXAAC/g/BA=")); +} + +function draw() { + var d = new Date(); + var h = d.getHours() % 12 || 12, m = d.getMinutes(), yyyy = d.getFullYear(), mm = d.getMonth(), dd = d.getDate(); + var time = (""+h).substr(-2) + ":" + ("0"+m).substr(-2); + g.reset(); // Reset the state of the graphics library + g.clear(); + g.drawImage(getImg()); //load bg image + //TIME + g.setFont("7x11Numeric7Seg",2); + g.setFontAlign(1,1); + g.setColor(0,0,1); + g.drawString(time, 97, 53, false /*clear background*/); + g.setColor(0,0,0); + g.drawString(time, 96, 52, false /*clear background*/); + //SECONDS + g.setFont("7x11Numeric7Seg",1); + //g.setFont("5x9Numeric7Seg"); + g.setFontAlign(-1,1); // align right bottom + g.setColor(0,0,1); + g.drawString(("0"+d.getSeconds()).substr(-2), 100, 42, 0); + g.setColor(0,0,0); + g.drawString(("0"+d.getSeconds()).substr(-2), 99, 41, 0); + //DATE + g.setFont("5x9Numeric7Seg",1); + g.setFontAlign(1,1); + g.setColor(0,0,1); + g.drawString(yyyy+" "+("0"+mm)+" "+dd, 100, 65, 0); + g.setColor(0,0,0); + g.drawString(yyyy+" "+("0"+mm)+" "+dd, 99, 64, 0); + //BATTERY + g.setColor(0,0,1); + g.drawString(E.getBattery(), 137, 53, 0); + g.setColor(0,0,0); + g.drawString(E.getBattery(), 136, 52, 0); + //STEPS + g.setColor(0,0,1); + g.drawString(Bangle.getHealthStatus("day").steps, 137, 65, 0); + g.setColor(0,0,0); + g.drawString(Bangle.getHealthStatus("day").steps, 136, 64, 0); + //WEEK DAY + g.setFont("8x12"); + g.setColor(0,0,1); + if (d.getDay()==0) { + g.drawString("SU", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("SU", 136, 42, 0); + } else if (d.getDay()==1) { + g.drawString("MO", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("MO", 136, 42, 0); + } else if (d.getDay()==2) { + g.drawString("TU", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("TU", 136, 42, 0); + } else if (d.getDay()==3) { + g.drawString("WE", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("WE", 136, 42, 0); + } else if (d.getDay()==4) { + g.setFont("Dylex7x13"); + g.drawString("TH", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("TH", 136, 42, 0); + } else if (d.getDay()==5) { + g.drawString("FR", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("FR", 136, 42, 0); + } else { + g.drawString("SA", 137, 43, 0); + g.setColor(0,0,0); + g.drawString("SA", 136, 42, 0); + } + if(wizible==1){ + Bangle.drawWidgets(); + } +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first +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; + if (on) { + secondInterval = setInterval(draw, 1000); + draw(); // draw immediately + } +}); +// Show launcher when middle button pressed +Bangle.setUI("clock"); + +//Toggle Widgets +Bangle.loadWidgets(); +Bangle.on('touch', function(button) { + if(wizible==0){ + wizible=1; + } + else if(wizible==1){ + wizible=0; + } +}); diff --git a/apps/bradbury/app.png b/apps/bradbury/app.png new file mode 100644 index 000000000..f7141d15e Binary files /dev/null and b/apps/bradbury/app.png differ diff --git a/apps/bradbury/metadata.json b/apps/bradbury/metadata.json new file mode 100644 index 000000000..456daa381 --- /dev/null +++ b/apps/bradbury/metadata.json @@ -0,0 +1,14 @@ +{ "id": "bradbury", + "name": "Bradbury Watch", + "shortName":"Bradbury", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.01", + "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"], + "storage": [ + {"name":"bradbury.app.js","url":"app.js"}, + {"name":"bradbury.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bradbury/screenshot.png b/apps/bradbury/screenshot.png new file mode 100644 index 000000000..914266668 Binary files /dev/null and b/apps/bradbury/screenshot.png differ diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 41eec666a..57f0ecf3d 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -21,3 +21,11 @@ Adds some preset modes and a custom one 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 diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md index 42ad619bd..f4eaf43af 100644 --- a/apps/bthrm/README.md +++ b/apps/bthrm/README.md @@ -2,7 +2,7 @@ When this app is installed it overrides Bangle.js's build 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. +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: @@ -16,21 +16,36 @@ as that requires live sensor data (rather than just BPM readings). Just install the app, then install an app that uses the heart rate monitor. -Once installed it'll automatically try and connect to the first bluetooth -heart rate monitor it finds. +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** + +### 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 This works with any heart rate monitor providing the standard Bluetooth -Heart Rate Service (`180D`) and characteristic (`2A37`). +Heart Rate Service (`180D`) and characteristic (`2A37`). It additionally supports +the location (`2A38`) characteristic and the Battery Service (`180F`), reporting +that information in the `BTHRM` event when they are available. So far it has been tested on: * CooSpo Bluetooth Heart Rate Monitor +* Polar H10 +* 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 recordet using the default HRM recorder. + ## Internals This replaces `Bangle.setHRMPower` with its own implementation. @@ -38,7 +53,6 @@ This replaces `Bangle.setHRMPower` with its own implementation. ## TODO * A widget to show connection state? -* Specify a specific device by address? ## Creator diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index 3a1f1cc4c..aa97d83b7 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -3,49 +3,48 @@ require('Storage').readJSON("bthrm.default.json", true) || {}, require('Storage').readJSON("bthrm.json", true) || {} ); - + var log = function(text, param){ + if (global.showStatusInfo) + showStatusInfo(text); if (settings.debuglog){ var logline = new Date().toISOString() + " - " + text; - if (param){ - logline += " " + JSON.stringify(param); - } + if (param) logline += ": " + JSON.stringify(param); print(logline); } }; - + log("Settings: ", settings); - + if (settings.enabled){ - function clearCache(){ + var clearCache = function() { return require('Storage').erase("bthrm.cache.json"); - } + }; - function getCache(){ + var getCache = function() { var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; - if (settings.btname && settings.btname == cache.name) return cache; + if (settings.btid && settings.btid === cache.id) return cache; clearCache(); return {}; - } - - function addNotificationHandler(characteristic){ - log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler); - characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler); - } - - function writeCache(cache){ + }; + + 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) { + if (oldCache !== cache) { log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache) + require('Storage').writeJSON("bthrm.cache.json", cache); } else { log("No changes, don't write cache"); } - - } + }; - function characteristicsToCache(characteristics){ + var characteristicsToCache = function(characteristics) { log("Cache characteristics"); var cache = getCache(); if (!cache.characteristics) cache.characteristics = {}; @@ -60,9 +59,10 @@ }; } writeCache(cache); - } + }; - function characteristicsFromCache(){ + var characteristicsFromCache = function(device) { + var service = { device : device }; // fake a BluetoothRemoteGATTService log("Read cached characteristics"); var cache = getCache(); if (!cache.characteristics) return []; @@ -76,43 +76,51 @@ 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; - } + }; log("Start"); var lastReceivedData={ }; - var serviceFilters = [{ - services: [ "180d" ] - }]; - - supportedServices = [ - "0x180d", "0x180f" + var supportedServices = [ + "0x180d", // Heart Rate + "0x180f", // Battery ]; + var bpmTimeout; + var supportedCharacteristics = { "0x2a37": { //Heart rate measurement - handler: function (event){ - var dv = event.target.value; + active: false, + handler: function (dv){ var flags = dv.getUint8(0); - + var 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); + if (supportedCharacteristics["0x2a37"].active) stopFallback(); + if (bpmTimeout) clearTimeout(bpmTimeout); + bpmTimeout = setTimeout(()=>{ + supportedCharacteristics["0x2a37"].active = false; + startFallback(); + }, 3000); + var sensorContact; - + if (flags & 2){ - sensorContact = (flags & 4) ? true : false; + sensorContact = !!(flags & 4); } - + var idx = 2 + (flags&1); - + var energyExpended; if (flags & 8){ energyExpended = dv.getUint16(idx,1); @@ -121,11 +129,11 @@ var interval; if (flags & 16) { interval = []; - maxIntervalBytes = (dv.byteLength - idx); + 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 + idx += 2; } } @@ -140,45 +148,44 @@ } if (settings.replace){ - var newEvent = { + var repEvent = { bpm: bpm, confidence: (sensorContact || sensorContact === undefined)? 100 : 0, src: "bthrm" }; - - log("Emitting HRM: ", newEvent); - Bangle.emit("HRM", newEvent); + + log("Emitting HRM", repEvent); + Bangle.emit("HRM_int", 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); + + log("Emitting BTHRM", newEvent); Bangle.emit("BTHRM", newEvent); } }, "0x2a38": { //Body sensor location - handler: function(data){ + handler: function(dv){ if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {}; - if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value; + lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10); } }, "0x2a19": { //Battery - handler: function (event){ + handler: function (dv){ if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {}; - if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0); + lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0); } } - }; var device; @@ -195,7 +202,7 @@ maxInterval: 1500 }; - function waitingPromise(timeout) { + var waitingPromise = function(timeout) { return new Promise(function(resolve){ log("Start waiting for " + timeout); setTimeout(()=>{ @@ -203,9 +210,13 @@ 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); }; @@ -215,36 +226,39 @@ }; } - if (settings.replace){ - var origIsHRMOn = Bangle.isHRMOn; + Bangle.origIsHRMOn = Bangle.isHRMOn; Bangle.isHRMOn = function() { if (settings.enabled && !settings.replace){ - return origIsHRMOn(); + return Bangle.origIsHRMOn(); } else if (settings.enabled && settings.replace){ return Bangle.isBTHRMOn(); } - return origIsHRMOn() || Bangle.isBTHRMOn(); + return Bangle.origIsHRMOn() || Bangle.isBTHRMOn(); }; } - function clearRetryTimeout(){ + var clearRetryTimeout = function(resetTime) { if (currentRetryTimeout){ log("Clearing timeout " + currentRetryTimeout); clearTimeout(currentRetryTimeout); currentRetryTimeout = undefined; } - } + if (resetTime) { + log("Resetting retry time"); + retryTime = initialRetryTime; + } + }; - function retry(){ + var retry = function() { log("Retry"); if (!currentRetryTimeout){ var clampedTime = retryTime < 100 ? 100 : retryTime; - - log("Set timeout for retry as " + clampedTime); + + log("Set timeout for retry as " + clampedTime); clearRetryTimeout(); currentRetryTimeout = setTimeout(() => { log("Retrying"); @@ -252,23 +266,23 @@ initBt(); }, clampedTime); - retryTime = Math.pow(retryTime, 1.1); + retryTime = Math.pow(clampedTime, 1.1); if (retryTime > maxRetryTime){ retryTime = maxRetryTime; } } else { log("Already in retry..."); } - } + }; var buzzing = false; - function onDisconnect(reason) { + var onDisconnect = function(reason) { log("Disconnect: " + reason); - log("GATT: ", gatt); - log("Characteristics: ", characteristics); - retryTime = initialRetryTime; - clearRetryTimeout(); - switchInternalHrm(); + log("GATT", gatt); + log("Characteristics", characteristics); + clearRetryTimeout(reason != "Connection Timeout"); + supportedCharacteristics["0x2a37"].active = false; + startFallback(); blockInit = false; if (settings.warnDisconnect && !buzzing){ buzzing = true; @@ -277,76 +291,78 @@ if (Bangle.isBTHRMOn()){ retry(); } - } + }; - function createCharacteristicPromise(newCharacteristic){ - log("Create characteristic promise: ", newCharacteristic); + 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", 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)); + log("Starting notifications", newCharacteristic); + var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); if (settings.gracePeriodNotification > 0){ log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); startPromise = startPromise.then(()=>{ log("Wait after connect"); - waitingPromise(settings.gracePeriodNotification) + return waitingPromise(settings.gracePeriodNotification); }); } return startPromise; }); - } else if (newCharacteristic.read){ - result = result.then(()=>{ - readData(newCharacteristic); - log("Reading data for " + newCharacteristic); - return newCharacteristic.read().then((data)=>{ - supportedCharacteristics[newCharacteristic.uuid].handler(data); - }); - }); } - return result.then(()=>log("Handled characteristic: ", newCharacteristic)); - } - - function attachCharacteristicPromise(promise, characteristic){ + return result.then(()=>log("Handled characteristic", newCharacteristic)); + }; + + var attachCharacteristicPromise = function(promise, characteristic) { return promise.then(()=>{ log("Handling characteristic:", characteristic); return createCharacteristicPromise(characteristic); }); - } - - function createCharacteristicsPromise(newCharacteristics){ - log("Create characteristics promise: ", newCharacteristics); + }; + + var createCharacteristicsPromise = function(newCharacteristics) { + log("Create characteristics promis ", newCharacteristics); var result = Promise.resolve(); for (var c of newCharacteristics){ if (!supportedCharacteristics[c.uuid]) continue; - log("Supporting characteristic: ", c); + log("Supporting characteristic", c); characteristics.push(c); if (c.properties.notify){ addNotificationHandler(c); } - + result = attachCharacteristicPromise(result, c); } return result.then(()=>log("Handled characteristics")); - } - - function createServicePromise(service){ - log("Create service promise: ", service); + }; + + var createServicePromise = function(service) { + log("Create service promise", service); var result = Promise.resolve(); result = result.then(()=>{ - log("Handling service: " + service.uuid); + log("Handling service" + service.uuid); return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); }); return result.then(()=>log("Handled service" + service.uuid)); - } - - function attachServicePromise(promise, service){ - return promise.then(()=>createServicePromise(service)); - } - - var reUseCounter = 0; + }; - function initBt() { + var attachServicePromise = function(promise, service) { + return promise.then(()=>createServicePromise(service)); + }; + + var initBt = function () { log("initBt with blockInit: " + blockInit); if (blockInit){ retry(); @@ -355,66 +371,64 @@ blockInit = true; - if (reUseCounter > 10){ - log("Reuse counter to high"); - gatt=undefined; - reUseCounter = 0; - } - var promise; - + var filters; + if (!device){ - var filters = serviceFilters; - if (settings.btname){ - log("Configured device name", settings.btname); - filters = [{name: settings.btname}]; + 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 }); - + 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); + 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); + log("Reuse device", device); } - + promise = promise.then(()=>{ if (gatt){ - log("Reuse GATT: ", gatt); + log("Reuse GATT", gatt); } else { - log("GATT is new: ", gatt); + log("GATT is new", gatt); characteristics = []; - var cachedName = getCache().name; - if (device.name != cachedName){ - log("Device name changed from " + cachedName + " to " + device.name + ", clearing cache"); + var cachedId = getCache().id; + if (device.id !== cachedId){ + log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); clearCache(); } var newCache = getCache(); - newCache.name = device.name; + 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); + log("Connecting..."); + var connectPromise = gatt.connect(connectSettings).then(function() { + log("Connected."); + }); if (settings.gracePeriodConnect > 0){ log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); connectPromise = connectPromise.then(()=>{ @@ -427,59 +441,69 @@ 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(); + if (!characteristics || characteristics.length === 0){ + characteristics = characteristicsFromCache(device); } }); promise = promise.then(()=>{ var characteristicsPromise = Promise.resolve(); - if (characteristics.length == 0){ + if (characteristics.length === 0){ characteristicsPromise = characteristicsPromise.then(()=>{ log("Getting services"); return gatt.getPrimaryServices(); }); characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services:", 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); + 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 waitingPromise(settings.gracePeriodService); }); } return result; }); - } else { for (var characteristic of characteristics){ characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true); } } - + return characteristicsPromise; }); - - promise = promise.then(()=>{ + + return promise.then(()=>{ log("Connection established, waiting for notifications"); - reUseCounter = 0; characteristicsToCache(characteristics); - clearRetryTimeout(); + clearRetryTimeout(true); }).catch((e) => { characteristics = []; log("Error:", e); onDisconnect(e); }); - } + }; Bangle.setBTHRMPower = function(isOn, app) { // Do app power handling @@ -487,16 +511,18 @@ 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); + 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) { + switchFallback(); if (!Bangle.isBTHRMConnected()) initBt(); } else { // not on log("Power off for " + app); + clearRetryTimeout(true); if (gatt) { if (gatt.connected){ - log("Disconnect with gatt: ", gatt); + log("Disconnect with gatt", gatt); try{ gatt.disconnect().then(()=>{ log("Successful disconnect"); @@ -510,8 +536,34 @@ } } }; - - var origSetHRMPower = Bangle.setHRMPower; + + if (settings.replace){ + Bangle.on("HRM", (e) => { + e.modified = true; + Bangle.emit("HRM_int", e); + }); + + Bangle.origOn = Bangle.on; + Bangle.on = function(name, callback) { + if (name == "HRM") { + Bangle.origOn("HRM_int", callback); + } else { + Bangle.origOn(name, callback); + } + }; + + Bangle.origRemoveListener = Bangle.removeListener; + Bangle.removeListener = function(name, callback) { + if (name == "HRM") { + Bangle.origRemoveListener("HRM_int", callback); + } else { + Bangle.origRemoveListener(name, callback); + } + }; + + } + + Bangle.origSetHRMPower = Bangle.setHRMPower; if (settings.startWithHrm){ @@ -521,28 +573,42 @@ Bangle.setBTHRMPower(isOn, app); } if ((settings.enabled && !settings.replace) || !settings.enabled){ - origSetHRMPower(isOn, app); + Bangle.origSetHRMPower(isOn, app); } }; } - - - var fallbackInterval; - - function switchInternalHrm(){ - 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); + + var fallbackActive = false; + var inSwitch = false; + + var stopFallback = function(){ + if (fallbackActive){ + Bangle.origSetHRMPower(0, "bthrm_fallback"); + fallbackActive = false; + log("Fallback to HRM disabled"); } - } + }; + + var startFallback = function(){ + if (!fallbackActive && settings.allowFallback) { + fallbackActive = true; + Bangle.origSetHRMPower(1, "bthrm_fallback"); + log("Fallback to HRM enabled"); + } + }; + + var 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"); @@ -550,18 +616,17 @@ for (var i = 0; i < Bangle._PWR.HRM.length; i++){ var app = Bangle._PWR.HRM[i]; log("Moving app " + app); - origSetHRMPower(0, app); + Bangle.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"); - var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); + gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e)); } }); } diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index cc533eedd..fadf2a5d8 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -1,7 +1,16 @@ -var btm = g.getHeight()-1; var intervalInt; var intervalBt; +var BODY_LOCS = { + 0: 'Other', + 1: 'Chest', + 2: 'Wrist', + 3: 'Finger', + 4: 'Hand', + 5: 'Ear Lobe', + 6: 'Foot', +} + function clear(y){ g.reset(); g.clearRect(0,y,g.getWidth(),y+75); @@ -15,17 +24,17 @@ function draw(y, type, event) { g.setFontAlign(0,0); g.setFontVector(40).drawString(str,px,y+20); str = "Event: " + type; - if (type == "HRM") { + 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); } - if (type == "BTHRM"){ + 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: " + event.location.toFixed(0) + "ms"; + 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= ""; @@ -33,19 +42,27 @@ function draw(y, type, event) { if (event.energy) str += " kJoule: " + event.energy.toFixed(0); g.setFontVector(12).drawString(str,px,y+60); } - } var firstEventBt = true; var firstEventInt = true; + +// This can get called for the boot code to show what's happening +function showStatusInfo(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){ + if (e.bpm === 0){ Bangle.buzz(100,0.2); } if (intervalBt){ diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json index 64e638b8a..fb284bcd2 100644 --- a/apps/bthrm/default.json +++ b/apps/bthrm/default.json @@ -7,10 +7,10 @@ "allowFallback": true, "warnDisconnect": false, "fallbackTimeout": 10, - "custom_replace": false, + "custom_replace": true, "custom_debuglog": false, - "custom_startWithHrm": false, - "custom_allowFallback": false, + "custom_startWithHrm": true, + "custom_allowFallback": true, "custom_warnDisconnect": false, "custom_fallbackTimeout": 10, "gracePeriodNotification": 0, diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index b35ebd6af..4d2cb811b 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,11 +2,11 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.08", + "version": "0.12", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", - "tags": "health,bluetooth", + "tags": "health,bluetooth,hrm,bthrm", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ diff --git a/apps/bthrm/recorder.js b/apps/bthrm/recorder.js index 21345a907..ed36b5aef 100644 --- a/apps/bthrm/recorder.js +++ b/apps/bthrm/recorder.js @@ -32,8 +32,45 @@ Bangle.removeListener('BTHRM', onHRM); if (Bangle.setBTRHMPower) 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 = "", src=""; + function onHRM(h) { + bpmConfidence = h.confidence; + bpm = h.bpm; + srv = h.src; + if (h.bpm > 0){ + active = true; + print("active" + h.bpm); + if (bpmTimeout) clearTimeout(bpmTimeout); + bpmTimeout = setTimeout(()=>{ + print("inactive"); + active = false; + },3000); + } + } + return { + name : "HR int", + fields : ["Heartrate", "Confidence"], + getValues : () => { + var r = [bpm,bpmConfidence,src]; + bpm = ""; bpmConfidence = ""; src=""; + return r; + }, + start : () => { + Bangle.origOn('HRM', onHRM); + if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder"); + }, + stop : () => { + Bangle.removeListener('HRM', 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/settings.js b/apps/bthrm/settings.js index 4b564d670..2b19ea46a 100644 --- a/apps/bthrm/settings.js +++ b/apps/bthrm/settings.js @@ -5,18 +5,26 @@ require('Storage').writeJSON(FILE, s); readSettings(); } - + function readSettings(){ settings = Object.assign( require('Storage').readJSON("bthrm.default.json", true) || {}, require('Storage').readJSON(FILE, true) || {} ); } - + var FILE="bthrm.json"; var settings; readSettings(); + 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 = { '': { 'title': 'Bluetooth HRM' }, @@ -35,7 +43,6 @@ case 1: writeSettings("enabled",true); writeSettings("replace",true); - writeSettings("debuglog",false); writeSettings("startWithHrm",true); writeSettings("allowFallback",true); writeSettings("fallbackTimeout",10); @@ -43,17 +50,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); @@ -61,12 +62,13 @@ } }; - if (settings.btname){ - var name = "Clear " + settings.btname; + if (settings.btname || settings.btid){ + var name = "Clear " + (settings.btname || settings.btid); mainmenu[name] = function() { - E.showPrompt("Clear current device name?").then((r)=>{ + E.showPrompt("Clear current device?").then((r)=>{ if (r) { writeSettings("btname",undefined); + writeSettings("btid",undefined); } E.showMenu(buildMainMenu()); }); @@ -78,22 +80,18 @@ mainmenu.Debug = function() { E.showMenu(submenu_debug); }; return mainmenu; } - - var 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); } @@ -103,57 +101,61 @@ function createMenuFromScan(){ E.showMenu(); - E.showMessage("Scanning"); + E.showMessage("Scanning for 4 seconds"); var submenu_scan = { - '' : { title: "Scan"}, '< Back': function() { E.showMenu(buildMainMenu()); } }; - var packets=10; - var scanStart=Date.now(); - NRF.setScan(function(d) { - packets--; - if (packets<=0 || Date.now() - scanStart > 5000){ - NRF.setScan(); - E.showMenu(submenu_scan); - } else if (d.name){ - print("Found device", d); - submenu_scan[d.name] = function(){ - E.showPrompt("Set "+d.name+"?").then((r)=>{ - if (r) { - writeSettings("btname",d.name); - } - E.showMenu(buildMainMenu()); + NRF.findDevices(function(devices) { + submenu_scan[''] = { title: `Scan (${devices.length} found)`}; + if (devices.length === 0) { + E.showAlert("No devices found") + .then(() => E.showMenu(buildMainMenu())); + return; + } else { + devices.forEach((d) => { + print("Found device", d); + var shown = (d.name || d.id.substr(0, 17)); + submenu_scan[shown] = function () { + E.showPrompt("Set " + shown + "?").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.showMenu(buildMainMenu()); + }); + }; }); - }; } - }, { filters: [{services: [ "180d" ]}]}); + E.showMenu(submenu_scan); + }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]}); } - - var 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': { @@ -164,10 +166,11 @@ format: v=>v+"s", onchange: v => { writeSettings("custom_fallbackTimout",v*1000); + if (settings.mode == 3) applyCustomSettings(); } }, }; - + var submenu_grace = { '' : { title: "Grace periods"}, '< Back': function() { E.showMenu(submenu_debug); }, @@ -212,51 +215,6 @@ } } }; - - var submenu = { - '' : { title: "Grace periods"}, - '< Back': function() { E.showMenu(buildMainMenu()); }, - 'Request': { - value: settings.gracePeriodRequest, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - onchange: v => { - writeSettings("gracePeriodRequest",v); - } - }, - 'Connect': { - value: settings.gracePeriodConnect, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - onchange: v => { - writeSettings("gracePeriodConnect",v); - } - }, - 'Notification': { - value: settings.gracePeriodNotification, - min: 0, - max: 3000, - step: 100, - format: v=>v+"ms", - 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/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 11569af0c..59bf9eb96 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -3,4 +3,19 @@ 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index f282bd187..dfb9bf515 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -1,16 +1,46 @@ # BW Clock +A very minimalistic clock to mainly show date and time. ![](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 BW clock provides many features and also 3rd party integrations: +- Bangle data such as steps, heart rate, battery or charging state. +- A timer can be set directly. *Requirement: Scheduler library* +- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled* +- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app* +- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app* + +Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden. + +## 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. + +## Menu structure +2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger. + +Simply click left / right to go through the menu entries such as Bangle, Timer etc. +and click up/down to move into this sub-menu. You can then click in the middle of the screen +to e.g. send a trigger via HomeAssistant once you selected it. + +``` + +5min + | + Bangle -- Timer[Optional] -- Agenda 1[Optional] -- Weather[Optional] -- HomeAssistant [Optional] + | | | | | + Bpm -5min Agenda 2 Temperature Trigger1 + | | | | + Steps ... ... ... + | + Battery +``` + ## Thanks to 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 5240e69ec..cd1979e98 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -1,24 +1,27 @@ -/* +/************ * Includes */ const locale = require('locale'); const storage = require('Storage'); -/* +/************ * Statics */ const SETTINGS_FILE = "bwclk.setting.json"; -const TIMER_IDX = "bwclk"; +const TIMER_IDX = "bwclk_timer"; +const TIMER_AGENDA_IDX = "bwclk_agenda"; const W = g.getWidth(); const H = g.getHeight(); -/* +/************ * Settings */ let settings = { - fullscreen: false, + screen: "Normal", showLock: true, - showInfo: 0, + hideColon: false, + menuPosX: 0, + menuPosY: 0, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; @@ -27,14 +30,18 @@ for (const key in saved_settings) { } -/* +/************ * Assets */ - // Manrope font Graphics.prototype.setLargeFont = function(scale) { - // Actual height 49 (50 - 2) - this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAfwAAAAAAAAf/AAAAAAAAf/8AAAAAAAf//wAAAAAAP///AAAAAAP///8AAAAAP////wAAAAP////4AAAAP////8AAAAH////8AAAAH////8AAAAB////8AAAAAH///+AAAAAAf//+AAAAAAB//+AAAAAAAH/+AAAAAAAAf+AAAAAAAAB/AAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///gAAAAAD////4AAAAA/////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAA///////4AAD/4AAAH/wAAP+AAAAP/AAB/wAAAAf8AAH/AAAAA/4AAf4AAAAB/gAB/gAAAAH+AAP8AAAAAf4AA/wAAAAB/gAD/AAAAAH+AAP8AAAAAf4AAf4AAAAB/gAB/gAAAAH+AAH+AAAAA/4AAf8AAAAH/AAB/4AAAA/8AAD/4AAAH/wAAP/8AAH/+AAAf//////4AAA///////AAAB//////4AAAD//////AAAAH/////4AAAAP////+AAAAAP////gAAAAAD///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAB/wAAAAAAAAH/AAAAAAAAA/4AAAAAAAAH/gAAAAAAAAf8AAAAAAAAD/gAAAAAAAAP+AAAAAAAAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAP///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAfwAAAH8AAAD/AAAB/wAAAf8AAAP/AAAD/wAAB/8AAAf/AAAP/wAAD/8AAB//AAAf/wAAH/8AAD//AAA//gAAf/8AAD/wAAB//wAAf+AAAP//AAB/wAAB//8AAH+AAAP//wAAf4AAB///AAD/AAAP/v8AAP8AAB/8/wAA/wAAP/j/AAD/AAB/8P8AAH+AAH/g/wAAf4AA/8D/AAB/wAH/gP8AAH/AA/+A/wAAf/AP/wD/AAA//D/+AP8AAD////wA/wAAH///+AD/AAAP///wAP8AAAf//+AA/wAAA///wAD/AAAB//+AAP8AAAB//gAA/wAAAB/4AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAD+AAAAHwAAAf4AAAAfwAAB/gAAAB/gAAH+AAAAP/AAAf4AAAA/8AAB/gAAAD/4AAH+ADAAH/wAAf4AeAAP/AAB/gD+AAP8AAH+Af+AA/4AAf4D/4AB/gAB/gP/AAH+AAH+B/8AAf4AAf4P/wAB/gAB/h//AAH+AAH+P/8AAf4AAf5//wAB/gAB/v//gAP+AAH+//+AA/4AAf//f8AH/AAB//5/8B/8AAH//D////gAAf/4P///+AAB//Af///wAAH/4A///+AAAf/AB///wAAB/4AD//+AAAH/AAH//gAAAP4AAD/4AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAA/+AAAAAAAAP/4AAAAAAAH//gAAAAAAB//+AAAAAAAf//4AAAAAAH///gAAAAAB///+AAAAAAf///4AAAAAH//9/gAAAAD///H+AAAAA///wf4AAAAP//8B/gAAAD///AH+AAAA///wAf4AAAH//8AB/gAAAf//AAH+AAAB//gAAf4AAAH/4AAB/gAAAf+AAAH+AAAB/gAf///8AAH4AB////wAAeAAH////AABgAAf///8AAAAAB////wAAAAAH////AAAAAAf///8AAAAAB////wAAAAAH////AAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAeAAAAAf//AB+AAAH///+AP8AAAf///4A/4AAB////gD/wAAH////Af/gAAf///8B/+AAB////wB/8AAH///+AB/wAAf4Af4AD/gAB/gB/AAP+AAH+AP8AAf4AAf4A/wAB/gAB/gD+AAH+AAH+AP4AAf4AAf4A/gAB/gAB/gD/AAH+AAH+AP8AAf4AAf4A/wAD/gAB/gD/gAf8AAH+AH/AD/wAAf4Af/Af+AAB/gB////4AAH+AD////AAAf4AH///8AAB/gAP///gAAH+AA///8AAAAAAA///AAAAAAAB//4AAAAAAAB/+AAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///gAAAAAB////4AAAAAf////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAAf//////4AAD/4D/wH/wAAP+AP8AP/AAB/wB/gAf8AAH/AH8AA/4AAf4A/wAB/gAB/gD/AAH+AAH8AP4AAf4AA/wA/gAB/gAD/AD+AAH+AAH8AP8AAf4AAf4A/wAB/gAB/gD/AAP+AAH+AP+AB/wAAf8Af8AP/AAA/4B/8B/8AAD/gH////gAAP8AP///8AAAfgAf///wAAA8AB///+AAADgAD///wAAAAAAD//+AAAAAAAH//gAAAAAAAH/4AAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAH+AAAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAf4AAAAADgAB/gAAAAA+AAH+AAAAAf4AAf4AAAAH/gAB/gAAAD/+AAH+AAAA//4AAf4AAAf//gAB/gAAH//+AAH+AAD///wAAf4AA///8AAB/gAf//+AAAH+AH///gAAAf4D///wAAAB/g///8AAAAH+f//+AAAAAf////gAAAAB////wAAAAAH///8AAAAAAf//+AAAAAAB///gAAAAAAH//wAAAAAAAf/8AAAAAAAB/+AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAB/wB//wAAAAf/wf//gAAAD//z///AAAAf/////+AAAD//////8AAAf//////4AAD///////gAAP////w//AAB/+f/8Af8AAH/Af/gA/4AAf4A/8AD/gAB/gB/wAH+AAP8AH+AAf4AA/wAf4AB/gAD/AB/gAH+AAP8AH+AAf4AA/wAf4AB/gAB/gB/wAH+AAH+AP/AAf4AAf8A/+AD/gAB/8f/8Af8AAD////4H/wAAP//////+AAAf//////4AAA///////AAAD//////8AAAH//z///gAAAH/+H//4AAAAH/gH//AAAAAAAAH/wAAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAP/wAAAAAAAD//wAAAAAAAf//wAAAAAAH///gABgAAA////AAPAAAD///+AB+AAAf///4AP4AAD////wB/wAAP/AP/AH/AAB/4Af+AP+AAH/AA/4A/4AAf4AB/gB/gAB/gAH+AH+AAP8AAP4Af4AA/wAA/gB/gAD/AAD+AH+AAP8AAP4Af4AA/4AB/gB/gAB/gAH+AH+AAH+AAfwA/4AAf8AD/AH/AAB/4Af4A/8AAD/4H/gP/wAAP//////+AAAf//////wAAA///////AAAB//////4AAAD//////AAAAH/////wAAAAH////+AAAAAH////AAAAAAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ExwqHCYlJyYoIicoFg=="), 64+(scale<<8)+(1<<16)); + // Actual height 47 (48 - 2) + this.setFontCustom( + 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("ExspGyUkJiQnISYnFQ=="), + 62+(scale<<8)+(1<<16) + ); return this; }; @@ -45,96 +52,327 @@ Graphics.prototype.setMediumFont = function(scale) { return this; }; + 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")) -}; +function imgLock(){ + return { + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) + } +} -var imgTemperature = { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA==")) -}; +function imgSteps(){ + return { + 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 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==")) -}; +function imgBattery(){ + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4AN4EAg4TBgd///9oEAAQv8ARQRDDQQgCEwQ4OA")) + } +} -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==")) -}; +function imgCharging() { + return { + 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 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=")) -}; +function imgBpm() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4AOn4CD/wCCjgCCv/8jF/wGYgOA5MB//BC4PDAQnjAQPnAQgANA")) + } +} -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")) -}; +function imgTemperature() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA==")) + } +} +function imgWeather(){ + return { + width : 24, height : 24, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AAcYAQ0MgEwAQUAngLB/8AgP/wACCgf/4Fz//OAQQICCIoaCEAQpGHA4ACA=")) + } +} + +function imgWind () { + return { + 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==")) + } +} + +function imgHumidity () { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("//7///+YCB+ICB8ACE4F/AQX9AQP54H//AOB+F/34CBj/gn8f4E+h/Aj0H4Ecg+AjED4ACE8E4gfwvEDEgICB/kHGwMP")) + } +} + +function imgTimer() { + return { + 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==")) + } +} + +function imgWatch() { + return { + 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")) + } +} + +function imgHomeAssistant() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4AF84CB4YCBwICBCAP+jFH/k8g/4kkH+AFB8ACB4cY4eHzPhgmZkHnzPn8fb4/gvwUD8EYARhAC")) + } +} + +function imgAgenda() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4AFnPP+ALBAQX4CIgLFAQvggEBAQvAgEDAQMCwEAgwTBhgiB/AlCGQ8BGQQ")) + } +} + +function imgMountain() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : atob("//////////////////////3///n///D//uZ//E8//A+/+Z+f8//P5//n7//3z//zn//5AAAAAAAA////////////////////") + } +} + +/************ + * 2D MENU with entries of: + * [name, icon, opt[customDownFun], opt[customUpFun], opt[customCenterFun]] + * + */ +var menu = [ + [ + function(){ return [ null, null ] }, + function(){ return [ "Week " + weekOfYear(), null ] }, + ], + [ + function(){ return [ "Bangle", imgWatch() ] }, + function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] }, + function(){ return [ getSteps(), imgSteps() ] }, + function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] }, + function(){ return [ measureAltitude, imgMountain() ]}, + ] +] /* - * INFO ENTRIES + * Timer Menu */ -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](); - } +try{ + require('sched'); + menu.push([ + function(){ + var text = isAlarmEnabled(TIMER_IDX) ? getAlarmMinutes(TIMER_IDX) + " min." : "Timer"; + return [text, imgTimer(), () => decreaseAlarm(TIMER_IDX), () => increaseAlarm(TIMER_IDX), null ] + }, + ]); +} catch(ex) { + // If sched is not installed, we hide this menu item } /* + * AGENDA MENU + * Note that we handle the agenda differently in order to hide old entries... + */ +var agendaIdx = 0; +var agendaTimerIdx = 0; +if(storage.readJSON("android.calendar.json") !== undefined){ + function nextAgendaEntry(){ + agendaIdx += 1; + } + + function previousAgendaEntry(){ + agendaIdx -= 1; + } + + menu.push([ + function(){ + var now = new Date(); + var agenda = storage.readJSON("android.calendar.json") + .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000) + .sort((a,b)=>a.timestamp - b.timestamp); + + if(agenda.length <= 0){ + return ["All done", imgAgenda()] + } + + agendaIdx = agendaIdx < 0 ? 0 : agendaIdx; + agendaIdx = agendaIdx >= agenda.length ? agendaIdx -1 : agendaIdx; + + var entry = agenda[agendaIdx]; + var title = entry.title.slice(0,14); + var date = new Date(entry.timestamp*1000); + var dateStr = locale.date(date).replace(/\d\d\d\d/,""); + dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; + + function dynImgAgenda(){ + if(isAlarmEnabled(TIMER_AGENDA_IDX) && agendaTimerIdx == agendaIdx){ + return imgTimer(); + } else { + return imgAgenda(); + } + } + + return [title + "\n" + dateStr, dynImgAgenda(), () => nextAgendaEntry(), () => previousAgendaEntry(), function(){ + try{ + var alarm = require('sched') + + // If other time, we disable the old one and enable this one. + if(agendaIdx != agendaTimerIdx){ + agendaTimerIdx = -1; + alarm.setAlarm(TIMER_AGENDA_IDX, undefined); + } + + // Disable alarm if enabled + if(isAlarmEnabled(TIMER_AGENDA_IDX)){ + agendaTimerIdx = -1; + alarm.setAlarm(TIMER_AGENDA_IDX, undefined); + alarm.reload(); + return + } + + // Otherwise, set alarm for given event + agendaTimerIdx = agendaIdx; + alarm.setAlarm(TIMER_AGENDA_IDX, { + msg: title, + timer : parseInt((date - now)), + }); + alarm.reload(); + } catch(ex){ } + }] + }, + ]); +} + + +/* + * WEATHER MENU + */ +if(storage.readJSON('weather.json') !== undefined){ + menu.push([ + function(){ return [ "Weather", imgWeather() ] }, + function(){ return [ getWeather().temp, imgTemperature() ] }, + function(){ return [ getWeather().hum, imgHumidity() ] }, + function(){ return [ getWeather().wind, imgWind() ] }, + ]); +} + + +/* + * HOME ASSISTANT MENU + */ +try{ + var triggers = require("ha.lib.js").getTriggers(); + var haMenu = [ + function(){ return [ "Home", imgHomeAssistant() ] }, + ]; + + triggers.forEach(trigger => { + haMenu.push(function(){ + return [trigger.display, trigger.getIcon(), () => {}, () => {}, function(){ + var ha = require("ha.lib.js"); + ha.sendTrigger("TRIGGER_BW"); + ha.sendTrigger(trigger.trigger); + }] + }); + }) + menu.push(haMenu); +} catch(ex){ + // If HomeAssistant is not installed, we hide this item +} + + +function getMenuEntry(){ + // In case the user removes HomeAssistant entries, showInfo + // could be larger than infoArray.length... + settings.menuPosX = settings.menuPosX % menu.length; + settings.menuPosY = settings.menuPosY % menu[settings.menuPosX].length; + var menuEntry = menu[settings.menuPosX][settings.menuPosY](); + + if(menuEntry[0] == null){ + return menuEntry; + } + + // For the first entry we always convert it into a callback function + // such that the menu is compatible with async functions such as + // measuring the pressure, altitude or sending http requests... + if(typeof menuEntry[0] !== 'function'){ + var value = menuEntry[0]; + menuEntry[0] = function(callbackFun){ + callbackFun(String(value), settings.menuPosX, settings.menuPosY); + } + } + return menuEntry; +} + + +/************ * Helper */ +function isFullscreen(){ + var s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked() + } else { + return s == "full" + } +} + function getSteps() { var steps = 0; try{ @@ -149,8 +387,7 @@ function getSteps() { // 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"; + return steps; } @@ -169,7 +406,7 @@ function getWeather(){ // Wind const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - weather.wind = Math.round(wind[1]) + " km/h"; + weather.wind = Math.round(wind[1]) + "kph"; return weather @@ -178,19 +415,33 @@ function getWeather(){ } return { - temp: "? °C", - hum: "-", - txt: "-", - wind: "? km/h", - wdir: "-", - wrose: "-" + temp: " ? ", + hum: " ? ", + txt: " ? ", + wind: " ? ", + wdir: " ? ", + wrose: " ? " }; } -function isAlarmEnabled(){ +// From https://weeknumber.com/how-to/javascript +function weekOfYear() { + 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); +} + + +function isAlarmEnabled(idx){ try{ var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); + var alarmObj = alarm.getAlarm(idx); if(alarmObj===undefined || !alarmObj.on){ return false; } @@ -201,37 +452,40 @@ function isAlarmEnabled(){ return false; } -function getAlarmMinutes(){ - if(!isAlarmEnabled()){ + +function getAlarmMinutes(idx){ + if(!isAlarmEnabled(idx)){ return -1; } var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); + var alarmObj = alarm.getAlarm(idx); return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); } -function increaseAlarm(){ + +function increaseAlarm(idx){ try{ - var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; - var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, { + var minutes = isAlarmEnabled(idx) ? getAlarmMinutes(idx) : 0; + var alarm = require('sched'); + alarm.setAlarm(idx, { timer : (minutes+5)*60*1000, }); alarm.reload(); } catch(ex){ } } -function decreaseAlarm(){ + +function decreaseAlarm(idx){ try{ - var minutes = getAlarmMinutes(); + var minutes = getAlarmMinutes(idx); minutes -= 5; var alarm = require('sched') - alarm.setAlarm(TIMER_IDX, undefined); + alarm.setAlarm(idx, undefined); if(minutes > 0){ - alarm.setAlarm(TIMER_IDX, { + alarm.setAlarm(idx, { timer : minutes*60*1000, }); } @@ -241,10 +495,26 @@ function decreaseAlarm(){ } -/* - * DRAW functions - */ +function measureAltitude(callbackFun){ + var oldX = settings.menuPosX; + var oldY = settings.menuPosY; + try{ + Bangle.getPressure().then(data=>{ + if(data && data.altitude && data.altitude > -100){ + callbackFun(Math.round(data.altitude) + "m", oldX, oldY); + } else { + callbackFun("???", oldX, oldY); + } + }); + }catch(ex){ + callbackFun("err", oldX, oldY); + } +} + +/************ + * DRAW + */ function draw() { // Queue draw again queueDraw(); @@ -259,11 +529,12 @@ function draw() { function drawDate(){ // Draw background - var y = H/5*2 + (settings.fullscreen ? 0 : 8); + var y = H/5*2; g.reset().clearRect(0,0,W,W); // Draw date - y -= settings.fullscreen ? 8 : 0; + y = parseInt(y/2)+4; + y += isFullscreen() ? 0 : 13; var date = new Date(); var dateStr = date.getDate(); dateStr = ("0" + dateStr).substr(-2); @@ -276,76 +547,99 @@ function drawDate(){ var dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr)); var fullDateW = dateW + 10 + dayW; - g.setFontAlign(-1,1); + 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.setMediumFont(); g.setColor(g.theme.fg); - g.drawString(dateStr, W/2 - fullDateW / 2, y+5); - - g.setSmallFont(); - g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+3); - g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-23); + g.drawString(dateStr, W/2 - fullDateW / 2, y+2); } function drawTime(){ // Draw background - var y = H/5*2 + (settings.fullscreen ? 0 : 8); + var y = H/5*2 + (isFullscreen() ? 0 : 8); g.setColor(g.theme.fg); g.fillRect(0,y,W,H); var date = new Date(); // Draw time g.setColor(g.theme.bg); - g.setFontAlign(0,-1); - var timeStr = locale.time(date,1); - y += settings.fullscreen ? 14 : 10; + g.setFontAlign(0,0); - var infoEntry = getInfoEntry(); - var infoStr = infoEntry[0]; - var infoImg = infoEntry[1]; - var printImgLeft = infoEntry[2] == "left"; + 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; + + // Set y coordinates correctly + y += parseInt((H - y)/2) + 5; + + var menuEntry = getMenuEntry(); + var menuTextFun = menuEntry[0]; + var menuImg = menuEntry[1]; + var printImgLeft = settings.menuPosY != 0; // Show large or small time depending on info entry - if(infoStr == null){ - y += 10; + if(menuTextFun == null){ g.setLargeFont(); - } else { - g.setMediumFont(); - } - g.drawString(timeStr, W/2, y); - - // Draw info if set - if(infoStr == null){ + g.drawString(timeStr, W/2, y); return; + } else { + y -= 15; + g.setMediumFont(); + g.drawString(timeStr, W/2, y); } - y += H/5*2-5; - 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); + // Async set the menu (could be that some data is async fetched) + menuTextFun((menuText, oldX, oldY) => { + + // We display the text IFF the user did not change the menu + if(settings.menuPosX != oldX || settings.menuPosY != oldY){ + return; + } + + // As its a callback, we have to ensure that the color + // font etc. is still correct... + g.setColor(g.theme.bg); + g.setFontAlign(0,0); + y += 35; + + if(menuText.split('\n').length > 1){ + g.setMiniFont(); + } else { + g.setSmallFont(); + } + + var imgWidth = 0; + if(menuImg){ + imgWidth = 24.0; + var strWidth = g.stringWidth(menuText); + var scale = imgWidth / menuImg.width; + g.drawImage( + menuImg, + W/2 + (printImgLeft ? -strWidth/2-4 : strWidth/2+4) - parseInt(imgWidth/2), + y - parseInt(imgWidth/2), + { scale: scale } + ); + } + g.drawString(menuText, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3); + }); } function drawLock(){ 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){ + if(isFullscreen()){ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} } else { Bangle.drawWidgets(); @@ -370,17 +664,6 @@ function queueDraw() { } -/* - * Load clock, widgets and listen for events - */ -Bangle.loadWidgets(); - -// Clear the screen once, at startup and set the correct theme. -var bgOrig = g.theme.bg -var fgOrig = g.theme.fg -g.setTheme({bg:fgOrig,fg:bgOrig}).clear(); -draw(); - // Stop updates when LCD is off, restart when on Bangle.on('lcdPower',on=>{ if (on) { @@ -394,57 +677,137 @@ Bangle.on('lcdPower',on=>{ Bangle.on('lock', 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! + for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;} + } + draw(); }); Bangle.on('charging',function(charging) { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; + + // Jump to battery + settings.menuPosX = 1; + settings.menuPosY = 1; draw(); }); Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.2); + var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better... + var left = parseInt(g.getWidth() * 0.22); var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.2); + var upper = parseInt(g.getHeight() * 0.22) + widget_size; var lower = g.getHeight() - upper; - 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(); - } + var is_left = e.x < left && !is_upper && !is_lower; + var is_right = e.x > right && !is_upper && !is_lower; + var is_center = !is_upper && !is_lower && !is_left && !is_right; if(is_lower){ Bangle.buzz(40, 0.6); - decreaseAlarm(); + settings.menuPosY = (settings.menuPosY+1) % menu[settings.menuPosX].length; + + // Handle custom menu entry function + var menuEntry = getMenuEntry(); + if(menuEntry.length > 2){ + menuEntry[2](); + } + + drawTime(); + } + + if(is_upper){ + if(e.y < widget_size){ + return; + } + + Bangle.buzz(40, 0.6); + settings.menuPosY = settings.menuPosY-1; + settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].length-1 : settings.menuPosY; + + // Handle custom menu entry function + var menuEntry = getMenuEntry(); + if(menuEntry.length > 3){ + menuEntry[3](); + } + drawTime(); } if(is_right){ + // A bit hacky but we ensure that always the first agenda entry is shown... + agendaIdx = 0; + Bangle.buzz(40, 0.6); - settings.showInfo = (settings.showInfo+1) % NUM_INFO; + settings.menuPosX = (settings.menuPosX+1) % menu.length; + settings.menuPosY = 0; drawTime(); } if(is_left){ + // A bit hacky but we ensure that always the first agenda entry is shown... + agendaIdx = 0; + Bangle.buzz(40, 0.6); - settings.showInfo = settings.showInfo-1; - settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo; + settings.menuPosY = 0; + settings.menuPosX = settings.menuPosX-1; + settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; drawTime(); } + + if(is_center){ + var menuEntry = getMenuEntry(); + if(menuEntry.length > 4 && menuEntry[4] != null){ + Bangle.buzz(80, 0.6).then(()=>{ + try{ + menuEntry[4](); + setTimeout(()=>{ + Bangle.buzz(80, 0.6); + drawTime(); + }, 250); + } catch(ex){ + // In case it fails, we simply ignore it. + } + } + ); + } + } }); E.on("kill", function(){ - storage.write(SETTINGS_FILE, settings); + try{ + storage.write(SETTINGS_FILE, settings); + } catch(ex){ + // If this fails, we still kill the app... + } }); +/* + * Draw clock the first time + */ +// 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. +g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear(); + // Show launcher when middle button pressed Bangle.setUI("clock"); + +// Load widgets and draw clock the first time +Bangle.loadWidgets(); + +// Cache draw function for dynamic screen to hide / show widgets +// Bangle.loadWidgets() could also be called later on but its much slower! +for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;} + +// Draw first time +draw(); diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index 8b13cd256..b6896ab5e 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,11 +1,11 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.06", - "description": "BW Clock.", + "version": "0.21", + "description": "A very minimalistic clock to mainly show date and time.", "readme": "README.md", "icon": "app.png", - "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}], + "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/bwclk/screenshot.png b/apps/bwclk/screenshot.png index b30ba4166..3a75f13d1 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 ea2dc780b..31bf6373e 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 fb5b153b8..8d982cac4 100644 Binary files a/apps/bwclk/screenshot_3.png and b/apps/bwclk/screenshot_3.png differ diff --git a/apps/bwclk/screenshot_4.png b/apps/bwclk/screenshot_4.png new file mode 100644 index 000000000..83de5c2ce Binary files /dev/null and b/apps/bwclk/screenshot_4.png differ diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index 0fdaf1a28..116253fda 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -4,8 +4,9 @@ // initialize with default settings... const storage = require('Storage') let settings = { - fullscreen: false, + screen: "Normal", showLock: true, + hideColon: false, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -16,15 +17,16 @@ 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(); }, }, @@ -35,6 +37,14 @@ settings.showLock = !settings.showLock; save(); }, + }, + 'Hide Colon': { + value: settings.hideColon, + format: () => (settings.hideColon ? 'Yes' : 'No'), + onchange: () => { + settings.hideColon = !settings.hideColon; + save(); + }, } }); }) diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog new file mode 100644 index 000000000..b67f29e94 --- /dev/null +++ b/apps/calclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial version +0.02: More compact rendering & app icon +0.03: Tell clock widgets to hide. 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..343d95c04 --- /dev/null +++ b/apps/calclock/calclock.js @@ -0,0 +1,119 @@ +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) { + g.setFont("Vector", 24); + + var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000); + var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); + g.drawString(timeStr, 5, y); + y += 24; + + g.setFont("12x20", 1); + if (isActive(event)) { + g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21); + } else { + var offset = 0-time.getTimezoneOffset()/1440; + var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset); + if(days > 0) { + var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days"; + g.drawString(daysStr,15*timeStr.length,y-21); + } + } + return y; +} + +function drawEventBody(event, y) { + g.setFont("12x20", 1); + var lines = g.wrapString(event.title, g.getWidth()-10); + if (lines.length > 2) { + lines = lines.slice(0,2); + lines[1] = lines[1].slice(0,-3)+"..."; + } + g.drawString(lines.join('\n'), 5, y); + y+=20 * lines.length; + if(event.location) { + g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y); + g.drawString(event.location, 20, y); + y+=20; + } + 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("#0ff"); + g.clearRect(5, 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(5,24,g.getWidth()-5,g.getHeight()); + updateCalendar(); + var y = 30; + y = drawCurrentEvents(y); + drawFutureEvents(y); +} + +function redraw() { + g.reset(); + if (current.find(e=>!isActive(e)) || next.find(isActive)) { + fullRedraw(); + } else { + drawCurrentEvents(30); + } +} + +g.clear(); +fullRedraw(); +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..7bac5c721 --- /dev/null +++ b/apps/calclock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "calclock", + "name": "Calendar Clock", + "shortName": "CalClock", + "version": "0.03", + "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.png b/apps/calclock/screenshot.png new file mode 100644 index 000000000..4ab503f2b Binary files /dev/null and b/apps/calclock/screenshot.png differ diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index cc8bb6306..0583ea45f 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -5,3 +5,7 @@ 0.05: Update calendar weekend colors for start on Sunday 0.06: Use larger font for dates 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 diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 3f4315811..f4676fc22 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -16,10 +16,15 @@ const white = "#ffffff"; const red = "#d41706"; const blue = "#0000ff"; const yellow = "#ffff00"; +let bgColor = color4; +let bgColorMonth = color1; +let bgColorDow = color2; +let bgColorWeekend = color3; +let fgOtherMonth = gray1; +let fgSameMonth = white; let settings = require('Storage').readJSON("calendar.json", true) || {}; -if (settings.startOnSun === undefined) - settings.startOnSun = false; +let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0; if (settings.ndColors === undefined) if (process.env.HWVERSION == 2) { settings.ndColors = true; @@ -28,19 +33,12 @@ if (settings.ndColors === undefined) } 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; + bgColor = white; + bgColorMonth = blue; + bgColorDow = black; + bgColorWeekend = yellow; + fgOtherMonth = blue; + fgSameMonth = black; } function getDowLbls(locale) { @@ -50,14 +48,14 @@ function getDowLbls(locale) { case "de_AT": case "de_CH": case "de_DE": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; } else { dowLbls = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; } break; case "nl_NL": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["zo", "ma", "di", "wo", "do", "vr", "za"]; } else { dowLbls = ["ma", "di", "wo", "do", "vr", "za", "zo"]; @@ -66,14 +64,14 @@ function getDowLbls(locale) { case "fr_BE": case "fr_CH": case "fr_FR": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; } else { dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; } break; case "sv_SE": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"]; } else { dowLbls = ["Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"]; @@ -81,21 +79,21 @@ function getDowLbls(locale) { break; case "it_CH": case "it_IT": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["Do", "Lu", "Ma", "Me", "Gi", "Ve", "Sa"]; } else { dowLbls = ["Lu", "Ma", "Me", "Gi", "Ve", "Sa", "Do"]; } break; case "oc_FR": - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["dg", "dl", "dm", "dc", "dj", "dv", "ds"]; } else { dowLbls = ["dl", "dm", "dc", "dj", "dv", "ds", "dg"]; } break; default: - if (settings.startOnSun) { + if (startOnSun) { dowLbls = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; } else { dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; @@ -110,7 +108,7 @@ function drawCalendar(date) { g.clearRect(0, 0, maxX, maxY); g.setBgColor(bgColorMonth); g.clearRect(0, 0, maxX, headerH); - if (settings.startOnSun){ + if (startOnSun){ g.setBgColor(bgColorWeekend); g.clearRect(0, headerH + rowH, colW, maxY); g.setBgColor(bgColorDow); @@ -150,7 +148,7 @@ function drawCalendar(date) { }); date.setDate(1); - const dow = date.getDay() + (settings.startOnSun ? 1 : 0); + const dow = date.getDay() + (startOnSun ? 1 : 0); const dowNorm = dow === 0 ? 7 : dow; const monthMaxDayMap = { @@ -242,5 +240,5 @@ Bangle.on("touch", area => { }); // Show launcher when button pressed -Bangle.setUI("clock"); // TODO: ideally don't set 'clock' mode +setWatch(() => load(), process.env.HWVERSION === 2 ? BTN : BTN3, { repeat: false, edge: "falling" }); // No space for widgets! diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 62d2513ae..48fd52d3e 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.07", + "version": "0.10", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], diff --git a/apps/calendar/settings.js b/apps/calendar/settings.js index 3c8f7d8e8..54ed50a64 100644 --- a/apps/calendar/settings.js +++ b/apps/calendar/settings.js @@ -1,8 +1,6 @@ (function (back) { var FILE = "calendar.json"; var settings = require('Storage').readJSON(FILE, true) || {}; - if (settings.startOnSun === undefined) - settings.startOnSun = false; if (settings.ndColors === undefined) if (process.env.HWVERSION == 2) { settings.ndColors = true; @@ -17,17 +15,8 @@ E.showMenu({ "": { "title": "Calendar" }, "< Back": () => back(), - 'Start Sunday': { - value: settings.startOnSun, - format: v => v ? "Yes" : "No", - onchange: v => { - settings.startOnSun = v; - writeSettings(); - } - }, 'B2 Colors': { value: settings.ndColors, - format: v => v ? "Yes" : "No", onchange: v => { settings.ndColors = v; writeSettings(); diff --git a/apps/calibration/ChangeLog b/apps/calibration/ChangeLog new file mode 100644 index 000000000..64bff2b31 --- /dev/null +++ b/apps/calibration/ChangeLog @@ -0,0 +1,3 @@ +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 diff --git a/apps/calibration/README.md b/apps/calibration/README.md new file mode 100644 index 000000000..ed1a29d9e --- /dev/null +++ b/apps/calibration/README.md @@ -0,0 +1,12 @@ +# Banglejs - Touchscreen calibration +A simple calibration app for the touchscreen + +## Usage + +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 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-icon.js b/apps/calibration/app-icon.js new file mode 100644 index 000000000..af66c3f68 --- /dev/null +++ b/apps/calibration/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkB/4AJ+EPBhQXg+BBDCyJaGGR5zIDBoQEL4QYOLYR3GBIouJR5AYBGBILBU5QMGFwgiFX4wwIEI4XGGBAgHd44+HD44XHNw4XWM5IIHCIoXWV5IXICQgXvLxAAKCYYXh5nMC6n8C4PPC5MAAA8PC4ZxBACAXOI653hU5zvJABASEC5PwHI4XcMBIXICIoXXJBAXHCAwXXJBAXHB5AfGC4ygJEAwXGQ5BoIQxoiDBYgXECwIuIBgb5ECIQJFGBQmCC4QHEDBwAFCxoYICx5ZELZoZJFiIXpA=")) \ No newline at end of file diff --git a/apps/calibration/app.js b/apps/calibration/app.js new file mode 100644 index 000000000..049430d45 --- /dev/null +++ b/apps/calibration/app.js @@ -0,0 +1,151 @@ +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], + xMaxActual: [this.target.xMax], + yMaxActual: [this.target.yMax], + }; + } + + load_settings() { + let settings = require('Storage').readJSON('calibration.json', true) || {active: false}; + + console.log('loaded settings:'); + console.log(settings); + + return 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]; + } + } + + 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() { + /* + * TODO: + * Present how to use the application + * + */ + } + + drawTarget() { + 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, 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); + 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) { + 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()); + +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', + btn : function(n) { + calibration.save_settings(this.settings); + load(); + }, + touch : touchHandler, +}; +Bangle.setUI(modes); +calibration.drawTarget(); diff --git a/apps/calibration/boot.js b/apps/calibration/boot.js new file mode 100644 index 000000000..03b17a03a --- /dev/null +++ b/apps/calibration/boot.js @@ -0,0 +1,14 @@ +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 || Bangle.disableCalibration) return; + + // reload the calibration offset at each touch event /!\ bad for the flash memory + if (cal_settings.reload === true) { + cal_settings = require('Storage').readJSON("calibration.json", true); + } + + // apply the calibration offset + 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/calibration.png b/apps/calibration/calibration.png new file mode 100644 index 000000000..3fb44beee Binary files /dev/null and b/apps/calibration/calibration.png differ diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json new file mode 100644 index 000000000..b60650300 --- /dev/null +++ b/apps/calibration/metadata.json @@ -0,0 +1,17 @@ +{ "id": "calibration", + "name": "Touchscreen Calibration", + "shortName":"Calibration", + "icon": "calibration.png", + "version":"0.03", + "description": "A simple calibration app for the touchscreen", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "tags": "tool", + "storage": [ + {"name":"calibration.app.js","url":"app.js"}, + {"name":"calibration.boot.js","url":"boot.js"}, + {"name":"calibration.settings.js","url":"settings.js"}, + {"name":"calibration.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"calibration.json"}] +} diff --git a/apps/calibration/settings.js b/apps/calibration/settings.js new file mode 100644 index 000000000..08c728d96 --- /dev/null +++ b/apps/calibration/settings.js @@ -0,0 +1,22 @@ +(function(back) { + var FILE = "calibration.json"; + var settings = Object.assign({ + active: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + "" : { "title" : "Calibration" }, + "< Back" : () => back(), + 'Active': { + value: !!settings.active, + onchange: v => { + settings.active = v; + writeSettings(); + } + }, + }); +}) \ No newline at end of file diff --git a/apps/cassioWatch/ChangeLog b/apps/cassioWatch/ChangeLog new file mode 100644 index 000000000..883bd2585 --- /dev/null +++ b/apps/cassioWatch/ChangeLog @@ -0,0 +1,12 @@ +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. diff --git a/apps/cassioWatch/README.md b/apps/cassioWatch/README.md new file mode 100644 index 000000000..aaeb3f122 --- /dev/null +++ b/apps/cassioWatch/README.md @@ -0,0 +1,11 @@ +# 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..91f6737ad --- /dev/null +++ b/apps/cassioWatch/app.js @@ -0,0 +1,175 @@ +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 = 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 "? k"; + } + + steps = Math.round(steps/1000); + return steps + "k"; +} + + +function draw() { + queueDraw(); + + g.reset(); + g.clear(); + 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("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(); + +g.reset(); +g.clear(); +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..abfee9b93 --- /dev/null +++ b/apps/cassioWatch/metadata.json @@ -0,0 +1,18 @@ +{ + "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.11", + "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 } + ] +} 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/chimer/ChangeLog b/apps/chimer/ChangeLog new file mode 100644 index 000000000..01bd00a0a --- /dev/null +++ b/apps/chimer/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Creation +0.02: Fixed some sleep bugs. Added a sleep mode toggle \ No newline at end of file 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..d5bc04950 --- /dev/null +++ b/apps/chimer/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "chimer", + "name": "Chimer", + "version": "0.02", + "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Reapeat 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..55160c9be --- /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..18358df9e --- /dev/null +++ b/apps/chimer/widget.js @@ -0,0 +1,134 @@ +(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 sleep(milliseconds) { + const date = Date.now(); + let currentDate = null; + do { + currentDate = Date.now(); + } while (currentDate - date < milliseconds); + } + + function chime() { + for (var i = 0; i < settings.repeat; i++) { + if (settings.type === 1) { + Bangle.buzz(100); + } else if (settings.type === 2) { + Bangle.beep(); + } else { + return; + } + sleep(150); + } + } + + 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(), + s = now.getSeconds(), + ms = now.getMilliseconds(); + if ( + (settings.sleep && h > settings.end) || + (settings.sleep && h >= settings.end && m !== 0) || + (settings.sleep && h < settings.start) + ) { + var mLeft = 60 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + return; + } + if (settings.freq === 1) { + if ((m !== lastMinute && m === 0) || (m !== lastMinute && m === 30)) + chime(); + lastHour = h; + lastMinute = m; + // check again in 30 minutes + switch (true) { + case m / 30 >= 1: + var mLeft = 30 - (m - 30), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 30 < 1: + var mLeft = 30 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + } + setTimeout(check, msLeft); + } else if (settings.freq === 2) { + if ( + (m !== lastMinute && m === 0) || + (m !== lastMinute && m === 15) || + (m !== lastMinute && m === 30) || + (m !== lastMinute && m === 45) + ) + chime(); + lastHour = h; + lastMinute = m; + // check again in 15 minutes + switch (true) { + case m / 15 >= 3: + var mLeft = 15 - (m - 45), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 >= 2: + var mLeft = 15 - (m - 30), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 >= 1: + var mLeft = 15 - (m - 15), + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + case m / 15 < 1: + var mLeft = 15 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + break; + } + setTimeout(check, msLeft); + } else if (settings.freq === 3) { + if (m !== lastMinute) chime(); + lastHour = h; + lastMinute = m; + // check again in 1 minute + + var mLeft = 1, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + } else { + if (h !== lastHour && m === 0) chime(); + lastHour = h; + // check again in 60 minutes + var mLeft = 60 - m, + sLeft = mLeft * 60 - s, + msLeft = sLeft * 1000 - ms; + setTimeout(check, msLeft); + } + } + + 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/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 46eddd32b..c398a89b6 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -23,3 +23,6 @@ 0.11: New color option: foreground color 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 diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index a6aa1a8b1..fc501a5d0 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,10 +1,5 @@ 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)); @@ -22,10 +17,16 @@ let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} ); -// Load step goal from pedometer widget as fallback + +// Load step goal from health app and 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; + let d = storage.readJSON("health.json", true) || {}; + settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined; + + if (settings.stepGoal == undefined) { + d = storage.readJSON("wpedom.json", true) || {}; + settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; + } } /* @@ -125,20 +126,11 @@ function draw() { 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); + + drawCircle(1); + drawCircle(2); + drawCircle(3); + if (circleCount >= 4) drawCircle(4); } function drawCircle(index) { @@ -294,7 +286,7 @@ function drawSteps(w) { writeCircleText(w, shortValue(steps)); - g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawStepsDistance(w) { @@ -319,7 +311,7 @@ function drawStepsDistance(w) { writeCircleText(w, shortValue(stepsDistance)); - g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawHeartRate(w) { @@ -490,8 +482,8 @@ function drawTemperature(w) { if (temperature) writeCircleText(w, locale.temp(temperature)); - - g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); + + g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }); } @@ -517,7 +509,7 @@ function drawPressure(w) { if (pressure) writeCircleText(w, Math.round(pressure)); - g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }); } @@ -543,7 +535,7 @@ function drawAltitude(w) { if (altitude) writeCircleText(w, locale.distance(Math.round(altitude))); - g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }); } @@ -614,8 +606,8 @@ function getWeatherIconByCode(code) { default: return weatherCloudy; } - default: - return undefined; + default: + return undefined; } } @@ -641,6 +633,7 @@ function formatSeconds(s) { function getSunData() { if (location != undefined && location.lat != undefined) { + const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); // get today's sunlight times for lat/lon return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined; } @@ -848,8 +841,8 @@ Bangle.loadWidgets(); // schedule a draw for the next minute setTimeout(function() { - // draw every 60 seconds - setInterval(draw,60000); + // 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 cb6bfcff8..ea00dc347 100644 --- a/apps/circlesclock/default.json +++ b/apps/circlesclock/default.json @@ -21,5 +21,6 @@ "circle2colorizeIcon": true, "circle3colorizeIcon": true, "circle4colorizeIcon": false, - "hrmValidity": 60 + "hrmValidity": 60, + "updateInterval": 60 } diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index b16f14c06..837fcaa88 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,7 +1,7 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.11", + "version":"0.13", "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"}], diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 0b9e94aca..fb23f8d5e 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -58,6 +58,16 @@ min: 0, max: 2, format: v => weatherData[v], onchange: x => save('weatherCircleData', weatherData[x]), + }, + /*LANG*/'update interval': { + value: settings.updateInterval, + min: 0, + max : 3600, + step: 30, + format: x => { + return x + 's'; + }, + onchange: x => save('updateInterval', x), } }; E.showMenu(menu); @@ -100,7 +110,7 @@ /*LANG*/'valid period': { value: settings.hrmValidity, min: 10, - max : 600, + max : 1800, step: 10, format: x => { return x + "s"; @@ -117,9 +127,9 @@ /*LANG*/'< Back': ()=>showMainMenu(), /*LANG*/'goal': { value: settings.stepGoal, - min: 2000, + min: 1000, max : 50000, - step: 2000, + step: 500, format: x => { return x; }, @@ -127,9 +137,9 @@ }, /*LANG*/'distance goal': { value: settings.stepDistanceGoal, - min: 2000, - max : 30000, - step: 1000, + min: 1000, + max : 50000, + step: 500, format: x => { return x; }, diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 8b40a87ac..27d4fc7f4 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -1,3 +1,6 @@ 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. diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js index 5e8c7f796..58ddd7ef5 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -1,3 +1,4 @@ +Bangle.setUI("clock"); Bangle.loadWidgets(); var s = Object.assign({ @@ -123,7 +124,7 @@ function drawMinutes() { var d = new Date(); var hours = s.MODE24 ? d.getHours().toString().padStart(2, ' ') : ((d.getHours() + 24) % 12 || 12).toString().padStart(2, ' '); var minutes = d.getMinutes().toString().padStart(2, '0'); - var textColor = NRF.getSecurityStatus().connected ? '#fff' : '#f00'; + var textColor = NRF.getSecurityStatus().connected ? '#99f' : '#fff'; var size = 50; var clock_x = (w - 20) / 2; if (dimSeconds) { @@ -307,4 +308,4 @@ NRF.on('disconnect', BTevent); dimSeconds = Bangle.isLocked(); drawWatch(); -Bangle.setUI("clock"); + diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 3998215d7..872211495 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.06", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index abedad99b..d4cc4df68 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -26,7 +26,6 @@ "< Back": () => back(), 'Buzz(dis)conn.?': { value: settings.BUZZ_ON_BT, - format: v => v ? "On" : "Off", onchange: v => { settings.BUZZ_ON_BT = v; writeSettings(); @@ -59,7 +58,6 @@ }, 'Red Saturday?': { value: settings.REDSAT, - format: v => v ? "On" : "Off", onchange: v => { settings.REDSAT = v; writeSettings(); @@ -67,7 +65,6 @@ }, 'Red Sunday?': { value: settings.REDSUN, - format: v => v ? "On" : "Off", onchange: v => { settings.REDSUN = v; writeSettings(); 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..f4bfe77a5 --- /dev/null +++ b/apps/cogclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: New clock +0.02: Use ClockFace library, add settings +0.03: Use ClockFace_menu.addSettingsFile diff --git a/apps/cogclock/app.js b/apps/cogclock/app.js new file mode 100644 index 000000000..d24031684 --- /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 = 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..29000b589 --- /dev/null +++ b/apps/cogclock/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "cogclock", + "name": "Cog Clock", + "version": "0.03", + "description": "A cross-shaped clock inside a cog", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS"], + "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.png b/apps/cogclock/screenshot.png new file mode 100644 index 000000000..f49709aef Binary files /dev/null and b/apps/cogclock/screenshot.png differ diff --git a/apps/cogclock/settings.js b/apps/cogclock/settings.js new file mode 100644 index 000000000..a91b033d0 --- /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", "loadWidgets" + ]); + E.showMenu(menu); +}); diff --git a/apps/color_catalog/Changelog b/apps/color_catalog/ChangeLog similarity index 100% rename from apps/color_catalog/Changelog rename to apps/color_catalog/ChangeLog diff --git a/apps/colorful_clock/ChangeLog b/apps/colorful_clock/ChangeLog new file mode 100644 index 000000000..54ee389e3 --- /dev/null +++ b/apps/colorful_clock/ChangeLog @@ -0,0 +1,3 @@ +... +0.03: First update with ChangeLog Added +0.04: Tell clock widgets to hide. diff --git a/apps/colorful_clock/app.js b/apps/colorful_clock/app.js index afc6b321f..ba6272e9b 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 ****/ @@ -241,7 +243,3 @@ refreshDisplay(); } }); - - Bangle.loadWidgets(); - - Bangle.setUI('clock'); diff --git a/apps/colorful_clock/metadata.json b/apps/colorful_clock/metadata.json index 5b6dbe87e..237acf81c 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.04", "description": "a colorful analog clock", "icon": "app-icon.png", "type": "clock", diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog index d1adafc4c..deb1072f5 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -4,3 +4,4 @@ 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) diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 4730111ac..dd398ffa6 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -34,7 +34,7 @@ var oldHeading = 0; Bangle.on('mag', function(m) { if (!Bangle.isLCDOn()) return; g.reset(); - if (isNaN(m.heading)) { + if (isNaN(m.heading)) { if (!wasUncalibrated) { g.clearRect(0,24,W,48); g.setFontAlign(0,-1).setFont("6x8"); @@ -49,7 +49,7 @@ Bangle.on('mag', function(m) { g.setFontAlign(0,0).setFont("6x8",3); var y = 36; g.clearRect(M-40,24,M+40,48); - g.drawString(Math.round(m.heading),M,y,true); + g.drawString(Math.round(360-m.heading),M,y,true); } diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json index 3e3b37f72..a3995a123 100644 --- a/apps/compass/metadata.json +++ b/apps/compass/metadata.json @@ -1,7 +1,7 @@ { "id": "compass", "name": "Compass", - "version": "0.06", + "version": "0.07", "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..9d55c1a91 --- /dev/null +++ b/apps/configurable_clock/ChangeLog @@ -0,0 +1,3 @@ +... +0.02: First update with ChangeLog Added +0.03: Tell clock widgets to hide. diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js index 157d57741..45c86c7e9 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 ****/ @@ -1377,4 +1378,3 @@ } }); - Bangle.setUI('clock'); diff --git a/apps/configurable_clock/metadata.json b/apps/configurable_clock/metadata.json index 28feae7e4..687a5b212 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.03", "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/contourclock/ChangeLog b/apps/contourclock/ChangeLog index 032edc9b5..d415a604d 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -6,3 +6,4 @@ 0.24: Added previews to the customizer. 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 diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index cdfadd217..d5c97edfa 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -7,6 +7,13 @@ if (settings.fontIndex==undefined) { require('Storage').writeJSON("myapp.json", settings); } +function queueDraw() { + setTimeout(function() { + draw(); + queueDraw(); + }, 60000 - (Date.now() % 60000)); +} + function draw() { var date = new Date(); // Draw day of the week @@ -24,7 +31,5 @@ Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); +queueDraw(); draw(); -setTimeout(function() { - setInterval(draw,60000); -}, 60000 - Date.now() % 60000); diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index ec29c390b..eb0dd39fb 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,7 +1,7 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.26", + "version":"0.27", "icon": "app.png", "description": "A Minimalist clockface with large Digits. Now with more fonts!", "screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}], diff --git a/apps/coretemp/ChangeLog b/apps/coretemp/ChangeLog index ad6f0742d..7386bbc35 100644 --- a/apps/coretemp/ChangeLog +++ b/apps/coretemp/ChangeLog @@ -1,3 +1,4 @@ 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 diff --git a/apps/coretemp/metadata.json b/apps/coretemp/metadata.json index cb12624ae..87cb42722 100644 --- a/apps/coretemp/metadata.json +++ b/apps/coretemp/metadata.json @@ -1,7 +1,7 @@ { "id": "coretemp", "name": "CoreTemp", - "version": "0.03", + "version": "0.04", "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..8402b3467 100644 --- a/apps/counter/ChangeLog +++ b/apps/counter/ChangeLog @@ -1,3 +1,4 @@ 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 diff --git a/apps/counter/counter.js b/apps/counter/counter.js index 3e0687944..0054ada6d 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('Swipe up to increase\nSwipe down to decrease\nPress button to reset.', x, 100 + y); +} else { + g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress BTN2 to reset.', x, 100 + y); +} Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/counter/metadata.json b/apps/counter/metadata.json index e455fda95..daba58d39 100644 --- a/apps/counter/metadata.json +++ b/apps/counter/metadata.json @@ -1,11 +1,11 @@ { "id": "counter", "name": "Counter", - "version": "0.03", + "version": "0.04", "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/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/daisy/ChangeLog b/apps/daisy/ChangeLog index d5844c62b..829ff3d13 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -4,3 +4,4 @@ 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 diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 5073db603..802ba6834 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.06", + "version":"0.07", "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..6397a81f4 100644 --- a/apps/daisy/settings.js +++ b/apps/daisy/settings.js @@ -41,7 +41,6 @@ }, 'Idle Warning': { value: !!s.idle_check, - format: v => v ? /*LANG*/"Yes":/*LANG*/"No", onchange: v => { s.idle_check = v; save(); diff --git a/apps/dane_tcr/ChangeLog b/apps/dane_tcr/ChangeLog index 4f6fe2edc..69424b1f4 100644 --- a/apps/dane_tcr/ChangeLog +++ b/apps/dane_tcr/ChangeLog @@ -4,4 +4,5 @@ 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 diff --git a/apps/dane_tcr/metadata.json b/apps/dane_tcr/metadata.json index 817d0c59b..5527c846d 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.08", "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..46988ec26 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,7 +50,6 @@ }, "Debug" : { value : settings.debug, - format : v => v?"On":"Off", onchange : saveChange('debug') }, '< Back': back diff --git a/apps/diceroll/ChangeLog b/apps/diceroll/ChangeLog new file mode 100644 index 000000000..89dff4011 --- /dev/null +++ b/apps/diceroll/ChangeLog @@ -0,0 +1 @@ +0.01: App created \ No newline at end of file diff --git a/apps/diceroll/app-icon.js b/apps/diceroll/app-icon.js new file mode 100644 index 000000000..4d6e7da16 --- /dev/null +++ b/apps/diceroll/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("ICABAAAAAAAAAAAAAAAAAAHAAAAP8AAAfn4AA/APwA+DwfAPg8HwD+AH8Az4HzAMPnwwDAfgMAwBgDAMCYAwDA2YMAwhmDAMIZAwDCGDMA2BgzAMgYAwDAGAMA8BgPADwYPAAPGPgAB9ngAAH/gAAAfgAAABgAAAAAAAAAAAAAAAAAA=")) diff --git a/apps/diceroll/app.js b/apps/diceroll/app.js new file mode 100644 index 000000000..d514ce92f --- /dev/null +++ b/apps/diceroll/app.js @@ -0,0 +1,108 @@ +var init_message = true; +var acc_data; +var die_roll = 1; +var selected_die = 0; +var roll = 0; +const dices = [4, 6, 10, 12, 20]; + +g.setFontAlign(0,0); + +Bangle.on('touch', function(button, xy) { + // Change die if not rolling + if(roll < 1){ + if(selected_die <= 3){ + selected_die++; + }else{ + selected_die = 0; + } + } + //Disable initial message + init_message = false; +}); + +function rect(){ + x1 = g.getWidth()/2 - 35; + x2 = g.getWidth()/2 + 35; + y1 = g.getHeight()/2 - 35; + y2 = g.getHeight()/2 + 35; + g.drawRect(x1, y1, x2, y2); +} + +function pentagon(){ + x1 = g.getWidth()/2; + y1 = g.getHeight()/2 - 50; + x2 = g.getWidth()/2 - 50; + y2 = g.getHeight()/2 - 10; + x3 = g.getWidth()/2 - 30; + y3 = g.getHeight()/2 + 30; + x4 = g.getWidth()/2 + 30; + y4 = g.getHeight()/2 + 30; + x5 = g.getWidth()/2 + 50; + y5 = g.getHeight()/2 - 10; + g.drawPoly([x1, y1, x2, y2, x3, y3, x4, y4, x5, y5], true); +} + +function triangle(){ + x1 = g.getWidth()/2; + y1 = g.getHeight()/2 - 57; + x2 = g.getWidth()/2 - 50; + y2 = g.getHeight()/2 + 23; + x3 = g.getWidth()/2 + 50; + y3 = g.getHeight()/2 + 23; + g.drawPoly([x1, y1, x2, y2, x3, y3], true); +} + +function drawDie(variant) { + if(variant == 1){ + //Rect, 6 + rect(); + }else if(variant == 3){ + //Pentagon, 12 + pentagon(); + }else{ + //Triangle, 4, 10, 20 + triangle(); + } +} + +function initMessage(){ + g.setFont("6x8", 2); + g.drawString("Dice-n-Roll", g.getWidth()/2, 20); + g.drawString("Shake to roll", g.getWidth()/2, 60); + g.drawString("Tap to change", g.getWidth()/2, 80); + g.drawString("Tap to start", g.getWidth()/2, 150); +} + +function rollDie(){ + acc_data = Bangle.getAccel(); + if(acc_data.diff > 0.3){ + roll = 3; + } + //Mange the die "roll" by chaning the number a few times + if(roll > 0){ + g.drawString("Rolling!", g.getWidth()/2, 150); + die_roll = Math.abs(E.hwRand()) % dices[selected_die] + 1; + roll--; + } + //Draw dice graphics + drawDie(selected_die); + //Draw dice number + g.setFontAlign(0,0); + g.setFont("Vector", 45); + g.drawString(die_roll, g.getWidth()/2, g.getHeight()/2); + //Draw selected die in right corner + g.setFont("6x8", 2); + g.drawString(dices[selected_die], g.getWidth()-15, 15); +} + +function main() { + g.clear(); + if(init_message){ + initMessage(); + }else{ + rollDie(); + } + Bangle.setLCDPower(1); +} + +var interval = setInterval(main, 300); \ No newline at end of file diff --git a/apps/diceroll/app.png b/apps/diceroll/app.png new file mode 100644 index 000000000..b695b7080 Binary files /dev/null and b/apps/diceroll/app.png differ diff --git a/apps/diceroll/diceroll_screenshot.png b/apps/diceroll/diceroll_screenshot.png new file mode 100644 index 000000000..71024edbb Binary files /dev/null and b/apps/diceroll/diceroll_screenshot.png differ diff --git a/apps/diceroll/metadata.json b/apps/diceroll/metadata.json new file mode 100644 index 000000000..81a2f8bfd --- /dev/null +++ b/apps/diceroll/metadata.json @@ -0,0 +1,14 @@ +{ "id": "diceroll", + "name": "Dice-n-Roll", + "shortName":"Dice-n-Roll", + "icon": "app.png", + "version":"0.01", + "description": "A dice app with a few different dice.", + "screenshots": [{"url":"diceroll_screenshot.png"}], + "tags": "game", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"diceroll.app.js","url":"app.js"}, + {"name":"diceroll.img","url":"app-icon.js","evaluate":true} + ] + } \ No newline at end of file diff --git a/apps/dinoClock/README.md b/apps/dinoClock/README.md new file mode 100644 index 000000000..7568731d9 --- /dev/null +++ b/apps/dinoClock/README.md @@ -0,0 +1,17 @@ +# dinoClock + +Watchface with T-Rex Dinosaur from Chrome. +It displays current temperature and weather. + +**Warning**: Element position and styles can change in the future. + +Based on the [Weather Clock](https://github.com/espruino/BangleApps/tree/master/apps/weatherClock). + +# Requirements + +**This clock requires Gadgetbridge and the weather app in order to get weather data!** + +See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather. + +![Screenshot](screens/screen1.png) + diff --git a/apps/dinoClock/app.js b/apps/dinoClock/app.js new file mode 100644 index 000000000..82192d234 --- /dev/null +++ b/apps/dinoClock/app.js @@ -0,0 +1,219 @@ +const storage = require('Storage'); +const locale = require("locale"); + + + + +// add modifiied 4x5 numeric font +(function(graphics) { + graphics.prototype.setFont4x5NumPretty = function() { + this.setFontCustom(atob("IQAQDJgH4/An4QXr0Fa/BwnwdrcH63BCHwfr8Ha/"),45,atob("AwIEBAQEBAQEBAQEBA=="),5); + }; +})(Graphics); + +// add font for days of the week +(function(graphics) { + graphics.prototype.setFontDoW = function() { + this.setFontCustom(atob("///////ADgB//////+AHAD//////gAAAH//////4D8B+A///////4AcAOAH//////4AcAOAAAAAB//////wA4AcAP//////wAAAAAAAA//////4AcAP//////wA4Af//////gAAAH//////5z85+c/OfnOAA4AcAOAH//////4AcAOAAAAAB//////wcAOAHB//////wAAAAAAAA///////ODnBzg5wc4AAAAD//////84OcH//8/+fAAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////84OcH//////AAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////gBwA///////AAAAAAAAAAAAA"),48,24,13); + }; +})(Graphics); + + +const SUN = 1; +const PART_SUN = 2; +const CLOUD = 3; +const SNOW = 4; +const RAIN = 5; +const STORM = 6; +const ERR = 7; + +/** +Choose weather icon based on weather const +Weather icons from https://icons8.com/icon/set/weather/ios-glyphs +Error icon from https://icons8.com/icon/set/error-cloud/ios-glyphs +**/ +function weatherIcon(weather) { + switch (weather) { + case SUN: + return atob("Hh4BAAAAAAAMAAAAMAAAAMAAAAMAABgMBgBwADgA4AHAAY/GAAB/gAAD/wAAH/4AAP/8AAP/8AfP/8+fP/8+AP/8AAP/8AAH/4AAD/wAAB/gAAY/GAA4AHABwADgBgMBgAAMAAAAMAAAAMAAAAMAAAAAAAA="); + case PART_SUN: + return atob("Hh4BAAAAAAAAAAAMAAAAMAAAEMIAAOAcAAGAYAAAeAAAA/AAAB/gAA5/gAA5/g+AB+D/gA4H/wAR//wGD//4OD//4EH//4AH//4Af//+Af//+A////A////A////A///+Af//+AH//4AAAAAAAAAAAAAAAA="); + case CLOUD: + return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB///wD///wD///wP///8f///+f///+////////////////////f///+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA="); + case SNOW: + return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf8/AA/8/AB/gHgH/wP4H/wP4P/gH8P/8/8P/8/8P///4H///4B///gAAAAAAMAAAAMAAAB/gGAA/AfgA/AfgB/gfgAMAfgAMAGAAAAAAAAAAAA="); + case RAIN: + return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H///4P///8P///8P///8P///4H///4B///gAAAAAAAAAABgBgABgBgABhhhgABgBgABgBgAAAAAAAAAAAAAAAAAAAAA="); + case STORM: + return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H/x/4P/g/8P/k/8P/E/8P/M/4H+MP4B+cHgAAfgAAA/gABg/AABgHAABgGBgAAGBgAAEBgAAEAAAAAAAAAAAAAAAAAA="); + case ERR: + default: + return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB/z/wD/z/wD/z/wP/z/8f/z/+f/z/+//z//////////////z//f/z/+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA="); + } +} + + +/** +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")) return weatherIcon(STORM); + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return weatherIcon(SNOW); + } + if (condition.includes("drizzle")|| + condition.includes("shower")) { + return weatherIcon(RAIN); + } + if (condition.includes("rain")) return weatherIcon(RAIN); + if (condition.includes("clear")) return weatherIcon(SUN); + if (condition.includes("few clouds")) return weatherIcon(PART_SUN); + if (condition.includes("scattered clouds")) return weatherIcon(CLOUD); + if (condition.includes("clouds")) return weatherIcon(CLOUD); + if (condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("ash") || + condition.includes("squalls") || + condition.includes("tornado")) { + return weatherIcon(CLOUD); + } + return weatherIcon(CLOUD); +} + +/* +* 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 weatherIcon(STORM); + case 3: return weatherIcon(RAIN); + case 5: return weatherIcon(RAIN); + case 6: return weatherIcon(SNOW); + case 7: return weatherIcon(CLOUD); + case 8: + switch (code) { + case 800: return weatherIcon(SUN); + case 801: return weatherIcon(PART_SUN); + default: return weatherIcon(CLOUD); + } + default: return weatherIcon(CLOUD); + } +} + +/** +Get weather stored in json file by weather app. +*/ +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} + +// 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)); +} + +// only draw the first time +function drawBg() { + var bgImg = require("heatshrink").decompress(atob("2E7wINKn///+AEaIVUgIUB//wCs/5CtRXrCvMD8AVTg4LFCv4VZ/iSLCrwWMCrMOAQMPCp7cBCojjFCo/xFgIVQgeHCopABCpcH44Vuh/AQQX/wAV7+F/Cq/nCsw/CCqyvRCvgODCqfAgEDCp4QCSIIVQgIOBDQgGDABX/NgIECCp8HCrM/CgP4CqKaCCqSfCCqq1BCqBuB54VqgYVG/gCECp0BwgCDCp8HgYCDCo/wCo0MgHAjACBj7rDABS1Bv4lBv4rPAAsPCo3+gbbPJAIVFiAXMFZ2AUQsAuAQHiOAgJeEA")); + g.reset(); + g.drawImage(bgImg,0,101); +} + +function square(x,y,w,e) { + g.setColor("#000").fillRect(x,y,x+w,y+w); + g.setColor("#fff").fillRect(x+e,y+e,x+w-e,y+w-e); +} + +function draw() { + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(); + h = ("0"+h).substr(-2); + m = ("0"+m).substr(-2); + + var day = d.getDate(), mon = d.getMonth(), dow = d.getDay(); + day = ("0"+day).substr(-2); + mon = ("0"+(mon+1)).substr(-2); + dow = ((dow+6)%7).toString(); + date = day+"."+mon; + + var weatherJson = getWeather(); + var wIcon; + var temp; + if(weatherJson && weatherJson.weather){ + var currentWeather = weatherJson.weather; + temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); + const code = currentWeather.code||-1; + if (code > 0) { + wIcon = chooseIconByCode(code); + } else { + wIcon = chooseIcon(currentWeather.txt); + } + }else{ + temp = ""; + wIcon = weatherIcon(ERR); + } + g.reset(); + g.clearRect(22,35,153,75); + g.setFont("4x5NumPretty",8); + g.fillRect(84,42,92,49); + g.fillRect(84,60,92,67); + g.drawString(h,22,35); + g.drawString(m,98,35); + + g.clearRect(22,95,22+4*2*4+2*4,95+2*5); + g.setFont("4x5NumPretty",2); + g.drawString(date,22,95); + + g.clearRect(22,79,22+24,79+13); + g.setFont("DoW"); + g.drawString(dow,22,79); + + g.drawImage(wIcon,126,81); + + g.clearRect(108,114,176,114+4*5); + if (temp != "") { + var tempWidth; + const mid=126+15; + if (temp[1][0]=="-") { + // do not account for - when aligning + const minusWidth=3*4; + tempWidth = minusWidth+(temp[1].length-1)*4*4; + x = mid-Math.round((tempWidth-minusWidth)/2)-minusWidth; + } else { + tempWidth = temp[1].length*4*4; + x = mid-Math.round(tempWidth/2); + } + g.setFont("4x5NumPretty",4); + g.drawString(temp[1],x,114); + square(x+tempWidth,114,6,2); + } + + // queue draw in one minute + queueDraw(); +} + +g.clear(); +drawBg(); +Bangle.setUI("clock"); // Show launcher when middle button pressed +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); + diff --git a/apps/dinoClock/app.png b/apps/dinoClock/app.png new file mode 100644 index 000000000..c05276ee3 Binary files /dev/null and b/apps/dinoClock/app.png differ diff --git a/apps/dinoClock/icon.js b/apps/dinoClock/icon.js new file mode 100644 index 000000000..2410dad14 --- /dev/null +++ b/apps/dinoClock/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AAVh/E/hgFC/O/AoMB8EZwc8AoUYgYFBgFgjAXDAowXBAo8B/ARBn4FGAAsBmAFE2ADBhwFEj4VEn+AgPvAontgfwv+ABIMCMwIVCgf4FIWAAoN3sAFCwERoEB0MHwF3gEF0MPwFEAoW/4ALD/4tCg/hAoYhB/5ZDwF+Aok0gEIkEf/4AB8eMBoM2bkw=")) diff --git a/apps/dinoClock/metadata.json b/apps/dinoClock/metadata.json new file mode 100644 index 000000000..a61ce122b --- /dev/null +++ b/apps/dinoClock/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "dinoClock", + "name": "Dino Clock", + "description": "Clock with dino from Chrome", + "screenshots": [{"url":"screens/screen1.png"}], + "icon": "app.png", + "version": "0.01", + "type": "clock", + "tags": "clock, weather, dino, trex, chrome", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"dinoClock.app.js","url":"app.js"}, + {"name":"dinoClock.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/dinoClock/screens/screen1.png b/apps/dinoClock/screens/screen1.png new file mode 100644 index 000000000..ca4386449 Binary files /dev/null and b/apps/dinoClock/screens/screen1.png differ diff --git a/apps/distortclk/ChangeLog b/apps/distortclk/ChangeLog new file mode 100644 index 000000000..4c7291526 --- /dev/null +++ b/apps/distortclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New face! +0.02: Improved clock 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..c375de96e --- /dev/null +++ b/apps/distortclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("j0ewkBiIAxHIQMJiBJEIxAaCAIQfHDgIUFDwwNCHYgVFiAVBHYgIDEghKCCIQGCFYoaDAYgORGIJ2DBwYIBHgQOPgAOIPIYOGAgQOFFgh7DHZQeDBwhoFQgh3JEAgOFFoqkHYRzgOfx4bCJ4gNGSIaJEABA7EAGA")) diff --git a/apps/distortclk/app.js b/apps/distortclk/app.js new file mode 100644 index 000000000..a9fdd1ef2 --- /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..125dac590 --- /dev/null +++ b/apps/distortclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "distortclk", + "name": "Distort Clock", + "shortName":"Distort Clock", + "version": "0.02", + "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/drinkcounter/ChangeLog b/apps/drinkcounter/ChangeLog new file mode 100644 index 000000000..d8d174c4c --- /dev/null +++ b/apps/drinkcounter/ChangeLog @@ -0,0 +1,4 @@ +0.10: Initial release - still work in progress +0.15: Added settings and calculations +0.20: Added status saving +0.25: Adopted for Bangle.js 1 - kind of \ No newline at end of file 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..323d9fb41 --- /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..2b8d7fe71 --- /dev/null +++ b/apps/drinkcounter/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "drinkcounter", + "name": "Drink Counter", + "shortName": "Drink Counter", + "version": "0.25", + "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..336229b73 --- /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/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 95952b9fe..16c550334 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -11,3 +11,6 @@ 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 - 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 diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md index bea20ef65..1835bc842 100644 --- a/apps/dtlaunch/README.md +++ b/apps/dtlaunch/README.md @@ -27,8 +27,8 @@ Bangle 2: ## Controls- Bangle 2 -**Touch** - icon to select, scond touch launches app +**Touch** - icon to select, second touch launches app -**Swipe Left** - move to next page of app icons +**Swipe Left/Up** - move to next page of app icons -**Swipe Right** - move to previous page of app icons +**Swipe Right/Down** - move to previous page of app icons diff --git a/apps/dtlaunch/app-b2.js b/apps/dtlaunch/app-b2.js index 8466a7414..8cd5790bb 100644 --- a/apps/dtlaunch/app-b2.js +++ b/apps/dtlaunch/app-b2.js @@ -89,22 +89,16 @@ function drawPage(p){ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{ selected = 0; oldselected=-1; - if(settings.swipeExit && dirLeftRight==1) showClock(); + if(settings.swipeExit && dirLeftRight==1) load(); if (dirUpDown==-1||dirLeftRight==-1){ ++page; if (page>maxPage) page=0; drawPage(page); - } else if (dirUpDown==1||dirLeftRight==1){ + } else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){ --page; if (page<0) page=maxPage; 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){ if (n<0 || n>3) return false; var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF; diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 7784972ca..36728f342 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.13", + "version": "0.16", "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..fac9c0fff 100644 --- a/apps/dtlaunch/settings-b2.js +++ b/apps/dtlaunch/settings-b2.js @@ -18,7 +18,6 @@ "< Back" : () => back(), 'Show clocks': { value: settings.showClocks, - format: v => v?"On":"Off", onchange: v => { settings.showClocks = v; writeSettings(); @@ -26,7 +25,6 @@ }, 'Show launchers': { value: settings.showLaunchers, - format: v => v?"On":"Off", onchange: v => { settings.showLaunchers = v; writeSettings(); @@ -34,7 +32,6 @@ }, 'Direct launch': { value: settings.direct, - format: v => v?"On":"Off", onchange: v => { settings.direct = v; writeSettings(); @@ -42,7 +39,6 @@ }, 'Swipe Exit': { value: settings.swipeExit, - format: v => v?"On":"Off", onchange: v => { settings.swipeExit = v; writeSettings(); @@ -50,7 +46,6 @@ }, 'One click exit': { value: settings.oneClickExit, - format: v => v?"On":"Off", onchange: v => { settings.oneClickExit = v; writeSettings(); 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/espruinoctrl/README.md b/apps/espruinoctrl/README.md index a7bca662c..7b2e434e7 100644 --- a/apps/espruinoctrl/README.md +++ b/apps/espruinoctrl/README.md @@ -17,7 +17,7 @@ showing available Espruino devices is popped up. device being connected to. 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 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/metadata.json b/apps/espruinoctrl/metadata.json index 5798c7842..5107bc6ae 100644 --- a/apps/espruinoctrl/metadata.json +++ b/apps/espruinoctrl/metadata.json @@ -5,8 +5,8 @@ "version": "0.01", "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..a12189707 --- /dev/null +++ b/apps/espruinoprog/custom.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + +

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..ebb55b23d --- /dev/null +++ b/apps/espruinoprog/metadata.json @@ -0,0 +1,17 @@ +{ + "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}, + {"name":"espruinoprog.json"} + ] +} diff --git a/apps/nato/changelog.txt b/apps/espruinoterm/ChangeLog similarity index 100% rename from apps/nato/changelog.txt rename to apps/espruinoterm/ChangeLog 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..348190db4 --- /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"); +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..660b3a86c --- /dev/null +++ b/apps/espruinoterm/interface.html @@ -0,0 +1,104 @@ + + + + + + + + +

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..25e6183e1 --- /dev/null +++ b/apps/espruinoterm/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "espruinoterm", + "name": "Espruino Terminal", + "shortName": "Espruino Term", + "version": "0.01", + "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/f9lander/ChangeLog b/apps/f9lander/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/f9lander/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/f9lander/README.md b/apps/f9lander/README.md new file mode 100644 index 000000000..16202f166 --- /dev/null +++ b/apps/f9lander/README.md @@ -0,0 +1,33 @@ +# F9 Lander + +Land a Falcon 9 booster on a drone ship. + +## Game play + +Attempt to land your Falcon 9 booster on a drone ship before running out of fuel. +A successful landing requires: + * setting down on the ship + * the booster has to be mostly vertical + * the landing speed cannot be too high + +## Controls + +The angle of the booster is controlled by tilting the watch side-to-side. The +throttle level is controlled by tilting the watch forward and back: + * screen horizontal (face up) means no throttle + * screen vertical corresponds to full throttle + +The fuel burn rate is proportional to the throttle level. + +## Creators +Liam Kl. B. + +Marko Kl. B. + +## Screenshots + +![](f9lander_screenshot1.png) + +![](f9lander_screenshot2.png) + +![](f9lander_screenshot3.png) diff --git a/apps/f9lander/app-icon.js b/apps/f9lander/app-icon.js new file mode 100644 index 000000000..572768a28 --- /dev/null +++ b/apps/f9lander/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcA/4AD/P8yVJkgCCye27dt2wRE//kCIuSuwRIBwgCCpwRQpIRRnYRQkmdCIvPCJICBEZ4RG/IRP/15CJ/z5IRPz4RM/gQB/n+BxICCn/z/P/BxQCDz7mIAX4Cq31/CJ+ebpiYE/IR/CNP/5IROnn//4jP5DFQ5sJCKAjPk3oCMMk4QRQAX4Ckn7jBAA/5CK8nCJPJNHA")) diff --git a/apps/f9lander/app.js b/apps/f9lander/app.js new file mode 100644 index 000000000..7e52104c0 --- /dev/null +++ b/apps/f9lander/app.js @@ -0,0 +1,150 @@ +const falcon9 = Graphics.createImage(` + xxxxx + xxxxx xxxxx + x x + x x + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxx + xxxxxxxxx + xx xxxxx xx +xx xx`); + +const droneShip = Graphics.createImage(` +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +`); + +const droneX = Math.floor(Math.random()*(g.getWidth()-droneShip.width-40) + 20) +const cloudOffs = Math.floor(Math.random()*g.getWidth()/2); + +const oceanHeight = g.getHeight()*0.1; + +const targetY = g.getHeight()-oceanHeight-falcon9.height/2; + +var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2, + y : 20, + vx : 0, + vy : 0, + mass : 100, + fuel : 100 }; + +var exploded = false; +var nExplosions = 0; +var landed = false; + +const gravity = 4; +const dt = 0.1; +const fuelBurnRate = 20*(176/g.getHeight()); +const maxV = 12; + +function flameImageGen (throttle) { + var str = " xxx \n xxx \n"; + str += "xxxxx\n".repeat(throttle); + str += " xxx \n x \n"; + return Graphics.createImage(str); +} + +function drawFalcon(x, y, throttle, angle) { + g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle}); + if (throttle>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}); + } +} + +function drawBG() { + 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); + g.setColor(1, 1, 0).fillCircle(g.getWidth(), 0, 20); + g.setColor(1, 1, 1).drawImage(droneShip, droneX, g.getHeight()-oceanHeight-1); +} + +function showFuel() { + g.setColor(0, 0, 0).setFont("4x6:2").setFontAlign(-1, -1, 0).drawString("Fuel: "+Math.abs(booster.fuel).toFixed(0), 4, 4); +} + +function renderScreen(input) { + drawBG(); + showFuel(); + drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle); +} + +function getInputs() { + var accel = Bangle.getAccel(); + var a = Math.PI/2 + Math.atan2(accel.y, accel.x); + var t = (1+accel.z); + if (t > 1) t = 1; + if (t < 0) t = 0; + if (booster.fuel<=0) t = 0; + return {throttle: t, angle: a}; +} + +function epilogue(str) { + g.setFont("Vector", 24).setFontAlign(0, 0, 0).setColor(0, 0, 0).drawString(str, g.getWidth()/2, g.getHeight()/2).flip(); + g.setFont("Vector", 16).drawString("<= again exit =>", g.getWidth()/2, g.getHeight()/2+20); + clearInterval(stepInterval); + Bangle.on("swipe", (d) => { if (d>0) load(); else load('f9lander.app.js'); }); +} + +function gameStep() { + if (exploded) { + if (nExplosions++ < 15) { + var r = Math.random()*25; + var x = Math.random()*30 - 15; + var y = Math.random()*30 - 15; + g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r); + if (nExplosions==1) Bangle.buzz(600); + } + else epilogue("You crashed!"); + } + 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) { + stepInterval = setInterval(gameStep, Math.floor(1000*dt)); + Bangle.removeListener("swipe"); +}); diff --git a/apps/f9lander/f9lander.png b/apps/f9lander/f9lander.png new file mode 100644 index 000000000..f03cc1645 Binary files /dev/null and b/apps/f9lander/f9lander.png differ diff --git a/apps/f9lander/f9lander_screenshot1.png b/apps/f9lander/f9lander_screenshot1.png new file mode 100644 index 000000000..ea7d8a834 Binary files /dev/null and b/apps/f9lander/f9lander_screenshot1.png differ diff --git a/apps/f9lander/f9lander_screenshot2.png b/apps/f9lander/f9lander_screenshot2.png new file mode 100644 index 000000000..a2f13d6c7 Binary files /dev/null and b/apps/f9lander/f9lander_screenshot2.png differ diff --git a/apps/f9lander/f9lander_screenshot3.png b/apps/f9lander/f9lander_screenshot3.png new file mode 100644 index 000000000..61b8be82f Binary files /dev/null and b/apps/f9lander/f9lander_screenshot3.png differ diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json new file mode 100644 index 000000000..75c6a0164 --- /dev/null +++ b/apps/f9lander/metadata.json @@ -0,0 +1,15 @@ +{ "id": "f9lander", + "name": "Falcon9 Lander", + "shortName":"F9lander", + "version":"0.01", + "description": "Land a rocket booster", + "icon": "f9lander.png", + "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], + "readme": "README.md", + "tags": "game", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"f9lander.app.js","url":"app.js"}, + {"name":"f9lander.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/fclock/ChangeLog b/apps/fclock/ChangeLog index 30e049f69..7e7307c59 100644 --- a/apps/fclock/ChangeLog +++ b/apps/fclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: First published version of app 0.02: Move to Bangle.setUI to launcher support +0.03: Tell clock widgets to hide. diff --git a/apps/fclock/fclock.app.js b/apps/fclock/fclock.app.js index afa0c5e2d..838a5578d 100644 --- a/apps/fclock/fclock.app.js +++ b/apps/fclock/fclock.app.js @@ -173,6 +173,9 @@ const drawHR = function () { } }; +// Show launcher when button pressed +Bangle.setUI("clock"); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -198,6 +201,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..dffb197a2 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.03", "description": "Simple design of a digital clock", "icon": "app.png", "type": "clock", diff --git a/apps/ffcniftya/ChangeLog b/apps/ffcniftya/ChangeLog index 420c553f5..6d2f50119 100644 --- a/apps/ffcniftya/ChangeLog +++ b/apps/ffcniftya/ChangeLog @@ -1,2 +1,6 @@ 0.01: New Clock Nifty A -0.02: Shows the current week number (ISO8601), can be disabled via settings "" +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/README.md b/apps/ffcniftya/README.md index 86f1f5c2d..80005fd3c 100644 --- a/apps/ffcniftya/README.md +++ b/apps/ffcniftya/README.md @@ -1,13 +1,12 @@ # Nifty-A Clock -Colors are black/white - photos have non correct camera color "blue" +Colors are black/white - photos have non correct camera color "blue". -## This is the clock +This is the clock: ![](screenshot_nifty.png) -## The week number (ISO8601) can be turned of in settings -(default is **"On"**) +The week number (ISO8601) can be turned off in settings (default is `On`) ![](screenshot_settings_nifty.png) diff --git a/apps/ffcniftya/app.js b/apps/ffcniftya/app.js index 5da1ec48e..2c1a54f6e 100644 --- a/apps/ffcniftya/app.js +++ b/apps/ffcniftya/app.js @@ -1,111 +1,59 @@ -const locale = require("locale"); -const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; -const CFG = require('Storage').readJSON("ffcniftya.json", 1) || {showWeekNum: true}; - -/* 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), -} - -function ISO8601_week_no(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480 - var tdt = new Date(date.valueOf()); - var dayn = (date.getDay() + 6) % 7; - tdt.setDate(tdt.getDate() - dayn + 3); - var firstThursday = tdt.valueOf(); - tdt.setMonth(0, 1); - if (tdt.getDay() !== 4) { - tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7); - } - return 1 + Math.ceil((firstThursday - tdt) / 604800000); -} - -function d02(value) { - return ('0' + value).substr(-2); -} - -function draw() { - g.reset(); - g.clearRect(0, widget, viewport.width, viewport.height); - const now = new Date(); - - 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); - - 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); - - g.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); - - 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 (CFG.showWeekNum) g.drawString(d02(ISO8601_week_no(now)), centerDatesScaleX, center.y + 15 * scale); - g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale); - g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale); -} - - -/* Minute Ticker *************************************/ - -let tickTimer; - -function clearTickTimer() { - if (tickTimer) { - clearTimeout(tickTimer); - tickTimer = undefined; +// 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 queueNextTick() { - clearTickTimer(); - tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000)); - // tickTimer = setTimeout(tick, 3000); +function format(value) { + return ("0" + value).substr(-2); } -function tick() { - draw(); - queueNextTick(); -} +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + init: function () { + const appRect = Bangle.appRect; -/* Init **********************************************/ + this.viewport = { + width: appRect.w, + height: appRect.h + }; -// Clear the screen once, at startup -g.clear(); -// Start ticking -tick(); + this.center = { + x: this.viewport.width / 2, + y: Math.round((this.viewport.height / 2) + appRect.y) + }; -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower', (on) => { - if (on) { - tick(); // Start ticking - } else { - clearTickTimer(); // stop ticking - } + 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); + + 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(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" }); - -// Load widgets -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -// Show launcher when middle button pressed -Bangle.setUI("clock"); \ No newline at end of file +clock.start(); \ No newline at end of file diff --git a/apps/ffcniftya/metadata.json b/apps/ffcniftya/metadata.json index ce91cc225..015c56119 100644 --- a/apps/ffcniftya/metadata.json +++ b/apps/ffcniftya/metadata.json @@ -1,7 +1,7 @@ { "id": "ffcniftya", "name": "Nifty-A Clock", - "version": "0.02", + "version": "0.04", "description": "A nifty clock with time and date", "icon": "app.png", "screenshots": [{"url":"screenshot_nifty.png"}], diff --git a/apps/ffcniftya/settings.js b/apps/ffcniftya/settings.js index 46e4ef5aa..aec1d680a 100644 --- a/apps/ffcniftya/settings.js +++ b/apps/ffcniftya/settings.js @@ -1,23 +1,15 @@ -(function(back) { - var FILE = "ffcniftya.json"; - // Load settings - var cfg = require('Storage').readJSON(FILE, 1) || { showWeekNum: true }; +(function (back) { + const settings = Object.assign({ showWeekNum: true }, require("Storage").readJSON("ffcniftya.json", true)); - function writeSettings() { - require('Storage').writeJSON(FILE, cfg); - } - - // Show the menu E.showMenu({ - "" : { "title" : "Nifty-A Clock" }, - "< Back" : () => back(), - 'week number?': { - value: cfg.showWeekNum, - format: v => v?"On":"Off", + "": { "title": "Nifty-A Clock" }, + "< Back": () => back(), + /*LANG*/"Show Week Number": { + value: settings.showWeekNum, onchange: v => { - cfg.showWeekNum = v; - writeSettings(); + settings.showWeekNum = v; + require("Storage").writeJSON("ffcniftya.json", settings); } } }); -}) \ No newline at end of file +}) diff --git a/apps/ffcniftyb/ChangeLog b/apps/ffcniftyb/ChangeLog index dedd31452..83b11eb78 100644 --- a/apps/ffcniftyb/ChangeLog +++ b/apps/ffcniftyb/ChangeLog @@ -1,2 +1,6 @@ 0.01: New Clock Nifty B -0.02: Added configuration \ No newline at end of file +0.02: Added configuration +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/README.md b/apps/ffcniftyb/README.md index e04243a0b..072f71cce 100644 --- a/apps/ffcniftyb/README.md +++ b/apps/ffcniftyb/README.md @@ -1,9 +1,6 @@ # Nifty Series B Clock - Display Time and Date -- Color Configuration - -## +- Colour Configuration ![](screenshot.png) - diff --git a/apps/ffcniftyb/app.js b/apps/ffcniftyb/app.js index 75d217ab4..540924fa5 100644 --- a/apps/ffcniftyb/app.js +++ b/apps/ffcniftyb/app.js @@ -1,118 +1,80 @@ -const locale = require("locale"); -const storage = require('Storage'); +var scale; +var screen; +var center; +var buf; +var img; -const is12Hour = (storage.readJSON("setting.json", 1) || {})["12hour"]; -const color = (storage.readJSON("ffcniftyb.json", 1) || {})["color"] || 63488 /* red */; - - -/* 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) { - return ('0' + value).substr(-2); +function format(value) { + return ("0" + value).substr(-2); } 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 = locale.month(now, 3); - const day2 = 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)); - // ticker = setTimeout(() => startTick(run), 3000); -} - -/* Init **********************************************/ - -g.clear(); -startTick(draw); - -Bangle.on('lcdPower', (on) => { - if (on) { - startTick(draw); - } else { - stopTick(); - } -}); - -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -Bangle.setUI("clock"); +clock.start(); \ No newline at end of file diff --git a/apps/ffcniftyb/metadata.json b/apps/ffcniftyb/metadata.json index 73f93ed36..019ae6eb3 100644 --- a/apps/ffcniftyb/metadata.json +++ b/apps/ffcniftyb/metadata.json @@ -1,8 +1,8 @@ { "id": "ffcniftyb", "name": "Nifty-B Clock", - "version": "0.02", - "description": "A nifty clock (series B) with time, date and color configuration", + "version": "0.04", + "description": "A nifty clock (series B) with time, date and colour configuration", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", diff --git a/apps/ffcniftyb/settings.js b/apps/ffcniftyb/settings.js index 00abf80b5..da350edd8 100644 --- a/apps/ffcniftyb/settings.js +++ b/apps/ffcniftyb/settings.js @@ -1,49 +1,31 @@ (function (back) { - const storage = require('Storage'); - const SETTINGS_FILE = "ffcniftyb.json"; + const settings = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true)); const colors = { - 65535: 'White', - 63488: 'Red', - 65504: 'Yellow', - 2047: 'Cyan', - 2016: 'Green', - 31: 'Blue', - 0: 'Black', + 65535: /*LANG*/"White", + 63488: /*LANG*/"Red", + 65504: /*LANG*/"Yellow", + 2047: /*LANG*/"Cyan", + 2016: /*LANG*/"Green", + 31: /*LANG*/"Blue", + 0: /*LANG*/"Black" } - function load(settings) { - return Object.assign(settings, storage.readJSON(SETTINGS_FILE, 1) || {}); - } + const menu = {}; + menu[""] = { title: "Nifty-B Clock" }; + menu["< Back"] = back; - function save(settings) { - storage.write(SETTINGS_FILE, settings) - } - - const settings = load({ - color: 63488 /* red */, + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings.color == color, + onchange: () => { + settings.color = color; + require("Storage").write("ffcniftyb.json", settings); + setTimeout(load, 10); + } + }; }); - const saveColor = (color) => () => { - settings.color = color; - save(settings); - back(); - }; - - function showMenu(items, opt) { - items[''] = opt || {}; - items['< Back'] = back; - E.showMenu(items); - } - - showMenu( - Object.keys(colors).reduce((menu, color) => { - menu[colors[color]] = saveColor(color); - return menu; - }, {}), - { - title: 'Color', - selected: Object.keys(colors).indexOf(settings.color) - } - ); + E.showMenu(menu); }); diff --git a/apps/files/files.js b/apps/files/files.js index e7b42c101..e81e9589f 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -1,7 +1,5 @@ const store = require('Storage'); -const boolFormat = (v) => v ? "On" : "Off"; - function showMainMenu() { const mainmenu = { '': { 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/football/ChangeLog b/apps/football/ChangeLog new file mode 100644 index 000000000..66b9882cc --- /dev/null +++ b/apps/football/ChangeLog @@ -0,0 +1,2 @@ +1.00: Initial implementation +1.01: Bug fixes and performance and visual improvements diff --git a/apps/football/README.md b/apps/football/README.md new file mode 100644 index 000000000..f751b927e --- /dev/null +++ b/apps/football/README.md @@ -0,0 +1,3 @@ +# Classic Football Chronometer Game + +Context: https://www.anaitgames.com/analisis/analisis-casio-football-14 diff --git a/apps/football/app-icon.js b/apps/football/app-icon.js new file mode 100644 index 000000000..7eec578c6 --- /dev/null +++ b/apps/football/app-icon.js @@ -0,0 +1 @@ +atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAA/wAAAAAADwAAAAAADwAAAAAADwAAAAAADwAAAAAA//AAAAAA//AAAAAA//AAAAAA//AAAAAADw8AD/AADw8AD/AADw8AD/AADw8AD/AP/wAAD/AP/wAAD/AP/wAAD/AP/wAAD/AA8PAAAAAA8PAAAAAA8PAAAAAA8PAAAAAAAA8AAAAAAA8AAAAAAA8AAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") diff --git a/apps/football/app.js b/apps/football/app.js new file mode 100644 index 000000000..d12f07e2b --- /dev/null +++ b/apps/football/app.js @@ -0,0 +1,474 @@ +// globals. TODO: move into an object +const digit = []; +let part = 0; +let endInc = 0; +var endGame = false; +let goalFrame = 0; +var stopped = true; +let score0 = 0; +let score1 = 0; +let inc = 0; +let msinc = 0; +let seq0 = 0; +let seq1 = 0; +let goaler = -1; +const w = g.getWidth(); +let owner = -1; + +const dash = { + width: 75, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4A/AH4A/AH4A/AH4A/AB0D/4AB+AJEBAX/BAk/CQ8PCQ4kDCQoIDCQgkDCQgkECQgIE4ASHFxH8JRgSEEgYSEPJAkEAH4A/AH4A/AH4A/AH4A/ACg=')) +}; + +function loadDigits () { + digit[0] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4AGn//AAngBIMfBIvABIMPBIuABIMHBIoIBg0DBAn+gYSBgIJE/kHBIOABIn4h4JB4F/BIfwj4JB8BQEAoIJBBoJOEv4JBEIJOEIwMHGoIJDIIIJBJIJOEBIQOCJwYJDOIR9DBISFCSIYJCTISlDBIXwBIZoBBP4J/BP4J/BNX+gED//gBIc/BIMB//ABIcf/gDB/+ABIcP/AhCBAYuBFoU+BIkDFoUcBIkBFoUIBIkAFogA/AAZPJMZJ3JRZKfIWZLHJbZL5bBP4J/BP4J/BKPgBIc/BIfABIcfBIeA/4AB/EPBIcBBIX8AwIJB/0DBIQECBIIOCAAQYBBIIiCAAQsBBIPwGwIAC4F/BIPgJQIACAoIJBBoIJDDIIJBJwZQDBIJODKAcAgxODKAZxBJwgABPYROEKASFDAAiRCJwhQCTYYAkA')) + }; + + digit[1] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4A2wAIHgIJIgYJIg4JIh4JIj4JIn4JIv4JI/4JHgIJIgYJIg4JIh4JIj4JIn4J/BP4J/BP4Jqj//BA0Ah/+BI8H/gJHgf4BI8B+AJHgHgBJFABJAA/J55jIO5KLJT5KzJY5L5nBP4J/BP4J/AAcfBJEPBJEHBJEDBJEBBJEABJN/BJE/BJEfBJEPBJEHBJEDBJEBBJEABJIA/AAwA=')) + }; + + digit[2] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4AGj//AAnwBIMPBIvgBIMHBIvABIMDBIuABIMBBIsBGQQIE/0DBIV/BIf8g4JCn4JD/EPKA/wj4JCKAngn4JCKAnAv4JCKAmA/4JCKAgEBQYZOEBIgADFgIJHIAKlJBI5oBBP4J/BP4J/BOcfBJEP/wJHg/8Aof/AAP+gf4BAUBBIX/gPwBIUDBIeA8AiDBIfAoA2DBIYSDJQQACEwZeCAAQ6DgF/BJATJE5I7IghPFBIUOMYomDO4g6EwCLDJwgiDAAhyFTohKEToheEBP4J/BP4J/BOHwBJHgBJHAv////8BImABAP//wJEAIIACBIf+BImABIX8g4JD4AJC/EPBIZACgfwj4JDKgUD8E/BIZoCgZODKAkDJwZQEgcBBIhQCgROEKAhOEKAhOEKAhOEKAgAm')) + }; + + digit[3] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4AGj//AAnwBIMPBIvgBIMHBIvABIMDBIuABIMBBIsBGQQIE/0DBIV/BIf8g4JCn4JD/EPKA/wj4JCKAngn4JCKAnAv4JCKAmA/4JCKAgEBQYZOEBIgADFgIJHIAKlJBI5oBBI58BBP4J/BP4J/BL8/BJEf/wJHh/8BI8H/AFD/4AB/0D+AICgIJDgPgBIUDBIQ5B4AiDBIeAwA2DBIYSDJQIJDEwZeCAAQ6DOQQACJwgTJE5I7JJ5JjEgIUDO4kDFAgJC/kDIwipNj4JIn7HIbZL5TBP4J/BP4J/BJs/BJEfBJEPBIgjB//8g4JDgIIB//+gYJDAgIACBwIJCDAIACwAJDFgIAC4F/IAgAC8E/KggAC+EfIgoAB/EPBIQIDKAROFKAZOGKAROGKAQJI4BOGKAQJI+CfHAEAA==')) + }; + digit[4] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AH4AswEBBJOABAoHBgPABIsDBIPgBIsHBIPwBIsPBIP4BIsfBIP8BIs/BIP+BIt/BIP/BIv/BIRQEAwQCBKAkDBIZQEg4JDKAkPBIZQEj4JIn4J/BP4J/BP4JjgAJFj//AYN/8AJDh/+C4QJEg/8C4XAv////+gYjCh+ABAIABgPwC4Q9BAAWAEYUCgYJD4FAFgYJDIAoJDEwRUDAARoGAAROCCZYnJHZJPJMZAABO46hCRYwAFT4YAFWYYAFY4YAFbYYJ/BP4J/BP4Jnj4JIh4JIg4JIgYJIgIJIgAJJv4JIn4JIj4JIh4JIg4JIgYJIgIJIgAJJAH4AGA=')) + }; + + digit[5] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AGUP/4AE/gJBg4JF/AJBgYJF+AJBgIJF8AJBwAJF4AJB4F/BImABIPgn4JEIoXwj4ID/wJC/BQEJwUA/hQEJwUA/xQEJwUA/5QEJwQJBKAhOCBIJQEJwQJBKAiVDFggAEIAgJFKgYJFNAYJ/BP4J/BP4Jmv/8BI8//AJHj/wBI8P8E//4ABBIcH4F/BIWABIUDwAIC//ABIUBgIJD8AeDgYJDGwkHBIZKEh4JDLwkfBIZyECZInJHZJPFkChEMYdwUIh3DFAiLDgKvIgbDIJQKvIUIgJFUIZ8FBP4J/BP4J/BL8PBJEHBJEDBJEBBIl//4ABwAJEBAQHBv4JCDAIAC8E/BIQsBAAXwj4JCIAIAC/EPBIRUBAAX8g4JCNAIAC/0DBI//gIJCJwZQCBIQIEKANAJwpQCJwxQCJwxQCJwxQCBJYAwA=')) + }; + + digit[6] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AGU//4AE8AJBj4JF4AJBh4JFwAJBg4JFBAMGgYIE/wSCgIJE/gJCwAJE/AJC4F/BIfwBIXgKAhOCg/wKAhOCg/4KAhOD/hQEOwUH/xQDJwRiCKAZOCBIRQDJwQJCGwQAEBIJKCBIxeCBP4J/BP4J/BOED//gBI0B//ABI0A/+ABI9/CoIAB/gJDnwpBAAP+BIccHoIACBIcIh4JDFgkfBIZAEBIhUEv4JDNAgJE/ATNn4nIHZBPGKARjFgIUCO4sDFASLFg48COQsPKARyGUILHGn6hBBIJyGco4J/BP4J/BP4Jm8AJDn4JD4AJDj4JDwAJDh4JDgP/AAP8AwIJB/0DBIQECBIIOCAAQYBBIP4EQIACwAJC+A2BAAXAv4JB8BKBAAQFBBIINBBIYZBBIIhBAAYtBBIJODKAcAgxODKAZnBJwhQCOIYAEPoROEKASZDAAilCJwhQCTYYAuA==')) + }; + + digit[7] = { + width: 80, + height: 128, + bpp: 4, + buffer : require("heatshrink").decompress(atob("AH4A/AEtVADdQE5Nf/4AayAnJgoma/J4LKDR2KKDZODUMadChKhiJwefE5RQXJwbxLKCxOEE5hQVJwgnNKCZOFE5pQTJwonOKCJOGE5xQRD4Q8EE5xQPJw4nPgFZzIAMqCdFE6IARJwgnhJwonhJwonhe5In/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4nlr4mE/NQE78FE4n1Ez5QGE0JQEJ0RQETsBQFJ0gABrJOkAH4A/AH4A/ADNZqAmkgv/yAnkr///JQjJwIABypOkAAP5J0oABUMJODKAShgEwhQiE/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/E/4n/AA+fE80JE8xQGE8JQFE8JQFE8RQEE8RQEE8ZQDE8ZQDE8hQCE8hQCE8pQBE8pQBE80JE80AE84A/AH4A/AH4A/AAQA==")) + }; + + digit[8] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AGUf/4AE+AJBh4JF8AJBg4JF4AJBgYJFwAIBgIJFgOAgeABAn+A4MD4F/BIf8g4JB8E/BIf4h4JB+BQEAoIJBBoJOEn4JBEIJOEv4JBGoJOEAIIHBKAgEBBIRQDDAQJCKAYsCBISFCSIYJCTISlDBIX4BIZoBBP4J/BP4J/BNkB//wBIcf/4DB//gBIcP/wDBv/ABIcH/ghCwH/AAP+gYtCj4pBAAUBFoUOHoIACwAtCgkHBIfAoA2DBIZAEJQIACKgheBAARoGBKInJHZBPJMZJ3JRZKfJWZDHJfM4J/BP4J/BP4JP+AJDj4JD8AJDh4JD4AJDg4JDwH/AAP+AwQCBgIJCAgQJBBwQACDAIJB/giBAAXAv4JB/A2BAAXgn4JB+BKBAAQFBBIINBBIYZBBIIhBBIYtBBIJODKAYJBJwhQCwECJwhQCwBxCAAh9CJwhQCTIYAEUoROEKASbDAFwA=')) + }; + + digit[9] = { + width: 80, + height: 128, + bpp: 1, + buffer: require('heatshrink').decompress(atob('AGUP/4AE/gJBg4JF/AJBgYJF+AJBgIJF8AJBwAJF4FAgHAv4JEwHAgHgn4JEgIJB+EfBAf+gYJB/BQE/kHBIIDBJwkPBIIXBJwkfBIIrBJwk/BIRQEJYIJCKAgOBBIXgIwYiBBIR7CQ4YJCR4SbDBISjCV4YJC/wJDFYIJ/BP4J/BP4Jjv/8BIcP/+AgE//AJDg//C4XwBIcDEYUP8E//4ABgIjCg/Av4JCwAjCgeABAQ5BuAJBgMBBIfgkAsDBIY2EIAIACJQhUBAAReENAIACOQgTJE5I7JJ5KhBMYwABO44ABRY4AFT4YAFWYYAFY4YAFbYYJ/BP4J/BP4Jnh4JIg4JIgYJIgIJEv//AAOABIgICA4N/BIQYBAAXgn4JCFgIAC+EfBIRABAAX4h4JCKgIAC/kHBIRoBAAX+gYJCn4JD/8BBIRODKAQJCBAhQBoBOFKAROGKAROGKAROGKAQJLAGA=')) + }; +} + +// sprites + +const left0 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('ADAwEDgUcCgEAA==') +}; + +const left1 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('ADAwEDh0ECAoAA==') +}; + +const left2 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('ADAwEBg4EBg0AA==') +}; + +const left4 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('ABgYVDgQEChEAA==') +}; + +const right0 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('AAwMCBwoDhQgAA==') +}; + +const right1 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('AAwMCBwuCAQUAA==') +}; + +const right2 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('AAwMCBgcCBgsAA==') +}; + +const right4 = left4; + +const ball0 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('AAAAAAAwMAAAAA==') +}; + +const ball1 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('AAAAAAAAGBgAAA==') +}; +const gol01 = { + width: 8, + height: 10, + bpp: 1, + transparent: 0, + buffer: atob('ABhkhIS0sIAAAA==') +}; + +const gol11 = { + width: 8, + height: 10, + bpp: 1, + buffer: require('heatshrink').decompress(atob('gEYk0hkMthsBBAI=')) + +}; + +loadDigits(); + + +function printNumber (n, x, y, options) { + if (n > 9 || n < -1) { + console.log(n); + return; // error + } + if (digit.length === 0) { + g.setColor(1, 1, 1); + if (options.scale == 0.2) { + g.setFont12x20(1); + } else { + g.setFont12x20(2.5); + } + g.drawString('' + n, x, y); + return; + } + const img = (n == -1) ? dash : digit[n]; + if (img) { + // g.setColor(0,0,0); + // g.fillRect(x,y,x+32*options.scale,64*options.scale); + g.setColor(1, 1, 1); + g.drawImage(img, x, y, options); + } +} + +g.setBgColor(0, 0, 0); +g.clear(); +g.setColor(1, 1, 1); +function onStop () { + if (goalFrame > 0) { + return; + } + stopped = !stopped; + if (stopped) { + // Bangle.beep(); + if (msinc == 0) { + // GOOL + if (owner == 0) { + score0++; + goaler = 0; + } else if (owner == 1) { + score1++; + goaler = 1; + } + goalFrame = 5; + } + let newOwner = 0; + if (msinc % 2) { + newOwner = 1; + } else { + newOwner = 0; + } + if (newOwner) { + seq0--; + seq1++; + } else { + seq0++; + seq1--; + } + if (seq0 < 0) seq0 = 0; + if (seq0 > 3) seq0 = 3; + if (seq1 < 0) seq1 = 0; + if (seq1 > 3) seq1 = 3; + owner = newOwner; + } + refresh(); + refresh_ms(); +} + +function onButtonPress() { + console.log('on.tap'); + setWatch(() => { + onButtonPress(); +}, BTN1); + Bangle.beep(); + if (endGame) { + score0 = 0; + score1 = 0; + seq0 = 0; + seq1 = 0; + part = 0; + inc = 0; + msinc = 0; + stopped = true; + endGame = false; + } else { + if (inc == 0) { + // autogame(); + stopped = false; + } else { + onStop(); + } + } +} + +setWatch(() => { + onButtonPress(); +}, BTN1); +/*Bangle.on('tap', function () { + onButtonPress(); +}); +*/ +g.setFont12x20(3); + +function refresh () { + g.clear(); + if (inc > 59) { + inc = 0; + part++; + } + if (part >= 2 && inc > 30) { + part = 2; + Bangle.buzz(); + endGame = true; + endInc = inc; + inc = 0; + } + if (inc > 44) { + inc = 0; + if (part < 2) { + part++; + } + if (part >= 2) { + if (score0 != score1) { + Bangle.buzz(); + endGame = true; + endInc = inc; + inc = 0; + } + } + // end of 1st or 2nd part of the game? + } + let two = (inc < 10) ? '0' + inc : '' + inc; + if (endGame) { + g.setColor(0, 0, 0); + g.fillRect(0, 64, w, h); + if (inc % 2) { + two = (endInc < 10) ? '0' + endInc : '' + endInc; + printNumber(-1, 2, 64 + 16, { scale: 0.4 }); + printNumber(part, 34, 64 + 16, { scale: 0.4 }); + printNumber(two[0], 74, 64 + 16, { scale: 0.4 }); + printNumber(two[1], 74 + 32, 64 + 16, { scale: 0.4 }); + } + } else { + // seconds + printNumber(0, 2, 64 + 16, { scale: 0.4 }); + printNumber(part, 34, 64 + 16, { scale: 0.4 }); + printNumber(two[0], 74, 64 + 16, { scale: 0.4 }); + printNumber(two[1], 74 + 32, 64 + 16, { scale: 0.4 }); + } + refresh_ms(); + refresh_score(); + refresh_pixels(); +} + +function refresh_pixels () { + let frame4 = inc % 2; + if (goalFrame > 0) { + frame4 = goalFrame % 2; + if (goaler == 0) { + g.drawImage(frame4 ? right4 : right0, 20, 10, { scale: 5 }); + g.setColor(1, 1, 1); + g.drawImage(gol11, w - 50, 10, { scale: 5 }); + } else if (goaler == 1) { + g.drawImage(frame4 ? left0 : left4, w - 50, 10, { scale: 5 }); + g.setColor(1, 1, 1); + g.drawImage(gol01, 30, 10, { scale: 5 }); + } + return; + } + if (endGame) { + if (score0 > score1) { + g.drawImage(frame4 ? right1 : right0, 5 + (seq0 * 10), 10, { scale: 5 }); + } else if (score0 < score1) { + g.drawImage(frame4 ? left0 : left1, w - 30 - (seq1 * 10), 10, { scale: 5 }); + } + return; + } + g.drawImage(frame4 ? right1 : right0, 5 + (seq0 * 10), 10, { scale: 5 }); + g.drawImage(frame4 ? left0 : left1, w - 30 - (seq1 * 10), 10, { scale: 5 }); + let bx = (owner == 0) ? w / 3 : w / 2; + bx += 2; + g.drawImage(frame4 ? ball0 : ball1, bx, 10, { scale: 5 }); + const liney = 60; + if (owner) { + g.drawLine(w-8, liney, 2*(w/3), liney); + } else { + g.drawLine(8, liney, w/3, liney); + } +} +let dots = 0; +function refresh_dots () { + if (endGame) { + dots = 0; + } else { + dots++; + } + if (dots % 2) { + g.setColor(1, 1, 1); + } else { + g.setColor(0, 0, 0); + } + const x = 67; + let y = 100; + g.fillRect(x, y, x + 4, y + 4); + y += 16; + g.fillRect(x, y, x + 4, y + 4); +} + +const h = g.getHeight(); + +function refresh_ms () { + if (endGame) { + if (inc % 2) { + printNumber(-1, 80 + 64 - 4, 64 + 16 + 8 + 16, { scale: 0.2 }); + printNumber(-1, 80 + 64 + 16 - 4, 64 + 32 + 8, { scale: 0.2 }); + } + return; + } + // nanoseconds + if (msinc > 59) { + msinc = 0; + } + g.setColor(0, 0, 0); + g.fillRect(80 + 64, h / 2, 80 + 64 + 32, g.getHeight()); + const two = (msinc < 10) ? '0' + msinc : '' + msinc; + printNumber(two[0], 80 + 64 - 4, 64 + 16 + 8 + 16, { scale: 0.2 }); + printNumber(two[1], 80 + 64 + 16 - 4, 64 + 32 + 8, { scale: 0.2 }); +} + +function refresh_score () { + g.setColor(0, 0, 0); + g.fillRect(0, h - 32, w, h); + let two = (score0 < 10) ? '0' + score0 : '' + score0; + printNumber(two[0], 64 - 16, 32 + 64 + 16 + 8 + 16, { scale: 0.2 }); + printNumber(two[1], 64, 32 + 64 + 32 + 8, { scale: 0.2 }); + two = (score1 < 10) ? '0' + score1 : '' + score1; + printNumber(two[0], 32 + 64, 32 + 64 + 16 + 8 + 16, { scale: 0.2 }); + printNumber(two[1], 32 + 64 + 16, 32 + 64 + 32 + 8, { scale: 0.2 }); +} +refresh(); + +setInterval(function () { + if (!stopped || endGame) { + inc++; + } + if (goalFrame > 0) { + goalFrame--; + } + refresh(); +}, 1000); + +setInterval(function () { + refresh_dots(); +}, 500); + +setInterval(function () { + if (!stopped) { + msinc++; + if (msinc > 59) { + msinc = 0; + } + } +}, 10); + +setInterval(function () { + if (!stopped) { + refresh_ms(); + } +}, 250); + +function autogame () { + if (endGame) { + return; + } + onStop(); + if (stopped) { + setTimeout(autogame, 500); + } else { + setTimeout(autogame, 315 + 10 * (Math.random() * 5)); + } +} + +Bangle.setOptions({ lockTimeout: 0, backlightTimeout: 0 }); +// autogame(); + diff --git a/apps/football/app.png b/apps/football/app.png new file mode 100644 index 000000000..80d7cea15 Binary files /dev/null and b/apps/football/app.png differ diff --git a/apps/football/media/ball0.png b/apps/football/media/ball0.png new file mode 100644 index 000000000..5b890c180 Binary files /dev/null and b/apps/football/media/ball0.png differ diff --git a/apps/football/media/ball1.png b/apps/football/media/ball1.png new file mode 100644 index 000000000..c72e56189 Binary files /dev/null and b/apps/football/media/ball1.png differ diff --git a/apps/football/media/dash.png b/apps/football/media/dash.png new file mode 100644 index 000000000..6a9b0c4ac Binary files /dev/null and b/apps/football/media/dash.png differ diff --git a/apps/football/media/digit0.png b/apps/football/media/digit0.png new file mode 100644 index 000000000..33856cc5e Binary files /dev/null and b/apps/football/media/digit0.png differ diff --git a/apps/football/media/digit1.png b/apps/football/media/digit1.png new file mode 100644 index 000000000..53b914ded Binary files /dev/null and b/apps/football/media/digit1.png differ diff --git a/apps/football/media/digit2.png b/apps/football/media/digit2.png new file mode 100644 index 000000000..7a7787b05 Binary files /dev/null and b/apps/football/media/digit2.png differ diff --git a/apps/football/media/digit3.png b/apps/football/media/digit3.png new file mode 100644 index 000000000..a197d5993 Binary files /dev/null and b/apps/football/media/digit3.png differ diff --git a/apps/football/media/digit4.png b/apps/football/media/digit4.png new file mode 100644 index 000000000..f2810a0b6 Binary files /dev/null and b/apps/football/media/digit4.png differ diff --git a/apps/football/media/digit5.png b/apps/football/media/digit5.png new file mode 100644 index 000000000..d8027c362 Binary files /dev/null and b/apps/football/media/digit5.png differ diff --git a/apps/football/media/digit6.png b/apps/football/media/digit6.png new file mode 100644 index 000000000..bd7980045 Binary files /dev/null and b/apps/football/media/digit6.png differ diff --git a/apps/football/media/digit7.png b/apps/football/media/digit7.png new file mode 100644 index 000000000..9ef0df11a Binary files /dev/null and b/apps/football/media/digit7.png differ diff --git a/apps/football/media/digit8.png b/apps/football/media/digit8.png new file mode 100644 index 000000000..6916a301a Binary files /dev/null and b/apps/football/media/digit8.png differ diff --git a/apps/football/media/digit9.png b/apps/football/media/digit9.png new file mode 100644 index 000000000..d8d327523 Binary files /dev/null and b/apps/football/media/digit9.png differ diff --git a/apps/football/media/digits.png b/apps/football/media/digits.png new file mode 100644 index 000000000..68ace56af Binary files /dev/null and b/apps/football/media/digits.png differ diff --git a/apps/football/media/digits128.png b/apps/football/media/digits128.png new file mode 100644 index 000000000..f363a7e8e Binary files /dev/null and b/apps/football/media/digits128.png differ diff --git a/apps/football/media/digits64.png b/apps/football/media/digits64.png new file mode 100644 index 000000000..445c14dfa Binary files /dev/null and b/apps/football/media/digits64.png differ diff --git a/apps/football/media/gol00.png b/apps/football/media/gol00.png new file mode 100644 index 000000000..3b16aa967 Binary files /dev/null and b/apps/football/media/gol00.png differ diff --git a/apps/football/media/gol01.png b/apps/football/media/gol01.png new file mode 100644 index 000000000..3b16aa967 Binary files /dev/null and b/apps/football/media/gol01.png differ diff --git a/apps/football/media/gol10.png b/apps/football/media/gol10.png new file mode 100644 index 000000000..178b6fe3d Binary files /dev/null and b/apps/football/media/gol10.png differ diff --git a/apps/football/media/gol11.png b/apps/football/media/gol11.png new file mode 100644 index 000000000..732fa815d Binary files /dev/null and b/apps/football/media/gol11.png differ diff --git a/apps/football/media/left0.png b/apps/football/media/left0.png new file mode 100644 index 000000000..20599cbb7 Binary files /dev/null and b/apps/football/media/left0.png differ diff --git a/apps/football/media/left1.png b/apps/football/media/left1.png new file mode 100644 index 000000000..b6ffc22f9 Binary files /dev/null and b/apps/football/media/left1.png differ diff --git a/apps/football/media/left2.png b/apps/football/media/left2.png new file mode 100644 index 000000000..11ff8885f Binary files /dev/null and b/apps/football/media/left2.png differ diff --git a/apps/football/media/left4.png b/apps/football/media/left4.png new file mode 100644 index 000000000..a7301a4f8 Binary files /dev/null and b/apps/football/media/left4.png differ diff --git a/apps/football/media/right0.png b/apps/football/media/right0.png new file mode 100644 index 000000000..ac418ad7b Binary files /dev/null and b/apps/football/media/right0.png differ diff --git a/apps/football/media/right1.png b/apps/football/media/right1.png new file mode 100644 index 000000000..31554cbc3 Binary files /dev/null and b/apps/football/media/right1.png differ diff --git a/apps/football/media/right2.png b/apps/football/media/right2.png new file mode 100644 index 000000000..8c8d0ece4 Binary files /dev/null and b/apps/football/media/right2.png differ diff --git a/apps/football/media/right4.png b/apps/football/media/right4.png new file mode 100644 index 000000000..83a78b52c Binary files /dev/null and b/apps/football/media/right4.png differ diff --git a/apps/football/metadata.json b/apps/football/metadata.json new file mode 100644 index 000000000..43e7ac1bf --- /dev/null +++ b/apps/football/metadata.json @@ -0,0 +1,31 @@ +{ + "id": "football", + "name": "football", + "shortName": "football", + "version": "1.01", + "type": "app", + "description": "Classic football game of the CASIO chronometer", + "icon": "app.png", + "allow_emulator": true, + "tags": "games", + "supports": [ + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "football.app.js", + "url": "app.js" + }, + { + "name": "football.img", + "url": "app-icon.js", + "evaluate": true + } + ], + "screenshots": [ + { + "url": "screenshot.png" + } + ] +} diff --git a/apps/football/screenshot.png b/apps/football/screenshot.png new file mode 100644 index 000000000..5742fe9e1 Binary files /dev/null and b/apps/football/screenshot.png differ diff --git a/apps/ftclock/ChangeLog b/apps/ftclock/ChangeLog index 83ec21ee6..c30dae69f 100644 --- a/apps/ftclock/ChangeLog +++ b/apps/ftclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: first release 0.02: RAM efficient version of `fourTwentyTz.js` (as suggested by @gfwilliams). 0.03: `mkFourTwentyTz.js` now handles new timezonedb.com CSV format +0.04: Tell clock widgets to hide. diff --git a/apps/ftclock/app.js b/apps/ftclock/app.js index b12db10f1..4f2cef895 100644 --- a/apps/ftclock/app.js +++ b/apps/ftclock/app.js @@ -33,6 +33,8 @@ function draw() { // Clear the screen once, at startup g.clear(); +// Show launcher when middle button pressed +Bangle.setUI("clock"); // Load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -47,5 +49,4 @@ Bangle.on('lcdPower',on=>{ drawTimeout = undefined; } }); -// Show launcher when middle button pressed -Bangle.setUI("clock"); + diff --git a/apps/ftclock/metadata.json b/apps/ftclock/metadata.json index 876feb1bb..96a4f84b9 100644 --- a/apps/ftclock/metadata.json +++ b/apps/ftclock/metadata.json @@ -1,7 +1,7 @@ { "id": "ftclock", "name": "Four Twenty Clock", - "version": "0.03", + "version": "0.04", "description": "A clock that tells when and where it's going to be 4:20 next", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}], diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index b3cd7e12d..8ef117933 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -81,6 +81,9 @@ function onInit(device) { if (crc==3435933210) version = "2v11.52"; if (crc==46757280) version = "2v11.58"; if (crc==3508163280 || crc==1418074094) version = "2v12"; + if (crc==4056371285) version = "2v13"; + if (crc==1038322422) version = "2v14"; + if (crc==2560806221) version = "2v15"; if (!ok) { version += `(⚠ update required)`; } diff --git a/apps/gallifr/ChangeLog b/apps/gallifr/ChangeLog index 0e1f45042..32c1057b0 100644 --- a/apps/gallifr/ChangeLog +++ b/apps/gallifr/ChangeLog @@ -1,2 +1,3 @@ 0.01: First released version 0.02: Changed setWatch to Bangle.setUI +0.03: Tell clock widgets to hide. diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js index d327bcdc1..8468eee48 100644 --- a/apps/gallifr/app.js +++ b/apps/gallifr/app.js @@ -238,10 +238,12 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); startTimers(); Bangle.loadWidgets(); drawAll(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/gallifr/metadata.json b/apps/gallifr/metadata.json index 9ce7d7f97..96fe243ed 100644 --- a/apps/gallifr/metadata.json +++ b/apps/gallifr/metadata.json @@ -2,7 +2,7 @@ "id": "gallifr", "name": "Time Traveller's Chronometer", "shortName": "Time Travel Clock", - "version": "0.02", + "version": "0.03", "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", "icon": "gallifr.png", "screenshots": [{"url":"screenshot_time.png"}], diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog index 29838413e..df36b6456 100644 --- a/apps/game1024/ChangeLog +++ b/apps/game1024/ChangeLog @@ -6,4 +6,6 @@ 0.06: Fixed issue 1609 added a message popup state handler to control unwanted screen redraw 0.07: Optimized the mover algorithm for efficiency (work in progress) 0.08: Bug fix at end of the game with victorious splash and glorious orchestra -0.09: Added settings menu, removed symbol selection button (*), added highscore reset \ No newline at end of file +0.09: Added settings menu, removed symbol selection button (*), added highscore reset +0.10: fixed clockmode in settings +0.11: Use default Bangle formatter for booleans diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json index e2c4bdb3e..f3b72aad3 100644 --- a/apps/game1024/metadata.json +++ b/apps/game1024/metadata.json @@ -1,7 +1,7 @@ { "id": "game1024", "name": "1024 Game", "shortName" : "1024 Game", - "version": "0.09", + "version": "0.11", "icon": "game1024.png", "screenshots": [ {"url":"screenshot.png" } ], "readme":"README.md", diff --git a/apps/game1024/settings.js b/apps/game1024/settings.js index c8e393663..b52e060b1 100644 --- a/apps/game1024/settings.js +++ b/apps/game1024/settings.js @@ -32,16 +32,15 @@ } }, "Exit press:": { - value: !settings.debugMode, // ! converts undefined to true + value: !settings.clockMode, format: v => v?"short":"long", onchange: v => { - settings.debugMode = v; + settings.clockMode = v; writeSettings(); }, }, "Debug mode:": { - value: !!settings.debugMode, // !! converts undefined to false - format: v => v?"On":"Off", + value: !!settings.debugMode, onchange: v => { settings.debugMode = v; writeSettings(); @@ -67,4 +66,4 @@ } // Show the menu E.showMenu(settingsMenu); - }) \ No newline at end of file + }) diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog index e2ee53ede..d8379b317 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -9,3 +9,4 @@ 0.09: Move event listener from widget to boot code, stops music from showing up in messages 0.10: Simplify touch events Remove date+time +0.11: Use default Bangle formatter for booleans diff --git a/apps/gbmusic/metadata.json b/apps/gbmusic/metadata.json index 0ded80452..bbe2a158d 100644 --- a/apps/gbmusic/metadata.json +++ b/apps/gbmusic/metadata.json @@ -2,7 +2,7 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.10", + "version": "0.11", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", "screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"}, diff --git a/apps/gbmusic/settings.js b/apps/gbmusic/settings.js index ae013fda5..6619eab1c 100644 --- a/apps/gbmusic/settings.js +++ b/apps/gbmusic/settings.js @@ -25,19 +25,16 @@ } } - const yesNo = (v) => translate(v ? "Yes" : "No"); let menu = { "": {"title": "Music Control"}, }; menu[translate("< Back")] = back; menu[translate("Auto start")] = { value: !!s.autoStart, - format: yesNo, onchange: save("autoStart"), }; menu[translate("Simple button")] = { value: !!s.simpleButton, - format: yesNo, onchange: save("simpleButton"), }; diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index 059767ece..f707ffb94 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -27,3 +27,4 @@ 0.25: workaround call notification Fix inflated step number 0.26: Include charging status in battery updates to phone +0.27: Use default Bangle formatter for booleans diff --git a/apps/gbridge/metadata.json b/apps/gbridge/metadata.json index db7119758..e6130b06b 100644 --- a/apps/gbridge/metadata.json +++ b/apps/gbridge/metadata.json @@ -1,7 +1,7 @@ { "id": "gbridge", "name": "Gadgetbridge", - "version": "0.26", + "version": "0.27", "description": "(NOT RECOMMENDED) Displays Gadgetbridge notifications from Android. Please use the 'Android Integration' Bangle.js app instead.", "icon": "app.png", "type": "widget", diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js index f9c7cde90..cf6c84c73 100644 --- a/apps/gbridge/settings.js +++ b/apps/gbridge/settings.js @@ -27,13 +27,11 @@ "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, "Show Icon" : { value: settings().showIcon, - format: v => v?"Yes":"No", onchange: setIcon }, "Find Phone" : function() { E.showMenu(findPhone); }, "Record HRM" : { value: !!settings().hrm, - format: v => v?"Yes":"No", onchange: v => updateSetting('hrm', v) } }; diff --git a/apps/geissclk/ChangeLog b/apps/geissclk/ChangeLog index 7458fadee..cd46173f7 100644 --- a/apps/geissclk/ChangeLog +++ b/apps/geissclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen 0.03: Changed setWatch to Bangle.setUI +0.04: Tell clock widgets to hide. diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js index f14ea5f39..5401fb142 100644 --- a/apps/geissclk/clock.js +++ b/apps/geissclk/clock.js @@ -142,11 +142,13 @@ Bangle.on('lcdPower',function(on) { animInterval = setInterval(iterate, 50); } }); -g.clear(); + +// Show launcher when button pressed +Bangle.setUI("clock");g.clear(); + Bangle.loadWidgets(); Bangle.drawWidgets(); iterate(); animInterval = setInterval(iterate, 50); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/geissclk/metadata.json b/apps/geissclk/metadata.json index 456854dbd..68bd2a970 100644 --- a/apps/geissclk/metadata.json +++ b/apps/geissclk/metadata.json @@ -1,7 +1,7 @@ { "id": "geissclk", "name": "Geiss Clock", - "version": "0.03", + "version": "0.04", "description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation", "icon": "clock.png", "type": "clock", diff --git a/apps/glbasic/ChangeLog b/apps/glbasic/ChangeLog index 89aee01f8..d97fd44d5 100644 --- a/apps/glbasic/ChangeLog +++ b/apps/glbasic/ChangeLog @@ -1,2 +1,3 @@ 0.20: New App! +0.21: Tell clock widgets to hide. diff --git a/apps/glbasic/glbasic.app.js b/apps/glbasic/glbasic.app.js index ff7837ada..c1f82f74c 100644 --- a/apps/glbasic/glbasic.app.js +++ b/apps/glbasic/glbasic.app.js @@ -178,6 +178,8 @@ function draw() { //////////////////////////////////////////////////// // Bangle.setBarometerPower(true); +Bangle.setUI("clock"); + Bangle.loadWidgets(); draw(); @@ -197,6 +199,5 @@ Bangle.on('lcdPower', on => { } }); -Bangle.setUI("clock"); Bangle.drawWidgets(); diff --git a/apps/glbasic/metadata.json b/apps/glbasic/metadata.json index 7c79097da..6d4c562a3 100644 --- a/apps/glbasic/metadata.json +++ b/apps/glbasic/metadata.json @@ -2,7 +2,7 @@ "id": "glbasic", "name": "GLBasic Clock", "shortName": "GLBasic", - "version": "0.20", + "version": "0.21", "description": "A clock with large numbers", "dependencies": {"widpedom":"app"}, "icon": "icon48.png", diff --git a/apps/golfview/ChangeLog b/apps/golfview/ChangeLog index b243db101..909b8feca 100644 --- a/apps/golfview/ChangeLog +++ b/apps/golfview/ChangeLog @@ -1 +1,2 @@ 0.01: New App! Very limited course support. +0.02: Course search added to BangleApps page \ No newline at end of file diff --git a/apps/golfview/custom.html b/apps/golfview/custom.html index 94bc551c0..f80e8dee5 100644 --- a/apps/golfview/custom.html +++ b/apps/golfview/custom.html @@ -7,30 +7,55 @@ integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"> + + -
- - -

+
+
+ +
+ + +
+
+
+
+

No course loaded, please search for a course then choose 'select'.

-

A course needs a few things to be parsed correctly by this tool.

-
    -
  • See official mapping guidelines here.
  • -
  • All holes and features must be within the target course's area.
  • -
  • Supported features are greens, fairways, tees, bunkers, water hazards and holes.
  • -
  • All features for a given hole should have the "ref" tag with the hole number as value. Shared features should - list ref values separated by ';'. example.
  • -
  • There must be 18 holes and they must have the following tags: handicap, par, ref, dist
  • -
  • For any mapping assistance or issues, please file in the official - repo
  • -
- Example Course - © OpenStreetMap contributors

+
+

A course needs a few things to be parsed correctly by this tool.

+
    +
  • See official mapping guidelines here.
  • +
  • All holes and features must be within the target course's area.
  • +
  • Supported features are greens, fairways, tees, bunkers, water hazards and holes.
  • +
  • All features for a given hole should have the "ref" tag with the hole number as value. Shared features + should + list ref values separated by ';'. example.
  • +
  • There must be 18 holes and they must have the following tags: handicap, par, ref, dist
  • +
  • For any mapping assistance or issues, please file in the official + repo
  • +
+ Example Course +
+
@@ -38,13 +63,47 @@ - - - - diff --git a/apps/gpsnav/waypoints.json b/apps/gpsnav/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/gpsnav/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index f923739f0..5867140fb 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -30,3 +30,4 @@ 0.26: Multiple bugfixes 0.27: Map drawing with light theme (fix #1023) 0.28: Show distance more accurately in conjunction with new locale app (fix #1523) +0.29: Use default Bangle formatter for booleans diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index 4595f616d..acd5433b2 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -22,7 +22,6 @@ function showMainMenu() { '': { 'title': 'GPS Record' }, 'RECORD': { value: !!settings.recording, - format: v=>v?"On":"Off", onchange: v => { settings.recording = v; updateSettings(); diff --git a/apps/gpsrec/metadata.json b/apps/gpsrec/metadata.json index c870157df..192b05edf 100644 --- a/apps/gpsrec/metadata.json +++ b/apps/gpsrec/metadata.json @@ -1,7 +1,7 @@ { "id": "gpsrec", "name": "GPS Recorder", - "version": "0.28", + "version": "0.29", "description": "(NOT RECOMMENDED) - please use the more flexible 'Recorder' app instead. Application that allows you to record a GPS track. Can run in background", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/ChangeLog similarity index 100% rename from apps/gpstouch/Changelog rename to apps/gpstouch/ChangeLog diff --git a/apps/ha/ChangeLog b/apps/ha/ChangeLog new file mode 100644 index 000000000..e78b4ccd0 --- /dev/null +++ b/apps/ha/ChangeLog @@ -0,0 +1,2 @@ +0.01: Release +0.02: Includeas the ha.lib.js library that can be used by other apps or clocks. \ No newline at end of file diff --git a/apps/ha/README.md b/apps/ha/README.md new file mode 100644 index 000000000..654a262c8 --- /dev/null +++ b/apps/ha/README.md @@ -0,0 +1,90 @@ +# Home Assistant +This app integrates your BangleJs into the HomeAssistant. + + +# How to use +Click on the left and right side of the screen to select the triggers that you +configured. Click in the middle of the screen to send the trigger to HomeAssistant. + +![](screenshot.png) + + +# Initial Setup +1.) First of all, make sure that HomeAssistant and the HomeAssistant Android App works. + +2.) Open your BangleJs Gadgetbridge App, click on the Settings icon of your BangleJs and enable "Allow Intent Access" + +3.) Enable sensor in HomeAssistant Andoird App/Configuration/Companion App/Manage Sensors/LastUpdate Trigger + +4.) At the bottom of the same screen click on "Add New Intent" and enter "com.espruino.gadgetbridge.banglejs.HA" + +5.) The HomeAssistant Android app must be restarted in order to listen for those actions + -- a "Force Stop" is necessary (through Android App settings) or restart your phone! + +This setup must be done only once -- now you are ready to configure your BangleJS to +control some devices or entities in your HomeAssistant :) + + +# Setup Trigger +1.) Upload the app and all corresponding triggers through the AppStore UI. You must specify +the display name, the trigger as well as an icon. +The following icons are currently supported: +- ha (default) +- light +- door +- fire + + +2.) Create an "automation" in the HomeAssistant WebUI for each trigger that you created on your BangleJs in order to tell HomeAssistant what you want to control. A sample configuration is shown in the image below -- I use this trigger to open the door: + +![](ha_automation.png) + +3.) Don't forget to select the action that should be executed at the bottom of each automation. + + +# Default Trigger +This app also implements two default trigger that can always be used: +- APP_STARTED -- Will be sent whenever the app is started. So you could do some actions already when the app is sarted without the need of any user interaction. +- TRIGGER -- Will be sent whenever some trigger is executed. So you could generically listen to that. + + +# How to use the library (ha.lib.js) in my own app/clk +This app inlcludes a library that can be used by other apps or clocks +to read all configured intents or to send a trigger. Example code: + +```js +// First of all impport the library +var ha = require("ha.lib.js"); + +// You can read all triggers that a user configured simply via +var triggers = ha.getTriggers(); + +// Get display name and icon of trigger +var display = triggers[0].display; +var icon = triggers[0].getIcon(); + +// Trigger the first configured trigger +ha.sendTrigger(triggers[0].trigger); + +// Send a custom trigger that is not configured by a user +ha.sendTrigger("MY_CUSTOM_TRIGGER"); +``` + + +# FAQ + +## Sometimes the trigger is not executed +While playing and testing a bit I found that it is very important that you allow the android HomeAssistant app, as well as BangleJs Gadgetbridge app to (1) run in background and (2), disable energy optimizations for both apps. +Otherwise, Android could stop one of both apps and the trigger will never be sent to HomeAssistant... + +If you still have problems, you can try another trick: +Install "MacroDroid" from the Android AppStore and start the HomeAssistant App +each time the "com.espruino.gadgetbridge.banglejs.HA" intent is send together +with the extra trigger: APP_STARTED. Then whenever you open the app on your BangleJs +it is ensured that HomeAssistant is running... + +## Thanks to +Icons created by Flaticon + +## Creator +- [David Peer](https://github.com/peerdavid). diff --git a/apps/ha/custom.html b/apps/ha/custom.html new file mode 100644 index 000000000..49f5a2eb8 --- /dev/null +++ b/apps/ha/custom.html @@ -0,0 +1,51 @@ + + + + + +

Upload Tigger

+

+

+ + + + + + diff --git a/apps/ha/ha.app.js b/apps/ha/ha.app.js new file mode 100644 index 000000000..d9199fb0e --- /dev/null +++ b/apps/ha/ha.app.js @@ -0,0 +1,75 @@ +/** + * This app uses the ha library to send trigger to HomeAssistant. + */ +var ha = require("ha.lib.js"); +var W = g.getWidth(), H = g.getHeight(); +var position=0; +var triggers = ha.getTriggers(); + + +function draw() { + g.reset().clearRect(Bangle.appRect); + + var h = 22; + g.setFont("Vector", h); + var trigger = triggers[position]; + var w = g.stringWidth(trigger.display); + + g.setFontAlign(-1,-1); + var icon = trigger.getIcon(); + g.setColor(g.theme.fg).drawImage(icon, 12, H/5-2); + g.drawString("Home", icon.width + 20, H/5); + g.drawString("Assistant", icon.width + 18, H/5+24); + + g.setFontAlign(0,0); + var ypos = H/5*3+20; + g.drawRect(W/2-w/2-8, ypos-h/2-8, W/2+w/2+5, ypos+h/2+5); + g.fillRect(W/2-w/2-6, ypos-h/2-6, W/2+w/2+3, ypos+h/2+3); + g.setColor(g.theme.bg).drawString(trigger.display, W/2, ypos); +} + + +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.3); + var right = g.getWidth() - left; + var isLeft = e.x < left; + var isRight = e.x > right; + + if(isRight){ + Bangle.buzz(40, 0.6); + position += 1; + position = position >= triggers.length ? 0 : position; + draw(); + } + + if(isLeft){ + Bangle.buzz(40, 0.6); + position -= 1; + position = position < 0 ? triggers.length-1 : position; + draw(); + } + + if(!isRight && !isLeft){ + ha.sendTrigger("TRIGGER"); + + // Now send the selected trigger + Bangle.buzz(80, 0.6).then(()=>{ + ha.sendTrigger(triggers[position].trigger); + setTimeout(()=>{ + Bangle.buzz(80, 0.6); + }, 250); + }); + } +}); + + +// Send intent that the we started the app. +ha.sendTrigger("APP_STARTED"); + +// Next load the widgets and draw the app +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Draw app +draw(); +setWatch(_=>load(), BTN1); diff --git a/apps/ha/ha.icon.js b/apps/ha/ha.icon.js new file mode 100644 index 000000000..9bf6af796 --- /dev/null +++ b/apps/ha/ha.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIjggOAAocH8AFDh/wAocfAok//gFDv/+Aof+vwoD/Ef3gFBgfwh4YCg/xx4FCh/z54FCj4LEn4XEv4jBGAX//k//4uBAokDAofAg/wnk8h/gLoIFE8ccnHH+Ef8+cnPn/EAAoYvB8ARBg4FB+EMmEPAoP4gkgj5BCwA+BAoXAHwIFC8EHAoZfBAoZfBAoZZDAsgAiA==")) \ No newline at end of file diff --git a/apps/ha/ha.lib.js b/apps/ha/ha.lib.js new file mode 100644 index 000000000..b09cbeab2 --- /dev/null +++ b/apps/ha/ha.lib.js @@ -0,0 +1,80 @@ +/** + * This library can be used to read all triggers that a user + * configured and send a trigger to homeassistant. + */ +function _getIcon(trigger){ + icon = trigger.icon; + if(icon == "light"){ + return { + width : 48, height : 48, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AAMBwAFE4AFDgYFJjgFBnAFBjwXBvAFBh4jBuAFCAQPwAQMHAQPgEQQCBEgcf/AvDn/8Aof//5GDAoJOBh+BAoOB+EP8YFB4fwgfnAoPnGANHAoPjHYQFBHYQFd44pDg47C4/gh/DIIZNFLIplGgF//wFIgZ9BRIUHRII7Ch4FBUIUOAoKzCjwFEhgCBmDpIVooFFh4oCAA4LFC5b7BAob1BAYI=")) + }; + } else if(icon == "door"){ + return { + width : 48, height : 48, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AAM4Aok/4AED///Aov4Aon8DgQGBAv4FpnIFKJv4FweAQFFAgQFB8AFDnADC")) + }; + } else if (icon == "fire"){ + return { + width : 48, height : 48, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("ABsDAokBwAFE4AFE8AFE+AFE/AFJgf8Aon+AocHAokP/8QAokYAoUfAok//88ApF//4kDAo//AgMQAgIFCjgFEjwFCOYIFFHQIFDn/+AoJ/BAoIqBAoN//xCBAoI5BDIPAgP//gFB8AFChYFBgf//EJAogOBAoSgBAoMHAQIFEFgXAAoJEBv4FCNoQFGVYd/wAFEYYIFIvwCBDoV8UwQCBcgUPwDwDfQMBaIYADA")) + }; + } + + // Default is always the HA icon + return { + width : 48, height : 48, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA==")) + }; +} + +exports.getTriggers = function(){ + var triggers = [ + {display: "Empty", trigger: "NOP", icon: "ha"}, + ]; + + try{ + triggers = require("Storage").read("ha.trigger.json"); + triggers = JSON.parse(triggers); + + // We lazy load all icons, otherwise, we have to keep + // all the icons n times in memory which can be + // problematic for embedded devices. Therefore, + // we lazy load icons only if needed using the getIcon + // method of each trigger... + triggers.forEach(trigger => { + trigger.getIcon = function(){ + return _getIcon(trigger); + } + }) + } catch(e) { + // In case there are no user triggers yet, we show the default... + } + + return triggers; +} + +exports.sendTrigger = function(triggerName){ + var retries=3; + + while(retries > 0){ + try{ + // Now lets send the trigger that we sould send. + Bluetooth.println(JSON.stringify({ + t:"intent", + action:"com.espruino.gadgetbridge.banglejs.HA", + extra:{ + trigger: triggerName + }}) + ); + retries = -1; + + } catch(e){ + retries--; + } + } +} \ No newline at end of file diff --git a/apps/ha/ha.png b/apps/ha/ha.png new file mode 100644 index 000000000..8fce958e4 Binary files /dev/null and b/apps/ha/ha.png differ diff --git a/apps/ha/ha_automation.png b/apps/ha/ha_automation.png new file mode 100644 index 000000000..9372cfa15 Binary files /dev/null and b/apps/ha/ha_automation.png differ diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json new file mode 100644 index 000000000..63308b933 --- /dev/null +++ b/apps/ha/metadata.json @@ -0,0 +1,25 @@ +{ + "id": "ha", + "name": "HomeAssistant", + "version": "0.02", + "description": "Integrates your BangleJS into HomeAssistant.", + "icon": "ha.png", + "type": "app", + "tags": "tool", + "readme": "README.md", + "supports": ["BANGLEJS2"], + "custom": "custom.html", + "screenshots": [ + {"url":"screenshot.png"}, + {"url":"screenshot_2.png"}, + {"url":"screenshot_3.png"} + ], + "data": [ + {"name":"ha.trigger.json" } + ], + "storage": [ + {"name":"ha.app.js","url":"ha.app.js"}, + {"name":"ha.lib.js","url":"ha.lib.js"}, + {"name":"ha.img","url":"ha.icon.js","evaluate":true} + ] +} diff --git a/apps/ha/screenshot.png b/apps/ha/screenshot.png new file mode 100644 index 000000000..dc059e2de Binary files /dev/null and b/apps/ha/screenshot.png differ diff --git a/apps/ha/screenshot_2.png b/apps/ha/screenshot_2.png new file mode 100644 index 000000000..55019c3b1 Binary files /dev/null and b/apps/ha/screenshot_2.png differ diff --git a/apps/ha/screenshot_3.png b/apps/ha/screenshot_3.png new file mode 100644 index 000000000..b9eae0b74 Binary files /dev/null and b/apps/ha/screenshot_3.png differ diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog index dac7d317e..fea8770fc 100644 --- a/apps/hardalarm/ChangeLog +++ b/apps/hardalarm/ChangeLog @@ -1,3 +1,4 @@ 0.01: Add a number to match to turn off alarm 0.02: Respect Quiet Mode 0.03: Fix hour/minute wrapping code for new menu system +0.04: Use default Bangle formatter for booleans diff --git a/apps/hardalarm/app.js b/apps/hardalarm/app.js index 0c72a2c8f..0aa33b21b 100644 --- a/apps/hardalarm/app.js +++ b/apps/hardalarm/app.js @@ -66,17 +66,14 @@ function editAlarm(alarmIndex) { }, /*LANG*/'Enabled': { value: en, - format: v=>v?"On":"Off", onchange: v=>en=v }, /*LANG*/'Repeat': { value: en, - format: v=>v?"Yes":"No", onchange: v=>repeat=v }, /*LANG*/'Auto snooze': { value: as, - format: v=>v?"Yes":"No", onchange: v=>as=v } }; diff --git a/apps/hardalarm/metadata.json b/apps/hardalarm/metadata.json index 1dab4501d..df287b426 100644 --- a/apps/hardalarm/metadata.json +++ b/apps/hardalarm/metadata.json @@ -2,7 +2,7 @@ "id": "hardalarm", "name": "Hard Alarm", "shortName": "HardAlarm", - "version": "0.03", + "version": "0.04", "description": "Make sure you wake up! Count to the right number to turn off the alarm", "icon": "app.png", "tags": "tool,alarm,widget", diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog index f70653d58..289c7ac2d 100644 --- a/apps/hcclock/ChangeLog +++ b/apps/hcclock/ChangeLog @@ -1,3 +1,4 @@ 0.01: Base code 0.02: Saved settings when switching color scheme -0.03: Added Button 3 opening messages (if app is installed) \ No newline at end of file +0.03: Added Button 3 opening messages (if app is installed) +0.04: Use `messages` library to check for new messages \ No newline at end of file diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js index de5163996..9558c052b 100644 --- a/apps/hcclock/hcclock.app.js +++ b/apps/hcclock/hcclock.app.js @@ -3,7 +3,7 @@ // Numbers Rect order (left, top, right, bottom) // Each number defines a set of rects to draw -const numbers = +const numbers = [ [// Zero [0, 0, 1, 0.2], @@ -64,7 +64,7 @@ const numbers = [0, 0.8, 1, 1], [0, 0, 0.1, 0.6], [0.9, 0, 1, 1] - ] + ] ]; const months = [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ]; @@ -103,7 +103,7 @@ function updateTime() let mo = now.getMonth(); let y = now.getFullYear(); let d = now.getDate(); - + if(h != hour) { hour = h; @@ -127,7 +127,7 @@ function updateTime() day = d; g.setFont("6x8", 2); g.setFontAlign(0, -1, 0); - g.drawString(fmtDate(d,mo,y,hour), 120, 120); + g.drawString(fmtDate(d,mo,y,hour), 120, 120); } drawMessages(); } @@ -136,7 +136,7 @@ function drawDigits(x, value) { if(!Bangle.isLCDOn()) // No need to draw when LCD Off return; - + drawChar(Math.floor(value/10), 15, x, 115, x+50); if(value%10 == 1) drawChar(value%10, 55, x, 155, x+50); @@ -228,27 +228,18 @@ function flipColors() // MESSAGE HANDLING() // -let messages_installed = require("Storage").read("messages.app.js") != undefined; +let messages_installed = require("Storage").read("messages") !== undefined; function handleMessages() { - if(messages_installed && hasMessages() > 0) - { - E.showMessage("Loading Messages..."); - load("messages.app.js"); - } + if(!hasMessages()) return; + E.showMessage("Loading Messages..."); + load("messages.app.js"); } function hasMessages() { - if(!messages_installed) - return false; - - var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new)) - return true; - else - return false; + return messages_installed && require("messages").status() === 'new'; } let msg = atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA=="); @@ -256,20 +247,21 @@ let had_messages = false; function drawMessages() { - if(!had_messages && hasMessages()) { + const has_messages = hasMessages(); + if(has_messages === had_messages) return; + if(has_messages) { g.setColor(255,255,255); g.drawImage(msg, 184, 212); g.setFont("6x8", 2); g.setFontAlign(0, -1, 0); g.drawString(">", 224, 216); - had_messages = true; - } - else if (had_messages && !hasMessages()) + } + else { g.setColor(0,0,0); g.fillRect(180, 210, 240, 240); - had_messages = false; } + had_messages = has_messages; } ////////////////////////////////////////// diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json index 0d4cbe0cd..b8f8c14b9 100644 --- a/apps/hcclock/metadata.json +++ b/apps/hcclock/metadata.json @@ -1,7 +1,7 @@ { "id": "hcclock", "name": "Hi-Contrast Clock", - "version": "0.03", + "version": "0.04", "description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.", "icon": "hcclock-icon.png", "type": "clock", diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 74be2faec..62d93e606 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -13,3 +13,4 @@ 0.12: Add setting for Daily Step Goal 0.13: Add support for internationalization 0.14: Move settings +0.15: Fix charts (fix #1366) diff --git a/apps/health/README.md b/apps/health/README.md index c6b379c0a..3cc234a3f 100644 --- a/apps/health/README.md +++ b/apps/health/README.md @@ -2,7 +2,7 @@ Logs health data to a file every 10 minutes, and provides an app to view it -**BETA - requires firmware 2v11** +**BETA - requires firmware 2v11 or later** ## Usage diff --git a/apps/health/app.js b/apps/health/app.js index a1fe1a4f7..c0a40bd93 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -46,7 +46,7 @@ function menuHRM() { function stepsPerHour() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(24); + var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.steps); g.clear(1); Bangle.drawWidgets(); @@ -57,7 +57,7 @@ function stepsPerHour() { function stepsPerDay() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(31); + var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); g.clear(1); Bangle.drawWidgets(); @@ -68,8 +68,8 @@ function stepsPerDay() { function hrmPerHour() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(24); - let cnt = new Uint8Array(23); + var data = new Uint16Array(24); + var cnt = new Uint8Array(23); require("health").readDay(new Date(), h=>{ data[h.hr]+=h.bpm; if (h.bpm) cnt[h.hr]++; @@ -84,8 +84,8 @@ function hrmPerHour() { function hrmPerDay() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(31); - let cnt = new Uint8Array(31); + var data = new Uint16Array(31); + var cnt = new Uint8Array(31); require("health").readDailySummaries(new Date(), h=>{ data[h.day]+=h.bpm; if (h.bpm) cnt[h.day]++; @@ -100,7 +100,7 @@ function hrmPerDay() { function movementPerHour() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(24); + var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.movement); g.clear(1); Bangle.drawWidgets(); @@ -111,7 +111,7 @@ function movementPerHour() { function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); - let data = new Uint16Array(31); + var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); g.clear(1); Bangle.drawWidgets(); @@ -183,7 +183,7 @@ function drawBarChart() { } // draw a fake 0 height bar if chart_index is outside the bounds of the array - if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len) + if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len && chart_max_datum > 0) bar_top = bar_bot - 100 * (chart_data[chart_index + bar - 1]) / chart_max_datum; else bar_top = bar_bot; diff --git a/apps/health/interface.html b/apps/health/interface.html index 0791acd24..a708e2645 100644 --- a/apps/health/interface.html +++ b/apps/health/interface.html @@ -113,7 +113,7 @@ function getMonthList() { Util.showModal("Deleting..."); Util.eraseStorage(filename,()=>{ Util.hideModal(); - getTrackList(); + getMonthList(); }); } if (task=="downloadcsv") { diff --git a/apps/health/metadata.json b/apps/health/metadata.json index 58762c77a..a038f67b5 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -1,7 +1,7 @@ { "id": "health", "name": "Health Tracking", - "version": "0.14", + "version": "0.15", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", diff --git a/apps/heart/ChangeLog b/apps/heart/ChangeLog index f6fd9793e..fe03575c9 100644 --- a/apps/heart/ChangeLog +++ b/apps/heart/ChangeLog @@ -11,5 +11,6 @@ Reduce memory usage by ~30% Generate scale based on defined minimum and maximum measurement Added background line on 50% to ease estimation of drawn values -0.06: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) -0.07: theme support +0.06: Tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) +0.07: Theme support +0.08: Use default Bangle formatter for booleans diff --git a/apps/heart/app.js b/apps/heart/app.js index 5428ea06b..c10185b5f 100644 --- a/apps/heart/app.js +++ b/apps/heart/app.js @@ -28,7 +28,6 @@ function showMainMenu() { '': { 'title': 'Heart Recorder' }, 'RECORD': { value: !!settings.isRecording, - format: v=>v?"On":"Off", onchange: v => { settings.isRecording = v; updateSettings(); diff --git a/apps/heart/metadata.json b/apps/heart/metadata.json index 6265dbfef..2071bdf08 100644 --- a/apps/heart/metadata.json +++ b/apps/heart/metadata.json @@ -2,7 +2,7 @@ "id": "heart", "name": "Heart Rate Recorder", "shortName": "HRM Record", - "version": "0.07", + "version": "0.08", "description": "Application that allows you to record your heart rate. Can run in background", "icon": "app.png", "tags": "tool,health,widget", diff --git a/apps/hebrew_calendar/customizer.html b/apps/hebrew_calendar/customizer.html index bea860e53..c6d529414 100644 --- a/apps/hebrew_calendar/customizer.html +++ b/apps/hebrew_calendar/customizer.html @@ -7,6 +7,333 @@ Hebrew Calendar Customizer + + @@ -20,11 +347,15 @@
- + - -
get your latitude and longitude from plus.codes or:
- + +
get your latitude and longitude from plus.codes or: +
+
- - diff --git a/apps/hebrew_calendar/customizer.mjs b/apps/hebrew_calendar/customizer.mjs deleted file mode 100644 index 06716a63b..000000000 --- a/apps/hebrew_calendar/customizer.mjs +++ /dev/null @@ -1,329 +0,0 @@ -import { - HebrewCalendar, - HDate, - Location, - Zmanim, -} from "https://cdn.skypack.dev/@hebcal/core@^3?min"; - -function onload(event) { - event.preventDefault(); - const latLon = getLatLonFromForm(); - const events = generateHebCal(latLon); - const calendar = serializeEvents(events); - console.debug(calendar); - globalThis["cal"] = calendar; - loadWatch(calendar); -} - -function loadWatch(json) { - sendCustomizedApp({ - id: "hebrew_calendar", - - storage: [ - { - name: "hebrew_calendar.app.js", - url: "app.js", - // content below is same as app.js except for the first line which customizes the hebrewCalendar object used - content: ` -let hebrewCalendar = ${json}; - -const dayInMS = 86400000; - -const DateProvider = { now: () => Date.now() }; - -const Layout = require("Layout"); -const Locale = require("locale"); - -let nextEndingEvent; - -function getCurrentEvents() { - const now = DateProvider.now(); - - const current = hebrewCalendar.filter( - (x) => x.startEvent <= now && x.endEvent >= now - ); - - nextEndingEvent = current.reduce((acc, ev) => { - return Math.min(acc, ev.endEvent); - }, Infinity); - - return current.map((event, i) => { - return { - type: "txt", - font: "12x20", - id: "currentEvents" + i, - label: event.desc, - pad: 2, - bgCol: g.theme.bg, - }; - }); -} - -function getUpcomingEvents() { - const now = DateProvider.now(); - - const futureEvents = hebrewCalendar.filter( - (x) => x.startEvent >= now && x.startEvent <= now + dayInMS - ); - - let warning; - let eventsLeft = hebrewCalendar.filter( - (x) => x.startEvent >= now && x.startEvent <= now + dayInMS * 14 - ).length; - - if (eventsLeft < 14) { - warning = { - startEvent: 0, - type: "txt", - font: "4x6", - id: "warning", - label: "only " + eventsLeft + " events left in calendar; update soon", - pad: 2, - bgCol: g.theme.bg, - }; - } - - return futureEvents - .slice(0, 2) - .map((event, i) => { - return { - startEvent: event.startEvent, - type: "txt", - font: "6x8", - id: "upcomingEvents" + 1, - label: event.desc + " at " + Locale.time(new Date(event.startEvent), 1), - pad: 2, - bgCol: g.theme.bg, - }; - }) - .concat(warning) - .sort(function (a, b) { - return a.startEvent - b.startEvent; - }); -} - -function dateTime() { - return ( - Locale.dow(new Date(), 1) + - " " + - Locale.date(new Date(), 1) + - " " + - Locale.time(new Date(), 1) - ); -} - -function makeLayout() { - return new Layout( - { - type: "v", - c: [ - { - type: "txt", - font: "6x8", - id: "title", - label: "-- Hebrew Calendar Events --", - pad: 2, - bgCol: g.theme.bg2, - }, - { - type: "txt", - font: "6x8", - id: "currently", - label: "Currently", - pad: 2, - bgCol: g.theme.bgH, - }, - ] - .concat(getCurrentEvents()) - .concat([ - { - type: "txt", - font: "6x8", - label: "Upcoming", - id: "upcoming", - pad: 2, - bgCol: g.theme.bgH, - }, - ]) - .concat(getUpcomingEvents()) - .concat([ - { - type: "txt", - font: "Vector14", - id: "time", - label: dateTime(), - pad: 2, - bgCol: undefined, - }, - ]), - }, - { lazy: true } - ); -} -let layout = makeLayout(); -// see also https://www.espruino.com/Bangle.js+Layout#updating-the-screen - -// timeout used to update every minute -let drawTimeout; - -function draw() { - layout.time.label = dateTime(); - layout.render(); - - // schedule a draw for the next minute - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function () { - drawTimeout = undefined; - draw(); - }, 60000 - (DateProvider.now() % 60000)); - console.log("updated time"); -} - -// update time and draw -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -draw(); - -function findNextEvent() { - return hebrewCalendar.find((ev) => { - return ev.startEvent > DateProvider.now(); - }); -} - -function updateCalendar() { - layout.clear(); - layout = makeLayout(); - layout.forgetLazyState(); - layout.render(); - - let nextChange = Math.min( - findNextEvent().startEvent - DateProvider.now() + 5000, - nextEndingEvent - DateProvider.now() + 5000 - ); - setTimeout(updateCalendar, nextChange); - console.log("updated events"); -} - -updateCalendar(); - -Bangle.setUI("clock"); - `, - }, - ], - }); -} - -document - .querySelector("button[type=submit]") - .addEventListener("click", onload, false); - -document.querySelector("#geoloc")?.addEventListener("click", (event) => { - event.preventDefault(); - navigator.geolocation.getCurrentPosition( - (pos) => { - const { - coords: { latitude, longitude }, - } = pos; - locationElements[0].value = latitude; - locationElements[1].value = longitude; - console.debug(pos); - }, - (err) => { - if (err.PERMISSION_DENIED) { - alert("permission required to use geolocation api; enter manually"); - } - if (err.POSITION_UNAVAILABLE) { - alert("position unavailable; enter manually"); - } - }, - { enableHighAccuracy: false } - ); -}); - -document.querySelector( - "#hDate" -).innerText = `Today is ${new Date().toLocaleDateString()} & ${new HDate().toString()}`; - -const locationElements = [ - document.querySelector("#lat"), - document.querySelector("#lon"), -]; - -function getLatLonFromForm() { - const latLon = locationElements.map((el) => el.value); - if (locationElements.every((x) => x.checkValidity())) { - return latLon; - } else { - console.debug("lat lon invalid error"); - return [0, 0]; - } -} - -function groupBy(arr, fn) { - return arr - .map(typeof fn === "function" ? fn : (val) => val[fn]) - .reduce((acc, val, i) => { - acc[val] = (acc[val] || []).concat(arr[i]); - return acc; - }, {}); -} - -function generateHebCal(latLon) { - const location = new Location( - ...latLon, - document.querySelector("#inIL").checked - ); - - const now = new Date(); - - const options = { - year: now.getFullYear(), - isHebrewYear: false, - candlelighting: true, - location, - addHebrewDates: true, - addHebrewDatesForEvents: true, - sedrot: true, - start: now, - end: new Date(now.getFullYear(), now.getMonth() + 3), - }; - - const events = HebrewCalendar.calendar(options).map((ev) => { - const { desc, eventTime, startEvent, endEvent } = ev; - - const zman = new Zmanim(ev.date, ...latLon.map(Number)); - - let output = { - desc, - startEvent: startEvent?.eventTime?.getTime() || zman.gregEve().getTime(), - endEvent: endEvent?.eventTime?.getTime() || zman.shkiah().getTime(), - }; - - if (eventTime) { - delete output.startEvent; - delete output.endEvent; - output.startEvent = eventTime.getTime(); - output.endEvent = eventTime.getTime() + 60000 * 15; - } - - return output; - }); - - // console.table(events) - - return events.sort((a, b) => { - return a.startEvent - b.startEvent; - }); -} - -function enc(data) { - return btoa(heatshrink.compress(new TextEncoder().encode(data))); -} - -function serializeEvents(events) { - // const splitByGregorianMonth = groupBy(events, (evt) => { - // return new Date(evt.startEvent).getMonth(); - // }); - return JSON.stringify(events); -} diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog index 480d7d448..b0ba92aef 100644 --- a/apps/hidcam/ChangeLog +++ b/apps/hidcam/ChangeLog @@ -1,3 +1,4 @@ 0.01: Core functionnality 0.02: Offer to enable HID if disabled 0.03: Adds Readme and tags to be used by App Loader +0.04: Adds Bangle.js 2 support, Buzz and Touch diff --git a/apps/hidcam/README.md b/apps/hidcam/README.md index 5e8d40817..fa8e0153b 100644 --- a/apps/hidcam/README.md +++ b/apps/hidcam/README.md @@ -7,7 +7,7 @@ Control the camera shutter from your phone using your watch 1. In settings, enable HID for "Keyboard & Media". 2. Pair your watch to your phone. 3. Load your camera app on your phone. -4. There you go, launch the app on your watch and press button 2 to trigger the shutter ! +4. There you go, launch the app on your watch and press the button (button 2 on Bangle.js 1) to trigger the shutter ! ## How does it work ? diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js index bb8ddf7e9..639018db3 100644 --- a/apps/hidcam/app.js +++ b/apps/hidcam/app.js @@ -1,25 +1,29 @@ var storage = require('Storage'); const settings = storage.readJSON('setting.json',1) || { HID: false }; - +const isB2 = process.env.HWVERSION === 2; var sendHid, camShot, profile; if (settings.HID=="kbmedia") { profile = 'camShutter'; sendHid = function (code, cb) { - try { - NRF.sendHIDReport([1,code], () => { - NRF.sendHIDReport([1,0], () => { - if (cb) cb(); - }); - }); - } catch(e) { - print(e); - } + + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; camShot = function (cb) { sendHid(0x80, cb); }; } else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { settings.HID = "kbmedia"; require("Storage").write('setting.json', settings); @@ -31,10 +35,15 @@ function drawApp() { g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); - g.fillCircle(122,127,60); - g.drawImage(storage.read("hidcam.img"),100,105); - const d = g.getWidth() - 18; - + if (!isB2) { // Bangle.js 1 + g.fillCircle(122,127,60); + g.drawImage(storage.read("hidcam.img"),100,105); + const d = g.getWidth() - 18; + } else { + g.fillCircle(90,95,60); + g.drawImage(storage.read("hidcam.img"),65,70); + const d = g.getWidth() - 18; + } function c(a) { return { width: 8, @@ -46,12 +55,27 @@ function drawApp() { g.fillRect(180,130, 240, 124); } -if (camShot) { - setWatch(function(e) { - E.showMessage('camShot !'); - setTimeout(drawApp, 1000); - camShot(() => {}); - }, BTN2, { edge:"falling",repeat:true,debounce:50}); - + if (camShot) { + if (!isB2) { // Bangle.js 1 + setWatch(function(e) { + E.showMessage('camShot !'); + Bangle.buzz(300, 1); + setTimeout(drawApp, 1000); + camShot(() => {}); + }, BTN2, { edge:"falling",repeat:true,debounce:50}); + } else { // Bangle.js 2 + setWatch(function(e) { + E.showMessage('camShot !'); + Bangle.buzz(300, 1); + setTimeout(drawApp, 1000); + camShot(() => {}); + }, BTN, { edge:"falling",repeat:true,debounce:50}); + Bangle.on('touch', function (wat, tap) { + E.showMessage('camShot !'); + Bangle.buzz(300, 1); + setTimeout(drawApp, 1000); + camShot(() => {}); + }); + } drawApp(); -} + } diff --git a/apps/hidcam/metadata.json b/apps/hidcam/metadata.json index b2ef33229..b57d41ed1 100644 --- a/apps/hidcam/metadata.json +++ b/apps/hidcam/metadata.json @@ -2,11 +2,11 @@ "id": "hidcam", "name": "Camera shutter", "shortName": "Cam shutter", - "version": "0.03", + "version": "0.04", "description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle", "icon": "app.png", "tags": "bluetooth,tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"hidcam.app.js","url":"app.js"}, diff --git a/apps/hidmsicswipe/changelog b/apps/hidmsicswipe/ChangeLog similarity index 100% rename from apps/hidmsicswipe/changelog rename to apps/hidmsicswipe/ChangeLog diff --git a/apps/homework/ChangeLog b/apps/homework/ChangeLog new file mode 100644 index 000000000..b9a5425d1 --- /dev/null +++ b/apps/homework/ChangeLog @@ -0,0 +1,2 @@ +... +0.10: First update with ChangeLog Added diff --git a/apps/homework/README.md b/apps/homework/README.md new file mode 100644 index 000000000..adad4ef39 --- /dev/null +++ b/apps/homework/README.md @@ -0,0 +1,4 @@ +# This is a simple homework app +Use the touchscreen to navigate. + +Requires the "textinput" library. (Tap keyboard) diff --git a/apps/homework/app-icon.js b/apps/homework/app-icon.js new file mode 100644 index 000000000..202b52b09 --- /dev/null +++ b/apps/homework/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQCyvd7oYTCwQYTCwgYRCwwYPIgpKQCA4YOBxIYMBhYLLHhgYEC5BsKDAYXHCwUBiUikAYIC4wtDC5IYCA4pEEC5QYBYRUCkQXJAA8K1Wq0AXHhGIxGAC5ZHHC8ZDDC4cM5qaBC8ZHHC68N6czmAXrL94X/C/4XHgUiCYIDDa54XXO/4XHAH4A/ABY=")) diff --git a/apps/homework/app.js b/apps/homework/app.js new file mode 100644 index 000000000..3d9be31c9 --- /dev/null +++ b/apps/homework/app.js @@ -0,0 +1,212 @@ +var Layout = require("Layout"); + +var homework = require("Storage").readJSON("homework.txt", "r"); +var mainCheckHomeworkMenu; + +var nhwmn = { // New homework Menu + "": { + "title": "New HW Subject:" + } + +}; + + + +function newHomeworkMenu() { + E.showMessage("Getting subjects..."); + var rawsubjects = require("Storage").read("homework.subjects.txt"); // This code reads out the subjects list and removes the newline character at the end + var splitsubjects = rawsubjects.split(","); + var lastItem = splitsubjects[splitsubjects.length - 1]; + var thiscurrentsubject; + var command; + lastItem = lastItem.slice(0, -1); + splitsubjects[splitsubjects.length - 1] = lastItem; + for (let i = 0; i < splitsubjects.length; i++) { // loop through array and add to menu + thiscurrentsubject = splitsubjects[i]; + command = addNewHomework(thiscurrentsubject); + nhwmn[splitsubjects[i]] = addNewHomework.bind(null, thiscurrentsubject); + } + nhwmn["Back"] = function() {E.showMenu(mainMenu);}; + console.log(nhwmn); + E.showMenu(nhwmn); +} +var mode = "mainmenu"; +var statusmsg; +var mainMenu = { + "": { + title: "--Main Menu--" + }, + "New Homework": function() { + newHomeworkMenu(); + mode = "newhomework"; + }, + "Check Homework": function() { + checkUnfinishedHomeworkAssembler(); + }, + "Reset Homework": function() { + E.showPrompt("Are you sure you want to delete homework.txt?", { + buttons: { + "No": false, + "Yes": true + } + }).then(function(v) { + if (v) { + require("Storage").write("homework.txt", '{"homework":[]}'); + homework = require("Storage").readJSON("homework.txt", "r"); + E.showMenu(mainMenu); + + }else{ + E.showMenu(mainMenu); + } + }); + }, +}; + +function checkUnfinishedHomeworkAssembler() { + homework = require("Storage").readJSON("homework.txt", "r"); + var hwcount = Object.keys(homework.homework).length; + mainCheckHomeworkMenu = { + '': { + 'title': 'Unfinished HW:' + } + }; + // This code snippet gets the unfinished HW and puts it in mainCheckHomeworkMenu + // btw mainCheckHomeworkMenu displays all the homework, when tapping on it you get more details with checkPreciseHomework function + for (var i = 0; i < hwcount; ++i) { + if (homework.homework[i].done === false) { + var currentsubject = i; //attempting to pass i + mainCheckHomeworkMenu[homework.homework[i].subject] = checkPreciseHomework.bind(null, currentsubject); + } + + } + mainCheckHomeworkMenu["See Archived HW"] = function() { + checkFinishedHomeworkAssembler(); + }; + mainCheckHomeworkMenu["Back to Main Menu"] = function() { + mode = "mainmenu"; + E.showMenu(mainMenu); + }; + console.log(mainCheckHomeworkMenu); + E.showMenu(mainCheckHomeworkMenu); +} + +function checkFinishedHomeworkAssembler() { + homework = require("Storage").readJSON("homework.txt", "r"); + var hwcount = Object.keys(homework.homework).length; + mainCheckHomeworkMenu = { + '': { + 'title': 'Archived HW:' + } + }; + + // This code snippet gets the unfinished HW and puts it in mainCheckHomeworkMenu + // btw mainCheckHomeworkMenu displays all the homework, when tapping on it you get more details with checkPreciseHomework function (currently being written) + for (var i = 0; i < hwcount; ++i) { + if (homework.homework[i].done === true) { + var currentsubject = i; //attempting to pass i + mainCheckHomeworkMenu[homework.homework[i].subject] = checkPreciseHomework.bind(null, currentsubject); + } + + } + mainCheckHomeworkMenu["Back"] = function() { + mode = "mainmenu"; + E.showMenu(mainMenu); + }; + E.showMenu(mainCheckHomeworkMenu); +} + +function checkPreciseHomework(subjectnum) { // P A I N + homework = require("Storage").read("homework.txt", "r"); + homework = JSON.parse(homework); + var subject = homework.homework[subjectnum].subject; + var task = homework.homework[subjectnum].task; + var taskmsg = "Task: " + homework.homework[subjectnum].task; + if (homework.homework[subjectnum].done === false) { + statusmsg = "Status: Unfinished"; + } else { + statusmsg = "Status: Finished"; + } + var datetimerecieved = homework.homework[subjectnum].datetimerecievehw; + var datetimerecievedmsg = "Recieved: " + homework.homework[subjectnum].datetimerecievehw; + var checkPreciseHomeworkMenu = { + '': { + 'title': subject + }, + }; + checkPreciseHomeworkMenu[subject] = function() {}, + checkPreciseHomeworkMenu[taskmsg] = function() {}, + checkPreciseHomeworkMenu[statusmsg] = function() { + status = "Status: Finished"; + var d = new Date(); + var currenttime = require("locale").time(d, 1); + var currentdate = require("locale").date(d); + var datetime = (currenttime + " " + currentdate); + delete homework.homework[subjectnum]; + homework.homework.push({ + subject: subject, + task: task, + done: true, + datetimerecievehw: datetimerecieved, + datetimehwdone: datetime + }); + require("Storage").write("homework.txt", JSON.stringify(homework)); + checkUnfinishedHomeworkAssembler(); + }, + checkPreciseHomeworkMenu[datetimerecievedmsg] = function() {}, + checkPreciseHomeworkMenu["Back"] = function() { + checkUnfinishedHomeworkAssembler(); + }, + + E.showMenu(checkPreciseHomeworkMenu); + + +} + +function pushHomework(subject, status, datetimehwdone) { + homework = require("Storage").readJSON("homework.txt", "r"); + +} + +function addNewHomework(subject) { // Pass subject + console.log(subject); + require("textinput").input().then(result => { + if (result === "") { + mode = "newhomework"; + newHomeworkMenu(); + } else { + var d = new Date(); + // update time and date + var currenttime = require("locale").time(d, 1); + var currentdate = require("locale").date(d); + var datetime = (currenttime + " " + currentdate); + homework.homework.push({ + subject: subject, + task: result, + done: false, + datetimerecievehw: datetime + }); // TODO: when HW is done, add datetimeendhw !!! + console.log("subject is" + subject); + + //homework.homework[subject] = result; + require("Storage").write("homework.txt", JSON.stringify(homework)); + E.showMenu(mainMenu); + + } + }); + +} + +function main() { // why does this still exist + if (mode === "mainmenu") { + E.showMenu(mainMenu); + + } else if (mode === "newhomework") { + newHomeworkMenu() + + } +} +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +main(); +//loop = setInterval(main, 1); diff --git a/apps/homework/app.png b/apps/homework/app.png new file mode 100644 index 000000000..e6174d46b Binary files /dev/null and b/apps/homework/app.png differ diff --git a/apps/homework/metadata.json b/apps/homework/metadata.json new file mode 100644 index 000000000..d2af99fd4 --- /dev/null +++ b/apps/homework/metadata.json @@ -0,0 +1,20 @@ + +{ "id": "homework", + "name": "Homework", + "shortName":"Homework", + "version":"0.10", + "description": "A simple app to manage homework", + "icon": "app.png", + "tags": "tool", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "custom": "subjects.html", + "data": [ + {"name":"homework.txt" }, + {"name":"homework.subjects.txt" } + ], + "storage": [ + {"name":"homework.app.js","url":"app.js"}, + {"name":"homework.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/homework/subjects.html b/apps/homework/subjects.html new file mode 100644 index 000000000..92c3023dd --- /dev/null +++ b/apps/homework/subjects.html @@ -0,0 +1,31 @@ + + + + + + +

Subjects:

+

Click

+ + + + + + diff --git a/apps/hralarm/ChangeLog b/apps/hralarm/ChangeLog index 4c21f3ace..11e79d703 100644 --- a/apps/hralarm/ChangeLog +++ b/apps/hralarm/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Use default Bangle formatter for booleans diff --git a/apps/hralarm/metadata.json b/apps/hralarm/metadata.json index 1fae68084..4c661b3fc 100644 --- a/apps/hralarm/metadata.json +++ b/apps/hralarm/metadata.json @@ -2,7 +2,7 @@ "id": "hralarm", "name": "Heart rate alarm", "shortName":"HR Alarm", - "version":"0.01", + "version":"0.02", "description": "This invisible widget vibrates whenever the heart rate gets close to the upper limit or goes over or under the configured limits", "icon": "widget.png", "type": "widget", diff --git a/apps/hralarm/settings.js b/apps/hralarm/settings.js index 3158ab8b7..02cdccdaf 100644 --- a/apps/hralarm/settings.js +++ b/apps/hralarm/settings.js @@ -17,7 +17,6 @@ '< Back': back, 'Enabled': { value: !!settings.enabled, - format: v => settings.enabled ? "On" : "Off", onchange: v => { settings.enabled = v; writeSettings(); diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index f05a9dc13..62956e8cd 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -7,3 +7,4 @@ 0.07: Update scaling for new firmware 0.08: Don't force backlight on/watch unlocked on Bangle 2 0.09: Grey out BPM until confidence is over 50% +0.10: Autoscale raw graph to maximum value seen diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 703b60c01..386341e6d 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -4,7 +4,7 @@ if (process.env.HWVERSION == 1) { } Bangle.setHRMPower(1); -var hrmInfo, hrmOffset = 0; +var hrmInfo = {}, hrmOffset = 0; var hrmInterval; var btm = g.getHeight()-1; var lastHrmPt = []; // last xy coords we draw a line to @@ -29,19 +29,34 @@ function onHRM(h) { hrmInterval = setInterval(readHRM,41); }, 40); } + updateHrm(); +} +Bangle.on('HRM', onHRM); +function updateHrm(){ var px = g.getWidth()/2; - g.setFontAlign(0,0); + g.setFontAlign(0,-1); g.clearRect(0,24,g.getWidth(),80); - g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75); - var str = hrmInfo.bpm; + g.setFont("6x8").drawString("Confidence "+(hrmInfo.confidence || "--")+"%", px, 70); + + updateScale(); + + g.setFontAlign(0,0); + var str = hrmInfo.bpm || "--"; g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45); px += g.stringWidth(str)/2; g.setFont("6x8").setColor(g.theme.fg); g.drawString("BPM",px+15,45); } -Bangle.on('HRM', onHRM); +function updateScale(){ + g.setFontAlign(-1,-1); + g.clearRect(2,70,40,78); + g.setFont("6x8").drawString(scale, 2, 70); +} + +var rawMax = 0; +var scale = 2000; var MID = (g.getHeight()+80)/2; /* On newer (2v10) firmwares we can subscribe to get HRM events as they happen */ @@ -49,19 +64,27 @@ Bangle.on('HRM-raw', function(v) { h=v; hrmOffset++; if (hrmOffset>g.getWidth()) { - hrmOffset=0; + let thousands = Math.round(rawMax / 1000) * 1000; + if (thousands > scale) scale = thousands; + g.clearRect(0,80,g.getWidth(),g.getHeight()); + updateScale(); + + hrmOffset=0; lastHrmPt = [-100,0]; } - - y = E.clip(btm-(8+v.filt/2000),btm-16,btm); + if (rawMax < v.raw) { + rawMax = v.raw; + } + y = E.clip(btm-(8+v.filt/3000),btm-24,btm); g.setColor(1,0,0).fillRect(hrmOffset,btm, hrmOffset, y); - y = E.clip(btm - (v.raw/45),84,btm); + y = E.clip(btm - (v.raw/scale*84),84,btm); g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y); lastHrmPt = [hrmOffset, y]; if (counter !==undefined) { counter = undefined; g.clearRect(0,24,g.getWidth(),g.getHeight()); + updateHrm(); } }); @@ -76,7 +99,8 @@ function countDown() { g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -g.reset().setFont("6x8",2).setFontAlign(0,0); +g.setColor(g.theme.fg); +g.reset().setFont("6x8",2).setFontAlign(0,-1); g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); countDown(); diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json index 10821d094..c5a5f4f4d 100644 --- a/apps/hrm/metadata.json +++ b/apps/hrm/metadata.json @@ -1,7 +1,7 @@ { "id": "hrm", "name": "Heart Rate Monitor", - "version": "0.09", + "version": "0.10", "description": "Measure your heart rate and see live sensor data", "icon": "heartrate.png", "tags": "health", diff --git a/apps/hrmaccevents/ChangeLog b/apps/hrmaccevents/ChangeLog index 5560f00bc..b8519d272 100644 --- a/apps/hrmaccevents/ChangeLog +++ b/apps/hrmaccevents/ChangeLog @@ -1 +1,5 @@ 0.01: New App! +0.02: Show status info on display + Allow recording to Bangle +0.03: Allow downloading recorded files + Make it work with more BTHRM configs diff --git a/apps/hrmaccevents/README.md b/apps/hrmaccevents/README.md new file mode 100644 index 000000000..ecd619152 --- /dev/null +++ b/apps/hrmaccevents/README.md @@ -0,0 +1,33 @@ +# Record HRM and accelerometer events + +Record events as they happen via bluetooth or to a file. +This app can use [BTHRM](https://banglejs.com/apps/#bthrm) as a reference. + +## Steps for usage + +* (Optional) Install [BTHRM](https://banglejs.com/apps/#bthrm) as reference (use ECG based sensor for best accuracy). + * Configure BTHRM to "Both"-Mode or use a version >= 0.12. This prevents data beeing lost because BTHRM can replace the HRM-events data with BTHRM data. +* Click "Start" in browser. +* Wait until the "Events" number starts to grow, that means there are events recorded. +* Record for some time, since BTHRM and HRM often need some seconds to start getting useful values. Consider 2000 events a useful minimum. +* (Recording to file) Stop the recording with a long press of the button and download log.csv with the Espruino IDE. +* (Recording to browser) Click "Stop" followed by "Save" and store the resulting file on your device. + + +## CSV data format + +The CSV data contains the following columns: + +* Time - Current time (milliseconds since 1970) +* Acc_x,Acc_y,Acc_z - X,Y,Z acceleration in Gs +* HRM_b - BPM figure reported by internal HRM algorithm in Bangle.js +* HRM_c - BPM confidence figure (0..100%) reported by internal HRM algorithm in Bangle.js +* HRM_r - `e.raw` from the `Bangle.on("HRM-raw"` event. This is the value that gets passed to the HRM algorithm. +* HRM_f - `e.filt` from the `Bangle.on("HRM-raw"` event. This is the filtered value that comes from the Bangle's HRM algorithm and which is used for peak detection +* PPG_r - `e.vcPPG` from the `Bangle.on("HRM-raw"` event. This is the PPG value direct from the sensor +* PPG_o - `e.vcPPGoffs` from the `Bangle.on("HRM-raw"` event. This is the PPG offset used to map `e.vcPPG` to `e.raw` so there are no glitches when the exposure values in the sensor change. +* BTHRM - BPM figure from external Bluetooth HRM device (this is our reference BPM) + +## Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/hrmaccevents/custom.html b/apps/hrmaccevents/custom.html index c0098eb12..a5bf9796f 100644 --- a/apps/hrmaccevents/custom.html +++ b/apps/hrmaccevents/custom.html @@ -1,166 +1,286 @@ Bangle.js Accelerometer streaming + - - - - +

+

+ +
+

+

+ + + + + +

+ + + + diff --git a/apps/hworldclock/hsuncalc.js b/apps/hworldclock/hsuncalc.js new file mode 100644 index 000000000..b1af0a0d9 --- /dev/null +++ b/apps/hworldclock/hsuncalc.js @@ -0,0 +1,298 @@ +/* Module suncalc.js + (c) 2011-2015, Vladimir Agafonkin + SunCalc is a JavaScript library for calculating sun/moon position and light phases. + https://github.com/mourner/suncalc + +PB: Usage: +E.setTimeZone(2); // 1 = MEZ, 2 = MESZ +SunCalc = require("suncalc.js"); +pos = SunCalc.getPosition(Date.now(), 53.3, 10.1); +times = SunCalc.getTimes(Date.now(), 53.3, 10.1); +rise = times.sunrise; // Date object +rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm +*/ +var exports={}; + +// 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 new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021 +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) + }; +} + +// calculates sun position for a given date and latitude/longitude + +exports.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: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg + altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg + }; +}; + + +// sun times configuration (angle, morning name, evening name) + +var times = [ + [-0.833, 'sunrise', 'sunset' ] +]; + +// 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 + +exports.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: fromJulian(Jnoon), + nadir: 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]] = fromJulian(Jrise); + result[time[2]] = fromJulian(Jset); + } + + 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 + }; +} + +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. + +getMoonIllumination = function (date) { + + var d = toDays(date || new Date()), + s = sunCoords(d), + m = moonCoords(d), + + sdist = 149598000, // distance from Earth to Sun in km + + phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)), + inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)), + angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) - + cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)); + + return { + fraction: (1 + cos(inc)) / 2, + phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI, + angle: angle + }; +}; + + +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 + +getMoonTimes = function (date, lat, lng, inUTC) { + var t = new Date(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; +}; \ No newline at end of file diff --git a/apps/hworldclock/hworldclock-icon.js b/apps/hworldclock/hworldclock-icon.js new file mode 100644 index 000000000..6e05d254c --- /dev/null +++ b/apps/hworldclock/hworldclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/ABEE+EA4EAj9E8HF//gn/gwP///wt/MgF//8gh/8gYLBwEP+EHAofghgFD4EOj//gEPA4ILBGgIxB/wFBgwFB/lsgCKBj/4oxHBvAFBJoV8gP4TQX+gJUBAAN/Aok+AoVgAoXogAfBjkA8AfBAoXAAoUYY4cAiCDEAooA/ABg")) diff --git a/apps/hworldclock/hworldclock.png b/apps/hworldclock/hworldclock.png new file mode 100644 index 000000000..565e0dc6b Binary files /dev/null and b/apps/hworldclock/hworldclock.png differ diff --git a/apps/hworldclock/metadata.json b/apps/hworldclock/metadata.json new file mode 100644 index 000000000..653cfc59c --- /dev/null +++ b/apps/hworldclock/metadata.json @@ -0,0 +1,25 @@ +{ + "id": "hworldclock", + "name": "Hanks World Clock", + "shortName": "Hanks World Clock", + "version": "0.23", + "description": "Current time zone plus up to three others", + "allow_emulator":true, + "icon": "app.png", + "screenshots": [{"url":"screenshot_hworld.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "storage": [ + {"name":"hworldclock.app.js","url":"app.js"}, + {"name":"hworldclock.img","url":"hworldclock-icon.js","evaluate":true}, + {"name":"hworldclock.settings.js","url":"settings.js"}, + {"name":"hsuncalc.js","url":"hsuncalc.js"} + ], + "data": [ + {"name":"hworldclock.settings.json"}, + {"name":"hworldclock.json"} + ] +} \ No newline at end of file diff --git a/apps/hworldclock/screenshot_hworld.png b/apps/hworldclock/screenshot_hworld.png new file mode 100644 index 000000000..565e0dc6b Binary files /dev/null and b/apps/hworldclock/screenshot_hworld.png differ diff --git a/apps/hworldclock/settings.js b/apps/hworldclock/settings.js new file mode 100644 index 000000000..26c946b5f --- /dev/null +++ b/apps/hworldclock/settings.js @@ -0,0 +1,50 @@ +(function(back) { + var FILE = "hworldclock.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": "Hanks World Clock" + }, + "< Back": () => back(), + "Seconds": stringInSettings("secondsMode", ["always", "when unlocked", "none"]), + "Color w. dark": stringInSettings("colorWhenDark", ["green", "default"]), + "Show SunInfo": { + value: (settings.showSunInfo !== undefined ? settings.showSunInfo : true), + onchange: v => { + settings.showSunInfo = v; + writeSettings(); + } + } + }; + + E.showMenu(mainmenu); + +}); diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog new file mode 100644 index 000000000..991f15abb --- /dev/null +++ b/apps/iconlaunch/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial release +0.02: implemented "direct launch" and "one click exit" settings +0.03: Use default Bangle formatter for booleans diff --git a/apps/iconlaunch/README.md b/apps/iconlaunch/README.md new file mode 100644 index 000000000..0d36fdeb4 --- /dev/null +++ b/apps/iconlaunch/README.md @@ -0,0 +1,12 @@ +# Icon launcher + +A launcher inspired by smartphones, with an icon-only scrollable menu. + +This launcher shows 9 apps per screen, making it much faster to navigate versus the default launcher. + +![A screenshot](screenshot1.png) +![Another screenshot](screenshot2.png) + +## Technical note + +The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function. diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js new file mode 100644 index 000000000..59e9eb0e3 --- /dev/null +++ b/apps/iconlaunch/app.js @@ -0,0 +1,211 @@ +const s = require("Storage"); +const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false }; + +if( settings.oneClickExit) + setWatch(_=> load(), BTN1); + +if (!settings.fullscreen) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +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) + ); +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; +}); +apps.forEach((app) => { + if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area +}); + +let scroll = 0; +let selectedItem = -1; +const R = Bangle.appRect; + +const iconSize = 48; + +const appsN = Math.floor(R.w / iconSize); +const whitespace = (R.w - appsN * iconSize) / (appsN + 1); + +const itemSize = iconSize + whitespace; + +function drawItem(itemI, r) { + g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); + let x = 0; + for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) { + if (!apps[i]) break; + x += whitespace; + if (!apps[i].icon) { + g.setFontAlign(0,0,0).setFont("12x20:2").drawString("?", x + r.x+iconSize/2, r.y + iconSize/2); + } else { + g.drawImage(apps[i].icon, x + r.x, r.y); + } + if (selectedItem == i) { + g.drawRect( + x + r.x - 1, + r.y - 1, + x + r.x + iconSize + 1, + r.y + iconSize + 1 + ); + } + x += iconSize; + } + drawText(itemI); +} + +function drawItemAuto(i) { + var y = idxToY(i); + g.reset().setClipRect(R.x, y, R.x2, y + itemSize); + drawItem(i, { + x: R.x, + y: y, + w: R.w, + h: itemSize + }); + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); +} + +let lastIsDown = false; + +function drawText(i) { + const selectedApp = apps[selectedItem]; + const idy = (selectedItem - (selectedItem % 3)) / 3; + if (!selectedApp || i != idy) return; + const appY = idxToY(idy) + iconSize / 2; + g.setFontAlign(0, 0, 0); + g.setFont("12x20"); + const rect = g.stringMetrics(selectedApp.name); + g.clearRect( + R.w / 2 - rect.width / 2, + appY - rect.height / 2, + R.w / 2 + rect.width / 2, + appY + rect.height / 2 + ); + g.drawString(selectedApp.name, R.w / 2, appY); +} + +function selectItem(id, e) { + const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1); + const appId = id * appsN + iconN; + if( settings.direct && apps[appId]) + { + load(apps[appId].src); + return; + } + if (appId == selectedItem && apps[appId]) { + const app = apps[appId]; + if (!app.src || s.read(app.src) === undefined) { + E.showMessage( /*LANG*/ "App Source\nNot found"); + } else { + load(app.src); + } + } + selectedItem = appId; + drawItems(); +} + +function idxToY(i) { + return i * itemSize + R.y - (scroll & ~1); +} + +function YtoIdx(y) { + return Math.floor((y + (scroll & ~1) - R.y) / itemSize); +} + +function drawItems() { + g.reset().clearRect(R.x, R.y, R.x2, R.y2); + g.setClipRect(R.x, R.y, R.x2, R.y2); + var a = YtoIdx(R.y); + var b = Math.min(YtoIdx(R.y2), 99); + for (var i = a; i <= b; i++) + drawItem(i, { + x: R.x, + y: idxToY(i), + w: R.w, + h: itemSize, + }); + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); +} + +drawItems(); +g.flip(); + +const itemsN = Math.ceil(apps.length / appsN); + +Bangle.setUI({ + mode: "custom", + drag: (e) => { + g.setColor(g.theme.fg); + g.setBgColor(g.theme.bg); + let dy = e.dy; + if (scroll + R.h - dy > itemsN * itemSize) { + dy = scroll + R.h - itemsN * itemSize; + } + if (scroll - dy < 0) { + dy = scroll; + } + scroll -= dy; + scroll = E.clip(scroll, 0, itemSize * (itemsN - 1)); + g.setClipRect(R.x, R.y, R.x2, R.y2); + g.scroll(0, dy); + if (dy < 0) { + g.setClipRect(R.x, R.y2 - (1 - dy), R.x2, R.y2); + let i = YtoIdx(R.y2 - (1 - dy)); + let y = idxToY(i); + while (y < R.y2) { + drawItem(i, { + x: R.x, + y: y, + w: R.w, + h: itemSize, + }); + i++; + y += itemSize; + } + } else { + // d>0 + g.setClipRect(R.x, R.y, R.x2, R.y + dy); + let i = YtoIdx(R.y + dy); + let y = idxToY(i); + while (y > R.y - itemSize) { + drawItem(i, { + x: R.x, + y: y, + w: R.w, + h: itemSize, + }); + y -= itemSize; + i--; + } + } + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); + }, + touch: (_, e) => { + if (e.y < R.y - 4) return; + var i = YtoIdx(e.y); + selectItem(i, e); + }, +}); diff --git a/apps/iconlaunch/app.png b/apps/iconlaunch/app.png new file mode 100644 index 000000000..1c8068c50 Binary files /dev/null and b/apps/iconlaunch/app.png differ diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json new file mode 100644 index 000000000..c7acc534f --- /dev/null +++ b/apps/iconlaunch/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "iconlaunch", + "name": "Icon Launcher", + "shortName" : "Icon launcher", + "version": "0.03", + "icon": "app.png", + "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", + "tags": "tool,system,launcher", + "type": "launch", + "supports": ["BANGLEJS2"], + "storage": [ + { "name": "iconlaunch.app.js", "url": "app.js" }, + { "name": "iconlaunch.settings.js", "url": "settings.js" } + ], + "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }], + "readme": "README.md" +} diff --git a/apps/iconlaunch/screenshot1.png b/apps/iconlaunch/screenshot1.png new file mode 100644 index 000000000..8695ead7a Binary files /dev/null and b/apps/iconlaunch/screenshot1.png differ diff --git a/apps/iconlaunch/screenshot2.png b/apps/iconlaunch/screenshot2.png new file mode 100644 index 000000000..b17efa78b Binary files /dev/null and b/apps/iconlaunch/screenshot2.png differ diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js new file mode 100644 index 000000000..bd1a4a597 --- /dev/null +++ b/apps/iconlaunch/settings.js @@ -0,0 +1,34 @@ +// make sure to enclose the function in parentheses +(function(back) { + let settings = Object.assign({ + showClocks: true, + fullscreen: false + }, require("Storage").readJSON("launch.json", true) || {}); + + let fonts = g.getFonts(); + function save(key, value) { + settings[key] = value; + require("Storage").write("launch.json",settings); + } + const appMenu = { + "": { "title": /*LANG*/"Launcher" }, + /*LANG*/"< Back": back, + /*LANG*/"Show Clocks": { + value: settings.showClocks == true, + onchange: (m) => { save("showClocks", m) } + }, + /*LANG*/"Fullscreen": { + value: settings.fullscreen == true, + onchange: (m) => { save("fullscreen", m) } + }, + /*LANG*/"Direct launch": { + value: settings.direct == true, + onchange: (m) => { save("direct", m) } + }, + /*LANG*/"One click exit": { + value: settings.oneClickExit == true, + onchange: (m) => { save("oneClickExit", m) } + } + }; + E.showMenu(appMenu); +}); diff --git a/apps/imageclock/ChangeLog b/apps/imageclock/ChangeLog new file mode 100644 index 000000000..af1b97b3d --- /dev/null +++ b/apps/imageclock/ChangeLog @@ -0,0 +1,10 @@ +0.01: New App +0.02: Allow drawing polys +0.03: Allow partly importing Amazfit decompiler formatted watchfaces +0.04: Allow writing all image data to separate file to save some RAM + Allow hiding elements on lock +0.05: Add precompilation to js for performance +0.06: Watchfaces can be refreshed partly +0.07: Allow wrapping drawing in timeouts to get faster reactions + Show/Hide widgets with swipe up or down +0.08: Use default Bangle formatter for booleans diff --git a/apps/imageclock/README.md b/apps/imageclock/README.md new file mode 100644 index 000000000..c05c09a16 --- /dev/null +++ b/apps/imageclock/README.md @@ -0,0 +1,297 @@ +# Imageclock + +This app is a highly customizable watchface. To use it, you need to select +a watchface from another source. There is a native format as described here. You can also load decompiled watchfaces for Amazfit BIP fitness trackers. + +# Usage + +## Install a watchface + +Choose the folder which contains the watchface, then clock "Upload to watch". + +## Usage on the watch + +Slide up/down to hide/show widgets. +Press button to start launcher. + +# Design watch faces + +## Folder structure + +* watchfacename + * resources/ + * face.json + * info.json + + +### resources + +This folder contains image files. It can have subfolders. These files will +be read and converted into a resource bundle used by the clock + +Folder types: + +* Number + * Contains files named 0.ext to 9.ext and minus.ext +* CodedImage + * Contains files named with only digits for codes, i.e. 721.ext + * Contains a file named fallback.ext in case no code matches + * Codes are evaluated as follows: 721 -> if not found check 720 -> if not found check 700 -> if found use +* MultiState + * Notifications: sound.ext, silent.ext, vibrate.ext + * other status icons: on.ext, off.ext +* Scale + * Contains the components of the scale, named 0.ext to y.ext, y beeing the last element of the scale + +### face.json + +This file contains the description of the watch face elements. + +#### Object types: + +##### Properties +``` +Properties: { + "Redraw": { + "Unlocked": 5000, + "Locked": 60000, + "Default": "Always", + "Events": ["HRM"], + "Clear": true + }, + "Events": ["lock","HRM"] +} +``` + +Possible values for `Default` are `Always`, `Change`. + +##### Images + +``` +"Image": { + "X": 0, + "Y": 0, + "Scale": 1, + "RotationValue": "Seconds", + "MinRotationValue": 0, + "MaxRotationValue": 60, + "ImagePath": [ "path", "in", "resources", "file" ] +} +``` + +`RotationValue` references one of the implemented numerical values. +Mandatory: +* `ImagePath` + +``` +"Image": { + "X": 0, + "Y": 0, + "Value": "BatteryPercentage", + "Steps": 7, + "ImagePath": [ "path", "in", "resources", "file" ] +} +``` +If `Value` and `Steps`are given, the value is scaled and `Steps` number of images starting at 0 are used to display the value. + +##### Coded Images +``` +"CodedImage": { + "X": 0, + "Y": 0, + "Value": "WeatherCode", + "ImagePath": [ "path", "in", "resources", "file" ] +} +``` +The `Value` field is one of the implemented numerical values. + +##### Number + +Can be aligned to bottom left, top left, bottom right, top right and center. Will currently force all numbers to +be integer. + +``` +"Number": { + "X": 123, + "Y": 123, + "Alignment": "BottomRight", + "Value": "Temperature", + "Spacing": 1, + "ImagePath": [ "path", "to", "numbers", "folder" ] +} +``` +The `Value` field is one of the implemented numerical values. +`Alignment` is either `BottomRight` or `TopLeft` + +Mandatory: +* `ImagePath` +* `Value` + +##### Scale + +``` +"Scale": { + "X": 123, + "Y": 123, + "Value": "Temperature", + "MinValue": "-20", + "MaxValue": "50", + "ImagePath": [ "path", "to", "scale", "folder" ], + "Segments": [ + { "X": 5, "Y": 5}, + { "X": 10, "Y": 10 } + ] +} +``` +The `Value` field is one of the implemented numerical values. +`MaxValue` and `MinValue` set the start and endpoints of the scale. + +Mandatory: +* `ImagePath` +* `Value` + +##### MultiState + +``` +"MultiState": { + "X": 0, + "Y": 0, + "Value": "Lock", + "ImagePath": ["icons", "status", "lock"] +} +``` +The `Value` field is one of the implemented multi state values. + +Mandatory: +* `ImagePath` +* `Value` + +##### Poly + +``` +"Poly":{ + "Filled": true, + "RotationValue": "Second", + "MinRotationValue": "0", + "MaxRotationValue": "60", + "ForegroundColor": "#00f", + "BackgroundColor": "#008", + "Vertices":[ + {"X":-1, "Y":13}, + {"X":0, "Y":15}, + {"X":1, "Y":13}, + {"X":2, "Y":0}, + {"X":1, "Y":-75}, + {"X":0, "Y":-80}, + {"X":-1, "Y":-75}, + {"X":-2, "Y":0} + ] +} +``` +The `RotationValue` field is one of the implemented numeric values. + +##### Rect + +``` +"Rect":{ + "X": 10, + "Y": 20, + "Width": 30, + "Height": 40, + "Filled": true, + "ForegroundColor": "#00f", + "BackgroundColor": "#008" +} +``` +The `RotationValue` field is one of the implemented numeric values. + +##### Nesting +``` +"Container": { + "X": 10, + "Y": 10, + "OtherContainer": { + "X": 5, + "Y": 5, + "SomeElement": { + "X": 2, + "Y": 2, + + } + } +} +``` +`SomeElement` will be drawn at X- and Y-position 2+5+10=17, because positions are relative to parent element. +Container names can be everything but other object names. + +#### Implemented data sources + +##### Numerical + +* Hour +* Hour12Analog +* Hour12 +* HourTens +* HourOnes +* Minute +* MinuteAnalog +* MinuteTens +* MinuteOnes +* Second +* SecondAnalog +* SecondTens +* SecondOnes +* WeekDay +* WeekDayMondayFirst +* Day +* DayTens +* DayOnes +* Month +* MonthTens +* MonthOnes +* Pulse +* Steps +* Temperature +* Pressure +* Altitude +* BatteryPercentage +* BatteryVoltage +* StepsGoal +* WeatherCode +* WeatherTemperature + +##### Multistate + +* on/off + * Lock + * Charge + * Alarm + * Bluetooth + * BluetoothPeripheral + * HRM + * Barometer + * Compass + * GPS + * StepsGoal + * WeatherTemperatureNegative +* on/off/vibrate + * Notifications +* celsius/fahrenheit/unknown + * WeatherTemperatureUnit + +### info.json + +This file contains information for the conversion process, it will not be +stored on the watch + +# TODO + +* Handle events and redraws better +* Performance improvements + * Mark elements with how often they need to be redrawn +* Finalize the file format +* Localization + +# Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/imageclock/app-icon.js b/apps/imageclock/app-icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/imageclock/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/imageclock/app.js b/apps/imageclock/app.js new file mode 100644 index 000000000..7b933b710 --- /dev/null +++ b/apps/imageclock/app.js @@ -0,0 +1,774 @@ +var watchface = require("Storage").readJSON("imageclock.face.json"); +var watchfaceResources = require("Storage").readJSON("imageclock.resources.json"); +var precompiledJs = eval(require("Storage").read("imageclock.draw.js")); +var settings = require('Storage').readJSON("imageclock.json", true) || {}; + +var performanceLog = {}; + +var startPerfLog = () => {}; +var endPerfLog = () => {}; +var printPerfLog = () => print("Deactivated"); +var resetPerfLog = () => {performanceLog = {};}; + +var colormap={ +"#000":0, +"#00f":1, +"#0f0":2, +"#0ff":3, +"#f00":4, +"#f0f":5, +"#ff0":6, +"#fff":7 +}; + +var palette = new Uint16Array([ +0x0000, //black #000 +0x001f, //blue #00f +0x07e0, //green #0f0 +0x07ff, //cyan #0ff +0xf800, //red #f00 +0xf81f, //magenta #f0f +0xffe0, //yellow #ff0 +0xffff, //white #fff +0xffff, //white +0xffff, //white +0xffff, //white +0xffff, //white +0xffff, //white +0xffff, //white +0xffff, //white +0xffff, //white +]) + +var p0 = g; +var p1; + +if (settings.perflog){ + startPerfLog = function(name){ + var time = getTime(); + if (!performanceLog.start) performanceLog.start={}; + performanceLog.start[name] = time; + }; + endPerfLog = function (name){ + var time = getTime(); + if (!performanceLog.last) performanceLog.last={}; + var duration = time - performanceLog.start[name]; + performanceLog.last[name] = duration; + if (!performanceLog.cum) performanceLog.cum={}; + if (!performanceLog.cum[name]) performanceLog.cum[name] = 0; + performanceLog.cum[name] += duration; + if (!performanceLog.count) performanceLog.count={}; + if (!performanceLog.count[name]) performanceLog.count[name] = 0; + performanceLog.count[name]++; + }; + + printPerfLog = function(){ + var result = ""; + var keys = []; + for (var c in performanceLog.cum){ + keys.push(c); + } + keys.sort(); + for (var k of keys){ + print(k, "last:", (performanceLog.last[k] * 1000).toFixed(0), "average:", (performanceLog.cum[k]/performanceLog.count[k]*1000).toFixed(0), "count:", performanceLog.count[k], "total:", (performanceLog.cum[k] * 1000).toFixed(0)); + } + }; +} + +function delay(t) { + return new Promise(function (resolve) { + setTimeout(resolve, t); + }); +} + +function prepareImg(resource){ + startPerfLog("prepareImg"); + //print("prepareImg: ", resource); + + if (resource.dataOffset !== undefined){ + resource.buffer = E.toArrayBuffer(require("Storage").read("imageclock.resources.data", resource.dataOffset, resource.dataLength)); + delete resource.dataOffset; + delete resource.dataLength; + if (resource.paletteData){ + result.palette = new Uint16Array(resource.paletteData); + delete resource.paletteData; + } + } + endPerfLog("prepareImg"); + return resource; +} + +function getByPath(object, path, lastElem){ + startPerfLog("getByPath"); + //print("getByPath", path,lastElem); + var current = object; + if (path.length) { + for (var c of path){ + if (!current[c]) return undefined; + current = current[c]; + } + } + if (lastElem!==undefined){ + if (!current["" + lastElem]) return undefined; + //print("Found by lastElem", lastElem); + current = current["" + lastElem]; + } + endPerfLog("getByPath"); + if (typeof current == "function"){ + //print("current was function"); + return undefined; + } + return current; +} + +function splitNumberToDigits(num){ + return String(num).split('').map(item => Number(item)); +} + +function isChangedNumber(element){ + return element.lastDrawnValue != getValue(element.Value); +} + +function isChangedMultistate(element){ + return element.lastDrawnValue != getMultistate(element.Value); +} + +function drawNumber(graphics, resources, element){ + startPerfLog("drawNumber"); + var number = getValue(element.Value); + var spacing = element.Spacing ? element.Spacing : 0; + var unit = element.Unit; + + var imageIndexMinus = element.ImageIndexMinus; + var imageIndexUnit = element.ImageIndexUnit; + var numberOfDigits = element.Digits; + + + //print("drawNumber: ", number, element); + if (number) number = number.toFixed(0); + + var isNegative; + var digits; + if (number == undefined){ + isNegative = true; + digits = []; + numberOfDigits = 0; + } else { + isNegative = number < 0; + if (isNegative) number *= -1; + digits = splitNumberToDigits(number); + } + + //print("digits: ", digits); + if (!numberOfDigits) numberOfDigits = digits.length; + var firstDigitX = element.X; + var firstDigitY = element.Y; + var imageIndex = element.ImageIndex ? element.ImageIndex : 0; + + var firstImage; + if (imageIndex){ + firstImage = getByPath(resources, [], "" + (0 + imageIndex)); + } else { + firstImage = getByPath(resources, element.ImagePath, 0); + } + + var minusImage; + if (imageIndexMinus){ + minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus)); + } else { + minusImage = getByPath(resources, element.ImagePath, "minus"); + } + + var unitImage; + //print("Get image for unit", imageIndexUnit); + if (imageIndexUnit !== undefined){ + unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit)); + //print("Unit image is", unitImage); + } else if (element.Unit){ + unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown")); + } + + var numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing); + if (isNegative && minusImage){ + //print("Adding to width", minusImage); + numberWidth += minusImage.width + spacing; + } + if (unitImage){ + //print("Adding to width", unitImage); + numberWidth += unitImage.width + spacing; + } + //print("numberWidth:", numberWidth); + + if (element.Alignment == "Center") { + firstDigitX = Math.round(element.X - (numberWidth/2)) + 1; + firstDigitY = Math.round(element.Y - (firstImage.height/2)) + 1; + } else if (element.Alignment == "BottomRight"){ + firstDigitX = element.X - numberWidth + 1; + firstDigitY = element.Y - firstImage.height + 1; + } else if (element.Alignment == "TopRight") { + firstDigitX = element.X - numberWidth + 1; + firstDigitY = element.Y; + } else if (element.Alignment == "BottomLeft") { + firstDigitX = element.X; + firstDigitY = element.Y - firstImage.height + 1; + } + + var currentX = firstDigitX; + if (isNegative && minusImage){ + //print("Draw minus at", currentX); + if (imageIndexMinus){ + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexMinus)); + } else { + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "minus"); + } + currentX += minusImage.width + spacing; + } + for (var d = 0; d < numberOfDigits; d++){ + var currentDigit; + var difference = numberOfDigits - digits.length; + if (d >= difference){ + currentDigit = digits[d-difference]; + } else { + currentDigit = 0; + } + //print("Digit " + currentDigit + " " + currentX); + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, currentDigit + imageIndex); + currentX += firstImage.width + spacing; + } + if (imageIndexUnit){ + //print("Draw unit at", currentX); + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, "" + (0 + imageIndexUnit)); + } else if (element.Unit){ + drawElement(graphics, resources, {X:currentX,Y:firstDigitY}, element, getMultistate(element.Unit,"unknown")); + } + element.lastDrawnValue = number; + + endPerfLog("drawNumber"); +} + +function drawElement(graphics, resources, pos, element, lastElem){ + startPerfLog("drawElement"); + var cacheKey = "_"+(lastElem?lastElem:"nole"); + if (!element.cachedImage) element.cachedImage={}; + if (!element.cachedImage[cacheKey]){ + var resource = getByPath(resources, element.ImagePath, lastElem); + if (resource){ + prepareImg(resource); + //print("lastElem", typeof resource) + if (resource) { + element.cachedImage[cacheKey] = resource; + //print("cache res ",typeof element.cachedImage[cacheKey]); + } else { + element.cachedImage[cacheKey] = null; + //print("cache null",typeof element.cachedImage[cacheKey]); + //print("Could not create image from", resource); + } + } else { + //print("Could not get resource from", element, lastElem); + } + } + + //print("cache ",typeof element.cachedImage[cacheKey], element.ImagePath, lastElem); + if(element.cachedImage[cacheKey]){ + //print("drawElement ",pos, path, lastElem); + //print("resource ", resource,pos, path, lastElem); + //print("drawImage from drawElement", image, pos); + var options={}; + if (element.RotationValue){ + options.rotate = radians(element); + } + if (element.Scale){ + options.scale = element.ScaleValue; + } + //print("options", options); + //print("Memory before drawing", process.memory(false)); + startPerfLog("drawElement_g.drawImage"); + try{ + graphics.drawImage(element.cachedImage[cacheKey] ,(pos.X ? pos.X : 0), (pos.Y ? pos.Y : 0), options);} catch (e) { + //print("Error", e, element.cachedImage[cacheKey]); + } + endPerfLog("drawElement_g.drawImage"); + } + endPerfLog("drawElement"); +} + +function getValue(value, defaultValue){ + if (typeof value == "string"){ + return numbers[value](); + } + if (value == undefined) return defaultValue; + return value; +} + +function getMultistate(name, defaultValue){ + if (typeof name == "string"){ + return multistates[name](); + } else { + if (name == undefined) return defaultValue; + } + return undefined; +} + +function drawScale(graphics, resources, scale){ + startPerfLog("drawScale"); + //print("drawScale", scale); + var segments = scale.Segments; + var imageIndex = scale.ImageIndex !== undefined ? scale.ImageIndex : 0; + + var value = scaledown(scale.Value, scale.MinValue, scale.MaxValue); + + //print("Value is ", value, "(", maxValue, ",", minValue, ")"); + + var segmentsToDraw = Math.ceil(value * segments.length); + + for (var i = 0; i < segmentsToDraw; i++){ + drawElement(graphics, resources, segments[i], scale, imageIndex + i); + } + scale.lastDrawnValue = segmentsToDraw; + + endPerfLog("drawScale"); +} + +function drawImage(graphics, resources, image, name){ + startPerfLog("drawImage"); + //print("drawImage", image.X, image.Y, name); + if (image.Value && image.Steps){ + var steps = Math.floor(scaledown(image.Value, image.MinValue, image.MaxValue) * (image.Steps - 1)); + //print("Step", steps, "of", image.Steps); + drawElement(graphics, resources, image, image, "" + steps); + } else if (image.ImageIndex !== undefined) { + drawElement(graphics, resources, image, image, image.ImageIndex); + } else { + drawElement(graphics, resources, image, image, name ? "" + name: undefined); + } + + endPerfLog("drawImage"); +} + +function drawCodedImage(graphics, resources, image){ + startPerfLog("drawCodedImage"); + var code = getValue(image.Value); + //print("drawCodedImage", image, code); + + if (image.ImagePath) { + var factor = 1; + var currentCode = code; + while (code / factor > 1){ + currentCode = Math.floor(currentCode/factor)*factor; + //print("currentCode", currentCode); + if (getByPath(resources, image.ImagePath, currentCode)){ + break; + } + factor *= 10; + } + if (code / factor > 1){ + //print("found match"); + drawImage(graphics, resources, image, currentCode); + } else { + //print("fallback"); + drawImage(graphics, resources, image, "fallback"); + } + } + image.lastDrawnValue = code; + + startPerfLog("drawCodedImage"); +} + +function getWeatherCode(){ + var jsonWeather = require("Storage").readJSON('weather.json'); + var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; + + if (weather && weather.code){ + return weather.code; + } + return undefined; +} + +function getWeatherTemperature(){ + var jsonWeather = require("Storage").readJSON('weather.json'); + var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined; + + var result = { unit: "unknown"}; + if (weather && weather.temp){ + //print("Weather is", weather); + var temp = require('locale').temp(weather.temp-273.15); + result.value = Number(temp.match(/[\d\-]*/)[0]); + var unit; + if (temp.includes("C")){ + result.unit = "celsius"; + } else if (temp.includes("F")){ + result.unit = "fahrenheit"; + } + } + return result; +} + +function scaledown(value, min, max){ + //print("scaledown", value, min, max); + var scaled = E.clip(getValue(value),getValue(min,0),getValue(max,1)); + scaled -= getValue(min,0); + scaled /= getValue(max,1); + return scaled; +} + +function radians(rotation){ + var value = scaledown(rotation.RotationValue, rotation.MinRotationValue, rotation.MaxRotationValue); + value -= rotation.RotationOffset ? rotation.RotationOffset : 0; + value *= 360; + value *= Math.PI / 180; + return value; +} + +function drawPoly(graphics, resources, element){ + startPerfLog("drawPoly"); + var vertices = []; + + startPerfLog("drawPoly_transform"); + for (var c of element.Vertices){ + vertices.push(c.X); + vertices.push(c.Y); + } + var transform = { x: element.X ? element.X : 0, + y: element.Y ? element.Y : 0 + }; + if (element.RotationValue){ + transform.rotate = radians(element); + } + vertices = graphics.transformVertices(vertices, transform); + + endPerfLog("drawPoly_transform"); + + if (element.Filled){ + startPerfLog("drawPoly_g.fillPoly"); + graphics.fillPoly(vertices,true); + endPerfLog("drawPoly_g.fillPoly"); + } else { + startPerfLog("drawPoly_g.drawPoly"); + graphics.drawPoly(vertices,true); + endPerfLog("drawPoly_g.drawPoly"); + } + + endPerfLog("drawPoly"); +} + +function drawRect(graphics, resources, element){ + startPerfLog("drawRect"); + var vertices = []; + + if (element.Filled){ + startPerfLog("drawRect_g.fillRect"); + graphics.fillRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); + endPerfLog("drawRect_g.fillRect"); + } else { + startPerfLog("drawRect_g.fillRect"); + graphics.drawRect(element.X, element.Y, element.X + element.Width, element.Y + element.Height); + endPerfLog("drawRect_g.fillRect"); + } + endPerfLog("drawRect"); +} + +function drawCircle(graphics, resources, element){ + startPerfLog("drawCircle"); + + if (element.Filled){ + startPerfLog("drawCircle_g.fillCircle"); + graphics.fillCircle(element.X, element.Y, element.Radius); + endPerfLog("drawCircle_g.fillCircle"); + } else { + startPerfLog("drawCircle_g.drawCircle"); + graphics.drawCircle(element.X, element.Y, element.Radius); + endPerfLog("drawCircle_g.drawCircle"); + } + endPerfLog("drawCircle"); +} + +var numbers = {}; +numbers.Hour = () => { return new Date().getHours(); }; +numbers.HourTens = () => { return Math.floor(new Date().getHours()/10); }; +numbers.HourOnes = () => { return Math.floor(new Date().getHours()%10); }; +numbers.Hour12 = () => { return new Date().getHours()%12; }; +numbers.Hour12Analog = () => { var date = new Date(); return date.getHours()%12 + (date.getMinutes()/59); }; +numbers.Hour12Tens = () => { return Math.floor((new Date().getHours()%12)/10); }; +numbers.Hour12Ones = () => { return Math.floor((new Date().getHours()%12)%10); }; +numbers.Minute = () => { return new Date().getMinutes(); }; +numbers.MinuteAnalog = () => { var date = new Date(); return date.getMinutes() + (date.getSeconds()/59); }; +numbers.MinuteTens = () => { return Math.floor(new Date().getMinutes()/10); }; +numbers.MinuteOnes = () => { return Math.floor(new Date().getMinutes()%10); }; +numbers.Second = () => { return new Date().getSeconds(); }; +numbers.SecondAnalog = () => { var date = new Date(); return date.getSeconds() + (date.getMilliseconds()/999); }; +numbers.SecondTens = () => { return Math.floor(new Date().getSeconds()/10); }; +numbers.SecondOnes = () => { return Math.floor(new Date().getSeconds()%10); }; +numbers.WeekDay = () => { return new Date().getDay(); }; +numbers.WeekDayMondayFirst = () => { var day = (new Date().getDay() - 1); if (day < 0) day = 7 + day; return day; }; +numbers.Day = () => { return new Date().getDate(); }; +numbers.DayTens = () => { return Math.floor(new Date().getDate()/10); }; +numbers.DayOnes = () => { return Math.floor(new Date().getDate()%10); }; +numbers.Month = () => { return new Date().getMonth() + 1; }; +numbers.MonthTens = () => { return Math.floor((new Date().getMonth() + 1)/10); }; +numbers.MonthOnes = () => { return Math.floor((new Date().getMonth() + 1)%10); }; +numbers.Pulse = () => { return pulse; }; +numbers.Steps = () => { return Bangle.getHealthStatus ? Bangle.getHealthStatus("day").steps : undefined; }; +numbers.StepsGoal = () => { return settings.stepsgoal || 10000; }; +numbers.Temperature = () => { return temp; }; +numbers.Pressure = () => { return press; }; +numbers.Altitude = () => { return alt; }; +numbers.BatteryPercentage = E.getBattery; +numbers.BatteryVoltage = NRF.getBattery; +numbers.WeatherCode = getWeatherCode; +numbers.WeatherTemperature = () => { return getWeatherTemperature().value; }; + +var multistates = {}; +multistates.Lock = () => { return Bangle.isLocked() ? "on" : "off"; }; +multistates.Charge = () => { return Bangle.isCharging() ? "on" : "off"; }; +multistates.Notifications = () => { return ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "off" : "vibrate"; }; +multistates.Alarm = () => { return (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"; }; +multistates.Bluetooth = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; +//TODO: Implement peripheral connection status +multistates.BluetoothPeripheral = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; }; +multistates.HRM = () => { return Bangle.isHRMOn ? "on" : "off"; }; +multistates.Barometer = () => { return Bangle.isBarometerOn() ? "on" : "off"; }; +multistates.Compass = () => { return Bangle.isCompassOn() ? "on" : "off"; }; +multistates.GPS = () => { return Bangle.isGPSOn() ? "on" : "off"; }; +multistates.WeatherTemperatureNegative = () => { return getWeatherTemperature().value ? getWeatherTemperature().value : 0 < 0; }; +multistates.WeatherTemperatureUnit = () => { return getWeatherTemperature().unit; }; +multistates.StepsGoal = () => { return (numbers.Steps() >= (settings.stepsgoal || 10000)) ? "on": "off"; }; + +function drawMultiState(graphics, resources, element){ + startPerfLog("drawMultiState"); + //print("drawMultiState", element); + var value = multistates[element.Value](); + //print("drawImage from drawMultiState", element, value); + drawImage(graphics, resources, element, value); + element.lastDrawnValue = value; + endPerfLog("drawMultiState"); +} + +var pulse,alt,temp,press; + + +var requestedDraws = 0; +var isDrawing = false; + +var drawingTime; + +var start; + +function initialDraw(resources, face){ + //print("Free memory", process.memory(false).free); + requestedDraws++; + if (!isDrawing){ + //print(new Date().toISOString(), "Can draw,", requestedDraws, "draws requested so far"); + isDrawing = true; + resetPerfLog(); + requestedDraws = 0; + //print(new Date().toISOString(), "Drawing start"); + startPerfLog("initialDraw"); + //start = Date.now(); + drawingTime = 0; + //print("Precompiled"); + var promise = precompiledJs(watchfaceResources, watchface); + + promise.then(()=>{ + var currentDrawingTime = Date.now(); + if (showWidgets){ + //print("Draw widgets"); + Bangle.drawWidgets(); + g.setColor(g.theme.fg); + g.drawLine(0,24,g.getWidth(),24); + } + lastDrawTime = Date.now() - start; + drawingTime += Date.now() - currentDrawingTime; + //print(new Date().toISOString(), "Drawing done in", lastDrawTime.toFixed(0), "active:", drawingTime.toFixed(0)); + isDrawing=false; + firstDraw=false; + requestRefresh = false; + endPerfLog("initialDraw"); + }).catch((e)=>{ + print("Error during drawing", e); + }); + + if (requestedDraws > 0){ + //print(new Date().toISOString(), "Had deferred drawing left, drawing again"); + requestedDraws = 0; + setTimeout(()=>{initialDraw(resources, face);}, 10); + } + } //else { + //print("queued draw"); + //} +} + +function handleHrm(e){ + if (e.confidence > 70){ + pulse = e.bpm; + if (!redrawEvents || redrawEvents.includes("HRM") && !Bangle.isLocked()){ + //print("Redrawing on HRM"); + initialDraw(watchfaceResources, watchface); + } + } +} + +function handlePressure(e){ + alt = e.altitude; + temp = e.temperature; + press = e.pressure; + if (!redrawEvents || redrawEvents.includes("pressure") && !Bangle.isLocked()){ + //print("Redrawing on pressure"); + initialDraw(watchfaceResources, watchface); + } +} + +function handleCharging(e){ + if (!redrawEvents || redrawEvents.includes("charging") && !Bangle.isLocked()){ + //print("Redrawing on charging"); + initialDraw(watchfaceResources, watchface); + } +} + + +function getMatchedWaitingTime(time){ + var result = time - (Date.now() % time); + //print("Matched timeout", time, result); + return result; +} + + + +function setMatchedInterval(callable, time, intervalHandler, delay){ + //print("Setting matched interval for", time); + var matchedTime = getMatchedWaitingTime(time + delay); + setTimeout(()=>{ + var interval = setInterval(callable, time); + if (intervalHandler) intervalHandler(interval); + callable(); + }, matchedTime); +} + +var unlockedDrawInterval; +var lockedDrawInterval; + +var lastDrawTime = 0; +var firstDraw = true; + +var lockedRedraw = getByPath(watchface, ["Properties","Redraw","Locked"]) || 60000; +var unlockedRedraw = getByPath(watchface, ["Properties","Redraw","Unlocked"]) || 1000; +var defaultRedraw = getByPath(watchface, ["Properties","Redraw","Default"]) || "Always"; +var redrawEvents = getByPath(watchface, ["Properties","Redraw","Events"]); +var clearOnRedraw = getByPath(watchface, ["Properties","Redraw","Clear"]); +var events = getByPath(watchface, ["Properties","Events"]); + +//print("events", events); +//print("redrawEvents", redrawEvents); + +function handleLock(isLocked, forceRedraw){ + //print("isLocked", Bangle.isLocked()); + if (lockedDrawInterval) clearInterval(lockedDrawInterval); + if (unlockedDrawInterval) clearInterval(unlockedDrawInterval); + if (!isLocked){ + if (forceRedraw || !redrawEvents || (redrawEvents.includes("unlock"))){ + //print("Redrawing on unlock", isLocked); + initialDraw(watchfaceResources, watchface); + } + setMatchedInterval(()=>{ + //print("Redrawing on unlocked interval"); + initialDraw(watchfaceResources, watchface); + },unlockedRedraw, (v)=>{ + unlockedDrawInterval = v; + }, lastDrawTime); + if (!events || events.includes("HRM")) Bangle.setHRMPower(1, "imageclock"); + if (!events || events.includes("pressure")) Bangle.setBarometerPower(1, 'imageclock'); + } else { + if (forceRedraw || !redrawEvents || (redrawEvents.includes("lock"))){ + //print("Redrawing on lock", isLocked); + initialDraw(watchfaceResources, watchface); + } + setMatchedInterval(()=>{ + //print("Redrawing on locked interval"); + initialDraw(watchfaceResources, watchface); + },lockedRedraw, (v)=>{ + lockedDrawInterval = v; + }, lastDrawTime); + Bangle.setHRMPower(0, "imageclock"); + Bangle.setBarometerPower(0, 'imageclock'); + } +} + + +var showWidgets = false; +var showWidgetsChanged = false; +var currentDragDistance = 0; + +Bangle.setUI("clock"); +Bangle.on('drag', (e)=>{ + currentDragDistance += e.dy; + if (Math.abs(currentDragDistance) < 10) return; + dragDown = currentDragDistance > 0; + currentDragDistance = 0; + if (!showWidgets && dragDown){ + //print("Enable widgets"); + if (WIDGETS && typeof WIDGETS === "object") { + for (let w in WIDGETS) { + var wd = WIDGETS[w]; + wd.draw = originalWidgetDraw[w]; + wd.area = originalWidgetArea[w]; + } + } + showWidgetsChanged = true; + } + if (showWidgets && !dragDown){ + //print("Disable widgets"); + clearWidgetsDraw(); + firstDraw = true; + showWidgetsChanged = true; + } + if (showWidgetsChanged){ + showWidgetsChanged = false; + //print("Draw after widget change"); + showWidgets = dragDown; + initialDraw(); + } + } +); + +if (!events || events.includes("pressure")){ + Bangle.on('pressure', handlePressure); + try{ + Bangle.setBarometerPower(1, 'imageclock'); + } catch (e){ + print("Error during barometer power up", e); + } +} +if (!events || events.includes("HRM")) { + Bangle.on('HRM', handleHrm); + Bangle.setHRMPower(1, "imageclock"); +} +if (!events || events.includes("lock")) { + Bangle.on('lock', handleLock); +} +if (!events || events.includes("charging")) { + Bangle.on('charging', handleCharging); +} + +var originalWidgetDraw = {}; +var originalWidgetArea = {}; + +function clearWidgetsDraw(){ + //print("Clear widget draw calls"); + if (WIDGETS && typeof WIDGETS === "object") { + originalWidgetDraw = {}; + originalWidgetArea = {}; + for (let w in WIDGETS) { + var wd = WIDGETS[w]; + originalWidgetDraw[w] = wd.draw; + originalWidgetArea[w] = wd.area; + wd.draw = () => {}; + wd.area = ""; + } + } +} + +setTimeout(()=>{ + Bangle.loadWidgets(); + clearWidgetsDraw(); +}, 0); + +handleLock(Bangle.isLocked()); diff --git a/apps/imageclock/app.png b/apps/imageclock/app.png new file mode 100644 index 000000000..cf057046b Binary files /dev/null and b/apps/imageclock/app.png differ diff --git a/apps/imageclock/custom.html b/apps/imageclock/custom.html new file mode 100644 index 000000000..af7a1835f --- /dev/null +++ b/apps/imageclock/custom.html @@ -0,0 +1,1144 @@ + + + + + + + + + + + +

Upload watchface:

+ +

+ Select format:
+ +
+ +
+

+ +

+ Options:
+ +
+ +
+

+ +

Select watchface folder:

+

or

+

Select watchface zip file:


+ +
+
+
+
+
+

Download Demo Watchface here:
+ digitalretro.zip
+ simpleanalog.zip
+

+ + + + + diff --git a/apps/imageclock/demomode.js b/apps/imageclock/demomode.js new file mode 100644 index 000000000..8c9d19195 --- /dev/null +++ b/apps/imageclock/demomode.js @@ -0,0 +1,29 @@ +var demostate = 0; +function demoMode(){ + lockedRedraw = 2000; + unlockedRedraw = 2000; + for (var c in multistates){ + multistates[c] = ()=>{return ["on","off"][demostate%2];}; + if (c == "WeatherTemperatureUnit") multistates[c] = ()=>{return ["celsius","fahrenheit"][demostate%2];}; + if (c == "Notifications") multistates[c] = ()=>{return ["on","off","vibrate"][demostate%3];}; + } + for (var c in numbers){ + if (c.contains("Minute")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);}; + if (c.contains("Second")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);}; + if (c.contains("Hour")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);}; + } + for (var c in numbers){ + if (c.contains("Ones")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);}; + if (c.contains("Tens")) numbers[c] = ()=>{return Math.floor((Math.random() * 9) + 1);}; + } + numbers.Pulse = ()=>{return Math.floor((Math.random() * 60) + 40);}; + numbers.Steps = ()=>{return Math.floor((Math.random() * 10000) + 10);}; + numbers.Temperature = ()=>{return Math.floor((Math.random() * 15) + 10);}; + numbers.Pressure = ()=>{return Math.floor((Math.random() * 1000) + 10);}; + numbers.Altitude = ()=>{return Math.floor((Math.random() * 1000) + 10);}; + numbers.WeatherCode = ()=>{return Math.floor((Math.random() * 800) + 100);}; + numbers.WeatherTemperature = ()=>{return Math.floor((Math.random() * 10) + 0);}; +} +demoMode(); +handleLock(false); +setInterval(()=>{demostate++;},1000); diff --git a/apps/imageclock/digitalretro.zip b/apps/imageclock/digitalretro.zip new file mode 100644 index 000000000..96ab90b6d Binary files /dev/null and b/apps/imageclock/digitalretro.zip differ diff --git a/apps/imageclock/metadata.json b/apps/imageclock/metadata.json new file mode 100644 index 000000000..c3ece0184 --- /dev/null +++ b/apps/imageclock/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "imageclock", + "name": "Imageclock", + "shortName": "Imageclock", + "version": "0.08", + "type": "clock", + "description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.", + "icon": "app.png", + "tags": "clock", + "supports": ["BANGLEJS2"], + "custom": "custom.html", + "customConnect": false, + "readme": "README.md", + "storage": [ + {"name":"imageclock.app.js","url":"app.js"}, + {"name":"imageclock.settings.js","url":"settings.js"}, + {"name":"imageclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/imageclock/settings.js b/apps/imageclock/settings.js new file mode 100644 index 000000000..a0c1ee9d2 --- /dev/null +++ b/apps/imageclock/settings.js @@ -0,0 +1,34 @@ +(function(back) { + var FILE = "imageclock.json"; + + var settings = Object.assign({ + stepsgoal: 10000, + perflog: false + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + E.showMenu({ + '': { 'title': 'Imageclock' }, + '< Back': back, + 'Steps goal': { + value: settings.stepsgoal, + min: 0, + step: 500, + max: 50000, + onchange: v => { + settings.stepsgoal = v; + writeSettings(); + } + }, + 'Performance log': { + value: !!settings.perflog, + onchange: v => { + settings.perflog = v; + writeSettings(); + } + } + }); +}) diff --git a/apps/imageclock/simpleanalog.zip b/apps/imageclock/simpleanalog.zip new file mode 100644 index 000000000..1301be055 Binary files /dev/null and b/apps/imageclock/simpleanalog.zip differ diff --git a/apps/imgclock/ChangeLog b/apps/imgclock/ChangeLog index 01a6a4248..0895bb66d 100644 --- a/apps/imgclock/ChangeLog +++ b/apps/imgclock/ChangeLog @@ -7,3 +7,5 @@ 0.06: Support 12 hour time 0.07: Don't cut off wide date formats 0.08: Use Bangle.setUI for button/launcher handling +0.09: Bangle.js 2 compatibility +0.10: Tell clock widgets to hide. diff --git a/apps/imgclock/app.js b/apps/imgclock/app.js index 0e4435638..7d74bee82 100644 --- a/apps/imgclock/app.js +++ b/apps/imgclock/app.js @@ -10,8 +10,8 @@ var IX = inf.x, IY = inf.y, IBPP = inf.bpp; var IW = 174, IH = 45, OY = 24; var bgwidth = img.charCodeAt(0); var bgoptions; -if (bgwidth<240) - bgoptions = { scale : 240/bgwidth }; +if (bgwidth{ draw(); } }); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/imgclock/b2_122240.png b/apps/imgclock/b2_122240.png new file mode 100644 index 000000000..1a3f4daaa Binary files /dev/null and b/apps/imgclock/b2_122240.png differ diff --git a/apps/imgclock/b2_122271.png b/apps/imgclock/b2_122271.png new file mode 100644 index 000000000..31733fb2c Binary files /dev/null and b/apps/imgclock/b2_122271.png differ diff --git a/apps/imgclock/b2_explode.png b/apps/imgclock/b2_explode.png new file mode 100644 index 000000000..5252bbcd2 Binary files /dev/null and b/apps/imgclock/b2_explode.png differ diff --git a/apps/imgclock/b2_thisisfine.png b/apps/imgclock/b2_thisisfine.png new file mode 100644 index 000000000..1b7daaf60 Binary files /dev/null and b/apps/imgclock/b2_thisisfine.png differ diff --git a/apps/imgclock/custom.html b/apps/imgclock/custom.html index 2511f8a54..1d8e06c07 100644 --- a/apps/imgclock/custom.html +++ b/apps/imgclock/custom.html @@ -5,6 +5,7 @@
+ Please wait...
@@ -12,13 +13,51 @@ diff --git a/apps/imgclock/metadata.json b/apps/imgclock/metadata.json index 799d11acc..94dff5f17 100644 --- a/apps/imgclock/metadata.json +++ b/apps/imgclock/metadata.json @@ -2,18 +2,19 @@ "id": "imgclock", "name": "Image background clock", "shortName": "Image Clock", - "version": "0.08", + "version": "0.10", "description": "A clock with an image as a background", "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", + "customConnect": true, "storage": [ {"name":"imgclock.app.js","url":"app.js"}, {"name":"imgclock.img","url":"app-icon.js","evaluate":true}, {"name":"imgclock.face.img"}, {"name":"imgclock.face.json"}, - {"name":"imgclock.face.bg","content":""} + {"name":"imgclock.face.bg","content":"X"} ] } diff --git a/apps/impwclock/ChangeLog b/apps/impwclock/ChangeLog index 6555fcc8f..0af7c99d6 100644 --- a/apps/impwclock/ChangeLog +++ b/apps/impwclock/ChangeLog @@ -3,3 +3,4 @@ 0.03: Move to Bangle.setUI to launcher support 0.04: Tweaks for compatibility with BangleJS2 0.05: Time-word now readable on Bangle.js 2 +0.06: Tell clock widgets to hide. diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js index c42dbda44..04421017b 100644 --- a/apps/impwclock/clock-impword.js +++ b/apps/impwclock/clock-impword.js @@ -154,6 +154,9 @@ Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -172,5 +175,4 @@ Bangle.on('touch',e=>{ } }); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/impwclock/metadata.json b/apps/impwclock/metadata.json index 733dbb957..1b92ea3ae 100644 --- a/apps/impwclock/metadata.json +++ b/apps/impwclock/metadata.json @@ -1,7 +1,7 @@ { "id": "impwclock", "name": "Imprecise Word Clock", - "version": "0.05", + "version": "0.06", "description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.", "icon": "clock-impword.png", "type": "clock", diff --git a/apps/info/ChangeLog b/apps/info/ChangeLog index 07afedd21..093dd4606 100644 --- a/apps/info/ChangeLog +++ b/apps/info/ChangeLog @@ -1 +1,3 @@ -0.01: Release \ No newline at end of file +0.01: Release +0.02: Recfactoring and show weather data +0.03: Show sizes for used, free and trash through storage.getStats \ No newline at end of file diff --git a/apps/info/info.app.js b/apps/info/info.app.js index c61a88045..ade3f3ebb 100644 --- a/apps/info/info.app.js +++ b/apps/info/info.app.js @@ -1,27 +1,99 @@ -var s = require("Storage"); +const storage = require("Storage"); const locale = require('locale'); var ENV = process.env; var W = g.getWidth(), H = g.getHeight(); var screen = 0; -const maxScreen = 2; + + +var screens = [ + { + name: "General", + items: [ + {name: "Steps", fun: () => getSteps()}, + {name: "HRM", fun: () => getBpm()}, + {name: "", fun: () => ""}, + {name: "Temp.", fun: () => getWeatherTemp()}, + {name: "Humidity", fun: () => getWeatherHumidity()}, + {name: "Wind", fun: () => getWeatherWind()}, + ] + }, + { + name: "Hardware", + items: [ + {name: "Battery", fun: () => E.getBattery() + "%"}, + {name: "Charge?", fun: () => Bangle.isCharging() ? "Yes" : "No"}, + {name: "TempInt.", fun: () => locale.temp(parseInt(E.getTemperature()))}, + {name: "Bluetooth", fun: () => NRF.getSecurityStatus().connected ? "Conn" : "NoConn"}, + {name: "GPS", fun: () => Bangle.isGPSOn() ? "On" : "Off"}, + {name: "Compass", fun: () => Bangle.isCompassOn() ? "On" : "Off"}, + ] + }, + { + name: "Software", + items: [ + {name: "Firmw.", fun: () => ENV.VERSION}, + {name: "Git", fun: () => ENV.GIT_COMMIT}, + {name: "Boot.", fun: () => getVersion("boot.info")}, + {name: "Settings.", fun: () => getVersion("setting.info")}, + ] + }, + { + name: "Storage [kB]", + items: [ + {name: "Total", fun: () => storage.getStats().totalBytes>>10}, + {name: "Free", fun: () => storage.getStats().freeBytes>>10}, + {name: "Trash", fun: () => storage.getStats().trashBytes>>10}, + {name: "", fun: () => ""}, + {name: "#File", fun: () => storage.getStats().fileCount}, + {name: "#Trash", fun: () => storage.getStats().trashCount}, + ] + }, +]; + + +function getWeatherTemp(){ + try { + var weather = storage.readJSON('weather.json').weather; + return locale.temp(weather.temp-273.15); + } catch(ex) { } + + return "?"; +} + + +function getWeatherHumidity(){ + try { + var weather = storage.readJSON('weather.json').weather; + return weather.hum = weather.hum + "%"; + } catch(ex) { } + + return "?"; +} + + +function getWeatherWind(){ + try { + var weather = storage.readJSON('weather.json').weather; + var speed = locale.speed(weather.wind).replace("mph", ""); + return Math.round(speed * 1.609344) + "kph"; + } catch(ex) { } + + return "?"; +} + function getVersion(file) { - var j = s.readJSON(file,1); + var j = storage.readJSON(file,1); var v = ("object"==typeof j)?j.version:false; return v?((v?"v"+v:"Unknown")):"NO "; } -function drawData(name, value, y){ - g.drawString(name, 5, y); - g.drawString(value, 100, y); -} - function getSteps(){ try{ return Bangle.getHealthStatus("day").steps; } catch(e) { - return ">= 2v12"; + return ">2v12"; } } @@ -29,53 +101,36 @@ function getBpm(){ try{ return Math.round(Bangle.getHealthStatus("day").bpm) + "bpm"; } catch(e) { - return ">= 2v12"; + return ">2v12"; } } +function drawData(name, value, y){ + g.drawString(name, 10, y); + g.drawString(value, 100, y); +} + function drawInfo() { g.reset().clearRect(Bangle.appRect); var h=18, y = h;//-h; // Header - g.setFont("Vector", h+2).setFontAlign(0,-1); - g.drawString("--==|| INFO ||==--", W/2, 0); + g.drawLine(0,25,W,25); + g.drawLine(0,26,W,26); + + // Info body depending on screen g.setFont("Vector",h).setFontAlign(-1,-1); + screens[screen].items.forEach(function (item, index){ + drawData(item.name, item.fun(), y+=h); + }); - // Dynamic data - if(screen == 0){ - drawData("Steps", getSteps(), y+=h); - drawData("HRM", getBpm(), y+=h); - drawData("Battery", E.getBattery() + "%", y+=h); - drawData("Voltage", E.getAnalogVRef().toFixed(2) + "V", y+=h); - drawData("IntTemp.", locale.temp(parseInt(E.getTemperature())), y+=h); - } - - if(screen == 1){ - drawData("Charging?", Bangle.isCharging() ? "Yes" : "No", y+=h); - drawData("Bluetooth", NRF.getSecurityStatus().connected ? "Conn." : "Disconn.", y+=h); - drawData("GPS", Bangle.isGPSOn() ? "On" : "Off", y+=h); - drawData("Compass", Bangle.isCompassOn() ? "On" : "Off", y+=h); - drawData("HRM", Bangle.isHRMOn() ? "On" : "Off", y+=h); - } - - // Static data - if(screen == 2){ - drawData("Firmw.", ENV.VERSION, y+=h); - drawData("Boot.", getVersion("boot.info"), y+=h); - drawData("Settings", getVersion("setting.info"), y+=h); - drawData("Storage", "", y+=h); - drawData(" Total", ENV.STORAGE>>10, y+=h); - drawData(" Free", require("Storage").getFree()>>10, y+=h); - } - - if(Bangle.isLocked()){ - g.setFont("Vector",h-2).setFontAlign(-1,-1); - g.drawString("Locked", 0, H-h+2); - } - + // Bottom + g.drawLine(0,H-h-3,W,H-h-3); + g.drawLine(0,H-h-2,W,H-h-2); + g.setFont("Vector",h-2).setFontAlign(-1,-1); + g.drawString(screens[screen].name, 2, H-h+2); g.setFont("Vector",h-2).setFontAlign(1,-1); - g.drawString((screen+1) + "/3", W, H-h+2); + g.drawString((screen+1) + "/" + screens.length, W, H-h+2); } drawInfo(); @@ -88,14 +143,15 @@ Bangle.on('touch', function(btn, e){ var isRight = e.x > right; if(isRight){ - screen = (screen + 1) % (maxScreen+1); + screen = (screen + 1) % screens.length; } if(isLeft){ screen -= 1; - screen = screen < 0 ? maxScreen : screen; + screen = screen < 0 ? screens.length-1 : screen; } + Bangle.buzz(40, 0.6); drawInfo(); }); @@ -104,5 +160,4 @@ Bangle.on('lock', function(isLocked) { }); Bangle.loadWidgets(); -for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} -// Bangle.drawWidgets(); \ No newline at end of file +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/info/metadata.json b/apps/info/metadata.json index f05f0e134..ac56cd5c3 100644 --- a/apps/info/metadata.json +++ b/apps/info/metadata.json @@ -1,7 +1,7 @@ { "id": "info", "name": "Info", - "version": "0.01", + "version": "0.03", "description": "An application that displays information such as battery level, steps etc.", "icon": "info.png", "type": "app", @@ -11,7 +11,8 @@ "screenshots": [ {"url":"screenshot_1.png"}, {"url":"screenshot_2.png"}, - {"url":"screenshot_3.png"}], + {"url":"screenshot_3.png"}, + {"url":"screenshot_4.png"}], "storage": [ {"name":"info.app.js","url":"info.app.js"}, {"name":"info.img","url":"info.icon.js","evaluate":true} diff --git a/apps/info/screenshot_1.png b/apps/info/screenshot_1.png index 97d42a896..6661c122c 100644 Binary files a/apps/info/screenshot_1.png and b/apps/info/screenshot_1.png differ diff --git a/apps/info/screenshot_2.png b/apps/info/screenshot_2.png index 2d25dd4e6..3d91fcabe 100644 Binary files a/apps/info/screenshot_2.png and b/apps/info/screenshot_2.png differ diff --git a/apps/info/screenshot_3.png b/apps/info/screenshot_3.png index 782e4a195..86bbb67cf 100644 Binary files a/apps/info/screenshot_3.png and b/apps/info/screenshot_3.png differ diff --git a/apps/info/screenshot_4.png b/apps/info/screenshot_4.png new file mode 100644 index 000000000..b8b59b1ef Binary files /dev/null and b/apps/info/screenshot_4.png differ diff --git a/apps/invader/ChangeLog b/apps/invader/ChangeLog new file mode 100644 index 000000000..6c5a33e59 --- /dev/null +++ b/apps/invader/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.11: Changes... diff --git a/apps/invader/README.md b/apps/invader/README.md new file mode 100644 index 000000000..7ed98defb --- /dev/null +++ b/apps/invader/README.md @@ -0,0 +1,23 @@ +# App Name + +Invader + +## Usage + +For fun! - I'm creating this demo to learn JavaScript with the Bangle.js 2 + +## Features + +Shoot the Alien, you have three lives + +## Controls + +Touch the lower Left or Right hand sides of the screen to move turret left or right, tap upper Right hand part of screen to fire, tap upper Left hand part of screen to restart + +## Requests + +bkumanchik on Espruino Forums + +## Creator + +Brian Kumanchik 2022 diff --git a/apps/invader/app-icon.js b/apps/invader/app-icon.js new file mode 100644 index 000000000..dc7003c84 --- /dev/null +++ b/apps/invader/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AH4AYgAACC/4X/C/YbTFbYv/F/4rTAC4v/F/4vfC5YnPGaYv/F/4vLc7b3TF/4v/F/4v/F/4vTA5YAPE64v/F/4fTa55DXF/4v/AH4A/AH4A/AH4A/AHIA==")) diff --git a/apps/invader/app.js b/apps/invader/app.js new file mode 100644 index 000000000..89e7462f6 --- /dev/null +++ b/apps/invader/app.js @@ -0,0 +1,452 @@ +// Brian Kumanchik +// Started 05-25-22 +// My Invader Demo, for Bangle.js 2, written JavaScript - using Espruino Web IDE + + +// note: resolution is 176x176 + + +// to do: +// upload to official app page +// make invader clock + + + +// - variables ----------------------------------------- +// invader variables +var inv_x = 77; +var inv_y = 20; +var i_anim_delay = 10; // invader animation (and move) delay +var inv_frame = 1; // invader start animation frame +var ix_speed = 6; // march speed +var i_dir = 1; // 1 = right, 0 = left +var been_hit = false; // invader hit state +// - shoot variables +var inv_shot_x = -32; +var inv_shot_y = -32; +var inv_fire_pause = 30; +var inv_fired = false; // invader fired state +// - explode variables +var been_hit = false; // invader hit state +var bx = -32; // blast x +var by = -32; // blast y +var blast_delay = 15; // invader blast delay - pause after explosion +var boom_play = false; + +// turret variables +var tur_x = 77; +var tur_y = 148; +var shot_fired = false; // turret fired state +var sx = -20; // turret shot starting x - off screen +var sy = -20; // turret shot starting y - off screen +var turret_been_hit = false; +var turret_blast_delay = 25; // keep blast active on screen for 60 frames +var turret_exp_frame = 1; // turret explode start animation frame +var turret_anim_delay = 3; // turret explode animation delay +var explosion_play = false; + +// misc variables +var score = 0; // starting score +var lives = 3; // starting lives +var game_state = 0; // game state - 0 = game not started, 1 = game running, 3 = game over +var ang = 0.1; +var start_been_pressed = false; // stops double press on restart +var fire_been_pressed = false; // stops auto fire + +// input(screen controller) variables +var BTNL, BTNR, BTNF, BTNS; // button - left, right, fire, start +var tap = {}; +// use tapping on screen for left, right, fire, start +Bangle.on('drag',e=>tap=e); +BTNL = { read : _=>tap.b && tap.x < 88 && tap.y > 88}; +BTNR = { read : _=>tap.b && tap.x > 88 && tap.y > 88}; +BTNF = { read : _=>tap.b && tap.x > 88 && tap.y < 88}; +BTNS = { read : _=>tap.b && tap.x < 88 && tap.y < 88}; + + +// - sprites ------------------------------------------- +// invader sprites +var invader_a = + require("heatshrink").decompress(atob("hcIwkBiIBBAQoECCQQFBgEQAIMBEhUBDoYWDAYI=")); +var invader_b = + require("heatshrink").decompress(atob("hcIwkBiIBBAQMQAoQEBgISCAYUQAIQAEB4YEBEAgEDAYIA==")); +var boom = + require("heatshrink").decompress(atob("hcJwkBiMQAIURgMQAgIKBAIICFAIMAAwIWBBAYSIEAgrDiA=")); +var inv_shot = + require("heatshrink").decompress(atob("gcFwkBiERiAABAYQ")); + +// turret sprites +var turret = + require("heatshrink").decompress(atob("h8IwkBiIABAYYACgAHFiEABggADCAInFgITBAAgOPA==")); +var tur_exp_a = + require("heatshrink").decompress(atob("h8IwkBiMRiACBAAwJEiAABBQgZCAAkAiAJBBoIUBgIABBgQACDIQ9ECQIA==")); +var tur_exp_b = + require("heatshrink").decompress(atob("h8IwkBiIBBAAUBiADCiMQAwQFDCIYXEB4IABgMAEYQXBiEAAQIQBAoIABDAQUCAAIVBA")); +var shot = + require("heatshrink").decompress(atob("gMDwkBAoIA==")); + + +// function to move and animate invader +function move_anim_inv() { + // invader anim code + i_anim_delay -= 1; + if ((i_anim_delay < 0) && !(been_hit)) { + i_anim_delay = 10; + + inv_frame += 1; + if (inv_frame > 2) { + inv_frame = 1; + } + + // move right + if (i_dir == 1){ + inv_x += ix_speed; + if (inv_x >= 142) { + inv_y += 8; // step down + i_dir = -1; + } + } + + // move left + if (i_dir < 1){ + inv_x -= ix_speed; + if (inv_x <= 10) { + inv_y += 8; // step down + i_dir = 1; + } + } + } +} + + +// function to make invader fire +function invader_fire() { + inv_fire_pause -= 1; + + if (!(inv_fired)) { // so once invader shot is fired it doesn't follow the invader still + inv_shot_x = inv_x + 8; + inv_shot_y = inv_y + 18; + } + + if (inv_fire_pause < 0) { + inv_fired = true; + inv_shot_y += 8; + } +} + + +// function to make turret explode (when hit) then start back in center +function turret_hit() { + if (turret_been_hit) { + if (!(explosion_play)) { + //Bangle.buzz(); + //Bangle.beep(); + } + + explosion_play = true; + turret_anim_delay -= 1; + turret_blast_delay -= 1; + + if (turret_anim_delay < 0) { + turret_exp_frame += 1; + if (turret_exp_frame > 2) { + Bangle.buzz(); + turret_exp_frame = 1; + } + turret_anim_delay = 3; + } + + if (turret_blast_delay < 0) { + turret_blast_delay = 21; + turret_been_hit = false; + explosion_play = false; + tur_x = 77; // reset turret x + tur_y = 148; // reset turret y + } + } +} + + +// function to make invader explode (when hit) then randomly start somewhere else +function invader_hit() { + if (been_hit) { + if (!(boom_play)) { + Bangle.buzz(); + //Bangle.beep(); + } + + inv_shot_x = -32; // hide shot + inv_shot_y = -32; // hide shot + inv_fire_pause = 30; // and reset pause + + boom_play = true; + blast_delay -= 1; + + if (blast_delay < 0) { + blast_delay = 15; + boom_play = false; + been_hit = false; + bx = -32; // move boom off screen (following invader) + by = -32; + // generate a random rounded number between 10 and 142; + inv_x = Math.floor(Math.random() * 142) + 10; + inv_y = 20; // move invader back up after being hit + i_dir = 1; // reset invader direction + } + } +} + + +// - setup stuff --------------------------------------- +function gameStart() { + setInterval(onFrame, 50); +} + + +// - main loop ------------------------------------------------------------- +function onFrame() { + + // game not started state (title screen) *************************** + if(game_state == 0) { + g.clear(); + + + if (!(BTNS.read())) { + start_been_pressed = false; // stops double press on restart + } + + + // draw text during game over state + g.setFont("4x6", 4); // set font and size x 2 + g.setColor(0,1,0); // set color (black) + g.drawString("INVADER", 33, 55); + + + // just animate invader + // invader anim code + i_anim_delay -= 1; + if(i_anim_delay < 0) { + i_anim_delay = 15; + + inv_frame += 1; + if (inv_frame > 2) { + inv_frame = 1; + } + } + + + // draw sprites during game over state + // next 2 line for a rotating invader on the title screen + //ang += 0.1; + //g.drawImage(invader_a, 88, 98, {scale:4, rotate:ang}); + if(inv_frame == 1) { + g.drawImage(invader_a, 88-22, 85, {scale:4}); + } + else if(inv_frame == 2) { + g.drawImage(invader_b, 88-22, 85, {scale:4}); + } + + // reset stuff + if(BTNS.read() && !(start_been_pressed)) { + turret_been_hit = false; + tur_x = 77; // reset turret to center of screen + tur_y = 148; // reset turret y + inv_x = 77; // reset invader to center of screen + inv_y = 20; // reset invader back to top + i_dir = 1; // reset invader direction + lives = 3; // reset lives + score = 0; // reset score + explosion_play = false; + game_state = 1; + turret_blast_delay = 25; + } + + + g.flip(); + } + + + // game over state ************************************************* + if(game_state == 3) { + g.clear(); + + // draw text during game over state + g.setFont("4x6", 2); // set font and size x 2 + g.setColor(0,0,0); // set color (black) + g.drawString("SCORE:" + score ,5, 5); + g.drawString("LIVES:" + lives ,117, 5); + g.drawString("GAME OVER", 52, 80); + + + // draw sprites during game over state + // - invader frame 2 + g.drawImage(invader_b, inv_x, inv_y, {scale:2}); + g.drawImage(tur_exp_b, tur_x, tur_y, {scale:2}); + g.drawImage(inv_shot, inv_shot_x, inv_shot_y, {scale:2}); + + + // reset stuff + if(BTNS.read()) { + //turret_been_hit = false; + //tur_x = 77; // reset turret to center of screen + //tur_y = 148; // reset turret y + //inv_x = 77; // reset invader to center of screen + //inv_y = 20; // reset invader back to top + //i_dir = 1; // reset invader direction + //lives = 3; // reset lives + //score = 0; // reset score + //explosion_play = false; + game_state = 0; + start_been_pressed = true; + //turret_blast_delay = 25; + } + + + g.flip(); + } + + + // not game over state (game running) ****************************** + if(game_state == 1) { + Bangle.setLCDPower(1); // optional - this keeps the watch LCD lit up + g.clear(); + + + if (!(BTNF.read())) { + fire_been_pressed = false; // stops auto fire + } + + + // call function to move and animate invader + move_anim_inv(); + + // call function to make invader fire + invader_fire(); + + + // check input (screen presses) + if(BTNL.read() && tur_x >= 12 && !(turret_been_hit)) { + tur_x -= 6; + } + else if(BTNR.read() && tur_x <= 140 && !(turret_been_hit)) { + tur_x += 6; + } + else if(BTNF.read() && !(turret_been_hit) && !(fire_been_pressed) && !(shot_fired)) { + shot_fired = true; + fire_been_pressed = true; // stops auto fire + sx=tur_x + 12; + sy=tur_y - 7; + } + + + // check for turret shot going off screen before allowing to fire again + if (shot_fired) { + sy -= 8; + if (sy < 22) { + shot_fired = false; + sx = -32; + sy = -32; + } + } + + + // check for invader shot going off screen before allowing to fire again + if (inv_shot_y > 150 + ) { + inv_fired = false; + inv_shot_x = inv_x - 1; + inv_shot_y = inv_y + 7; + inv_fire_pause = 30; + } + + + // check for turret shot and invader collision + if ((sx >= inv_x) && (sx <= inv_x + 20) && (sy <= inv_y + 14)) { + sx = -32; + sy = -32; + been_hit = true; + score += 10; + } + + + // check for invader shot and turret collision + if ((inv_shot_x + 4) >= (tur_x) && (inv_shot_x) <= (tur_x + 24) && (inv_shot_y + 8) >= (tur_y + 6)) { + if (!(turret_been_hit)) { + lives -= 1; + + if (lives == 0) { + game_state = 3; + Bangle.buzz(); + } + turret_been_hit = true; + } + } + + + // - draw sprites ---------------------------------- + // invader sprites + if(!(been_hit)) { + if(inv_frame == 1) { + // - invader frame 1 + g.drawImage(invader_a, inv_x, inv_y, {scale:2}); + } + else if(inv_frame == 2) { + // - invader frame 2 + g.drawImage(invader_b, inv_x, inv_y, {scale:2}); + } + } + else { + // - invader explosion + g.drawImage(boom, inv_x, inv_y, {scale:2}); + } + // - invader shot + if (inv_fired) { + g.drawImage(inv_shot, inv_shot_x, inv_shot_y, {scale:2}); + } + else { + g.drawImage(inv_shot, -32, -32, {scale:2}); + } + + // turret sprites + if(!(turret_been_hit)) { + // - undamaged turret + g.drawImage(turret, tur_x, tur_y, {scale:2}); + } + else { + if(turret_exp_frame == 1) { + // - turret explosion frame 1 + g.drawImage(tur_exp_a, tur_x, tur_y, {scale:2}); + } + else if(turret_exp_frame == 2) { + // - turret explosion frame 2 + g.drawImage(tur_exp_b, tur_x, tur_y, {scale:2}); + } + } + // - turret shot + g.drawImage(shot, sx, sy, {scale:2}); + + + // call function to make invader explode then randomly start somewhere else + invader_hit(); + + + // call function to make turret explode (when hit) then start back in center + turret_hit(); + + + // - draw text ------------------------------------- + g.setFont("4x6", 2); // set font and size x 2 + g.setColor(0,0,0); // set color (black) + g.drawString("SCORE:" + score ,5,5); + g.drawString("LIVES:" + lives ,117,5); + + + g.flip(); + } + +} // end main loop --------------------------------------------------------- + + +gameStart(); + + diff --git a/apps/invader/app.png b/apps/invader/app.png new file mode 100644 index 000000000..3b9f82205 Binary files /dev/null and b/apps/invader/app.png differ diff --git a/apps/invader/metadata.json b/apps/invader/metadata.json new file mode 100644 index 000000000..bb74f5122 --- /dev/null +++ b/apps/invader/metadata.json @@ -0,0 +1,15 @@ +{ "id": "invader", + "name": "Invader", + "shortName":"Invader", + "version":"0.11", + "description": "A Space Invader game-like demo - work in progress", + "icon": "app.png", + "screenshots" : [ { "url":"screenshot_0.png" }, { "url":"screenshot_1.png" }, { "url":"screenshot_2.png" } ], + "tags": "game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"invader.app.js","url":"app.js"}, + {"name":"invader.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/invader/screenshot.png b/apps/invader/screenshot.png new file mode 100644 index 000000000..ae936f6c7 Binary files /dev/null and b/apps/invader/screenshot.png differ diff --git a/apps/invader/screenshot_0.png b/apps/invader/screenshot_0.png new file mode 100644 index 000000000..804f3e435 Binary files /dev/null and b/apps/invader/screenshot_0.png differ diff --git a/apps/invader/screenshot_1.png b/apps/invader/screenshot_1.png new file mode 100644 index 000000000..14164bc6e Binary files /dev/null and b/apps/invader/screenshot_1.png differ diff --git a/apps/invader/screenshot_2.png b/apps/invader/screenshot_2.png new file mode 100644 index 000000000..5f6ade79c Binary files /dev/null and b/apps/invader/screenshot_2.png differ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index b6a386bcb..87a71a1ed 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -7,3 +7,4 @@ 0.07: Added more details from music (instead of Undefined), added more app identifiers 0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements 0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365) +0.10: Added more bundleIds diff --git a/apps/ios/boot.js b/apps/ios/boot.js index a3b23a79d..6b4ad7c5b 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -63,6 +63,7 @@ E.on('notify',msg=>{ "name" : string, */ var appNames = { + "ch.publisheria.bring": "Bring", "com.apple.facetime": "FaceTime", "com.apple.mobilecal": "Calendar", "com.apple.mobilemail": "Mail", @@ -73,6 +74,9 @@ E.on('notify',msg=>{ "com.apple.podcasts": "Podcasts", "com.apple.reminders": "Reminders", "com.apple.shortcuts": "Shortcuts", + "com.apple.TestFlight": "TestFlight", + "com.apple.ScreenTimeNotifications": "ScreenTime", + "com.apple.wifid.usernotification": "WiFi", "com.atebits.Tweetie2": "Twitter", "com.burbn.instagram" : "Instagram", "com.facebook.Facebook": "Facebook", @@ -99,23 +103,42 @@ E.on('notify',msg=>{ "com.toyopagroup.picaboo": "Snapchat", "com.ubercab.UberClient": "Uber", "com.ubercab.UberEats": "UberEats", + "com.unitedinternet.mmc.mobile.gmx.iosmailer": "GMX", + "com.valvesoftware.Steam": "Steam", "com.vilcsak.bitcoin2": "Coinbase", "com.wordfeud.free": "WordFeud", + "com.yourcompany.PPClient": "PayPal", "com.zhiliaoapp.musically": "TikTok", + "de.no26.Number26": "N26", "io.robbie.HomeAssistant": "Home Assistant", + "net.superblock.Pushover": "Pushover", "net.weks.prowl": "Prowl", "net.whatsapp.WhatsApp": "WhatsApp", - "net.superblock.Pushover": "Pushover", "nl.ah.Appie": "Albert Heijn", "nl.postnl.TrackNTrace": "PostNL", "org.whispersystems.signal": "Signal", "ph.telegra.Telegraph": "Telegram", "tv.twitch": "Twitch", - // could also use NRF.ancsGetAppInfo(msg.appId) here }; var unicodeRemap = { - '2019':"'" + '2019':"'", + '260':"A", + '261':"a", + '262':"C", + '263':"c", + '280':"E", + '281':"e", + '321':"L", + '322':"l", + '323':"N", + '324':"n", + '346':"S", + '347':"s", + '377':"Z", + '378':"z", + '379':"Z", + '380':"z", }; var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); //if (appNames[msg.appId]) msg.a diff --git a/apps/ios/metadata.json b/apps/ios/metadata.json index eb75a6dbc..63f02262f 100644 --- a/apps/ios/metadata.json +++ b/apps/ios/metadata.json @@ -1,7 +1,7 @@ { "id": "ios", "name": "iOS Integration", - "version": "0.09", + "version": "0.10", "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", diff --git a/apps/isoclock/ChangeLog b/apps/isoclock/ChangeLog index 809091ce4..7b57ecfa9 100644 --- a/apps/isoclock/ChangeLog +++ b/apps/isoclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app based on digiclock with some small tweaks. 0.02: Swap to Bangle.setUI for launcher/buttons +0.03: Tell clock widgets to hide. diff --git a/apps/isoclock/checkout b/apps/isoclock/checkout new file mode 100644 index 000000000..e69de29bb diff --git a/apps/isoclock/isoclock.js b/apps/isoclock/isoclock.js index 59f28e66e..7526660b9 100644 --- a/apps/isoclock/isoclock.js +++ b/apps/isoclock/isoclock.js @@ -89,8 +89,8 @@ Bangle.on('lcdPower',on=>{ } }); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - // Show launcher when button pressed Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/isoclock/metadata.json b/apps/isoclock/metadata.json index 313153dde..488afcb41 100644 --- a/apps/isoclock/metadata.json +++ b/apps/isoclock/metadata.json @@ -2,7 +2,7 @@ "id": "isoclock", "name": "ISO Compliant Clock Face", "shortName": "ISO Clock", - "version": "0.02", + "version": "0.03", "description": "Tweaked fork of digiclock for ISO date and time", "icon": "isoclock.png", "type": "clock", diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog new file mode 100644 index 000000000..f2c991fd0 --- /dev/null +++ b/apps/kanawatch/ChangeLog @@ -0,0 +1,5 @@ +0.01: First release +0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug +0.03: Reduce code size, refresh once a minute and faster refresh +0.04: Show a random kana every minute to improve learning +0.05: Tell clock widgets to hide. diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md new file mode 100644 index 000000000..e213949dc --- /dev/null +++ b/apps/kanawatch/README.md @@ -0,0 +1,19 @@ +# kanawatch + +A simple watchface design with hiragana and katakana +cards for learning. + +## Changelog + +0.01: First release +0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug +0.03: Reduce code size, refresh once a minute and faster refresh +0.04: Show a random kana every minute to improve learning + +## Author + +Written by pancake in 2022, powered by insomnia + +## Screenshots + +![hiragana and katakana](screenshot.png) diff --git a/apps/kanawatch/app-icon.js b/apps/kanawatch/app-icon.js new file mode 100644 index 000000000..a17f21d56 --- /dev/null +++ b/apps/kanawatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxEBAH4A/AEn/AAgrrAA4ttGL4hF9fGsU1pMNmti43rGLwcD/3MxEAud413p6uuvFzgGI5n+GDQaD6F8i2p8KKH8Opi186AwYC4Xv08A0fnXhfn0cA0/vGCoVC7+ItHNE4vQ+oxH5toxHfGCYTC8t/xaKH5VY+CUIxd/8owSCIPxymB8wkH8UA2yTI82Byn4F6AXCwNH7YjI7UATAwAD7dHHgYuP4sAc5XLgHrBpXAjngGBwOCrmJ/whJ1syBgXw7v6Bov+xObF5rWDgHWKJWEt3l4mQjkAoHzBwvWgHhGBgMC1WIDQuw1/L427z8ygAABp+R3vqH4+I1QvO/1R5YZF+t1FINWuMAy/W+BuKZ4NRT4ReL7kc+waG/fy/n/9kA74tLAAP2jncAgPBF5W5yIeLZgPxEgf3CJOR3JTCF5WU3wvL6sA/YFC7e0CJO+ygDB94vKt3aF5fHoQDB+/dzdL4nb+YRG7VuAYP5F5VF9ovL3dP3t8pOKgFw0+CjmT84RE9tFAYP+F6/uwMm1Hd/vCk3oQYWGl3XF6aPK/e0oVwrohCmu9Bof5sVF+yPSd5PtuWA9m7o///uCwH9B4m9gHKd6W5yIuG9NV3v+//Gjn/2VA9wQF6UA2AFCyO5AYPcF5Xcjh1DAAPnp/SEYnJiy2EAAXTgGvAgP2jncAgPBF44wC/1R5a7EsZHCAAPegEA3afH4sA4wEB5dROgP/FxBgD1WIPgky/QGD5MAxYfCAAuGjnvAgNHuBLCF5nhgHWAoWvuwEC9mWLwN+Fw6aB1wEB60A44EB6ovJGAebxJSC1lF4/AyMNoXBzUN/IuF5kmyP8VgOJrgKCFxUB8QOB8Ec4CnCLIMAmWr+v/9Vy/otD+WWmu7BAXAjnFF5xgD21H7f//u+0vN/CKH9Ojse4+QHC7dH2wuPgPVCAP4yk98wqHAAf734OF82ByhCDF5pgD/9/xfhGBYAF8OLv/lFyIABU4XfxFo5ouP5toxHfFyZhE9+ngGj84tL8+jgGn94uVSQvQvkW1KUI8Opi186AIDFygwF/3MxEAuew6fp9PT2FzgGI5n+FzQwFAAPr42fu9JpN3z/G9YPFFzAxIABYtbGKItfGZYrlAH4A+A")) diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js new file mode 100644 index 000000000..088dab785 --- /dev/null +++ b/apps/kanawatch/app.js @@ -0,0 +1,273 @@ +const stripe_width = 32; +const stripe_pos = 40; +const stripe2_pos = 110; +const h = g.getHeight(); +const w = g.getWidth(); + +/// ///////////////////////////////////////// +const katakana = {}; +const hiragana = {}; +function image(x,y,b) { + return { + bpp:1, width:x,height:y, + buffer:require('heatshrink').decompress(atob(b)) + }; +} +katakana['A'] = image(56, 51, "v//AAfwAon//AGF/wGT/gGM/A3F/BDEn/wJQoGCj4RB//gAxUB//AAwcDAwsH/+AAwcP/4tCAwMf/wGEn/8Awl/JYYGBKQkf/I9DAwJgBGwQGDGwRlBAwJsE+42DAwPzGwYGB+J7EQIIvDQIIFEAw5DEAwRDDgCIEAxCPBKIcAR4IhER4hnCLAg9BLAgoBAwgoBcQiCBMwj0BHogGBHogGBfoooEQQREFEIgGBAokAhAGFA="); +katakana['I'] = image(54, 55, "AAkEAws+AokB/wGEg//Awk//gTE//gAwcPCYt/CYkDCYsfCYv//A0F4A0ECYg0BCYggBCYn/KwhBBGgl/EAgtBEAgMBEAZOBEAgMBEAYZB/+ABggTDBgQnDAoIaDJoIaDFgIABDQQFC74aBBgX8v4aBEwWBDQQgB/EHDQQ6BwEfGoX/+AJBDQMDWAKMBDQMPAQIaDiBFCPAgaDU4hrDDQiuDDX4acSAIaCA="); +katakana['U'] = image(52, 55, "AAMP/gGE//ABlH/AAnvAon+Bk5EDv/vIgcHBkHPBgZwBBgn/Bi8B/+PBgcf/AMFw/wBgYEDgED/6qEv4MEKYK3F8AFDj7EED4LREv/4CQn/wASEFginBDAgfEDAIfDn67BC4YABH4QXBCQcHZoQkEEoYMCHAYlBFYZEBLwk/MgpQEAAw"); +katakana['E'] = image(58, 45, "h//AAfwgYGE/0AAwn/wE/AwngDgv4DjhDCv/wJQkf/gGEg//AwkB//AA4gc/Dn4cjbAv/34GF94GF/YGF/wcjwA="); +katakana['O'] = image(57, 54, "AAcf+AGEh/8AwkH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/GIsf/A4P/4AE+F/Awn4n4GE/kfAwn+h4cFg4GFwYGF4IGFKwYFBMQpxFAwJxEAwJxEAwJxEAwJxEK4JxEAwKqEMoQGE/o4En/8HAl//iqEAwKqEv/+VQgNBVQgNBcYgNBcYhLBcYhSCHAQKBAwI4CAwY4CD4IGBHASxBAYI4CAwY4CYwIGBHAQGBD4I4CBIJfCHASmDHAV/PYQ4Cj5QCHAUPLwQ4CgQGCOIgABOIgABHAIGEAAY="); +katakana['KA'] = image(54, 54, "AAMP/AGEv/gAocB/+AAwcH/wTEj4arg//AAf+j4GE/F/AwnhAon/w4aZHAMP/hTEn/wKYn/4BTDgf/KYgQCDQYQCBIQQDBIQQCBIc/DQouCDQQuCEghJBEhITBH4RTBLoRTEBIJTGCAUPNwoTCDQQWBDoIuCj4TCJIX/CYQ/BZQInBH4U//0HwBTBGgPwXAXwh4PBXAXAv4PCZIIgBEYTJBn5SBDQXABAIzBCYJcCDQXwgbOCAwIDBQgI4CgEOJwIADkAGFA"); +katakana['KI'] = image(58, 55, "AAU+Awv/4AGEn/wAwkP/gGEgf/Dkk/CAc//4ABwAGBj4GC8ATBAAf4h4GE/woBAAmAAwvgFAYcIwAcD/BFDFARFD/kBIoYACv5FBAAcfRL94DgkfHgf/95EBD4RgDD4MHLwf8AogAd+CPFGwiJCS4XHJgSGB8CJEkCJJUwYABg5pDD4amTNwKmXYbgcDLoY="); +katakana['KU'] = image(55, 55, "AAMHwAGEh/8Awkf/AGEv/wAwn/4AFDgf/EQkH/whF/4ACAwM/AoQQCBgY5BgIGDHIMHAwY5Bh4GD8AhEIAQFDIAIhBBIJACEIJpEj45CNIV/NgRpBDQIrBEoPgDQJlBEoQaDEoV/RwUP/wPBQ4Uf/gPBQ4QsBKAKSD8BvCSQXDDQYYBNYIaCGYIqBDQU//kPXoYYBj5QCEIPgj60DKoMcWga7FKoYABKogaDbojPBbojMDGob/ECYJBCbgYaDE4IaEPoIaDEAI1EbYQZECYgtBCZQGCLol/KwxxEAwJqEgIMFgIZEgA="); +katakana['KE'] = image(60, 54, "AAMcAwsD/4HFn/wBxl/8AGEg/+BxkP/gOF//ABxcB/+AA4kf/BCGAAZOBv4HEIQIOGAwgOBh4OFGYIOFn4OFEgoOBAwvgh52BKgYDBOwJUDv5nBBwY6BAYM/BwIKBJgJjBBQSbCWoQVBRgK1D/4oDBwJJBWos/WIS1CgIVCJoRGBWowCCj61HYgpRCdIjEGLgTLEIwTLEfAv/GYqtBEghyBGYjoCAwwkDAwQVEYwYjEHQt/CopeBQgQOEIIgOBPgxeFgZ7FA"); +katakana['KO'] = image(49, 46, "v//AAYFF34FE74FE94FE+4FE/IFE/gFE/w0Dgf/AocB/+AAwf/4BHE8AFDn/wAocf/AFDh/8AocHGH4w6YZf7Aon9YYoFEejBhEAAIA="); +katakana['SA'] = image(58, 53, "AAcD/wDBg4DC//AgEB/+AgE/+AKBv/ggEP/gGBj/4DgP/DnU//4A34CQ+DAIcEDAIcDDAQDDDAYDCDAYDD/4cDIgJADAAUfIAQACh4jCAAUHD4QACJwIfBAAQtBEYgGBI4QUDFQkP/4qEVYQvEAAIxCEIK5CBwV/AwsfAwocCAwYcCJogcBNIp3F"); +katakana['SI'] = image(56, 52, "gFwAwt+Awv/8AGF/gFDgP//4GGCocDAwIVDBoX/wAHCn4VFg4GB4AxEAwsfAworBEQYABv4GFj4DCjgrCBQYRFn/4JQfAIgIGD+F/JQcD/gGBMARQCOwcH/wNBCoUP/0PAwIrBj/8OwQGBn4fBGIIGCAQIlB+BcBAQKvDBIQRB8AfBIQUH4AXBP4RXBGgJmERoJsFAwv//yaFbYghBQIYaCeAi9FPQTZGdxKFCFASECFAZPBEIgNCJQaZEAwhDDAwRJDTAYGEQAiQBPIgAGA"); +katakana['SU'] = image(60, 51, "gH/AAYGBh4GD/AOG4AOF/gONDo+ABxAACgY7CAAd/+AGEg4OG//gAwkP/wGEgJCCAAcfKIQzEIQIzEIQozOj4zFEgIzFn4kHGYv/M4okIGYt/IQqXBFghuBHYs/bAY6DCwrJECod/HgYVB8ZLEcoMfLQYECCwYVB+BTBCwT7CCwYrBAYIKCCoQDC8BXBEIQSBNoQVBBYP4EAIoCOQPHCoYTB/xdBIwQ8B+6SET4N/dYn/4aCFFgKRFgC+EgPghivEAoI"); +katakana['SE'] = image(57, 53, "gEH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/+AGEj/4AwkP/g4JjA4EBQQ4D/4DD4E/AwIuBv/vAoP/FwILCAAIuBv4GEBgn//wFEAwITEh//CgfwAwMfCIRGB/4BB/5xBAgJTBIQQGBwP/75CBAwOAD4JCBAwRmDDIKYBOIQGDOIQGDOIQbBAwSqBAwiqBAwiqBDYg4Cv4GCHAUfAwQ4Cg4GCHAUBbwbjnHAgADcYYADUYQxEEYq6CVwbDBdQi6CZQYqBAAZcCAwY1BEYi5DAAQ8CegfgA="); +katakana['SO'] = image(52, 52, "gGAAol8AYUD/Ef4AGCn/3/wFCg/+v/wAwV/8//Bgk//AMD8f/FoQMBj/8Bgfg//gBgcPFoYMBFocP/kHFof/4AtDBgMDFoYMBFoYMBgIIBgADBwAtDj4dBHQQMCFoYqCHQQqCFoc/BIIPCCwQtDKYIpBB4IwDIAQwCh45CBIVAFgSmDFIaaDOIYfCVgYfBRYYfCTASTCUoY1BQgZPCD4l/D4kfH4g4BH4YYBH4gFBGQd//4yDBYIyDn4SEJQIlEBgRXEHAg+BFYZRGZYQADBYgAG"); +katakana['TA'] = image(55, 56, "AAMHwAGEh/8Awkf/AGEv/gAwn/4AFDgf/EQkH/4oF/4ACAwM/AoX+FAQGCHIMBCYY5BEIIAC+AhFIAIhDHIQFDF4IhBJQMHF4JDDNIUfHIRpCv5sCn/wDQJsCDwIaBEIIKBwEf/9gOAQaB/gbBFAIPB+YsC/AaB54RBFAIaBAIOAEoJvBOgPh/+DNAJWB+//DQPBQIZyBM4f4LQSQC8EPKAIpBFAMPPgKKCgEcYIZwBiAGDbohwEZ4bdEFILxFf4ghBXwLjEDQhLBCYoaEE4IaDdIQaDBgLBCDIRQENYYTIewRkEAwJCFHYicBOIkAEAhDBS4IAJ"); +katakana['TI'] = image(57, 54, "AAkGAwsfwAGE//gAocP//wBgn//gEBgIFBAAIeBAof/wAYBAwkHAof+gEDAwf4E4YAB4AGBv4TDAAM/AwoxDKQhABLQwiCAAV/MIglBMIglBHwRwDNARbF//3Awv7Awv9Awv+Awv/MQQAD34GF74GFKAUHOIYABSAJxGaYp4Uv54FP40/P4oGHQwQGKKgt/AwrUEMIQGEVYIGLg4bMFII+Fv5TGNAsPQgsHTIoAG"); +katakana['TU'] = image(54, 53, "AAMBwAGEj4FEgf8AYPwgFgn/4BIP/g+Av/ggEP/n/gP/4EAv/v/wQBFQP/z/4CAMAg/+DAMfEIICBDAN/FgN/8YYBBAIaBw4hDDQIVBAYMAn/wDAIhCCwIhDCwIBBwAIBHAIYBEIQYDBAIuBwAjBFQghCJgQhEAIIhDEYQPBh5HBM4IhDQQQhCwYeBCwMBCoSPB/0CIQQhBAQKWDvytBCYTBDv5tBZYYTCAAQTCAAYTFHAITEj4TF/4TEh4TFv4TEg//JgIMDMYIMEO4ImD/53BAAM/AwIsEEAgFBEAZNBIIgTCFocfJwo6BPgpHEgZAEgEOAogAGA=="); +katakana['TE'] = image(57, 51, "h//AAfwg4GE/kDAwn+gIGE/8AAwuAv4GE4E/Awngj4GFNWJNF/gGF/5UF/+/AwvfAwvvAwv3Awv7GJn8IQV/4BJEv59Fn/wAwkf/DJFEAYABg/+AwjJBAxbQBwAGFH4gGBH4gGIIwgGNG4IGEg//LYjyBAwiyBAxc/EQoGGFIJTLdYJvEgF+fIsYAwo="); +katakana['TO'] = image(42, 54, "//AAgU/+AECh/8AgUD/4U/CgYPDn//wAUC/4VCCgIlDAgIKCCgIKCCgP//wUD//gCgQKCn/zBQQ+BDYP8CgMBEAQBBj4KBKYIKC54yBBQP7KYIKCG4QKB35YBBQIUCGQPjNAUD+BXDnB9Dgy8/CicAA="); +katakana['MA'] = image(57, 50, "/4AE/l/A4s/AwvfAwoAN/YGF/oxGHokf/wGLh4GN/4GSg4GChgGDwARBAw3gAwv4Awo7BAwn/4ACBAwIKB+AGDgJtBAwcAUgOPAwYLB94GDgaFCAwTBDAwcfAwoyBAwgyBAwgyCAwgcBAwgyBNgL0ENgIADn6oHDijhFW4wcB4AGDKwPwBwl/fwzUJDgZOFgAGGngGFhADCA"); +katakana['MI'] = image(52, 53, "gPwAwkf/wFDgf///gAwU/AwIVCBgX//AME//8gEHAoQGCBgYGCv4GDFIMPBggoE4A2CCoIuCAweAAwc/BghYBMwswNw0PNwkBGAIbEG4gMCOoYMCOoQMDAwRnE4BYDKYQTEKYRuCKYY8GgCjDAAV+LAtgcTMDbYhTCHobICBwbBDBghZDZwmAZoYGCAogGBCYgiBEIidCBwQ2DS4QMCVYT2CSAb2DBoLpFn72EdJAA=="); +katakana['MU'] = image(59, 54, "AAMDwAHFv/AAwkf/gVF/4VG8AGEh4VHFgoVPFdZBdRogVBgP4CokBFogVBn/wTIkHEwYrCv4ODCoMP/wVDFIP/JYQVCBwgVBGYLICCoTIDCoQCBBwQhCn5RCCoR/DNoZCDDIRRDCoQODg4+CIQYvGCoZCCCoZRDAQV//4SBRAM//4ABwEfAgQAB/ARBAAkPAwvxAwv+Dgv/8YGF/gkD/xCB543DH4P5AoaBBewsAvgGFhgGFAAQ="); +katakana['ME'] = image(55, 54, "AAcB8AGEgf/AwkP/wGEj/8Awk/+AGEv4iF//AFAuAAwcHFAsPFA34AYNwFAQvBgICCFAUHCAIoDDwQoDn4DBKIf/MYIoCDwIGB/5RBAwWDKIYGB456Dv//75RDAwP/JQQmBAwJ6Dj4GBOYYGCOYcP/5zEg//OYgGNDYw3BAwgvBAwaABAwgaBOARZC/wGDOoP8MQI1D+AGDFwPAAwJaBDAQNCJIc/AQJsBTYL3COQc/4ATBXoYdCSgU8J4SNCmCNCNQqoDAwQuBAwgFDFAITEAwK1DAAKZEAAIMFAA4="); +katakana['MO'] = image(55, 49, "j//AAfAv4GFAon/wIGFgYFE/0HAwn8h4GE/AvF8A4Bv4DCAAQzBAocB/+AAwYxBCYkH/wGEh/8MIv4Awk/+AGEGyJfFAFP9AwpOBNuikeAwxfEHoLpFNoZACAwZABIgIACJYYABIAYGCIAYwCHIoABA="); +katakana['NA'] = image(57, 55, "AAV/8AGEn/wAwkf/AGEh/8AwkH/wGEgf/AwkB/+AA4n/4A4rGoIAE/IGF/wGF/9/Awu/AwvfAwvvAwv3AwpQCOOqqEWLV/H4pGGn5GFAw0fJosfJooGGn4GGKgq6BLQoGEg4GFh4GFPoIpEDYIwFv5MFLQ4GFg6EFgaZFAAw"); +katakana['NI'] = image(56, 43, "h//AAf4A25+/AH4AuWggA5A="); +katakana['NU'] = image(55, 51, "g//AAcAh4GFj4FD/0An4GD/kAv4GD/EADQnwgIGE8EDAwnAAwuAIIgvBAAcPF4IADn4vBAAd/8AGEFAIDBAQIsBFAMDCAIoDh4eBj4oCj4GBFAd/CIJRBgBZCAQIlD/+HQIIGD54oCNwZKDPQZPDOYRdDOYqmBOYi0BOYjCBBogGGYQSAEAwimDGATdDAwQTBH4JFBLIP8AwYTB+AqBAwITB4AGBE4bADBIJyBUIJ6CVgXgJAQzBg+BAoJkCgxcBCYRIEPArlEH4YGDO4ibBeQs+AokAsAGF"); +katakana['NE'] = image(61, 55, "AAX/4AGEg/+Bws/+AGEgP/wAHEh/8Cwt/8AGEgf/Bwsf/AMEAAYnBj4GDHwQOEDAMHA4hVBn4WFJIIADHwMPA4hgCAwZkFCQKCGBwpHBPQwOFFAJyGBwt/BwozBBwpwDGYiYEEgP+iAkF4IPDCoP8j7WCUAXhbwYVB/4RBU4n4QISfD54vBS4f+FASPD+AEB+AFB/IjBFIPnA4LzCGAfAeYIjBGAP4eYQCBwZuBeYUH/EfIwJRCAoIDBg6ACnCmDR4oqBDIKfEHgKuFS4g5CBwo8CWwqOCAAQ8DcYg8Vn48FAAo="); +katakana['NO'] = image(47, 52, "AAcHAokP/gFDj/4Aod/+AFD//gAgUB//AAoUD/4oE/woJn4oLEQYoBwAoIh4oEj4oFJZ8HERU/EQhFEDgIiDH4JFDh4iEH4t/NAYcFHII/Dj4cEv4/DCwIcDCwIcDCwI5DCwhEBHIYQBKwf/GYYhBCwc/FoYKBFoYEBFoQKCE4RrBE4YFCHwQyBHAYnBJ4YFBcBN/AgcAPgYABA="); +katakana['HA'] = image(62, 52, "AAP/wEH/gGCgf/gE/+AHCh4MB//AA4QMBCIQeD4ARCDwv4Dwt/8AeEgI4BDwkH/weFj4eEAgIeF8AeEAgQeEAgQeEAgQeEAgQeGMggeCMggeCQYiACQYYbCDwgbCIogbCIoZZDIoYTCMggTCEwn/CYJFDBYZFDBYYmDv4LBEwYDDg4aCh5JCDQYiDaIQWBNAQ5CMAYLDcgYmCCwgqCGIYTBFwL7EJIIWEAgPgh4WDNAPACwgMBCwiHB/wWEFwV/CwZVB/YWEDgPHXgYuBDwLbDKQPwh60CGwWAngGDgAFBkAHEsAFEAAQA=="); +katakana['HI'] = image(47, 51, "//AAgUB/+AAoUD/4QDg/+AocP/gFDj/4Aoc/+AFDv/gFw8BwIuDj+DFwf/FwcP/4uD///FwQKB/wuBJwIFBFwM/AoP8//PAgP/+IDCAAJdBAAXwg4FDEoQKCIIIgCLoQFBKYV//5qDB4aMuF1YFDFwIRDUIQAC+YFE8YFE44FEw4FEUgn+Aon8WwhKBXggA="); +katakana['HU'] = image(49, 50, "/4AEv4FE34FE74FE94FE+4FE/YFE/oFE/w0Dg//AocD/+AAoUB//AI4ngAod/+AFDn4FEj/4Aon8AocPAokHHgg2BHhYFDHgJCLJBZCEAopIFAoxIEAoxOEApc/AojSBbwplEAoZxBAocPAojICBQhBCGYIFDBYRZCa4P/NYQuCPoYFBSoZGFZYsPAgYABA="); +katakana['HE'] = image(61, 43, "AAMH8AHF/4HFh//wAOF/wOG/AHEv4eFg//DwoOBDwgOCDwk//YeEgf/x4eEn/8n4eDgP/4AeEj/8DAIeCBwPgLgkfDYIeECYQeDh4LBIwIeC//wDIIeCBYJdCDwV/BwIwBDwIOBCQYeBn4pCDwRIBIAQeCMIJPD/AOB4CED4BhBMwf/MISbD/kHPovwj4ODDwV/UYhYBKQJ2DRoIGDHQINEcARCCWYgGEDwIOFgb+FDwL2EDwQGFIQoeCBw0YA40AA=="); +katakana['HO'] = image(61, 54, "AAV/8AGEgf/Bwsf/AHF//AAwkH/wOFn/wAwkB/+AA4kP/g8Rg//AAngv4HFCYIAE/EfA4vAAwv+Eo3wn4HFwAGFJwZ5UgfAPIJzDn/x/+PEgR/BAoJzDP4N/8JzD//D/6KDFYI8BCwYrCCAItBPQOH/wWDCgIQBCwf/4P/wIWCCQIBDWgYBCZ4KJBE4LPDEYInBh5sBBgKLBNgQ0CJoIWB4ACCBgIiBBwP8EYU/TQLXBHQQECFAI8BCwIqB8DzCDYMPAgQbCMoI3BF4IRB44OBWwQUBv4TBJIV//InBHgQCBw4OBHgUH/EfNgKOCj0A3BsCQwNgeaSdCABA="); +katakana['N'] = image(54, 50, "ggGFngFEgP+AwkPAws/AwkB/4GEh4GFn4Gaj///gNF/AGF4BEJAwITBgOAAwQTBh4GCnwJCCgVwLgRwMHAgTBHAgTGv4TEgYTFMIITEMAsHMBY0B+ClFCYiPFEAITEv//OIQMCTg3gBgggEDIIgDGYIgDMIJVDDAIABIIILCFoYYCJwZ0BHQgsBBgZnBBggnCKgYhBMIi3FgAFFgAA=="); +katakana['WA'] = image(51, 50, "/4Ay4A3E/AFCh4GBAoUBAoPgAwU///8AoUHBgOAD4nwAoUf//+AoUDGRYSBGQYSCGQd/94yDh/9GQZFB34yDn/zGQcPAgYSCG4YSBC4YSNv4SKJYJwDLwISEn5QDS4QSDDAJjDDAJ2DGIJ2DUYQ+DQYKcFFYYXBDASOCGIQFDGIQRCDwTaCG4YFBEgbHHN4hiFg6HEA="); +katakana['WO'] = image(50, 52, "/4AE34FE94FE/YFE/wYYGocB/+AAwd/8AFDn/4AocP/gFDgf/KovADAnwDB43B45EE+IFE/F/KAkfBgmHAonhAonwDAn8h4MEN5X/N4l/N4k/KwkfRwgoBDwcHOohoBOoYFBEgY2BEgYFBEgYFBJIYXBFQYpBFQZ3CAoIWCKoQQCGwQLDHgR8CAoQdCAoQvCOYYFFn5gENgKREbYgAGA"); +katakana['RA'] = image(51, 50, "n//AAcHAongAon8j4GEwYFE+F/Aof+h4ME4IFE/BYr+4FE/wFE//fAon7BgpYE//vAon9CQo3Ev/gAocP/gFDgP/wASX+ASJgYSFXwJ2ECQivBDAoSEWIs//wFDbYIrDAoI+DAoIYDQ4IYCFIIABDALlDGIJhBewS/EJQQYCG4YkED4QFDD4JJF4AFDA"); +katakana['RI'] = image(43, 53, "AAf/7/4AgMf/f/AgMD/9/8AFBv/v/gEBh/9/+AgEB/+/+AKBn/3/wEBg/+//AFX4q3v4qDh/8FQQPBz4PDAYQvBEYQvCEYI/CGYRPBB4cfIYQpBB4cH/5TCDwJjD/4kCn4EBCgN/AgIUBDoP/FIJHBAAIyCDIYjBIYYaBQ4QaBJoZHDAAoA="); +katakana['RU'] = image(61, 53, "AAUH/wHFn/wAgUB/+B/+AA4UP/gBBCgd/8ABBAwUD/4BBBwcf/ABBA4f/4ABBHQg8FHQI8/HksYHgwYBHgkPF4I8EvwlCHwOAg4gBEYI8CCIQjBHgITBCIP+HgU/CwIRBDAIgB4AMCAgMfEAIMBDAIOCBgQYCIwQMCPYJTBAQI8BBwUHEoN/8P/IYN/+AvBj4LBBwOAj/7BwZGB/4ABBwXAAQIODM4QOFHgIOC/4OBh4OCAYJGBv4OCn4OBHgJKBAYJkBIQISBaIYhCCwIOBSoTqBJQISBeYUHd4U+bYUwcAYAKA"); +katakana['RE'] = image(51, 51, "//AAocf/AFDgf/CQl/8AFDh/8AocB/+AAwc/+AFDg/+GX4ECgwyEgPgGQk+GQkP+IyDC4IyE//3GQc//gyDh//GQYYB8YyD//4GQc//wyDDAOBGQUH//gGQRvB/BlD/4DBGQU/CwIyCj4YBMoQkBBIIyBBAIYBGQIkBDAIDBGgIiD+AFBGoIyBv4eCGQIABJwQvBAAJnDEgTLCEgY8CIYLLDEgZVCAoZuBb4iaBfAj+EgE4AokAA"); +katakana['RO'] = image(50, 47, "/4AEn4FE94FE/YFE/wYF34YS4A1BgIYB+A8Cv/v/gFCj4YBAoUHDH4Y/DEbglDBQ8CAAYA=="); +katakana['YU'] = image(59, 46, "gP/AAX+A4M/A4fggEHAwf8BwIGD/4GBj4VFgYVGv4HDwEAh4GD+A+Eg46CAAf/4AGEj/4Coo6CCqJFBCot/KAIADh5QCQAhQBCrM/Myk/M3JQGh5QFMyIRBAH6NB"); +katakana['YO'] = image(50, 49, "v//AAefAonnAon5Aon+DDA1DgP/wA8E8AFDj/4AocHDFZjfDCJjxDD5WE/+/AonvAon7PgoYX/g3DAAQ"); +hiragana['A'] = image(52, 50, "gEB/wGEn/AAocD/gMcg//AAfgv4FD/wMYFIRNa54HDgYyCBgYsEBgX/+AGBHQYpBCQQaCh4JBJQPwgIdBBAP/wASB4H/j/8MIP8j5fBBIP/4P8gf+j/7/hVBj/jA4PH/C/Bn4RBv8Aj/3/Ef55FB/9/wI+D+/wj40BHwIWBL4QJB+BFBwAmB/4MBD4M/94MBD4JAB/4cBNYN/BgM//AsB/n/z4bBQgOHX4QVB/B3B/CQCAQTSC8BFCB4Q4CB4UAgIIBRQOAXojREn/gaIgAC"); +hiragana['I'] = image(58, 50, "v/gAgUggEf/AGCnkAg/+AwU/gEB/+AAwQZBDgcP/gcECQIcFCQIJCCol/4AGBgYLBj/wCokHCAIABFAIQCCon/DgQECn4cDCoItCAAI+BDggVCLoZeB+BgCCocPPQZUBwZdDJAQcEGAIcEGAIcEGQPDDghIBDggyBDggyBx4cBjxIC8aaCCAIyBLAMDM4IyBSARnC//HUIk/+IyBCASdBLAJKCGQOf/kDJQV/GQRKCJ4XgEYRPC/CoCDgOHNwl/8P/84jCDgM//5HCDgMHAwIjBgP8DwIsBQgYVBSQgVBaYZnCTIgtBbQhDCUAYkCfwYOCGIgAHA"); +hiragana['U'] = image(46, 50, "h//Aoc////8AFBAgIABgEDAofACwIAB/wWD//4CwgdBCIeAFQUfCwIADCwIAMj//+AEBv4tDAgQLBHAYFBAgf/8YFE54FECwRTB/wkCAoP7IAd/OgR2CKwcBQ4kH/hMEJYQcC4AWIh4WEn4tJg6EEj6EEVgIQDE4l/CAbABCAZqBBQgQDBQIQCXwIyCYYTIFeIhlCBQjxCLIQWBMgbdFvzYJ"); +hiragana['E'] = image(55, 50, "gF//4GE/4AB+AFBgIGC/+AgEDAwYNBg4FC/wGBh4GC/gGF/ArFFIQAD4BRVn42FLAIGEJQYGBLAhEBLAhEBLAf/8ArDBIIyEj5fCRYZYEEgJYEN4JNFDQouFDQKcBFwYGFMIIGDLQRJFAwgaBOYQuC8Y2DFwODAwcP/0HXAc//EPcQnAj5LCPAU/MwR4Cv5ECPAQ9CLoUBd4auE/guBVwf5PARaC+5qCAwXnJwSXB//HI4QGCw5ACAwUHNIn+gj/HAAg"); +hiragana['O'] = image(54, 50, "gEB/0AggGCg/4gE8AwUf8EA/gGCv+AB4QaDv/wDQn/CwIaCgP/4AaDgf/wAaCgPn/4PBAAXv/0HAwef/kfAoX+n/4v4GCAgPxCYfg/4jBAAWBGwQ1BgEDJoJQCJoJRBLYcPCAJrCgEcKAaGEHgSGDF4QPCJYYxCHoYMBn5YDBgoGBDIP8FQKiBDwabBFoIzCv/gEAJQCMwWfKAIbBh58BDQMH/l/4IaCh/xTgIaCn/P/BrD/8/4CGD/i3BDQfz/gaDv/P+AaCCAIaEHQQaDv/hGoV4h//g4VB8JnBa4ePZYRkBBwKNCbwPwCYR/C44CB4BtBfgSaD8ACBYQQWBAAYA=="); +hiragana['KA'] = image(55, 49, "gEH/AGEh/wAwkf8AGEn/AAwl/wEAhgGC/4CBngCBgP+AQP8AwMDAYIyDAYUPAwQ2CAwY2Cj/4gP/AAP4j/wgYGC/gGBg4GC/0/8EPAwsfCgd/4E/Awt/FIf/LgJmBE4IGCMwMf8JjBHwIPB4IDBgZmBv+DAYMHMwP/BQRfBOwIKCL4J2BOIQvBAgJxCGQIEBHAKPCCwIYDCwQBBQoRGBviIDIQJRC4AdCXAYdCKIcHboQ/CboY4BboghBboZKCFAYhBjAoDh/8nzME+CfBF4V/RgP/EgKVBwYGBFAMH/zIBFAQeBAwIoDboRRD4DrBJQUHAQJsDAAwA="); +hiragana['KI'] = image(48, 50, "AAMB+AFDh4FL/AFDg4FIn//AAX4ArpHC/xNEAov/LQgFCDgYAlF4UfPx8/g/8CoQbBKgQhCAoMDFAkHAoeAh4FEDgQAB4E/FgIUBwE/HwQdBn/gAoM+AoPAAoMMAohFCAqIpCgI7C4BEBI4oICAoZfE4C9BAob2EAoISCaQgACA="); +hiragana['KU'] = image(33, 45, "AAsB4ADC+ADC/wDBgf/wADMg//CYIDDh4DDD4UfAY/8AY34AZRDCh4DCg4DCgYbCgI/CgH/BgU/BgREBBgIQB8AMCFIRNDLoJ2Cv42DJwQdDFQIdDFQQdDFQIdDHYRkDgYhCgADDnwDChyzE"); +hiragana['KE'] = image(50, 49, "AAUB/0Ag/gAwN/wAICgEfBIIIBB4P4BAYPCh/wDAcD/gYE/4FBDAU/4AYEGIgOCDAQOBh//AAP+v+DAoX/7/AAof3+E/AoX9/gYD/9/gYFD/4YE/5QCGIJQDHYRvCJQU/N4JKCKAYYCKAQYWmAYEjwYEx6lDh/zUocDMgIYDv6cBKgUf/4yBBAMH/4eC4EBNQUfAQN/DYMPE4TjCAQQkCYgSJBDYLEBn7QCAQIbCE4UDDYP/PIV/CgLpD4EPP4UH+AkBAoIACCgIADh6LCAAMDAoYA=="); +hiragana['KO'] = image(52, 50, "h//AAX+gAFD//gBgn/BgvwBiWAAon4GwUBDIQACCQQFCn//4AFCg4lBCQc/DwYfBKQJdEDwYAB8CIihAFEgJJDIgQFEg5KEMgITEj/8D4hwED4JqEOIIfEv5eEg4fEFg0PHIwsEBigmFCYkOv65CJYPnbgn+ZgIAD8IMFewvgCYjRBE4IMDegQABIoUfAoK7HA=="); +hiragana['SA'] = image(51, 50, "AAMB/gFE/+AAwcf+AFDgf+DIl/4AFDg4fEgAfLgIfCj//AFQzCn/gLJYMELI5mEh6GGBgUHGAP4CAQ3COYILCBgUDIgYZBAoYmBn5REDwPgQQPgDAIVBj4fBJ4d+CQI1CgeAXhgSDKoYSEQQp1GQQpFBawXwD4IGBg42BaQngBgRlDBgmABgjzBRYZDCPIYvCv//MQoACA=="); +hiragana['SI'] = image(45, 50, "v/AAgUD/wKDj/wAof/wAECg/8BQc/8AbD/4bE/AbEFgcHFgk/FgcBFgkPDYhIgFgIKDFh8eFgn+FgcH/4sDv+/FgUD/osDn/vFgQ2BFgcf+YsD/+fFgUP/gsDv/HFgSKBLId/8IsCHgIXBSod/EIIKBwIhCv/4h4WBAQOAv/+IIP8AQIAC4AYBAAIkBn4KDJQIKDCwYpBCwRWCAoJhDAoK1DAAg="); +hiragana['SU'] = image(52, 50, "AAUf8AFDgP+BjH/AYP/AAnvAon+BjJAUgf9BgZFB/4MDn4kEg4MFGIwMED4QME+E/+AyC/x0DFgPABwIMC/gMGDIn8gYMFv/4EwcP/+AKYf/BgRACBgYRB/4mCgF/AwJ6DBgoTCRohNDTZE/VAkP/gFDE4PAUQhGCI4YeEUIgYBD4gMBEpI4GgIFEAAo"); +hiragana['SE'] = image(56, 50, "AAcP/ADB//AAwP8AwkHA34FBAAn+A1JalmAGFvinFv4GF//PXghEBAwfBAwoNGEQP/+AGDn4GFh//8AGDg5PCgF/AYP/wAGEgj/CAwQADAw4mCAwZCCAAQ8BFQgGBAAQGBj4GFJQIGEJQIGEgYGFGIIGCIQQVDHQgACA"); +hiragana['SO'] = image(53, 50, "gP/AAXggEPAweAgF/AoX+gEDBgfwgEfCYoFD/EAg4MFAAQMCAAQwBBhQpBJQozBAAU/IAIACIYJUBAAV//gsJD4IsEn4sEOAn+NIn/+4FEAA39AwvvAwqQDAAP7UYhmCx5bDuBVB4BCDg5bEJ4JoEgJ1EEQKCESwIFEg5vEEA4TFh4TFv4TGYgiLBCYrFG/5dDd4YHCOQKkBDQjbDDQQwDWgR5DAwSGEEAgAEA=="); +hiragana['TA'] = image(52, 50, "gEP+AGE/4Mjgf/AAXAgE/AoX8BjUAgP+GYkf8AFDBhHnEIQMBEQQhBn/jFAWAgYMD/AMH/gMF4f/F4UH/kQGYd/KIIACg4VBBgmAQ4gMFUJcB/8DDQZgBv6iD/wuEn/gKIJGDEIl/4KCDC4KPE/+BBgYXBBgY5BAIImCj4MBTIKFB/wMBAAKSB8EPAwXnUYIMDCwLYD95RBEAIZCFQN/AwPBKISpBwEGQAgAGA=="); +hiragana['TI'] = image(51, 49, "gED/wGEv/AAocP/AFDgP/CQk/8AFDg/8Bgn/wAFDj/wBQYAqJ4M/LBZrMJYZ+Ch5aDv/f/4bCBQIABCoMDHAYTBv4+Ej4MEg4DB4IMCAoIcCwE/TwU/+ASBEQI8BVQJLCv/gS4cP/kBMgYWBjyoEgLbJEYYSCQQkHCQg2EHASCEv4SBgYOBOQ70BQoYrBEQIABFYR/DJASRED4YFCBgJDDA="); +hiragana['TU'] = image(59, 45, "AAUP/4FFAAIGCAoX//EAg4GD//ACYYAB/kBAwgOBn4OFDgoOBAYX+BYP8j4GBwEAAgPDGwQ+C/F/BgIABCwOMLQl/+AGEg/+NIv/8BwF/gGEKwIqDAAM/HAYzDEhkfEgsDEgxJGh5JFHQPACqQrBCpkfCopXBCogcBCog5BK4jSCAwxtDDYK8EZIQcCAoQcDCYTjCJgQGCEYT0DIAYGGEgQGDEgRcEv5UEA="); +hiragana['TE'] = image(57, 50, "/4AFv4GF34GF74GF94GF+4GF/YGF/oGF/w7Cn//4BCDAwOAAwpQEj4ZDAxP8AyUPAwwiFg4GMgZFFAw0BLQqlBNAkAv4GG8AGEn/wKgv4KhZGGHALeGH4oxNh4xFOJBjGEYt/VQwVFg//BwhOBAAI7Dv4GBHYYcBCwgcB/5CEDgQyFGYgrCUwkPKAwAC"); +hiragana['TO'] = image(46, 49, "gEH/AFDj/wAod/4AECgP/Cwn8C0cICwcDBoIWC/4NBCwMfEgV/4f/BoIWBv//LAMH/4AB8AWBAoWAgE/BQYlBDYUAh4FBHwQPEEIJQDFYJhCgYwCLQQqCDYQKDDYIKDn5xEEAYQB/x8JDYkDCAkPYIk/JoQWTAol/AocZQwR6B8aNCAAOPAgf+TIZqBAongT4QfCBYY9BW4R1BA="); +hiragana['NA'] = image(55, 50, "AAd/wEAn4CBgH/BIXAgEB/wJEgf8AQIJCg/4AQIJBgEP+ACBBIMAj/gAQYsBEoIoCGwf/GwkB/8P/4AC4f+j4GDw/4n4GDj/wv4FC/0/8AMD/l/4IGD/H/wYGD+P/g4vELARtCMQRtDMQQKDL4YKCMQQKDMQQKDR4QKCTIYKCFYQ2bOoI2C4BgCGwWASAQ2BGQKJC8DNBBAIAB+DNBPYf4ZoKrDAgPwT4K7BAwRdBB4K3BVYIqCVYY6BAwKrB/0DVY3+v/hAwf8n4SBdIXwnxEBAwXgnBEBAwShBO4IbBSYSVCOYQAHA"); +hiragana['NI'] = image(57, 50, "AAMPwAGE//gAocf//wgFwgEH////kH/AZBAwP+gf+Bof/wP/gEDAwWAAIMBAwc/FgIGDj4sBv4GBE4P8HAIdBE4IqBAwYgBKAIGCKAYKBAwN/EYIGDn4jBAwZfBDAQfBLIPAAwZZBDgItENYN/CAIfBIAIGCLIRfDLIXwAwc/RQJmCHAPv/0PEoI4B+f/AwcH/P/w50D/l/wZ0CgP+j/BK4Q4Bg/gJoQ4BwIGBIwU/4EwAQI4CIYICCAYY/EJQMHHATcCbAQKEHARGBGgQqBCIc/D4IGDaITCDT4PAAQJfCQQRYDeQQGDSIIGEYYIGEE4IGEDgYFCcAQ+CGQZsCABAA="); +hiragana['NU'] = image(58, 50, "gEP/AGEgf//wHE/4ABAwc/AwIPDh4OC8AGBg4GCEwUBAwX8Dod/EgoHC4AsF+BJFjAGDg4iEFgRfF/+AAwk/IwQjDFIgjDvAjDMYJlCgRHB4ABBFIUf/ABBFIXH/0HCoUf+BcBLwQpBCogpBCYIVDv+ACohNBn/wCoRxBCohNCMoIVBOIQVBAIJNCCAIVCEYIQBCoOAb4QtDCAQtC/gjCdIIXCN4QwBC4SVBDQIXBEYUP/gXBI4QEBHwPD/8ODgR/CwZNCCYN/8P/5/4GQOf+DtBKgXv/jtBKgX5/0PAwJxB/0/DAL8CvkDJYP/IYMMgFgg//fot/VYQACgYGFAAoA=="); +hiragana['NE'] = image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA=="); +hiragana['NO'] = image(54, 50, "h4GFn+AAocB/0IAwcH/F//4AB+Ef8IFC//A/+PAwcD/0fAoX8h/wDQk/4ITDAgMDAwcH/hGC/EAj/wIwXggF/4AGB/+AJIIFBGQJJCDQoWBDQf/wZlBDQIWBh41Dx5kE/0/Mgn4IgIGD8f8MgYaBL4IaEPQJrD/6RCGoRkCKAR/BKAgaBKAoaFNYoWCKIIaC8BKCDQWAIYQaCgJCCDQRyDDQRXDEoOBK4ahBW4K+CAgKcBDgLcBMwIwC/1/4JHBCYP5CoQwC4aND/atBRofDAgPgdQaSBHgX4hxXBHQXAhAOBAwKXCAAJlBbIIAH"); +hiragana['HA'] = image(50, 50, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAA4A=="); +hiragana['HI'] = image(59, 50, "gP/AAOAA4U/AwPwAwUHAwP+CwYVC4AGCj4GB/AGCgYOCCod/AwPgGokH/g8GHQY8CHQYVCHQg8CwEfCAYEBgYQDAgV/JYYEBh5LDj/4GoJKEGoJLCAwP4JYZ9C/BLCNwSGDQgSGDOoaGDAwg6BEYQHDh//EomDAIP+ToaQBEIIvCKoJyCJgPH/yDCEIIVB4BNBMwIgB+CZCn/n4f+h5jBAQMw/+BOgKyCCoN/PIICBS4I0BCoQJBJQJqCBIP5NQfgD4KACn5tDGQSDEwADBTIJaBGQKZEDISvCToR8BeAQDBAQLbCb4RSCAAcHcQYACvwGFg45BAAj/DAAw="); +hiragana['HU'] = image(55, 50, "gED/gGEg/4AwkP+EAhwGCj/ggF+AwU/4EB/wGCv+Ag4GD/4kBAwM//4AB84GBv4GC54GBAoX/x/+gIGDh/+gYFC/0P/kHAwX8AwMPAwX4j5cCGwJOBAwJIDj5jBv4QCAwIpBNoU/+AiBNoIGCJYJtBAwPhFwPANQXjAwOAgEEv+P/A2C/H+CoI2BTIIhBwY2Bh/xwH+UgUf+CwBUgSgBBYKkCn/gh/gToI1B4Ef4AvCBIM/4ZmCIAN/44oBSgKdCFAJ3CLAY0BUgQoBGgIGBEIUPAwSID+AGBQIZHBJQRECd4Q9DI4QvBJwQ2Cj4sBGATRBJwLcDFgTcDC4QGEEILqEAwIbDIARoCBgQAGA="); +hiragana['HE'] = image(55, 50, "AAUf+AGEn/gAwl/4AECBQP/wAYC4EB/4YDwED/wYDwEH/gGCCIMP/AFBgIRBGwcDCIN/GwUH/EP/4bCDAP/AAI2C+4GCHwMfAoX/JgM/AwYjBv4GI8YGCFoN/wIGBgYCBFwIiBHYJfBNAPAn/8IwIGBwAaBh/wAwOD//4R4IfBg//+B2BDoJKB+AoBg/+JQPjOwMP/n/z/nQIMf/IOB76BBn/3/gVBMgN/94nBOQX/7/gAwKbBOwSOCHoJMCEIMH/v/CAJxBh/7/hcCF4X4KYLEC5/wj5KBEIOfGwJRCL4PzF4V/JIQvBCYJJCH4JxB4AGB/xCCFQIJDDoIMBBIRNBAQJdCIwKUCeAb5CPgQACSgIFDSgIFEAAg="); +hiragana['HO'] = image(51, 50, "AAN+AokP+AFDgf+Bgl/4ASE/ASVv//AAX8h4FD/+BAonwn4FD/0HBgnAAogoBgP/HAk/8AFDg5LEgASM/gSFwADBFQIAC8E4Iof+/5FE5/wAof5/0fAwc/8YFD8f8PAYEB54MDJ4SRDJ4KRDj/gNYaoCLAYWBLAYWCLAQWCDYJvDgYSCCwV/NYQWBGQc/+AyDg4yBj4MBgYSBAQP4OwPwbIglBQAgpBBgZiBBgYYBBgY1CU4S0DFoIRCAAo="); +hiragana['MA'] = image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB"); +hiragana['MI'] = image(50, 50, "h+AAocD/gFDgP/CQl/4AFDn/gv//AAOP/E/AoXj/0HAoX4/+BAoX+DAuf+EfAoXn/gYD/P/gYEBG48f+AFDg5QMMYkf8BvE/BvE/wYE/4YEKAIYYgZSCDAMBJgQYCCgYDBFoYDBj4tCDAJlDDAMBGYYYBNYYYBn4xCg/4h6ECPgIHBPgfBDwaVBQgYvBToYYCFYauBaIIwB5/wcAfz/0PAoX8cAn/IgQFC55dBAoXxFILtC/grBGgL5BYIoAGA=="); +hiragana['MU'] = image(58, 50, "AAV/4AGEj/wAwkH/gGEgP/Aod+Dgv/wAcEj/gDgkH/AcEgP+Dgt/Dg3wn4mBHwYGBDAIyCAwP/8AGBAoQODh4GC/4sBgYGD/AcCAAO/IQQcC4IkCDgI7Bj5YBg//w/8EAIjCwIEBv/gMQPgLAMPFYP//h1BgZpC/4LCNwIxB4YoBFoIxB/AjBNIMH/v+n5UB/4qBn/fIoIJBv+PLYUPQwPhOIUD/gvBGYMH/3/BAX/457CBAP/84GBDgIlB/YGBCYJwB/qECDgKREwBCC34YBDgfvLYP+HIM/+YYCIwM/MoIYB/hGBMoQEBz4nBKQfDAwODGQXwKQQMB/P4j4GBAQP+ngtBUgIRBg6aBRwKiBwOAf4TNBAobjCAogAEA"); +hiragana['ME'] = image(57, 50, "gEP+AGEg/4AwkD/gGEgP+Dgv/Awt/wAGEn/Agf/BIUf8EP/40CHAMf/4tBAYP4AQImBCIP8n4GB4EH//+AwXgEwP/v4CB/EBAYIPBg4jBAwX8BYJFBCQRKDFYIGBJQJxBIgUfAQIrBAYMPCAIfBBQR8CAwR8DMAZ8Cv4GCGIQGDGIU/AwR8BAwKqCWoU/FoS1Cj4tCHASEBWogGBUAQKBAwItBHARpB8BlBBQKuCAQIKBO4SqCBQX8AwX4h/9/wGC/kP/n/DYSlCv+P/ArB4K+B4/4SIV+j/jWIX8n0P+JSBDoMOMwJWBAwOCMwM//ZOCMwI4C75nB/5bC45nBv+DAwPhTgXAb4PAoCfCQQifBYoYAHA"); +hiragana['MO'] = image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA"); +hiragana['YA'] = image(54, 50, "gEf+AGEv/AAocB/4MEg/8DUv///Aj//wEDAwIcBAwMP//8BgIGBn//+IFBAwICB54GCDQQAC/0HAgXAn45BD4IDBn45Bv4MBAYPgGYJKCFAIbB8EAgf+DQRbEv/4LYYaBOQU/4EPCwIhCCYJrCgf8CYkP+BlBCYQaBv6GDOwQaECYIaEKwIaD4JWDgP+CYIaCg/4NQYTB8Z+BFwef+4aCMgN/74aCn/z/zXCIAOH/IaCh5CB44aBJoU+a4QyBwFwDQLGBCAOBX4adBGIJMBRIQaBUYI4CDQJnDFYJ7EDQKzCDQYECAA4"); +hiragana['YU'] = image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA"); +hiragana['YO'] = image(55, 50, "AAMHAwsP+AGEn/gAwl/4AFDgP/BgkD/whF/AGEj4oFEIsA/+AEIgoFg/8EIooFJQ3/JRcHJSgoGJQxEEg//FIkfAws/Cgv/AwUGJQX/HwMP8AoB74GBj/gh/+IoU/4BzBBQJBCJQIKBNQRzBv+AWoIIDJAP4SoMBIgIkBOYMDHoKTBAIIRBXgQBBB4IfBEIQYBFALgCCwMP/iVCJAXwJ4QfDcAX/4JRBSoRvBEIZ2DcAQGCFQIhBPoIYBcAQGBDAJqBCgQ6Bg7rIAAY="); +hiragana['RA'] = image(48, 50, "gEP4AFDj//wAFE/gFE/4TCn4FBBgQFCBgQRC//gBgN/BYUP/EBAog3BGIIFCgH/BAIFCh4FEgQFEBoXwAqsfAoIuBAoROBEwIFBIwP+AoPnLIWALwZfBNQf/+AFE/AFBEIM/AoR6Bh/8OoIzBg4FBRgQFCL4UD/wlBAoikCAoM/W4QFBj5dCAoMGAohpDg4FEHYJ1EAog5DDgJWCb4Y/Cg7RDaARFCAoZFBAobiEeoruCAoQtCAoI+DAAgA="); +hiragana['RI'] = image(40, 49, "ngEDn/AAg9/4Ef/AEBwF//4EBwP//4HBw4EB4F/x4EB8F/z4EB+H/n4EDAQIjBCwUPAgUAAgX+gEH/n//gEDHIMDAg3wAgP+AgvgAhBeBAhmAAiJ3BAhf8AgRUBAhBXBAAJtBAgSgCVgRcBAAJXCEwIEDj5SCBoJDCBAKSBBASSBXwKICAgQmCAgIcCv4SCAgI0DeAY="); +hiragana['RU'] = image(51, 50, "gf/AAXAgF/AoX8gEPBgeAgIFD/EAn4MEg4FD8EACQoACn4lBAAUf/4FDDYOAAoQuBHwIACv/wDwgkEh/+DwoFDDw5ECDwRLDMwg5BLIZMBNgh/FGgIeB+AVB4AeBEYJmBBAJQBDgPBOocf/AoCVIU/Kwc/+5WDg/+Kwl/5/wh4mBh/4/A2CFgMOAoJDC8GBMgUHGAJQCCQKpCBgISBgf+SQMPCQN/4H/4YSBGIIwBCgMBDoTMCn/AEIROCLoKFEAIJvBTwZvCTAarFNIQFCXASyCYoYxBAoYAEA="); +hiragana['RE'] = image(56, 50, "gEf8AGF+AGigP/wAGDg//GYQGBh//C4M/AYICB/AGDv///gGC+P/AwQKB+YGB/wNC+//w4GDBYMDAwn4AwQ3BFQIGF8AGF4AGFgAGEAYMDHwIGBAYIGDn5XBAwhlBAwd/Axh6CAwSPBAwMHAxEDAwqdBAwidDAw5IBOoQGDU4QGDUAIGE//fAwufCgrmCh4iCAwk4nwGE/EcAwbSBjAGFegReCUgIGJOYIUEQIYGCIYOAAwPgAwIAIA="); +hiragana['RO'] = image(50, 50, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAgAD"); +hiragana['WA'] = image(51, 50, "AAV/4AFDh/4AocB/4DBj/ggE/AQMD/0Ag/8DgWAgH/AQMP+ASB//AgISBAoIDC4Ef///+ASBh4FB/4SBgYFC+E/4IFC/8H/F///9//g/8f/3/x/+j/nAQPwv/j/H/wf+I4N/KAJlBv+P9/4MoMP/f9/xlBAIIqBwAUBn/vFwIdBg40BNIIOBIIR7B+BbC8B7BKoX4uAyCAwM+GQX5//f8IyCn/z/hHCK4N/4/8h/8/4EB/4lBF4P/z5wB8f+RYJjBPoPAFwO/BQP4IQX/wJkCTAUfVYf4gf4BgS4BbQRiCcgbSCAAILEcALkCAAM/DoYeCC4ZLBfoIeD/ASEDAhoBAoYlBDwcAg/ABggAEA="); +hiragana['N'] = image(54, 50, "AAVgAYUP8EHwAGCv/Av4RD/8D/wFCgf8g/8DQf4j/4AwU/8E/+AaDwF//4VBgIfB/4GCD4MPAwcf+YFB/4jBn4FC/4jBAof/4AYC//n/+DBYeD/wZC/f/FgIrCGIQsCKYU/444CKYP/z4xCvxOBv+/8EBQQP4B4KFCCoJeCNIYPBQgQKBj53CAYSbBCYQDBHgJbCTYUDOQZHBM4QTBTYX/GQQxBP4Y8BDQRGBTYY4Eh5MDHgZTDAojdEbAYGEHgIGEv7/DHgIhFfAh1EEIg8GEIg8GTYYhDHhYAF"); +/// ///////////////////////////////////////// + +let kana = katakana.KA; +let scroll = 0; + +let hiramode = false; +let curkana = 'KA'; +function next () { + let found = false; + for (const k of Object.keys(katakana).sort()) { + if (found) { + kana = hiramode ? hiragana[k] : katakana[k]; + curkana = k; + return; + } + if (curkana === k) { + found = true; + } + } + curkana = 'KA'; + kana = hiramode ? hiragana[curkana] : katakana[curkana]; + updateWatch(ohhmm); +} + +function randKana() { + try { + const keys = Object.keys(katakana); + const total = keys.length; + let index = 0 | (Math.random() * total); + curkana = keys[index]; + kana = hiramode ? hiragana[curkana] : katakana[curkana]; + } catch (e) { + randKana(); + } +} + +function prev () { + let oldk = ''; + let count = 0; + for (const k of Object.keys(katakana).sort()) { + if (curkana === k) { + if (count > 0) { + curkana = oldk; + kana = katakana[curkana]; + return; + } + } + oldk = k; + count++; + } + curkana = oldk; + kana = katakana[curkana]; + updateWatch(ohhmm); +} + +const kanacolors = { + A: [] +}; + + +function updateWatch (hhmm) { + g.setFontAlign(-1, -1, 0); + g.setBgColor(0, 0, 0); + g.setColor(0, 0, 0); + var whitecolor = false; + if (curkana.indexOf('A') != -1) { + g.setColor(1, 0, 0); + whitecolor = true; + } else if (curkana.indexOf('I') != -1) { + g.setColor(0, 1, 0); + } else if (curkana.indexOf('U') != -1) { + g.setColor(0, 0, 1); + whitecolor = true; + } else if (curkana.indexOf('E') != -1) { + g.setColor(1, 1, 0); + } else { + g.setColor(0, 1, 1); + } + g.fillRect(0, 0, w, h); + + g.setFont('Vector', 50); + if (whitecolor) { + g.setColor(0, 0, 0); + } else { + g.setColor(0.5, 0.5, 0.5); + } + x = 26; + y = h - 42; + g.drawString(hhmm, x - 3, y - 3); + if (whitecolor) { + g.setColor(1, 1, 1); + } else { + g.setColor(0, 0, 0); + } + g.drawString(hhmm, x, y - 1); + + drawKana(4 + (g.getWidth() / 6), 60); + drawMonthDay(); + Bangle.drawWidgets(); +} + +function drawMonthDay() { + g.setFont('Vector', 20); + g.setColor(1,1,1); + g.setFontAlign(-1, -1, 0); + g.drawString(month, 4, 112); + g.setFontAlign(1, -1, 0); + g.drawString(day, w, 112); +} + +function getPhoneme(k) { + switch (k) { + case "TU": return "TSU"; + case "TI": return "CHI"; + case "SI": return "SHI"; + case "HU": return "FU"; + } + return k; +} + +function drawKana (x, y) { + g.setColor(0, 0, 0); + g.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1); + g.setColor(1, 1, 1); + g.drawImage(kana, x + 20, 40, { scale: 1.6 }); + g.setColor(1, 1, 1); + g.setFont('Vector', 24); + g.drawString(getPhoneme(curkana), 4, 32); + g.drawString(hiramode ? 'H' : 'K', w - 20, 32); +} + +var ohhmm = ''; + +function tickWatch () { + const now = Date(); + month = now.getMonth() + 1; + day = now.getDate(); + function zpad (n) { + return (n < 10) ? '0' + n : n; + } + const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes()); + if (hhmm !== ohhmm) { + randKana(); + updateWatch(hhmm); + ohhmm = hhmm; + } +} + +Bangle.on('touch', function (tap, top) { + if (top.x < w / 4) { + prev(); + } else if (top.x > (w - (w / 4))) { + next(); + } else { + hiramode = !hiramode; + } + kana = hiramode ? hiragana[curkana] : katakana[curkana]; + updateWatch(ohhmm); +}); + +g.clear(true); +// show launcher when button pressed +Bangle.setUI('clock'); +Bangle.loadWidgets(); +tickWatch(); +setInterval(tickWatch, 1000 * 60); + + diff --git a/apps/kanawatch/app.png b/apps/kanawatch/app.png new file mode 100644 index 000000000..cf081937b Binary files /dev/null and b/apps/kanawatch/app.png differ diff --git a/apps/kanawatch/fontmaker.zip b/apps/kanawatch/fontmaker.zip new file mode 100644 index 000000000..39c7d5d53 Binary files /dev/null and b/apps/kanawatch/fontmaker.zip differ diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json new file mode 100644 index 000000000..b14703979 --- /dev/null +++ b/apps/kanawatch/metadata.json @@ -0,0 +1,31 @@ +{ + "id": "kanawatch", + "name": "Kanawatch", + "shortName": "Kanawatch", + "version": "0.05", + "type": "clock", + "description": "Learn Hiragana and Katakana", + "icon": "app.png", + "allow_emulator": true, + "tags": "clock", + "supports": [ + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "kanawatch.app.js", + "url": "app.js" + }, + { + "name": "kanawatch.img", + "url": "app-icon.js", + "evaluate": true + } + ], + "screenshots": [ + { + "url": "screenshot.png" + } + ] +} diff --git a/apps/kanawatch/screenshot.png b/apps/kanawatch/screenshot.png new file mode 100644 index 000000000..b1ed879aa Binary files /dev/null and b/apps/kanawatch/screenshot.png differ diff --git a/apps/kbmorse/ChangeLog b/apps/kbmorse/ChangeLog new file mode 100644 index 000000000..c85361374 --- /dev/null +++ b/apps/kbmorse/ChangeLog @@ -0,0 +1,2 @@ +0.01: New Keyboard! +0.02: Temporarily fix because of firmware bug. diff --git a/apps/kbmorse/README.md b/apps/kbmorse/README.md new file mode 100644 index 000000000..2d5aa166f --- /dev/null +++ b/apps/kbmorse/README.md @@ -0,0 +1,25 @@ +# Morse Keyboard + +A library that provides the ability to input text by entering morse code. + +![demo](demo.gif) + +## Usage + + +* Press `BTN1` to input a dot, `BTN3` to input a dash, and `BTN2` to accept the +character for your current input. +* Long-press `BTN1` to toggle UPPERCASE for your next character. +* Long-press `BTN2` to finish editing. +* Tap the left side of the screen for backspace. +* Swipe left/right to move the cursor. +* Input three spaces in a row for a newline. + +The top/bottom of the screen show which characters start with your current input, +so basically you just look which side includes the letter you want to type, and +press that button to narrow your selection, until it appears next to `BTN2`. + + +## For Developers + +See the README for `kbswipe`/`kbtouch` for instructions on how to use this in your app. \ No newline at end of file diff --git a/apps/kbmorse/app.png b/apps/kbmorse/app.png new file mode 100644 index 000000000..0abc7e67d Binary files /dev/null and b/apps/kbmorse/app.png differ diff --git a/apps/kbmorse/demo.gif b/apps/kbmorse/demo.gif new file mode 100644 index 000000000..991c8c68d Binary files /dev/null and b/apps/kbmorse/demo.gif differ diff --git a/apps/kbmorse/lib.js b/apps/kbmorse/lib.js new file mode 100644 index 000000000..997f2cb16 --- /dev/null +++ b/apps/kbmorse/lib.js @@ -0,0 +1,247 @@ +exports.input = function(options) { + options = options || {}; + let text = options.text; + if ("string"!= typeof text) text = ""; + let code = "", + cur = text.length, // cursor position + uc = !text.length, // uppercase + spc = 0; // consecutive spaces entered + + const codes = { + // letters + "a": ".-", + "b": "-...", + "c": "-.-.", + "d": "-..", + "e": ".", + // no é + "f": "..-.", + "g": "--.", + "h": "....", + "i": "..", + "j": ".---", + "k": "-.-", + "l": ".-..", + "m": "--", + "n": "-.", + "o": "---", + "p": ".--.", + "q": "--.-", + "r": ".-.", + "s": "...", + "t": "-", + "u": "..-", + "v": "...-", + "w": ".--", + "x": "-..-", + "y": "-.--", + "z": "--..", + //digits + "1": ".----", + "2": "..---", + "3": "...--", + "4": "....-", + "5": ".....", + "6": "-....", + "7": "--...", + "8": "---..", + "9": "----.", + "0": "-----", + // punctuation + ".": ".-.-.-", + ",": "--..--", + ":": "---...", + "?": "..--..", + "!": "-.-.--", + "'": ".----.", + "-": "-....-", + "_": "..--.-", + "/": "-..-.", + "(": "-.--.", + ")": "-.--.-", + "\"": ".-..-.", + "=": "-...-", + "+": ".-.-.", + "*": "-..-", + "@": ".--.-.", + "$": "...-..-", + "&": ".-...", + }, chars = Object.keys(codes); + + function choices(start) { + return chars.filter(char => codes[char].startsWith(start)); + } + function char(code) { + if (code==="") return " "; + for(const char in codes) { + if (codes[char]===code) return char; + } + const c = choices(code); + if (c.length===1) return c[0]; // "-.-.-" is nothing, and only "-.-.--"(!) starts with it + return null; + } + + return new Promise((resolve, reject) => { + const Layout = require("Layout"); + let layout = new Layout({ + type: "h", c: [ + { + type: "v", width: Bangle.appRect.w-8, bgCol: g.theme.bg, c: [ + {id: "dots", type: "txt", font: "6x8:2", label: "", fillx: 1, bgCol: g.theme.bg}, + {filly: 1, bgCol: g.theme.bg}, + { + type: "h", fillx: 1, c: [ + {id: "del", type: "txt", font: "6x8", label: " + ({type: "txt", font: "6x8", height: Math.floor(Bangle.appRect.h/3), r: 1, label: l}) + ) + } + ] + }); + + function update() { + let dots = [], dashes = []; + layout.pick.label = (code==="" ? " " : ""); + choices(code).forEach(char => { + const c = codes[char]; + if (c===code) { + layout.pick.label = char; + } + const next = c.substring(code.length, code.length+1); + if (next===".") dots.push(char); + else if (next==="-") dashes.push(char); + }); + if (!code && spc>1) layout.pick.label = atob("ABIYAQAAAAAAAAAABwABwABwABwABwABwOBwOBwOBxwBxwBxwB/////////xwABwABwAAOAAOAAOAA=="); + g.setFont("6x8:2"); + const wrap = t => g.wrapString(t, Bangle.appRect.w-60).join("\n"); + layout.del.label = cur ? atob("AAwIAQ/hAiKkEiKhAg/gAA==") : " "; + layout.code.label = code; + layout.dots.label = wrap(dots.join(" ")); + layout.dashes.label = wrap(dashes.join(" ")); + if (uc) { + layout.pick.label = layout.pick.label.toUpperCase(); + layout.dots.label = layout.dots.label.toUpperCase(); + layout.dashes.label = layout.dashes.label.toUpperCase(); + } + let label = text.slice(0, cur)+"|"+text.slice(cur); + layout.text.label = g.wrapString(label, Bangle.appRect.w-80).join("\n") + .replace("|", atob("AAwQAfPPPAwAwAwAwAwAwAwAwAwAwAwAwPPPPA==")); + layout.update(); + layout.render(); + } + + function add(d) { + code += d; + const l = choices(code).length; + if (l===1) done(); + else if (l<1) { + Bangle.buzz(20); + code = code.slice(0, -1); + } else update(); + } + function del() { + if (code.length) code = code.slice(0, -1); // delete last dot/dash + else if (cur) { // delete char at cursor + text = text.slice(0, cur-1)+text.slice(cur); + cur--; + } else Bangle.buzz(20); // (already) at start of text + spc = 0; + uc = false; + update(); + } + + function done() { + let c = char(code); + if (c!==null) { + if (uc) c = c.toUpperCase(); + uc = false; + text = text.slice(0, cur)+c+text.slice(cur); + cur++; + code = ""; + if (c===" ") spc++; + else spc = 0; + if (spc>=3) { + text = text.slice(0, cur-3)+"\n"+text.slice(cur); + cur -= 2; + uc = true; + spc = 0; + } + update(); + } else { + console.log(`No char for ${code}!`); + Bangle.buzz(20); + } + } + + g.reset().clear(); + update(); + + if (Bangle.btnWatches) Bangle.btnWatches.forEach(clearWatch); + Bangle.btnWatches = []; + + // BTN1: press for dot, long-press to toggle uppercase + let ucTimeout; + const UC_TIME = 500; + Bangle.btnWatches.push(setWatch(e => { + if (ucTimeout) clearTimeout(ucTimeout); + ucTimeout = null; + if (e.state) { + // pressed: start UpperCase toggle timer + ucTimeout = setTimeout(() => { + ucTimeout = null; + uc = !uc; + update(); + }, UC_TIME); + } else if (e.time-e.lastTime { + if (enterTimeout) clearTimeout(enterTimeout); + enterTimeout = null; + if (e.state) { + // pressed: start UpperCase toggle timer + enterTimeout = setTimeout(() => { + enterTimeout = null; + resolve(text); + }, ENTER_TIME); + } else if (e.time-e.lastTime { + add("-"); + }, BTN3, {repeat: true, edge: "falling"})); + + // Left-hand side: backspace + if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler); + Bangle.touchHandler = side => { + if (side===1) del(); + }; + Bangle.on("touch", Bangle.touchHandler); + + // swipe: move cursor + if (Bangle.swipeHandler) Bangle.removeListener("swipe", Bangle.swipeHandler); + Bangle.swipeHandler = dir => { + cur = Math.max(0, Math.min(text.length, cur+dir)); + update(); + }; + Bangle.on("swipe", Bangle.swipeHandler); + }); +}; diff --git a/apps/kbmorse/metadata.json b/apps/kbmorse/metadata.json new file mode 100644 index 000000000..9111d514d --- /dev/null +++ b/apps/kbmorse/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "kbmorse", + "name": "Morse keyboard", + "version": "0.02", + "description": "A library for text input as morse code", + "icon": "app.png", + "type": "textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"} + ] +} diff --git a/apps/kbmorse/screenshot.png b/apps/kbmorse/screenshot.png new file mode 100644 index 000000000..9050a45cd Binary files /dev/null and b/apps/kbmorse/screenshot.png differ diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog new file mode 100644 index 000000000..19739fa64 --- /dev/null +++ b/apps/kbmulti/ChangeLog @@ -0,0 +1,4 @@ +0.01: New keyboard +0.02: Introduce setting "Show help button?". Make setting firstLaunch invisible by removing corresponding code from settings.js. Add marker that shows when character selection timeout has run out. Display opened text on launch when editing existing text string. Perfect horizontal alignment of buttons. Tweak help message letter casing. +0.03: Use default Bangle formatter for booleans +0.04: Allow moving the cursor diff --git a/apps/kbmulti/README.md b/apps/kbmulti/README.md new file mode 100644 index 000000000..80b2b077a --- /dev/null +++ b/apps/kbmulti/README.md @@ -0,0 +1,17 @@ +# Multitap Keyboard + +A library that provides the ability to input text in a style familiar to anyone who had a mobile phone before they went all touchscreen. + +Swipe right for Space, left for Backspace, down for cursor moving mode, and up for Caps lock. Swipe left and right to move the cursor in moving mode. Tap the '?' button in the app if you need a reminder! + +At time of writing, only the [Noteify app](http://microco.sm/out/Ffe9i) uses a keyboard. + +Uses the multitap keypad logic originally from here: http://www.espruino.com/Morse+Code+Texting + +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) + +Written by: [Sir Indy](https://github.com/sir-indy) and [Thyttan](https://github.com/thyttan) + +For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/kbmulti/app.png b/apps/kbmulti/app.png new file mode 100644 index 000000000..5607a0553 Binary files /dev/null and b/apps/kbmulti/app.png differ diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js new file mode 100644 index 000000000..aa54dab9c --- /dev/null +++ b/apps/kbmulti/lib.js @@ -0,0 +1,203 @@ +//Multitap logic originally from here: http://www.espruino.com/Morse+Code+Texting + +exports.input = function(options) { + options = options||{}; + var text = options.text; + if ("string"!=typeof text) text=""; + + var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {}; + if (settings.firstLaunch===undefined) { settings.firstLaunch = true; } + if (settings.charTimeout===undefined) { settings.charTimeout = 500; } + if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; } + + var fontSize = "6x15"; + var Layout = require("Layout"); + var letters = { + "1":".,!?1","2":"ABC2","3":"DEF3", + "4":"GHI4","5":"JKL5","6":"MNO6", + "7":"PQRS7","8":"TUV80","9":"WXYZ9", + }; + var helpMessage = 'Swipe:\nRight: Space\nLeft:Backspace\nUp: Caps lock\nDown:Move mode'; + + var charTimeout; // timeout after a key is pressed + var charCurrent; // current character (index in letters) + var charIndex; // index in letters[charCurrent] + var textIndex = text.length; + var textWidth = settings.showHelpBtn ? 10 : 14; + var caps = true; + var layout; + var btnWidth = g.getWidth()/3; + + function getMoveChar(){ + return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x1F\xE1\x00\x10\x00\x10\x01\x0F\xF0\x04\x01\x00"; + } + + function getMoreChar(){ + return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xDB\x1B`\x00\x00\x00"; + } + + + function getCursorChar(){ + return "\x00\x0B\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xAA\xAA\x80"; } + + function displayText(hideMarker) { + layout.clear(layout.text); + + let charsBeforeCursor = textIndex; + let charsAfterCursor = Math.min(text.length - textIndex, (textWidth)/2); + + + let start = textIndex - Math.ceil(textWidth - charsAfterCursor); + let startMore = false; + if (start > 0) {start++; startMore = true} + if (start < 0) start = 0; + let cursor = textIndex + 1; + + let end = cursor + Math.floor(start + textWidth - cursor); + if (end <= text.length) {end--; if (startMore) end--;} + if (end > text.length) end = text.length; + + let pre = (start > 0 ? getMoreChar() : "") + text.slice(start, cursor); + let post = text.slice(cursor, end) + (end < text.length - 1 ? getMoreChar() : ""); + + layout.text.label = pre + (hideMarker ? " " : (moveMode? getMoveChar():getCursorChar())) + post; + layout.render(layout.text); + } + + function deactivateTimeout(charTimeout) { + if (charTimeout!==undefined) { + clearTimeout(charTimeout); + charTimeout = undefined; + } + } + + function backspace() { + deactivateTimeout(charTimeout); + if (textIndex > -1){ + text = text.slice(0, textIndex) + text.slice(textIndex + 1); + if (textIndex > -1) textIndex --; + newCharacter(); + } + } + + function setCaps() { + caps = !caps; + for (var key in letters) { + layout[key].label = caps ? letters[key].toUpperCase() : letters[key].toLowerCase(); + } + layout.render(); + } + + function newCharacter(ch) { + displayText(); + if (ch && textIndex < text.length) textIndex ++; + charCurrent = ch; + charIndex = 0; + } + + function onKeyPad(key) { + deactivateTimeout(charTimeout); + // work out which char was pressed + if (key==charCurrent) { + charIndex = (charIndex+1) % letters[charCurrent].length; + text = text.slice(0, -1); + } else { + newCharacter(key); + } + var newLetter = letters[charCurrent][charIndex]; + let pre = text.slice(0, textIndex); + let post = text.slice(textIndex, text.length); + + text = pre + (caps ? newLetter.toUpperCase() : newLetter.toLowerCase()) + post; + + // set a timeout + charTimeout = setTimeout(function() { + charTimeout = undefined; + newCharacter(); + }, settings.charTimeout); + displayText(charTimeout); + } + + var moveMode = false; + + function onSwipe(dirLeftRight, dirUpDown) { + if (dirUpDown == -1) { + setCaps(); + } else if (dirUpDown == 1) { + moveMode = !moveMode; + displayText(false); + } else if (dirLeftRight == 1) { + if (!moveMode){ + text = text.slice(0, textIndex + 1) + " " + text.slice(++textIndex); + newCharacter(); + } else { + if (textIndex < text.length) textIndex++; + displayText(false); + } + } else if (dirLeftRight == -1) { + if (!moveMode){ + backspace(); + } else { + if (textIndex > -1) textIndex--; + displayText(false); + } + } + } + + function onHelp(resolve,reject) { + Bangle.removeListener("swipe", onSwipe); + E.showPrompt( + helpMessage, {title: "Help", buttons : {"Ok":true}} + ).then(function(v) { + Bangle.on('swipe', onSwipe); + generateLayout(resolve,reject); + layout.render(); + }); + } + + function generateLayout(resolve,reject) { + layout = new Layout( { + type:"v", c: [ + {type:"h", c: [ + {type:"txt", font:"12x20", label:text.slice(-12), id:"text", fillx:1}, + (settings.showHelpBtn ? {type:"btn", font:'6x8', label:'?', cb: l=>onHelp(resolve,reject), filly:1 } : {}), + ]}, + {type:"h", c: [ + {type:"btn", font:fontSize, label:letters[1], cb: l=>onKeyPad(1), id:'1', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[2], cb: l=>onKeyPad(2), id:'2', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[3], cb: l=>onKeyPad(3), id:'3', width:btnWidth, filly:1 }, + ]}, + {type:"h", filly:1, c: [ + {type:"btn", font:fontSize, label:letters[4], cb: l=>onKeyPad(4), id:'4', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[5], cb: l=>onKeyPad(5), id:'5', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[6], cb: l=>onKeyPad(6), id:'6', width:btnWidth, filly:1 }, + ]}, + {type:"h", filly:1, c: [ + {type:"btn", font:fontSize, label:letters[7], cb: l=>onKeyPad(7), id:'7', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[8], cb: l=>onKeyPad(8), id:'8', width:btnWidth, filly:1 }, + {type:"btn", font:fontSize, label:letters[9], cb: l=>onKeyPad(9), id:'9', width:btnWidth, filly:1 }, + ]}, + ] + },{back: ()=>{ + deactivateTimeout(charTimeout); + Bangle.setUI(); + Bangle.removeListener("swipe", onSwipe); + g.clearRect(Bangle.appRect); + resolve(text); + }}); + } + + return new Promise((resolve,reject) => { + g.clearRect(Bangle.appRect); + if (settings.firstLaunch) { + onHelp(resolve,reject); + settings.firstLaunch = false; + require('Storage').writeJSON("kbmulti.settings.json", settings); + } else { + generateLayout(resolve,reject); + displayText(false); + Bangle.on('swipe', onSwipe); + layout.render(); + } + }); +}; diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json new file mode 100644 index 000000000..a1f6ffa81 --- /dev/null +++ b/apps/kbmulti/metadata.json @@ -0,0 +1,18 @@ +{ "id": "kbmulti", + "name": "Multitap keyboard", + "version":"0.04", + "description": "A library for text input via multitap/T9 style keypad", + "icon": "app.png", + "type":"textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_1.png"},{"url":"screenshot_2.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"}, + {"name":"kbmulti.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"kbmulti.settings.json"} + ] +} diff --git a/apps/kbmulti/screenshot_1.png b/apps/kbmulti/screenshot_1.png new file mode 100644 index 000000000..37e6e5da2 Binary files /dev/null and b/apps/kbmulti/screenshot_1.png differ diff --git a/apps/kbmulti/screenshot_2.png b/apps/kbmulti/screenshot_2.png new file mode 100644 index 000000000..d150d13bf Binary files /dev/null and b/apps/kbmulti/screenshot_2.png differ diff --git a/apps/kbmulti/screenshot_3.png b/apps/kbmulti/screenshot_3.png new file mode 100644 index 000000000..882ea7386 Binary files /dev/null and b/apps/kbmulti/screenshot_3.png differ diff --git a/apps/kbmulti/settings.js b/apps/kbmulti/settings.js new file mode 100644 index 000000000..96e72b290 --- /dev/null +++ b/apps/kbmulti/settings.js @@ -0,0 +1,30 @@ +(function(back) { + function settings() { + var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {}; + if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; } + if (settings.charTimeout===undefined) { settings.charTimeout = 500; } + return settings; + } + + function updateSetting(setting, value) { + var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {}; + settings[setting] = value; + require('Storage').writeJSON("kbmulti.settings.json", settings); + } + + var mainmenu = { + "" : { "title" : /*LANG*/"Multitap keyboard" }, + "< Back" : back, + /*LANG*/'Character selection timeout [ms]': { + value: settings().charTimeout, + min: 200, max: 1500, step : 50, + format: v => v, + onchange: v => updateSetting("charTimeout", v), + }, + /*LANG*/'Show help button?': { + value: !!settings().showHelpBtn, + onchange: v => updateSetting("showHelpBtn", v) + } + }; + E.showMenu(mainmenu); + }) diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog index 5560f00bc..17e824c00 100644 --- a/apps/kbtouch/ChangeLog +++ b/apps/kbtouch/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Introduced settings to customize the layout and functionality of the keyboard. diff --git a/apps/kbtouch/README.md b/apps/kbtouch/README.md index 513ba9239..6bd0337a8 100644 --- a/apps/kbtouch/README.md +++ b/apps/kbtouch/README.md @@ -2,6 +2,17 @@ A library that provides an on-screen keyboard for text input. +## Settings +Text size - small or big text font. Default=Big. Suggested=Small. + +Offset keyboard - display the keyboard on top, making it faster to see what character you have selected. Default=No. Suggested=Yes. + +Loop around - should the keyboard highlight loop around when going past the edges? Default=Yes. Suggested=No. + +One-to-one input and release to select - should the input correspond directly to discrete areas on the screen, instead of being handled by scaled relative changes in position on swipes? Default=No. Suggested=Yes. + +Speed scaling - how much should a swipe move the highligt on the keyboard? Higher number corresponds to slower movement. Not applicable if using one-to-one input. Default=24. Suggested=15. + ## Usage In your app's metadata, add: diff --git a/apps/kbtouch/lib.js b/apps/kbtouch/lib.js index 3dfdce00c..db90440b9 100644 --- a/apps/kbtouch/lib.js +++ b/apps/kbtouch/lib.js @@ -69,13 +69,24 @@ var KEYEXTRA = [ String.fromCharCode(27,91,53,126), // 0x84 page up String.fromCharCode(27,91,54,126), // 0x85 page down ]; + +var settings = Object.assign({ + // default values + textSize: 1, + offsetKeyboard: 0, + loopAround: 1, + oneToOne: 0, + speedScaling: 24 +}, require('Storage').readJSON("kbtouch.settings.json", true) || {}); + // state const R = Bangle.appRect; var kbx = 0, kby = 0, kbdx = 0, kbdy = 0, kbShift = false, flashToggle = false; -const PX=12, PY=16, DRAGSCALE=24; -var xoff = 3, yoff = g.getHeight()-PY*4; +const PX=12, PY=16, DRAGSCALE=settings.speedScaling; +var xoff = 3, yoff = g.getHeight()-PY*(4+5*settings.offsetKeyboard); function draw() { + "ram"; var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; //g.drawImage(KEYIMG,0,yoff); g.reset().setFont("6x8:2"); @@ -88,9 +99,9 @@ function draw() { g.drawString(map[1],xoff,yoff+PY); g.drawString(map[2],xoff,yoff+PY*2); g.drawString(map[3],xoff,yoff+PY*3); - var l = g.setFont("6x8:4").wrapString(text+(flashToggle?"_":" "), R.w-8); - if (l.length>2) l=l.slice(-2); - g.drawString(l.join("\n"),R.x+4,R.y+4); + var l = g.setFont(settings.textSize ? "6x8:4":"6x8:2").wrapString(text+(flashToggle?"_":" "), R.w-8); + if (l.length>2+2*settings.textSize) l=l.slice(-(2+2*settings.textSize)); + g.drawString(l.join("\n"),R.x+4,R.y+4 +82*settings.offsetKeyboard); g.flip(); } @@ -104,24 +115,49 @@ function draw() { return new Promise((resolve,reject) => { Bangle.setUI({mode:"custom", drag:e=>{ - kbdx += e.dx; - kbdy += e.dy; - var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE); - kbdx -= dx*DRAGSCALE; - kbdy -= dy*DRAGSCALE; - if (dx || dy) { - kbx = (kbx+dx+15)%15; - kby = (kby+dy+4)%4; + if (settings.oneToOne) { + kbx = Math.max(Math.min(Math.floor((e.x-16) / (6*2)) , 13) , 0); + kby = Math.max(Math.min(Math.floor((e.y-120) / (8*2)) , 3) , 0); + //print(e.y, kby, e.x, kbx); + } + + if (!settings.oneToOne) { + kbdx += e.dx; + kbdy += e.dy; + var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE); + kbdx -= dx*DRAGSCALE; + kbdy -= dy*DRAGSCALE; + if (dx || dy) { + if (settings.loopAround) { + kbx = (kbx+dx+15)%15; + kby = (kby+dy+4)%4; + } else { + kbx = Math.max(Math.min((kbx+dx),13),0); + kby = Math.max(Math.min((kby+dy),3),0); + } + } + } + draw(); + + if (!e.b && e.y>Bangle.appRect.y && settings.oneToOne /*&& settings.releaseToSelect*/) { + var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; + var ch = map[kby][kbx]; + if (ch=="\2") kbShift=!kbShift; + else if (ch=="\b") text = text.slice(0,-1); + else text += ch; + Bangle.buzz(20); draw(); } },touch:()=>{ - var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; - var ch = map[kby][kbx]; - if (ch=="\2") kbShift=!kbShift; - else if (ch=="\b") text = text.slice(0,-1); - else text += ch; - Bangle.buzz(20); - draw(); + if ( !settings.oneToOne /*|| !settings.releaseToSelect*/) { + var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; + var ch = map[kby][kbx]; + if (ch=="\2") kbShift=!kbShift; + else if (ch=="\b") text = text.slice(0,-1); + else text += ch; + Bangle.buzz(20); + draw(); + } },back:()=>{ clearInterval(flashInterval); Bangle.setUI(); diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json index da8b6c3c6..89d121d63 100644 --- a/apps/kbtouch/metadata.json +++ b/apps/kbtouch/metadata.json @@ -1,14 +1,16 @@ { "id": "kbtouch", "name": "Touch keyboard", - "version":"0.01", + "version":"0.02", "description": "A library for text input via onscreen 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":"kbtouch.settings.js","url":"settings.js"} + ], + "sortorder":-1 } diff --git a/apps/kbtouch/settings.js b/apps/kbtouch/settings.js new file mode 100644 index 000000000..871cc5d32 --- /dev/null +++ b/apps/kbtouch/settings.js @@ -0,0 +1,59 @@ +(function(back) { + function settings() { + let settings = require('Storage').readJSON("kbtouch.settings.json", true) || {}; + if (settings.textSize===undefined) settings.textSize=1; + if (settings.offsetKeyboard===undefined) settings.offsetKeyboard=0; + if (settings.loopAround===undefined) settings.loopAround=1; + if (settings.oneToOne===undefined) settings.oneToOne=0; + if (settings.speedScaling===undefined) settings.speedScaling=24; + return settings; + } + + function updateSetting(setting, value) { + let settings = require('Storage').readJSON("kbtouch.settings.json", true) || {}; + settings[setting] = value; + require('Storage').writeJSON("kbtouch.settings.json", settings); + } + + var mainmenu = { + "" : { "title" : /*LANG*/"Touch Keyboard" }, + "< Back" : back, + /*LANG*/'Text size': { + value: settings().textSize, + min: 0, max: 1, + format: v => [/*LANG*/"Small",/*LANG*/"Big"][v], + onchange: v => updateSetting("textSize", v) + }, + /*LANG*/'Offset keyboard': { + value: settings().offsetKeyboard, + min: 0, max: 1, + format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], + onchange: v => updateSetting("offsetKeyboard", v) + }, + /*LANG*/'Loop around': { + value: settings().loopAround, + min: 0, max: 1, + format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], + onchange: v => updateSetting("loopAround", v) + }, + /*LANG*/'One-to-one input and release to select': { + value: settings().oneToOne, + min: 0, max: 1, + format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], + onchange: v => updateSetting("oneToOne", v) + }, + /*LANG*/'Speed scaling': { + value: settings().speedScaling, + min: 1, max: 24, step : 1, + format: v => v, + onchange: v => updateSetting("speedScaling", v) + } + ///*LANG*/'Release to select': { + // value: 1|settings().fontSize, + // min: 0, max: 1, + // format: v => [/*LANG*/"No",/*LANG*/"Yes"][v], + // onchange: v => updateSetting("releaseToSelect", v) + //} + }; + E.showMenu(mainmenu); +}) diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog index 3767a9548..4e8c49c50 100644 --- a/apps/kitchen/ChangeLog +++ b/apps/kitchen/ChangeLog @@ -11,3 +11,4 @@ 0.11: Detect when waypoints.json is not present, error E-WPT 0.12: Added stepo2 as a replacement for stepo and digi 0.13: Added long press BTN2 toggle gpsrec status in GPS clock +0.14: Move waypoints.json (and editor) to 'waypoints' app diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md index 102881d15..3049d9c6d 100644 --- a/apps/kitchen/README.md +++ b/apps/kitchen/README.md @@ -60,7 +60,7 @@ The following buttons depend on which face is currently in use ![](screenshot_stepo.jpg) - now replaced by Stepo2 but still available if you install manually -- Requires one of the pedominter widgets to be installed +- Requires one of the pedominter widgets to be installed - Displays the time in large font - Display current step count in a doughnut gauge - Show step count in the middle of the doughnut gauge @@ -208,14 +208,8 @@ which will obviously limit this. ### Waypoint Editor -Clicking on the download icon of gpsnav in the app loader invokes the -waypoint editor. The editor downloads and displays the current -`waypoints.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 waypoint` -button. A new markable entry is created by using the `Add name` -button. The edited `waypoints.json` file is uploaded to the Bangle by -clicking the `Upload` button. +Clicking on the download icon of `Waypoints` in the app loader invokes the +waypoint editor. See the `Waypoints` app for more information. ### Calibration of the Compass diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js index 5564b2807..2c2cebaef 100644 --- a/apps/kitchen/kitchen.app.js +++ b/apps/kitchen/kitchen.app.js @@ -23,7 +23,7 @@ function nextFace(){ iface += 1 iface = iface % FACES.length; face = FACES[iface](); - + g.clear(); g.reset(); face.init(gpsObj, swObj, hrmObj, tripObject); @@ -64,7 +64,7 @@ function buttonReleased(btn) { clearInterval(pressTimer); pressTimer = undefined; } - + if ( dur >= 1.5 ) { switch(btn) { case 1: @@ -165,11 +165,11 @@ GPS.prototype.getLastFix = function() { GPS.prototype.determineGPSState = function() { this.log_debug("determineGPSState"); gpsPowerState = Bangle.isGPSOn(); - + //this.log_debug("last_fix.fix " + this.last_fix.fix); //this.log_debug("gpsPowerState " + this.gpsPowerState); //this.log_debug("last_fix.satellites " + this.last_fix.satellites); - + if (!gpsPowerState) { this.gpsState = this.GPS_OFF; this.resetLastFix(); @@ -178,9 +178,9 @@ GPS.prototype.determineGPSState = function() { } else { this.gpsState = this.GPS_SATS; } - + this.log_debug("gpsState=" + this.gpsState); - + if (this.gpsState !== this.GPS_OFF) { if (this.listenerCount === 0) { Bangle.on('GPS', processFix); @@ -196,9 +196,9 @@ GPS.prototype.determineGPSState = function() { } }; -GPS.prototype.getGPSTime = function() { +GPS.prototype.getGPSTime = function() { var time; - + if (this.last_fix !== undefined && this.last_fix.time !== undefined && this.last_fix.time.toUTCString !== undefined && (this.gpsState == this.GPS_SATS || this.gpsState == this.GPS_RUNNING)) { time = this.last_fix.time.toUTCString().split(" "); @@ -216,7 +216,7 @@ GPS.prototype.toggleGPSPower = function() { this.gpsPowerState = Bangle.isGPSOn(); this.gpsPowerState = !this.gpsPowerState; Bangle.setGPSPower((this.gpsPowerState ? 1 : 0), 'kitchen'); - + this.resetLastFix(); this.determineGPSState(); @@ -247,11 +247,11 @@ GPS.prototype.processFix = function(fix) { //this.log_debug("GPS:processFix()"); //this.log_debug(fix); this.last_fix.time = fix.time; - + if (this.gpsState == this.GPS_TIME) { this.gpsState = this.GPS_SATS; } - + if (fix.fix) { //this.log_debug("Got fix - setting state to GPS_RUNNING"); this.gpsState = this.GPS_RUNNING; @@ -271,10 +271,10 @@ GPS.prototype.formatTime = function(now) { GPS.prototype.timeSince = function(t) { var hms = t.split(":"); var now = new Date(); - + var sn = 3600*(now.getHours()) + 60*(now.getMinutes()) + 1*(now.getSeconds()); var st = 3600*(hms[0]) + 60*(hms[1]) + 1*(hms[2]); - + return (sn - st); }; @@ -313,7 +313,7 @@ GPS.prototype.getWPdistance = function() { GPS.prototype.getWPbearing = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else @@ -321,7 +321,7 @@ GPS.prototype.getWPbearing = function() { } GPS.prototype.loadFirstWaypoint = function() { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + var waypoints = require("waypoints").load(); this.wp_index = 0; this.wp_current = waypoints[this.wp_index]; log_debug(this.wp_current); @@ -345,10 +345,10 @@ GPS.prototype.markWaypoint = function() { return; log_debug("GPS::markWaypoint()"); - - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + + var waypoints = require("waypoints").load(); this.wp_current = waypoints[this.wp_index]; - + if (this.waypointHasLocation()) { waypoints[this.wp_index] = {name:this.wp_current.name, lat:0, lon:0}; } else { @@ -356,12 +356,12 @@ GPS.prototype.markWaypoint = function() { } this.wp_current = waypoints[this.wp_index]; - require("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); log_debug("GPS::markWaypoint() written"); } GPS.prototype.nextWaypoint = function(inc) { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; + var waypoints = require("waypoints").load(); this.wp_index+=inc; if (this.wp_index>=waypoints.length) this.wp_index=0; if (this.wp_index<0) this.wp_index = waypoints.length-1; @@ -520,7 +520,7 @@ function STOPWATCH() { this.redrawLaps = true; this.redrawTime = true; } - + STOPWATCH.prototype.log_debug = function(o) { //console.log(o); } @@ -531,7 +531,7 @@ STOPWATCH.prototype.timeToText = function(t) { let secs = Math.floor(t/1000)%60; let text; - if (hrs === 0) + if (hrs === 0) text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); else text = (""+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2); @@ -551,7 +551,7 @@ STOPWATCH.prototype.stopStart = function() { if (this.running) this.tStart = Date.now() + this.tStart - this.tCurrent; - + this.tTotal = Date.now() + this.tTotal - this.tCurrent; this.tCurrent = Date.now(); this.redrawButtons = true; @@ -623,7 +623,7 @@ STOPWATCH.prototype.drawLaptimes = function() { g.setFont("Vector",24); g.setFontAlign(-1,-1); g.clearRect(4, 205, 239, 229); // clear the last line of the lap times - + let laps = 0; for (let i in this.lapTimes) { g.drawString(this.lapTimes.length-i + ": " + this.timeToText(this.lapTimes[i]), 4, this.timeY + 40 + i*24); @@ -645,7 +645,7 @@ STOPWATCH.prototype.drawTime = function() { g.setFont("Vector",38); g.setFontAlign(0,0); g.clearRect(0, this.timeY-21, 200, this.timeY+21); - g.setColor(0xFFC0); + g.setColor(0xFFC0); g.drawString(txtTotal, xTotal, this.timeY); // current lap time @@ -691,7 +691,7 @@ function HRM() { this.bpm = 0; this.confidence = 0; } - + HRM.prototype.log_debug = function(o) { //console.log(o); } @@ -782,7 +782,7 @@ Debug Object function DEBUG() { this.logfile = require("Storage").open("debug.log","a"); } - + DEBUG.prototype.log = function(msg) { let timestamp = new Date().toString().split(" ")[4]; let line = timestamp + ", " + msg + "\n"; diff --git a/apps/kitchen/metadata.json b/apps/kitchen/metadata.json index ab2e7183c..9c9f7b2ec 100644 --- a/apps/kitchen/metadata.json +++ b/apps/kitchen/metadata.json @@ -1,14 +1,14 @@ { "id": "kitchen", "name": "Kitchen Combo", - "version": "0.13", + "version": "0.14", "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'", "icon": "kitchen.png", "type": "clock", "tags": "tool,outdoors,gps", "supports": ["BANGLEJS"], "readme": "README.md", - "interface": "waypoints.html", + "dependencies" : { "waypoints":"type" }, "storage": [ {"name":"kitchen.app.js","url":"kitchen.app.js"}, {"name":"stepo2.kit.js","url":"stepo2.kit.js"}, @@ -16,6 +16,5 @@ {"name":"gps.kit.js","url":"gps.kit.js"}, {"name":"compass.kit.js","url":"compass.kit.js"}, {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/kitchen/waypoints.html b/apps/kitchen/waypoints.html deleted file mode 100644 index d02260732..000000000 --- a/apps/kitchen/waypoints.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - -

List of waypoints

- - - - - - - - - - - - -
NameLat.Long.Actions
-
-

Add a new waypoint

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - diff --git a/apps/kitchen/waypoints.json b/apps/kitchen/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/kitchen/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file diff --git a/apps/largeclock/ChangeLog b/apps/largeclock/ChangeLog index 8c9b24be9..f35a02c54 100644 --- a/apps/largeclock/ChangeLog +++ b/apps/largeclock/ChangeLog @@ -8,3 +8,4 @@ 0.08: Use Bangle.setUI for button/launcher handling 0.09: fix font size for latest firmwares 0.10: Configure the side text direction based on the wrist on which you wear your watch +0.11: Use default Bangle formatter for booleans diff --git a/apps/largeclock/metadata.json b/apps/largeclock/metadata.json index dde790786..204243089 100644 --- a/apps/largeclock/metadata.json +++ b/apps/largeclock/metadata.json @@ -1,7 +1,7 @@ { "id": "largeclock", "name": "Large Clock", - "version": "0.10", + "version": "0.11", "description": "A readable and informational digital watch, with date, seconds and moon phase", "icon": "largeclock.png", "type": "clock", diff --git a/apps/largeclock/settings.js b/apps/largeclock/settings.js index f996666ab..4ebf842ce 100644 --- a/apps/largeclock/settings.js +++ b/apps/largeclock/settings.js @@ -74,7 +74,6 @@ "BTN3 app": () => showApps("BTN3"), "On right hand": { value: !!settings.right_hand, - format: v=>v?"Yes":"No", onchange: v=>{ settings.right_hand = v; s.writeJSON("largeclock.json", settings); diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 7248f69c3..44866b9f3 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -13,3 +13,4 @@ 0.12: Add an option to hide clocks from the app list (fix #1015) Add /*LANG*/ tags for internationalisation 0.13: Add fullscreen mode +0.14: Use default Bangle formatter for booleans diff --git a/apps/launch/metadata.json b/apps/launch/metadata.json index da76fc4bb..19ca74e73 100644 --- a/apps/launch/metadata.json +++ b/apps/launch/metadata.json @@ -2,7 +2,7 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.13", + "version": "0.14", "description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "readme": "README.md", "icon": "app.png", diff --git a/apps/launch/settings.js b/apps/launch/settings.js index 5d37e1c1b..496a6d77e 100644 --- a/apps/launch/settings.js +++ b/apps/launch/settings.js @@ -26,12 +26,10 @@ }, /*LANG*/"Show Clocks": { value: settings.showClocks == true, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", onchange: (m) => { save("showClocks", m) } }, /*LANG*/"Fullscreen": { value: settings.fullscreen == true, - format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", onchange: (m) => { save("fullscreen", m) } } }; diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index e622feb1f..f97ddf540 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -19,4 +19,6 @@ 0.19: Alarms can not go bigger than 100. 0.20: Use alarm for alarm functionality instead of own implementation. 0.21: Add custom theming. -0.22: Fix alarm and add build in function for step counting. \ No newline at end of file +0.22: Fix alarm and add build in function for step counting. +0.23: Add warning for low flash memory +0.24: Add ability to disable alarm functionality \ No newline at end of file diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 07ca51fd9..06a89a957 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -12,6 +12,7 @@ let settings = { themeColor1BG: "#FF9900", themeColor2BG: "#FF00DC", themeColor3BG: "#0094FF", + disableAlarms: false, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -147,8 +148,7 @@ var iconCharging = { buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A")) }; -var iconNoBattery = { - text: "NO BAT", +var iconWarning = { width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA")) @@ -321,19 +321,21 @@ function drawState(){ if(!isAlarmEnabled()){ var bat = E.getBattery(); + var flash = storage.getFree() / process.env.STORAGE; var current = new Date(); var hours = current.getHours(); - var iconImg = - Bangle.isCharging() ? iconCharging : - bat < 30 ? iconNoBattery : - Bangle.isGPSOn() ? iconSatellite : - hours % 4 == 0 ? iconSaturn : - hours % 4 == 1 ? iconMars : - hours % 4 == 2 ? iconMoon : - iconEarth; - g.drawImage(iconImg, 23, 118); + var iconMsg = + Bangle.isCharging() ? { icon: iconCharging, text: "STATUS" } : + bat < 30 ? { icon: iconWarning, text: "BAT" } : + flash < 0.1 ? { icon: iconWarning, text: "DISK" } : + Bangle.isGPSOn() ? { icon: iconSatellite, text: "STATUS" } : + hours % 4 == 0 ? { icon: iconSaturn, text: "STATUS" } : + hours % 4 == 1 ? { icon: iconMars, text: "STATUS" } : + hours % 4 == 2 ? { icon: iconMoon, text: "STATUS" } : + { icon: iconEarth, text: "STATUS" }; + g.drawImage(iconMsg.icon, 23, 118); g.setColor(cWhite); - g.drawString("STATUS", 23+26, 108); + g.drawString(iconMsg.text, 23+26, 108); } else { // Alarm within symbol g.setColor(color2); @@ -721,12 +723,12 @@ Bangle.on('touch', function(btn, e){ } if(lcarsViewPos == 0){ - if(is_upper){ + if(is_upper && !settings.disableAlarms){ feedback(); increaseAlarm(); drawState(); return; - } if(is_lower){ + } if(is_lower && !settings.disableAlarms){ feedback(); decreaseAlarm(); drawState(); diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js index b64feb30e..e4b9b0a78 100644 --- a/apps/lcars/lcars.settings.js +++ b/apps/lcars/lcars.settings.js @@ -13,6 +13,7 @@ themeColor1BG: "#FF9900", themeColor2BG: "#FF00DC", themeColor3BG: "#0094FF", + disableAlarms: false, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { @@ -102,6 +103,14 @@ settings.themeColor3BG = bg_code[v]; save(); }, - } + }, + 'Disable alarm functionality': { + value: settings.disableAlarms, + format: () => (settings.disableAlarms ? 'Yes' : 'No'), + onchange: () => { + settings.disableAlarms = !settings.disableAlarms; + save(); + }, + }, }); }) diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json index 40da1b37f..6533ddd52 100644 --- a/apps/lcars/metadata.json +++ b/apps/lcars/metadata.json @@ -3,7 +3,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.22", + "version":"0.24", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog index 2c6d2b5db..4c89bae76 100644 --- a/apps/lightswitch/ChangeLog +++ b/apps/lightswitch/ChangeLog @@ -2,3 +2,4 @@ 0.02: Add the option to enable touching the widget only on clock and settings. 0.03: Settings page now uses built-in min/max/wrap (fix #1607) 0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area. +0.05: Prevent drawing into app area. diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json index 54dc8389f..b8da2f759 100644 --- a/apps/lightswitch/metadata.json +++ b/apps/lightswitch/metadata.json @@ -2,7 +2,7 @@ "id": "lightswitch", "name": "Light Switch Widget", "shortName": "Light Switch", - "version": "0.04", + "version": "0.05", "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", "icon": "images/app.png", "screenshots": [ diff --git a/apps/lightswitch/widget.js b/apps/lightswitch/widget.js index 829f75102..d9d4d421d 100644 --- a/apps/lightswitch/widget.js +++ b/apps/lightswitch/widget.js @@ -86,7 +86,7 @@ })(this.image); // clear widget area - g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 24); + g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 23); // draw shine if backlight is active if (this.isOn) g.drawImage(atob(icons.shine), this.x, this.y); diff --git a/apps/limelight/ChangeLog b/apps/limelight/ChangeLog index 9db0e26c5..8fe3a0b2c 100644 --- a/apps/limelight/ChangeLog +++ b/apps/limelight/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Tell clock widgets to hide. diff --git a/apps/limelight/limelight.app.js b/apps/limelight/limelight.app.js index 20d79deeb..84ded1039 100644 --- a/apps/limelight/limelight.app.js +++ b/apps/limelight/limelight.app.js @@ -10,6 +10,8 @@ * */ +Bangle.setUI('clock'); + g.clear(); const SETTINGS_FILE = "limelight.json"; @@ -259,5 +261,4 @@ Bangle.on('lcdPower',on=>{ } }); -Bangle.setUI('clock'); draw(); diff --git a/apps/limelight/metadata.json b/apps/limelight/metadata.json index 7c3736e1a..e484a2825 100644 --- a/apps/limelight/metadata.json +++ b/apps/limelight/metadata.json @@ -1,7 +1,7 @@ { "id": "limelight", "name": "Limelight", - "version": "0.01", + "version": "0.02", "description": "Simple analogue clock (with configurable fonts) based on the work of @Andreas_Rozek (Simple_Clock)", "icon": "limelight.png", "readme":"README.md", diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 2bc71fd75..7b3146e15 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -11,6 +11,8 @@ const speedUnits = { // how many kph per X? "kmh": 1, "kph": 1, "km/h": 1, + "kmt": 1, + "km/tim": 1, "mph": 1.60934, "kts": 1.852 }; @@ -95,6 +97,25 @@ var locales = { day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", // No translation for english... }, + "en_IE": { + lang: "en_IE", + decimal_point: ".", + thousands_sep: ",", + currency_symbol: "€", + int_curr_symbol: "EUR", + currency_first: true, + speed: 'kmh', + distance: { "0": "m", "1": "km" }, + temperature: '°C', + ampm: { 0: "am", 1: "pm" }, + timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, + datePattern: { 0: "%d %b %Y", 1: "%d/%m/%Y" }, // 28 Feb 2020" // "28/03/2020"(short) + abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", + month: "January,February,March,April,May,June,July,August,September,October,November,December", + abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat", + day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday", + // No translation for english... + }, "en_NAV": { // navigation units nautical miles and knots lang: "en_NAV", decimal_point: ".", @@ -139,7 +160,7 @@ var locales = { currency_symbol: "$", currency_first: true, int_curr_symbol: "USD", speed: "mph", - distance: { 0: "yd", 1: "mi" }, + distance: { 0: "m", 1: "mi" }, temperature: "°F", ampm: { 0: "am", 1: "pm" }, timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, @@ -564,7 +585,7 @@ var locales = { month: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro", abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab", day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado", - trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "certo", on: "ligado", off: "desligado" } + trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "confirmar", on: "ativado", off: "desativado" } }, "cs_CZ": { // THIS NEVER WORKED PROPERLY - many chars are not in the ISO8859-1 codepage and we use CODEPAGE_CONVERSIONS lang: "cs_CZ", diff --git a/apps/ltherm/ChangeLog b/apps/ltherm/ChangeLog new file mode 100644 index 000000000..374bd5cd6 --- /dev/null +++ b/apps/ltherm/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Use barometer temperature if available diff --git a/apps/ltherm/app.js b/apps/ltherm/app.js index 2cbf26e5f..552420a85 100644 --- a/apps/ltherm/app.js +++ b/apps/ltherm/app.js @@ -1,11 +1,10 @@ -function drawTemperature() { +function drawTemperature(h) { g.reset(1).clearRect(0,24,g.getWidth(),g.getHeight()); g.setFont("6x8",2).setFontAlign(0,0); var x = g.getWidth()/2; var y = g.getHeight()/2 + 10; g.drawString("Temp", x, y - 45); g.setFontVector(70).setFontAlign(0,0); - var h = E.getTemperature(); if (avg.length < 10) { avg[avg.length] = h; } else { @@ -18,7 +17,13 @@ function drawTemperature() { } const avg = []; setInterval(function() { - drawTemperature(); + if (Bangle.getPressure){ + Bangle.getPressure().then((p)=>{ + drawTemperature(p.temperature); + }); + } else { + drawTemperature(E.getTemperature()); + } }, 2000); E.showMessage(/*LANG*/"Loading..."); Bangle.loadWidgets(); diff --git a/apps/ltherm/metadata.json b/apps/ltherm/metadata.json index 83b295a3d..58c1f613c 100644 --- a/apps/ltherm/metadata.json +++ b/apps/ltherm/metadata.json @@ -2,7 +2,7 @@ "id": "ltherm", "name": "Localized Thermometer", "shortName": "Thermometer", - "version": "0.01", + "version": "0.02", "description": "Displays the current temperature in localized units.", "icon": "thermf.png", "tags": "tool", diff --git a/apps/macwatch/ChangeLog b/apps/macwatch/ChangeLog new file mode 100644 index 000000000..221d3fb57 --- /dev/null +++ b/apps/macwatch/ChangeLog @@ -0,0 +1 @@ +0.01: Created my first BangleJS app! diff --git a/apps/macwatch/README.md b/apps/macwatch/README.md new file mode 100644 index 000000000..1b7add3e5 --- /dev/null +++ b/apps/macwatch/README.md @@ -0,0 +1,26 @@ +# MacWatch + +A very simple clock using big numbers in the original Macintosh Chicago font. + +Touch the screen to show the date in numerical format. + +Touch the screen again to revert to the time. + +Time updates every 15 seconds. + +## In dark mode + +![](screenshot-dark-time.png) +![](screenshot-dark-date.png) + +## In light mode + +![](screenshot-light-time.png) +![](screenshot-light-date.png) + +## The watch in use +![](photo-watch-in-use.jpg) + +## Creator + +Written by Giles Booth | [twitter](https://twitter.com/blogmywiki) | [blog](http://www.suppertime.co.uk/blogmywiki/) diff --git a/apps/macwatch/app-icon.js b/apps/macwatch/app-icon.js new file mode 100644 index 000000000..f4592b508 --- /dev/null +++ b/apps/macwatch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4AOh//AggADC8YRCBIwXlI4oX/C5QWOC74WPC/4X/C6AA/AH4AmXwK5DAobBNC60P+DqHFxwXWCJAWRC44vUPwouRJKQXWBxAXlOAIgQbBDeLFxAXXAAa/RC64A/AH4AKA==")) diff --git a/apps/macwatch/app.js b/apps/macwatch/app.js new file mode 100644 index 000000000..fb9712a36 --- /dev/null +++ b/apps/macwatch/app.js @@ -0,0 +1,54 @@ +var font = atob("f3/gMB/7+AAAACA///AAAAAAQcHhsZ+LhAAAgUhsPh38eAAADAoJCI///BAA8XhkMhn8eAAAPz/0Mhn4eAAAgEAh8f+HgAAAb3/kMh/7eAAAeH5hML/z8AAAAAADYbAAAAAA"); + +function draw() { + g.reset(); + g.setFontCustom(font, 48, 8, 1801); + g.setFontAlign(0, -1, 0); + if (showDate) { + if (g.theme.dark) { + g.setColor("#00ffff"); // cyan date numbers for dark mode + } + else { + g.setColor("#0000ff"); // blue date numbers for light mode + } + line1 = ("0"+(new Date()).getDate()).substr(-2); + line2 = ("0"+((new Date()).getMonth()+1)).substr(-2); + } + else { + if (g.theme.dark) { + g.setColor(1,1,1); // white time numbers for dark mode + } + else { + g.setColor(0); // black time numbers for light mode + } + var d = new Date(); + var da = d.toString().split(" "); + line1 = da[4].substr(0,2); + line2 = da[4].substr(3,2); + } + g.drawString(line1, 95, 30, true); + g.drawString(line2, 95, 106, true); + } + +// handle switch display on by pressing BTN1 +Bangle.on('lcdPower', function(on) { + if (on) draw(); +}); + +Bangle.on('touch', function(on) { + if (on) { + showDate = !showDate; // toggle date mode on and off + draw(); + } +}); + +g.clear(); +var showDate = 0; +setInterval(draw, 15000); // refresh display every 15s +draw(); + +// Show launcher when button pressed +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/macwatch/app.png b/apps/macwatch/app.png new file mode 100644 index 000000000..9618b8b50 Binary files /dev/null and b/apps/macwatch/app.png differ diff --git a/apps/macwatch/metadata.json b/apps/macwatch/metadata.json new file mode 100644 index 000000000..0a2558380 --- /dev/null +++ b/apps/macwatch/metadata.json @@ -0,0 +1,17 @@ +{ "id": "macwatch", + "name": "MacWatch", + "shortName":"MacWatch", + "icon": "app.png", + "version":"0.01", + "description": "Simple clock with classic Mac font", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot-dark-date.png"}], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"macwatch.app.js","url":"app.js"}, + {"name":"macwatch.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/macwatch/photo-watch-in-use.jpg b/apps/macwatch/photo-watch-in-use.jpg new file mode 100644 index 000000000..16ae57c3e Binary files /dev/null and b/apps/macwatch/photo-watch-in-use.jpg differ diff --git a/apps/macwatch/screenshot-dark-date.png b/apps/macwatch/screenshot-dark-date.png new file mode 100644 index 000000000..68d1d80f9 Binary files /dev/null and b/apps/macwatch/screenshot-dark-date.png differ diff --git a/apps/macwatch/screenshot-dark-time.png b/apps/macwatch/screenshot-dark-time.png new file mode 100644 index 000000000..dc9e32df2 Binary files /dev/null and b/apps/macwatch/screenshot-dark-time.png differ diff --git a/apps/macwatch/screenshot-light-date.png b/apps/macwatch/screenshot-light-date.png new file mode 100644 index 000000000..e4bee235f Binary files /dev/null and b/apps/macwatch/screenshot-light-date.png differ diff --git a/apps/macwatch/screenshot-light-time.png b/apps/macwatch/screenshot-light-time.png new file mode 100644 index 000000000..45abff84a Binary files /dev/null and b/apps/macwatch/screenshot-light-time.png differ diff --git a/apps/macwatch2/ChangeLog b/apps/macwatch2/ChangeLog new file mode 100644 index 000000000..5eafe64d2 --- /dev/null +++ b/apps/macwatch2/ChangeLog @@ -0,0 +1,5 @@ +0.01: Created first version of the app with numeric date, only works in light mode +0.02: New icon, shimmied date right a bit +0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background +0.04: Changed clock to use 12/24 hour format based on locale +0.05: Tell clock widgets to hide. diff --git a/apps/macwatch2/IMG_3782 crop.JPG b/apps/macwatch2/IMG_3782 crop.JPG new file mode 100644 index 000000000..2c0bc375a Binary files /dev/null and b/apps/macwatch2/IMG_3782 crop.JPG differ diff --git a/apps/macwatch2/README.md b/apps/macwatch2/README.md new file mode 100644 index 000000000..4fdb9b6e9 --- /dev/null +++ b/apps/macwatch2/README.md @@ -0,0 +1,19 @@ +# MacWatch2 + +A clock inspired by Susan Kare's original 1984 Macintosh desktop design. + +Ideas for development: +- [x] fix dark mode either by forcing black on white or doing proper inverse display and text +- [ ] date in text format +- [ ] tap to load different info in window +- [ ] unlock to show seconds, perhaps flip time and date? +- [ ] incorporate widgets somehow either by leaving space or adding Chicago font widgets of my own in top bar + +![](screenshot.png) + +![](IMG_3782%20crop.JPG) + +## Creator + +Written by Giles Booth | [twitter](https://twitter.com/blogmywiki) | [blog](http://www.suppertime.co.uk/blogmywiki/) +Improvements for accuracy, dark mode and widgets running in background by [Peer David](https://gist.github.com/peerdavid) diff --git a/apps/macwatch2/app-icon.js b/apps/macwatch2/app-icon.js new file mode 100644 index 000000000..be7d5e060 --- /dev/null +++ b/apps/macwatch2/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AH4A/AH4AfdIoX/C/4X4CIwYQC6wPIDBwX/C6IYNC7TVFC9IEFO6YX/VBQWPC7CmDVIoX/C/4X2AH4A/ADwA==")) diff --git a/apps/macwatch2/app.js b/apps/macwatch2/app.js new file mode 100644 index 000000000..4556e06ac --- /dev/null +++ b/apps/macwatch2/app.js @@ -0,0 +1,64 @@ +// 68k Mac Finder desktop themed clock +// by Giles Booth @blogmywiki +// improvements by Peer David + +var img = require("heatshrink").decompress(atob("2GwgP4C6cf8AVTg/ACqcDwADBDCMBCoICCCqACEj8zAwXwmcYgEGswYHhxwBjEDGocwCoVgQxHwCoMzjwVBwPzngrCnlmDAsfNoIVBIQMBwZBEAAIVIjwVD8YVNIIc/FY9+CpcwCo9gCo0PQYUzmIVGo1is1ACokGNoaDC+PzhkAg+Gnl/aiIA/AD//AClVACmqACgr/Fd2vVqP+FYNUbKMNFYOsCqMOFa+t/f/35LC/AODK43uFYUCgGACAUB/IFDFZP6gArEsArTgFhz9w+ArRsOZzOYFaQVCFan4FaiFHFZuIFaeYQZbbVf5LbK1gVRhwrX15MGABX+K/4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Fe+v/4AQ/wrBq4VR/orBAClVACgr/Ff4r/AAmr6or/q/6Fae/A=")); + +var font = atob("f3/gMB/7+AAAACA///AAAAAAQcHhsZ+LhAAAgUhsPh38eAAADAoJCI///BAA8XhkMhn8eAAAPz/0Mhn4eAAAgEAh8f+HgAAAb3/kMh/7eAAAeH5hML/z8AAAAAADYbAAAAAA"); + +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() { + queueDraw(); + + // Fix theme to "light" + g.setTheme({bg:"#fff", fg:"#000", dark:false}).clear(); + g.reset(); + g.drawImage(img,0,0); + + g.setFontCustom(font, 48, 8, 1033); + g.setFontAlign(0, -1, 0); + g.setColor(0,0,0); + var d = new Date(); + var dt = require("locale").time(d, 1); + var hh = dt.split(":")[0]; + var mm = dt.split(":")[1]; + g.drawString(hh, 52, 65, true); + g.drawString(mm, 132, 65, true); + g.drawString(':', 93,65); + dd = ("0"+(new Date()).getDate()).substr(-2); + mo = ("0"+((new Date()).getMonth()+1)).substr(-2); + yy = ("0"+((new Date()).getFullYear())).substr(-2); + g.setFontCustom(font, 48, 8, 521); + g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true); + + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + + +// handle switch display on by pressing BTN1 +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI("clock"); + +// Load widgets but hide them +Bangle.loadWidgets(); +draw(); diff --git a/apps/macwatch2/app.png b/apps/macwatch2/app.png new file mode 100644 index 000000000..efcdead19 Binary files /dev/null and b/apps/macwatch2/app.png differ diff --git a/apps/macwatch2/metadata.json b/apps/macwatch2/metadata.json new file mode 100644 index 000000000..14c48c749 --- /dev/null +++ b/apps/macwatch2/metadata.json @@ -0,0 +1,17 @@ +{ "id": "macwatch2", + "name": "MacWatch2", + "shortName":"MacWatch2", + "icon": "app.png", + "version":"0.05", + "description": "Classic Mac Finder clock", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"macwatch2.app.js","url":"app.js"}, + {"name":"macwatch2.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/macwatch2/screenshot.png b/apps/macwatch2/screenshot.png new file mode 100644 index 000000000..732b29d7f Binary files /dev/null and b/apps/macwatch2/screenshot.png differ diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog index 52f705301..02f7d109b 100644 --- a/apps/matrixclock/ChangeLog +++ b/apps/matrixclock/ChangeLog @@ -1,4 +1,7 @@ 0.01: Initial Release 0.02: Support for Bangle 2 0.03: Keep the date from being overwritten, use correct colour from theme for clearing -0.04: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.04: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.05: Added support to other color themes (other then black) +0.06: Added support for 24 hour clock enabled from settings +0.07: Tell clock widgets to hide. diff --git a/apps/matrixclock/README.md b/apps/matrixclock/README.md index 010524b60..01aef6544 100644 --- a/apps/matrixclock/README.md +++ b/apps/matrixclock/README.md @@ -2,6 +2,25 @@ ![](app.png) +## Settings +Please use the setting->App->Matrix Clock Menu to change the settings + +| Setting | Description | +|-------------|--------------------------------------------------------------------------------------------------------------------| +| Color | By default set to **'theme'** to follow the theme colors. Selector also offers a selection of other colour schemes | +| Time Format | Choose between 12 hour and 24 hour time format | +| Intensity | Changes the number of matrix streams that are falling | + +## Colour Themes + +Some of the colours schemes that are available from the settings screen + +| ![](matrix_green_on_black.jpg) | ![](matrix_black_on_white.jpg) | ![](matrix_white_on_gray.jpg) | +|-------------------------------|-------------------------------|-----| +| green on black | white on black | white on gray | + + + ## Requests Please reach out to adrian@adriankirk.com if you have feature requests or notice bugs. diff --git a/apps/matrixclock/matrix_black_on_white.jpg b/apps/matrixclock/matrix_black_on_white.jpg new file mode 100644 index 000000000..545545c65 Binary files /dev/null and b/apps/matrixclock/matrix_black_on_white.jpg differ diff --git a/apps/matrixclock/matrix_green_on_black.jpg b/apps/matrixclock/matrix_green_on_black.jpg new file mode 100644 index 000000000..7caa38bec Binary files /dev/null and b/apps/matrixclock/matrix_green_on_black.jpg differ diff --git a/apps/matrixclock/matrix_white_on_gray.jpg b/apps/matrixclock/matrix_white_on_gray.jpg new file mode 100644 index 000000000..dc9d2f3ba Binary files /dev/null and b/apps/matrixclock/matrix_white_on_gray.jpg differ diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js index 2e4ba1ac4..9618c3a47 100644 --- a/apps/matrixclock/matrixclock.js +++ b/apps/matrixclock/matrixclock.js @@ -3,24 +3,107 @@ * * Matrix Clock * - * A simple clock inspired by the movie. - * Text shards move down the screen as a background to the + * A simple clock inspired by the movie. + * Text shards move down the screen as a background to the * time and date **/ const Locale = require('locale'); -const SHARD_COLOR =[0,1.0,0]; +const PREFERENCE_FILE = "matrixclock.settings.json"; +const settings = Object.assign({color: "theme", time_format: '12 hour', intensity: 'light'}, + require('Storage').readJSON(PREFERENCE_FILE, true) || {}); + +var format_time; +if(settings.time_format == '24 hour'){ + format_time = (t) => format_time_24_hour(t); +} else { + format_time = (t) => format_time_12_hour(t); +} + +const colors = { + 'gray' :[0.5,0.5,0.5], + 'green': [0,1.0,0], + 'red' : [1.0,0.0,0.0], + 'blue' : [0.0,0.0,1.0], + 'black': [0.0,0.0,0.0], + 'purple': [1.0,0.0,1.0], + 'white': [1.0,1.0,1.0], + 'yellow': [1.0,1.0,0.0] +}; + +const color_schemes = { + 'black on white': ['white','black'], + 'green on white' : ['white','green'], + 'green on black' : ['black','green'], + 'red on black' : ['black', 'red'], + 'red on white' : ['white', 'red'], + 'white on gray' : ['gray', 'white'], + 'white on red' : ['red', 'white'], + 'white on blue': ['blue','white'], + 'white on purple': ['purple', 'white'] +}; + +function int2Color(color_int){ + var blue_int = color_int & 31; + var blue = (blue_int)/31.0; + + var green_int = (color_int >> 5) & 31; + var green = (green_int)/31.0; + + var red_int = (color_int >> 11) & 31; + var red = red_int/ 31.0; + return [red,green,blue]; +} + +var fg_color = colors.black; +var bg_color = colors.white; + +// now lets deal with the settings +if(settings.color === "theme"){ + bg_color = int2Color(g.theme.bg); + if(g.theme.bg === 0) { + fg_color = colors.green; + } else { + fg_color = int2Color(g.theme.fg); + } +} else { + var color_scheme = color_schemes[settings.color]; + bg_color = colors[color_scheme[0]]; + fg_color = colors[color_scheme[1]]; + g.setBgColor(bg_color[0],bg_color[1],bg_color[2]); +} +if(fg_color === undefined) + fg_color = colors.black; + +if(bg_color === undefined) + bg_color = colors.white; + +const intensity_schemes = { + 'light': 3, + 'medium': 4, + 'high': 5 +}; + +var noShards = intensity_schemes.light; +if(settings.intensity !== undefined){ + noShards = intensity_schemes[settings.intensity]; +} +if(noShards === undefined){ + noShards = intensity_schemes.light; +} + const SHARD_FONT_SIZE = 12; const SHARD_Y_START = 30; + const w = g.getWidth(); /** -* The text shard object is responsible for creating the -* shards of text that move down the screen. As the -* shard moves down the screen the latest character added -* is brightest with characters being coloured darker and darker -* going back to the eldest -*/ + * The text shard object is responsible for creating the + * shards of text that move down the screen. As the + * shard moves down the screen the latest character added + * is brightest with characters being coloured darker and darker + * going back to the eldest + */ class TextShard { constructor(x,y,length){ @@ -34,44 +117,46 @@ class TextShard { this.txt = []; } /** - * The add method call adds another random character to - * the chain - */ + * The add method call adds another random character to + * the chain + */ add(){ this.txt.push(randomChar()); } /** - * The show method displays the latest shard image to the - * screen with the following rules: - * - latest addition is brightest, oldest is darker - * - display up to defined length of characters only - * of the shard to save cpu - */ + * The show method displays the latest shard image to the + * screen with the following rules: + * - latest addition is brightest, oldest is darker + * - display up to defined length of characters only + * of the shard to save cpu + */ show(){ g.setFontAlign(-1,-1,0); for(var i=0; i this.length - 2){ color_strength = 0; - } - g.setColor(color_strength*SHARD_COLOR[0], - color_strength*SHARD_COLOR[1], - color_strength*SHARD_COLOR[2]); + } + var bg_color_strength = 1 - color_strength; + g.setColor(Math.abs(color_strength*fg_color[0] - bg_color_strength*bg_color[0]), + Math.abs(color_strength*fg_color[1] - bg_color_strength*bg_color[1]), + Math.abs(color_strength*fg_color[2] - bg_color_strength*bg_color[2]) + ); g.setFont("Vector",SHARD_FONT_SIZE); - g.drawString(this.txt[idx], this.x, this.y + idx*SHARD_FONT_SIZE); + g.drawString(this.txt[idx], this.x, this.y + idx*SHARD_FONT_SIZE); } } /** - * Method tests to see if any part of the shard chain is still - * visible on the screen - */ + * Method tests to see if any part of the shard chain is still + * visible on the screen + */ isVisible(){ - return (this.y + (this.txt.length - this.length - 2)*SHARD_FONT_SIZE < g.getHeight()); + return (this.y + (this.txt.length - this.length - 2)*SHARD_FONT_SIZE < g.getHeight()); } /** - * resets the shard back to the top of the screen - */ + * resets the shard back to the top of the screen + */ reset(){ this.y = SHARD_Y_START; this.txt = []; @@ -79,8 +164,8 @@ class TextShard { } /** -* random character chooser to be called by the shard when adding characters -*/ + * random character chooser to be called by the shard when adding characters + */ const CHAR_CODE_START = 33; const CHAR_CODE_LAST = 126; const CHAR_CODE_LENGTH = CHAR_CODE_LAST - CHAR_CODE_START; @@ -90,11 +175,10 @@ function randomChar(){ // Now set up the shards // we are going to have a limited no of shards (to save cpu) -// but randomize the x value and length every reset to make it look as if there +// but randomize the x value and length every reset to make it look as if there // are more var shards = []; -const NO_SHARDS = 3; -const channel_width = g.getWidth()/NO_SHARDS; +const channel_width = g.getWidth()/noShards; function shard_x(i){ return i*channel_width + Math.random() * channel_width; @@ -104,7 +188,7 @@ function shard_length(){ return Math.floor(Math.random()*5) + 3; } -for(var i=0; i 99 || value < 0) - throw "must be between in range 0-99"; - if(value < 10) - return "0" + value.toString(); - else - return value.toString(); + var value = (num | 0); + if(value > 99 || value < 0) + throw "must be between in range 0-99"; + if(value < 10) + return "0" + value.toString(); + else + return value.toString(); } // The interval reference for updating the clock @@ -215,12 +304,12 @@ function startTimers(){ clearTimers(); if (Bangle.isLCDOn()) { intervalRef = setInterval(() => { - if (!shouldRedraw()) { - //console.log("draw clock callback - skipped redraw"); - } else { - draw_clock(); - } - }, 100 + if (!shouldRedraw()) { + //console.log("draw clock callback - skipped redraw"); + } else { + draw_clock(); + } + }, 100 ); draw_clock(); } else { @@ -239,11 +328,9 @@ Bangle.on('lcdPower', (on) => { } }); +Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); -Bangle.setUI("clock"); - - diff --git a/apps/matrixclock/matrixclock.settings.js b/apps/matrixclock/matrixclock.settings.js new file mode 100644 index 000000000..1f22a045f --- /dev/null +++ b/apps/matrixclock/matrixclock.settings.js @@ -0,0 +1,52 @@ +(function(back) { + const PREFERENCE_FILE = "matrixclock.settings.json"; + var settings = Object.assign({color : "theme", time_format: '12 hour', intensity: "light"}, + require('Storage').readJSON(PREFERENCE_FILE, true) || {}); + + console.log("loaded:" + JSON.stringify(settings)); + + function writeSettings() { + console.log("saving:" + JSON.stringify(settings)); + require('Storage').writeJSON(PREFERENCE_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 + E.showMenu({ + "" : { "title" : "Matrix Clock" }, + "< Back" : () => back(), + "Colour": stringInSettings("color", ['theme', + 'black on white', + 'green on white', + 'green on black', + 'red on white', + 'white on gray', + 'white on red', + 'white on blue' + ]), + "Time Format": stringInSettings("time_format", ['12 hour','24 hour']), + "Intensity": stringInSettings("intensity", ['light', + 'medium', + 'high']) + }); +}) \ No newline at end of file diff --git a/apps/matrixclock/metadata.json b/apps/matrixclock/metadata.json index 122cee3a1..718b878e5 100644 --- a/apps/matrixclock/metadata.json +++ b/apps/matrixclock/metadata.json @@ -1,10 +1,10 @@ { "id": "matrixclock", "name": "Matrix Clock", - "version": "0.04", + "version": "0.07", "description": "inspired by The Matrix, a clock of the same style", "icon": "matrixclock.png", - "screenshots": [{"url":"screenshot_matrix.png"}], + "screenshots": [{"url":"matrix_green_on_black.jpg"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], @@ -12,6 +12,8 @@ "allow_emulator": true, "storage": [ {"name":"matrixclock.app.js","url":"matrixclock.js"}, + { "name":"matrixclock.settings.js","url":"matrixclock.settings.js"}, {"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true} - ] + ], + "data": [{"name": "matrixclock.settings.json"}] } diff --git a/apps/matrixclock/screenshot_matrix.png b/apps/matrixclock/screenshot_matrix.png deleted file mode 100644 index 3d843848c..000000000 Binary files a/apps/matrixclock/screenshot_matrix.png and /dev/null differ diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index 05b422406..e3b164942 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -5,3 +5,4 @@ Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365) 0.06: Support 12 hour time 0.07: Use Bangle.setUI for button/launcher handling +0.08: Tell clock widgets to hide. diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index f1254860b..bd133206e 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -209,6 +209,9 @@ Bangle.on('lcdPower',function(on) { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -216,5 +219,3 @@ Bangle.drawWidgets(); timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/mclock/metadata.json b/apps/mclock/metadata.json index 513f823a1..a7d56f752 100644 --- a/apps/mclock/metadata.json +++ b/apps/mclock/metadata.json @@ -1,7 +1,7 @@ { "id": "mclock", "name": "Morphing Clock", - "version": "0.07", + "version": "0.08", "description": "7 segment clock that morphs between minutes and hours", "icon": "clock-morphing.png", "type": "clock", diff --git a/apps/menusmall/metadata.json b/apps/menusmall/metadata.json index aafb7da28..51ab825bd 100644 --- a/apps/menusmall/metadata.json +++ b/apps/menusmall/metadata.json @@ -4,7 +4,7 @@ "version": "0.02", "description": "Replace Bangle.js 2's menus with a version that contains smaller text", "icon": "app.png", - "type": "boot", + "type": "bootloader", "tags": "system", "supports": ["BANGLEJS2"], "storage": [ diff --git a/apps/menuwheel/metadata.json b/apps/menuwheel/metadata.json index 1ad042344..5f49b640c 100644 --- a/apps/menuwheel/metadata.json +++ b/apps/menuwheel/metadata.json @@ -9,7 +9,7 @@ {"url":"screenshot_b1_dark.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_light.png"}, {"url":"screenshot_b2_dark.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_light.png"} ], - "type": "boot", + "type": "bootloader", "tags": "system", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 47697277c..262cba1fa 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -48,3 +48,22 @@ 0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going 0.34: Don't buzz for 'map' update messages 0.35: Reset graphics colors before rendering a message (possibly fix #1752) +0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362) +0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items +0.38: Add telegram foss handling +0.39: Set default color for message icons according to theme +0.40: Use default Bangle formatter for booleans +0.41: Add notification icons in the widget +0.42: Fix messages ignoring "Vibrate: Off" setting +0.43: Add new Icons (Airbnb, warnwetter) +0.44: Separate buzz pattern for incoming calls +0.45: Added new app colors and icons +0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off + Fix message removal from widget bar (previously caused exception as .hide has been removed) +0.47: Add new Icons (Nextbike, Mattermost, etc.) +0.48: When getting new message from the clock, only buzz once the messages app is loaded +0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally +0.50: Add `getMessages` and `status` functions to library + Option to disable auto-open of messages + Option to make message icons monochrome (not colored) + messages widget buzz now returns a promise \ No newline at end of file diff --git a/apps/messages/README.md b/apps/messages/README.md index da2701f35..2e583d1c2 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -12,8 +12,11 @@ You can change settings by going to the global `Settings` app, then `App Setting and `Messages`: * `Vibrate` - This is the pattern of buzzes that should be made when a new message is received +* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received * `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds -* `Unread Timer` - When a new message is received we go into the Messages app. +* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and +buzz every `Repeat` seconds. This is how long we continue to do that. +* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app. If there is no user input for this amount of time then the app will exit and return to the clock where a ringing bell will be shown in the Widget bar. * `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font @@ -22,12 +25,13 @@ it starts getting clipped. * `Auto-Open Music` - Should the app automatically open when the phone starts playing music? * `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app? * `Flash Icon` - Toggle flashing of the widget icon. +* `Widget messages` - The maximum amount of message icons to show on the widget. ## New Messages When a new message is received: -* If you're in an app, the Bangle will buzz and a 'new message' icon appears in the Widget bar. You can tap this bar to view the message. +* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message. * If you're in a clock, the Messages app will automatically start and show the message When a message is shown, you'll see a screen showing the message title and text. diff --git a/apps/messages/app-newmessage.js b/apps/messages/app-newmessage.js new file mode 100644 index 000000000..328927c70 --- /dev/null +++ b/apps/messages/app-newmessage.js @@ -0,0 +1,5 @@ +/* Called when we have a new message when we're in the clock... +BUZZ_ON_NEW_MESSAGE is set so when messages.app.js loads it knows +that it should buzz */ +global.BUZZ_ON_NEW_MESSAGE = true; +eval(require("Storage").read("messages.app.js")); diff --git a/apps/messages/app.js b/apps/messages/app.js index 644f780b4..40dff9635 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -13,11 +13,11 @@ /* For example for maps: // a message -{"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"} +require("messages").pushMessage({"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}) // maps -{"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="} +require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="}); // call -{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true} +require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true}) */ var Layout = require("Layout"); @@ -48,13 +48,13 @@ we should start a timeout for settings.unreadTimeout to return to the clock. */ var unreadTimeout; /// List of all our messages -var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; +var MESSAGES = require("messages").getMessages(); if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one if (msg && msg.id!=="music" && msg.new && active!="map" && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { - if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); + if (WIDGETS["messages"]) WIDGETS["messages"].buzz(msg.src); else Bangle.buzz(); } if (msg && msg.id=="music") { @@ -67,104 +67,6 @@ function saveMessages() { require("Storage").writeJSON("messages.json",MESSAGES) } -function getBackImage() { - return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); -} -function getNotificationImage() { - return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); -} -function getFBIcon() { - return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); -} -function getPosImage() { - return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); -} -function getNegImage() { - return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); -} -/* -* icons should be 24x24px with 1bpp colors and 'Transparency to Color' -* http://www.espruino.com/Image+Converter -*/ -function getMessageImage(msg) { - if (msg.img) return atob(msg.img); - var s = (msg.src||"").toLowerCase(); - if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); - if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); - if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); - if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); - if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); - if (s=="facebook") return getFBIcon(); - if (s=="gmail") return getNotificationImage(); - if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); - if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); - if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); - if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); - if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); - if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); - if (s=="mail") return getNotificationImage(); - if (s=="messenger") return getFBIcon(); - if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); - if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); - if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); - if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw"); - if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA"); - if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); - if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); - if (s=="sms message") return getNotificationImage(); - if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA"); - if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); - if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); - if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); - if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); - if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); - if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); - if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); - if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); - if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); - if (msg.id=="back") return getBackImage(); - return getNotificationImage(); -} -function getMessageImageCol(msg,def) { - return { - // generic colors, using B2-safe colors - "alarm": "#fff", - "mail": "#ff0", - "music": "#f0f", - "phone": "#0f0", - "sms message": "#0ff", - // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) - // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) - "bibel": "#54342c", - "discord": "#738adb", - "facebook": "#4267b2", - "gmail": "#ea4335", - "google home": "#fbbc05", - "hangouts": "#1ba261", - "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background - "instagram": "#dd2a7b", - "liferando": "#ee5c00", - "messenger": "#0078ff", - "nina": "#e57004", - "outlook mail": "#0072c6", - "post & dhl": "#f2c101", - "signal": "#00f", - "skype": "#00aff0", - "slack": "#e51670", - "snapchat": "#ff0", - "teams": "#464eb8", - "telegram": "#0088cc", - "threema": "#000", - "to do": "#3999e5", - "twitch": "#6441A4", - "twitter": "#1da1f2", - "whatsapp": "#4fce5d", - "wordfeud": "#e7d3c7", - "youtube": "#f00", - }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); -} - function showMapMessage(msg) { active = "map"; var m; @@ -195,13 +97,13 @@ function showMapMessage(msg) { ]}); g.reset().clearRect(Bangle.appRect); layout.render(); - Bangle.setUI("updown",function() { - // any input to mark as not new and return to menu + function back() { // mark as not new and return to menu msg.new = false; saveMessages(); layout = undefined; checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); - }); + } + Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back } var updateLabelsInterval; @@ -224,8 +126,6 @@ function showMusicMessage(msg) { var sliceLength = offset + maxLen > text.length ? text.length - offset : maxLen; return text.substr(offset, sliceLength).padEnd(maxLen, " "); } - - function back() { clearInterval(updateLabelsInterval); updateLabelsInterval = undefined; @@ -254,7 +154,6 @@ function showMusicMessage(msg) { layout = new Layout({ type:"v", c: [ {type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ - { type:"btn", src:getBackImage, cb:back }, { type:"v", fillx:1, c: [ { type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:artistName, pad:2, id:"artist" }, { type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:albumName, pad:2, id:"album" } @@ -267,7 +166,7 @@ function showMusicMessage(msg) { {type:"btn", pad:8, label:"\0"+atob("EhKBAMAB+AB/gB/wB/8B/+B//B//x//5//5//x//B/+B/8B/wB/gB+AB8ABw"), cb:()=>Bangle.musicControl("next")}, // next ]}:{}, {type:"txt", font:"6x8:2", label:msg.dur?fmtTime(msg.dur):"--:--" } - ]}); + ]}, { back : back }); g.reset().clearRect(Bangle.appRect); layout.render(); @@ -302,12 +201,9 @@ function showMessageScroller(msg) { }, select : function(idx) { if (idx>=lines.length-2) showMessage(msg.id); - } + }, + back : () => showMessage(msg.id) }); - // ensure button-press on Bangle.js 2 takes us back - if (process.env.HWVERSION>1) Bangle.btnWatches = [ - setWatch(() => showMessage(msg.id), BTN1, {repeat:1,edge:"falling"}) - ]; } function showMessageSettings(msg) { @@ -390,16 +286,15 @@ function showMessage(msgid) { } } function goBack() { + layout = undefined; msg.new = false; saveMessages(); // read mail cancelReloadTimeout(); // don't auto-reload to clock now checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); } var buttons = [ - {type:"btn", src:getBackImage(), cb:goBack} // back ]; if (msg.positive) { - buttons.push({fillx:1}); - buttons.push({type:"btn", src:getPosImage(), cb:()=>{ + buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{ msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); @@ -407,8 +302,8 @@ function showMessage(msgid) { }}); } if (msg.negative) { - buttons.push({fillx:1}); - buttons.push({type:"btn", src:getNegImage(), cb:()=>{ + if (buttons.length) buttons.push({width:32}); // nasty hack... + buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{ msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,false); @@ -419,27 +314,23 @@ function showMessage(msgid) { layout = new Layout({ type:"v", c: [ {type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ - { type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{ - cancelReloadTimeout(); // don't auto-reload to clock now - showMessageSettings(msg); - }}, { type:"v", fillx:1, c: [ {type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 }, title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{}, ]}, + { type:"btn", src:require("messages").getMessageImage(msg), col:require("messages").getMessageImageCol(msg, g.theme.fg2), pad: 3, cb:()=>{ + cancelReloadTimeout(); // don't auto-reload to clock now + showMessageSettings(msg); + }}, ]}, {type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{ // allow tapping to show a larger version showMessageScroller(msg); } }, {type:"h",fillx:1, c: buttons} - ]}); + ]},{back:goBack}); g.reset().clearRect(Bangle.appRect); layout.render(); - // ensure button-press on Bangle.js 2 takes us back - if (process.env.HWVERSION>1) Bangle.btnWatches = [ - setWatch(goBack, BTN1, {repeat:1,edge:"falling"}) - ]; } @@ -463,8 +354,18 @@ function checkMessages(options) { // we have >0 messages var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); // If we have a new message, show it - if (options.showMsgIfUnread && newMessages.length) - return showMessage(newMessages[0].id); + if (options.showMsgIfUnread && newMessages.length) { + showMessage(newMessages[0].id); + // buzz after showMessage, so beingbusy during layout doesn't affect the buzz pattern + if (global.BUZZ_ON_NEW_MESSAGE) { + // this is set if we entered the messages app by loading `messages.new.js` + // ... but only buzz the first time we view a new message + global.BUZZ_ON_NEW_MESSAGE = false; + // messages.buzz respects quiet mode - no need to check here + WIDGETS.messages.buzz(newMessages[0].src); + } + return; + } // no new messages: show playing music? (only if we have playing music to show) if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play")) return showMessage('music'); @@ -475,23 +376,22 @@ function checkMessages(options) { // Otherwise show a menu E.showScroller({ h : 48, - c : Math.max(MESSAGES.length+1,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + c : Math.max(MESSAGES.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) draw : function(idx, r) {"ram" - var msg = MESSAGES[idx-1]; + var msg = MESSAGES[idx]; if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH); else g.setColor(g.theme.fg); g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); - if (idx==0) msg = {id:"back", title:"< Back"}; if (!msg) return; var x = r.x+2, title = msg.title, body = msg.body; - var img = getMessageImage(msg); + var img = require("messages").getMessageImage(msg); if (msg.id=="music") { title = msg.artist || /*LANG*/"Music"; body = msg.track; } if (img) { var fg = g.getColor(); - g.setColor(getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering + g.setColor(require("messages").getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering .setColor(fg); // only color the icon x += 50; } @@ -510,32 +410,29 @@ function checkMessages(options) { if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); 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 }, - select : idx => { - if (idx==0) load(); - else showMessage(MESSAGES[idx-1].id); - } + select : idx => showMessage(MESSAGES[idx].id), + back : () => load() }); } + function cancelReloadTimeout() { if (!unreadTimeout) return; clearTimeout(unreadTimeout); unreadTimeout = undefined; } - g.clear(); + Bangle.loadWidgets(); Bangle.drawWidgets(); + setTimeout(() => { - var unreadTimeoutSecs = settings.unreadTimeout; - if (unreadTimeoutSecs===undefined) unreadTimeoutSecs=60; - if (unreadTimeoutSecs) - unreadTimeout = setTimeout(function() { - print("Message not seen - reloading"); - load(); - }, unreadTimeoutSecs*1000); + var unreadTimeoutMillis = (settings.unreadTimeout || 60) * 1000; + if (unreadTimeoutMillis) { + unreadTimeout = setTimeout(load, unreadTimeoutMillis); + } // only openMusic on launch if music is new - var newMusic = MESSAGES.some(m=>m.id==="music"&&m.new); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1,openMusic:newMusic&&settings.openMusic}); -},10); // if checkMessages wants to 'load', do that + var newMusic = MESSAGES.some(m => m.id === "music" && m.new); + checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic }); +}, 10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/lib.js b/apps/messages/lib.js index f584c9e93..d8599c93d 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -40,9 +40,12 @@ exports.pushMessage = function(event) { require("Storage").writeJSON("messages.json",messages); // if in app, process immediately if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); - // if we've removed the last new message, hide the widget - if (event.t=="remove" && !messages.some(m=>m.new)) { - if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); + // update the widget icons shown + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true); + // if no new messages now, make sure we don't load the messages app + if (event.t=="remove" && exports.messageTimeout && !messages.some(m=>m.new)) { + clearTimeout(exports.messageTimeout); + delete exports.messageTimeout; } // ok, saved now if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) { @@ -59,30 +62,25 @@ exports.pushMessage = function(event) { var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; var appSettings = require('Storage').readJSON('messages.settings.json',1)||{}; var unlockWatch = appSettings.unlockWatch; - var quietNoAutOpn = appSettings.quietNoAutOpn; - delete appSettings; // don't auto-open messages in quiet mode if quietNoAutOpn is true - if(quiet && quietNoAutOpn) { - loadMessages = false; - } - // first, buzz - if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages){ - WIDGETS.messages.buzz(); - if(unlockWatch != false){ - Bangle.setLocked(false); - Bangle.setLCDPower(1); // turn screen on - } - } + if((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn) + loadMessages = false; + delete appSettings; // after a delay load the app, to ensure we have all the messages if (exports.messageTimeout) clearTimeout(exports.messageTimeout); exports.messageTimeout = setTimeout(function() { exports.messageTimeout = undefined; // if we're in a clock or it's important, go straight to messages app if (loadMessages){ - return load("messages.app.js"); + if(!quiet && unlockWatch){ + Bangle.setLocked(false); + Bangle.setLCDPower(1); // turn screen on + } + // we will buzz when we enter the messages app + return load("messages.new.js"); } - if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know - WIDGETS.messages.show(); + if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz once to let someone know + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages); }, 500); } /// Remove all messages @@ -99,5 +97,135 @@ exports.clearAll = function(event) { if (inApp) return onMessagesModified(); // if we have a widget, update it if (global.WIDGETS && WIDGETS.messages) - WIDGETS.messages.hide(); + WIDGETS.messages.update(messages); } + +/** + * @returns {array} All messages + */ +exports.getMessages = function() { + if ("undefined"!=typeof MESSAGES) return MESSAGES; // loaded/managed by app + return require("Storage").readJSON("messages.json",1)||[]; +} + +/** + * Check if there are any messages + * @returns {string} "new"/"old"/"none" + */ + exports.status = function() { + try { + let status= "none"; + for(const m of exports.getMessages()) { + if (["music", "map"].includes(m.id)) continue; + if (m.new) return "new"; + status = "old"; + } + return status; + } catch(e) { + return "none"; // don't bother e.g. the widget with errors + } +}; + +exports.getMessageImage = function(msg) { + /* + * icons should be 24x24px or less with 1bpp colors and 'Transparency to Color' + * http://www.espruino.com/Image+Converter + */ + if (msg.img) return atob(msg.img); + const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); + if (s=="airbnb") return atob("GBgBAAAAAAAAAAAAADwAAH4AAGYAAMMAAIEAAYGAAYGAAzzAA2bABmZgBmZgDGYwDDwwCDwQCBgQDDwwB+fgA8PAAAAAAAAAAAAA"); + if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); + if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); + if (s=="bring") return atob("GBgBAAAAAAAAAAAAAAAAAHwAAFoAAf+AA/+AA/+AA/+AA/eAA+eAA0+AAx+AA7+AA/+AA//AA/+AAf8AAAIAAAAAAAAAAAAAAAAA"); + if (s=="calendar" || s=="etar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); + if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); + if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); + if (s=="gmx") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEJmfmd8Zuc85v847/88Z9s8fttmHIHiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + if (s=="google") return atob("GBiBAAAAAAD/AAP/wAf/4A/D4B8AwDwAADwAAHgAAHgAAHAAAHAH/nAH/nAH/ngH/ngAHjwAPDwAfB8A+A/D8Af/4AP/wAD/AAAAAA=="); + if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); // 2 bit unpaletted + if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); + if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); + if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); + if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); + if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4MADgcYHAcYOA8MOB8OeD8GcD8GcH8GcD8HcD8HeBwHeAAOfAAOfgAePwA8P8D8H//4D//wB//gAf/AAH4A"); + if (s=="n26") return atob("GBgBAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAOIAAOIAAPIAANoAANoAAM4AAMYAAMYAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA"); + if (s=="nextbike") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAACAfgDAPwDAP4HAH4N4H8f8D82GMd8CMDsDMGMDMGGGGMHOD4D8AAAAAAAAAAAAAAAAAAAAAAAA"); + if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); + if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); + if (s=="paypal") return atob("GBgBAAAAAAAAAAAAAf+AAf/AAf/gA//gA//gA//wA//wA//wA//wB//wB//wB//gB/+AB/gAB/gAB/gAAPgAAPgAAAAAAAAAAAAA"); + if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw"); + if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA"); + if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); + if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA"); + if (s=="steam") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAfgAAwwAAvQABvQABvQADvQgDww4H/g+f8A/zwAf9gAH9AAB8AAACAAAcAAAAAAAAAAAAAAAAAA"); + if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); + if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); + if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + if (s=="to do" || s=="opentasks") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); + if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); + if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); + if (s=="warnapp") return atob("GBgBAAAAAAAAAAAAAH4AAP8AA//AA//AD//gP//gf//4f//+/+P+/8H//8n//4n/fxh/fzg+Pj88Dn44AA4AAAwAAAwAAAgAAAAA"); + if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); + if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); + if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); + if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + // if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below) + return atob("FhKBAH//+P//yf/+c//z5/+fz/z/n+f/Pz/+ef/8D///////////////////////f//4///A"); +}; + +exports.getMessageImageCol = function(msg,def) { + let iconColorMode = (require('Storage').readJSON("messages.settings.json", 1) || {}).iconColorMode; + if (iconColorMode == 'mono') + return g.theme.fg; + const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); + return { + // generic colors, using B2-safe colors + // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used + "airbnb": "#f00", + "mail": "#ff0", + "music": "#f0f", + "phone": "#0f0", + "sms message": "#0ff", + // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) + // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) + "bibel": "#54342c", + "bring": "#455a64", + "discord": "#738adb", + "etar": "#36a18b", + "facebook": "#4267b2", + "gmail": "#ea4335", + "gmx": "#1c449b", + "google": "#4285F4", + "google home": "#fbbc05", +// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background + "instagram": "#dd2a7b", + "lieferando": "#ee5c00", + "messenger": "#0078ff", + "mattermost": "#00f", + "n26": "#36a18b", + "nextbike": "#00f", + "newpipe": "#f00", + "nina": "#e57004", + "opentasks": "#409f8f", + "outlook mail": "#0072c6", + "paypal": "#003087", + "post & dhl": "#f2c101", + "signal": "#00f", + "skype": "#00aff0", + "slack": "#e51670", + "snapchat": "#ff0", + "steam": "#171a21", + "teams": "#464eb8", + "telegram": "#0088cc", + "telegram foss": "#0088cc", + "to do": "#3999e5", + "twitch": "#6441A4", + "twitter": "#1da1f2", + "whatsapp": "#4fce5d", + "wordfeud": "#e7d3c7", + "youtube": "#f00", + }[s]||(def !== undefined?def:g.theme.fg); +}; diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index 9a2b0a880..da2e0945a 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.35", + "version": "0.50", "description": "App to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", @@ -10,6 +10,7 @@ "readme": "README.md", "storage": [ {"name":"messages.app.js","url":"app.js"}, + {"name":"messages.new.js","url":"app-newmessage.js"}, {"name":"messages.settings.js","url":"settings.js"}, {"name":"messages.img","url":"app-icon.js","evaluate":true}, {"name":"messages.wid.js","url":"widget.js"}, diff --git a/apps/messages/screenshot-notify.gif b/apps/messages/screenshot-notify.gif index 3d0ed0b32..e5cc669bd 100644 Binary files a/apps/messages/screenshot-notify.gif and b/apps/messages/screenshot-notify.gif differ diff --git a/apps/messages/settings.js b/apps/messages/settings.js index adea36f12..0edb17797 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -1,9 +1,15 @@ (function(back) { + const iconColorModes = ['color', 'mono']; + function settings() { let settings = require('Storage').readJSON("messages.settings.json", true) || {}; - if (settings.vibrate===undefined) settings.vibrate="."; + if (settings.vibrate===undefined) settings.vibrate=":"; + if (settings.vibrateCalls===undefined) settings.vibrateCalls=":"; if (settings.repeat===undefined) settings.repeat=4; + if (settings.vibrateTimeout===undefined) settings.vibrateTimeout=60; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + if (settings.maxMessages===undefined) settings.maxMessages=3; + if (settings.iconColorMode === undefined) settings.iconColorMode = iconColorModes[0]; settings.unlockWatch=!!settings.unlockWatch; settings.openMusic=!!settings.openMusic; settings.maxUnreadTimeout=240; @@ -20,12 +26,19 @@ "" : { "title" : /*LANG*/"Messages" }, "< Back" : back, /*LANG*/'Vibrate': require("buzz_menu").pattern(settings().vibrate, v => updateSetting("vibrate", v)), + /*LANG*/'Vibrate for calls': require("buzz_menu").pattern(settings().vibrateCalls, v => updateSetting("vibrateCalls", v)), /*LANG*/'Repeat': { value: settings().repeat, min: 0, max: 10, format: v => v?v+"s":/*LANG*/"Off", onchange: v => updateSetting("repeat", v) }, + /*LANG*/'Vibrate timer': { + value: settings().vibrateTimeout, + min: 0, max: settings().maxUnreadTimeout, step : 10, + format: v => v?v+"s":/*LANG*/"Off", + onchange: v => updateSetting("vibrateTimeout", v) + }, /*LANG*/'Unread timer': { value: settings().unreadTimeout, min: 0, max: settings().maxUnreadTimeout, step : 10, @@ -40,24 +53,35 @@ }, /*LANG*/'Auto-Open Music': { value: !!settings().openMusic, - format: v => v?/*LANG*/'Yes':/*LANG*/'No', onchange: v => updateSetting("openMusic", v) }, /*LANG*/'Unlock Watch': { value: !!settings().unlockWatch, - format: v => v?/*LANG*/'Yes':/*LANG*/'No', onchange: v => updateSetting("unlockWatch", v) }, /*LANG*/'Flash Icon': { value: !!settings().flash, - format: v => v?/*LANG*/'Yes':/*LANG*/'No', onchange: v => updateSetting("flash", v) }, /*LANG*/'Quiet mode disables auto-open': { value: !!settings().quietNoAutOpn, - format: v => v?/*LANG*/'Yes':/*LANG*/'No', onchange: v => updateSetting("quietNoAutOpn", v) }, + /*LANG*/'Disable auto-open': { + value: !!settings().noAutOpn, + onchange: v => updateSetting("noAutOpn", v) + }, + /*LANG*/'Widget messages': { + value:0|settings().maxMessages, + min: 1, max: 5, + onchange: v => updateSetting("maxMessages", v) + }, + /*LANG*/'Icon color mode': { + value: Math.max(0,iconColorModes.indexOf(settings().iconColorMode)), + min: 0, max: iconColorModes.length - 1, + format: v => iconColorModes[v], + onchange: v => updateSetting("iconColorMode", iconColorModes[v]) + } }; E.showMenu(mainmenu); -}) +}); diff --git a/apps/messages/widget.js b/apps/messages/widget.js index 4b368ffd6..a5e1f8b6c 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,5 +1,12 @@ -WIDGETS["messages"]={area:"tl", width:0, iconwidth:24, -draw:function(recall) { +(() => { + +function filterMessages(msgs) { + return msgs.filter(msg => msg.new && msg.id != "music") + .map(m => m.src) // we only need this for icon/color + .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); +} + +WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) { // If we had a setTimeout queued from the last time we were called, remove it if (WIDGETS["messages"].i) { clearTimeout(WIDGETS["messages"].i); @@ -8,43 +15,67 @@ draw:function(recall) { Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; - let settings = require('Storage').readJSON("messages.settings.json", true) || {}; - if (settings.flash===undefined) settings.flash = true; + let settings = Object.assign({flash:true, maxMessages:3, repeat:4, vibrateTimeout:60},require('Storage').readJSON("messages.settings.json", true) || {}); if (recall !== true || settings.flash) { + var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages); g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); - g.drawImage(settings.flash && (c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y-1); + for(let i = 0;i < msgsShown;i++) { + const msg = this.msgs[i]; + const colors = [g.theme.bg, g.setColor(require("messages").getMessageImageCol(msg)).getColor()]; + if (settings.flash && (c&1)) { + if (colors[1] == g.theme.fg) { + colors.reverse(); + } else { + colors[1] = g.theme.fg; + } + } + g.setColor(colors[1]).setBgColor(colors[0]); + // draw the icon, or '...' if too many messages + g.drawImage(i == (settings.maxMessages - 1) && this.msgs.length > settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messages").getMessageImage(msg), + this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/}); + } } - if (settings.repeat===undefined) settings.repeat = 4; - if (c<120 && (Date.now()-this.l)>settings.repeat*1000) { + if (csettings.repeat*1000) { // the period between vibrations this.l = Date.now(); WIDGETS["messages"].buzz(); // buzz every 4 seconds } WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000); if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); -},show:function(quiet) { - WIDGETS["messages"].t=Date.now(); // first time - WIDGETS["messages"].l=Date.now()-10000; // last buzz - if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing - WIDGETS["messages"].width=this.iconwidth; +},update:function(rawMsgs, quiet) { + const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); + this.msgs = filterMessages(rawMsgs); + if (this.msgs.length === 0) { + delete this.t; + delete this.l; + } else { + this.t=Date.now(); // first time + this.l=Date.now()-10000; // last buzz + if (quiet) this.t -= 500000; // if quiet, set last time in the past so there is no buzzing + } + this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages); Bangle.drawWidgets(); - Bangle.setLCDPower(1);// turns screen on -},hide:function() { - delete WIDGETS["messages"].t; - delete WIDGETS["messages"].l; - WIDGETS["messages"].width=0; - Bangle.drawWidgets(); -},buzz:function() { - if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode - require("buzz").pattern((require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || "."); +},buzz:function(msgSrc) { // return a promise + if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode + var pattern; + if (msgSrc != undefined && msgSrc.toLowerCase() == "phone") { + // special vibration pattern for incoming calls + pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateCalls; + } else { + pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate; + } + if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here + return require("buzz").pattern(pattern); },touch:function(b,c) { var w=WIDGETS["messages"]; - if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+24) return; load("messages.app.js"); }}; + /* We might have returned here if we were in the Messages app for a message but then the watch was never viewed. In that case we don't want to buzz but should still show that there are unread messages. */ -if (global.MESSAGES===undefined) (function() { - var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true); +if (global.MESSAGES===undefined) + WIDGETS["messages"].update(require("messages").getMessages(), true); })(); diff --git a/apps/messagesmusic/ChangeLog b/apps/messagesmusic/ChangeLog index 5560f00bc..9f4cafb0e 100644 --- a/apps/messagesmusic/ChangeLog +++ b/apps/messagesmusic/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app. diff --git a/apps/messagesmusic/README.md b/apps/messagesmusic/README.md index 7aa9209df..85608118d 100644 --- a/apps/messagesmusic/README.md +++ b/apps/messagesmusic/README.md @@ -6,9 +6,9 @@ Making the music controls accessible this way lets one start a music stream on t It is suggested to use Messages Music along side the app Quick Launch. -Messages Music v0.01 has been verified to work with Messages v0.31 on Bangle.js 2 fw2v13. +Messages Music v0.02 has been verified to work with Messages v0.41 on Bangle.js 2 fw2v14. -Music Messages should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much. +Messages Music should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much. Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986). diff --git a/apps/messagesmusic/app.js b/apps/messagesmusic/app.js index a6f7e075e..27f3f6e4d 100644 --- a/apps/messagesmusic/app.js +++ b/apps/messagesmusic/app.js @@ -1,7 +1,6 @@ let showMusic = () => { Bangle.CLOCK = 1; // To pass condition in messages library require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true}); - Bangle.CLOCK = undefined; }; var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict diff --git a/apps/messagesmusic/metadata.json b/apps/messagesmusic/metadata.json index edc6835ed..c29ffbc34 100644 --- a/apps/messagesmusic/metadata.json +++ b/apps/messagesmusic/metadata.json @@ -1,7 +1,7 @@ { "id": "messagesmusic", "name":"Messages Music", - "version":"0.01", + "version":"0.02", "description": "Uses Messages library to push a music message which in turn displays Messages app music controls", "icon":"app.png", "type": "app", diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog index e92bad2e3..d1ac3e388 100644 --- a/apps/miclock/ChangeLog +++ b/apps/miclock/ChangeLog @@ -2,3 +2,4 @@ 0.03: Localization 0.04: move jshint to the top 0.05: Use Bangle.setUI for button/launcher handling +0.06: Tell clock widgets to hide. diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js index b3d6bea8d..cb3235406 100644 --- a/apps/miclock/clock-mixed.js +++ b/apps/miclock/clock-mixed.js @@ -77,11 +77,13 @@ Bangle.on('lcdPower', function(on) { drawMixedClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(drawMixedClock, 5E3); drawMixedClock(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/miclock/metadata.json b/apps/miclock/metadata.json index 6eece46b0..2c216dc33 100644 --- a/apps/miclock/metadata.json +++ b/apps/miclock/metadata.json @@ -1,7 +1,7 @@ { "id": "miclock", "name": "Mixed Clock", - "version": "0.05", + "version": "0.06", "description": "A mix of analog and digital Clock", "icon": "clock-mixed.png", "type": "clock", diff --git a/apps/miclock2/ChangeLog b/apps/miclock2/ChangeLog index 5560f00bc..534332e63 100644 --- a/apps/miclock2/ChangeLog +++ b/apps/miclock2/ChangeLog @@ -1 +1,3 @@ 0.01: New App! +0.02: Redraw only when seconds change +0.03: Fix typo in redraw check diff --git a/apps/miclock2/clock-mixed.js b/apps/miclock2/clock-mixed.js index d928a5185..bb1537313 100644 --- a/apps/miclock2/clock-mixed.js +++ b/apps/miclock2/clock-mixed.js @@ -6,6 +6,7 @@ const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 }; const Center = { "x": 120, "y": 96 }; const Widths = { hour: 2, minute: 2 }; var buf = Graphics.createArrayBuffer(240,192,1,{msb:true}); +var lastDate = new Date(); function rotatePoint(x, y, d) { rad = -1 * d / 180 * Math.PI; @@ -45,10 +46,10 @@ function setLineWidth(x1, y1, x2, y2, lw) { ]; } - function drawMixedClock(force) { - if ((force || Bangle.isLCDOn()) && buf.buffer) { - var date = new Date(); + var date = new Date(); + if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) { + lastDate = date; var dateArray = date.toString().split(" "); var isEn = locale.name.startsWith("en"); var point = []; diff --git a/apps/miclock2/metadata.json b/apps/miclock2/metadata.json index dc1b49822..094d0995a 100644 --- a/apps/miclock2/metadata.json +++ b/apps/miclock2/metadata.json @@ -1,7 +1,7 @@ { "id": "miclock2", "name": "Mixed Clock 2", - "version": "0.01", + "version": "0.03", "description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.", "icon": "clock-mixed.png", "type": "clock", diff --git a/apps/minimal_clock/ChangeLog b/apps/minimal_clock/ChangeLog new file mode 100644 index 000000000..54ee389e3 --- /dev/null +++ b/apps/minimal_clock/ChangeLog @@ -0,0 +1,3 @@ +... +0.03: First update with ChangeLog Added +0.04: Tell clock widgets to hide. diff --git a/apps/minimal_clock/app.js b/apps/minimal_clock/app.js index d78790347..47eca3c66 100644 --- a/apps/minimal_clock/app.js +++ b/apps/minimal_clock/app.js @@ -3,6 +3,7 @@ let outerRadius = Math.min(CenterX,CenterY) * 0.9; + Bangle.setUI('clock'); Bangle.loadWidgets(); /**** updateClockFaceSize ****/ @@ -225,6 +226,5 @@ } }); - Bangle.loadWidgets(); - Bangle.setUI('clock'); + Bangle.loadWidgets(); diff --git a/apps/minimal_clock/metadata.json b/apps/minimal_clock/metadata.json index 1702d97a9..3089780ce 100644 --- a/apps/minimal_clock/metadata.json +++ b/apps/minimal_clock/metadata.json @@ -1,7 +1,7 @@ { "id": "minimal_clock", "name": "Minimal Analog Clock", "shortName":"Minimal Clock", - "version":"0.03", + "version":"0.04", "description": "a minimal analog clock - just with some hands and no clock face", "icon": "app-icon.png", "type": "clock", diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog index a8b6efc81..5949a786d 100644 --- a/apps/minionclk/ChangeLog +++ b/apps/minionclk/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fixed rendering for Espruino v2.06 0.04: Fixed overlapped rendering of dates 0.05: Use Bangle.setUI for button/launcher handling +0.06: Tell clock widgets to hide. diff --git a/apps/minionclk/app b/apps/minionclk/app new file mode 100644 index 000000000..e69de29bb diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js index 9648e3d89..c61f8d3bf 100644 --- a/apps/minionclk/app.js +++ b/apps/minionclk/app.js @@ -78,8 +78,10 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + Bangle.loadWidgets(); startDrawing(); -// Show launcher when button pressed -Bangle.setUI("clock"); + diff --git a/apps/minionclk/metadata.json b/apps/minionclk/metadata.json index 44fc2a82d..4df2ddc6b 100644 --- a/apps/minionclk/metadata.json +++ b/apps/minionclk/metadata.json @@ -1,7 +1,7 @@ { "id": "minionclk", "name": "Minion clock", - "version": "0.05", + "version": "0.06", "description": "Minion themed clock.", "icon": "minionclk.png", "type": "clock", diff --git a/apps/mmind/mmind.info b/apps/mmind/mmind.info index 2e79822b1..b4b822508 100644 --- a/apps/mmind/mmind.info +++ b/apps/mmind/mmind.info @@ -5,7 +5,7 @@ "icon": "mmind.png", "version":"0.01", "description": "This is the classic game for masterminds", - "type": "game", + "type": "app", "tags": "mastermind, game, classic", "readme":"README.md", "supports": ["BANGLEJS2"], diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog new file mode 100644 index 000000000..9a2ab0ff4 --- /dev/null +++ b/apps/multitimer/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial version +0.02: Update for time_utils module +0.03: Use default Bangle formatter for booleans diff --git a/apps/multitimer/README.md b/apps/multitimer/README.md new file mode 100644 index 000000000..f1e2eb281 --- /dev/null +++ b/apps/multitimer/README.md @@ -0,0 +1,10 @@ +# Multi Timer +With this app, you can set timers and chronographs (stopwatches) and watch them count down/up in real time. You can also set alarms - swipe left or right to switch between the three functions. + +"Hard mode" is also available for timers and alarms. It will double the number of buzz counts and you will have to swipe the screen five to eight times correctly - make a mistake, and you will need to start over. + +## WARNING +* Editing timers in another app (such as the default Alarm app) is not recommended. Editing alarms should not be a problem (in theory). +* This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched). +* To avoid potential conflicts with other apps that uses sched (especially ones that make use of the data and js field), this app only lists timers and alarms that it created - any made outside the app will be ignored. GB alarms are currently an exception as they do not make use of the data and js field. +* A keyboard app is only used for adding messages to timers and is therefore not strictly needed. diff --git a/apps/multitimer/alarm.js b/apps/multitimer/alarm.js new file mode 100644 index 000000000..eb1b3b259 --- /dev/null +++ b/apps/multitimer/alarm.js @@ -0,0 +1,148 @@ +//sched.js, modified +// Chances are boot0.js got run already and scheduled *another* +// 'load(sched.js)' - so let's remove it first! +if (Bangle.SCHED) { + clearInterval(Bangle.SCHED); + delete Bangle.SCHED; +} + +function hardMode(tries, max) { + var R = Bangle.appRect; + + function adv() { + tries++; + hardMode(tries, max); + } + + if (tries < max) { + g.clear(); + g.reset(); + g.setClipRect(R.x,R.y,R.x2,R.y2); + var code = Math.abs(E.hwRand()%4); + if (code == 0) dir = "up"; + else if (code == 1) dir = "right"; + else if (code == 2) dir = "down"; + else dir = "left"; + g.setFont("6x8:2").setFontAlign(0,0).drawString(tries+"/"+max+"\nSwipe "+dir, (R.x2-R.x)/2, (R.y2-R.y)/2); + var drag; + Bangle.setUI({ + mode : "custom", + 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; + //horizontal swipes + if (Math.abs(dx)>Math.abs(dy)+10) { + //left + if (dx<0 && code == 3) adv(); + //right + else if (dx>0 && code == 1) adv(); + //wrong swipe - reset + else startHM(); + } + //vertical swipes + else if (Math.abs(dy)>Math.abs(dx)+10) { + //up + if (dy<0 && code == 0) adv(); + //down + else if (dy>0 && code == 2) adv(); + //wrong swipe - reset + else startHM(); + } + } + } + }); + } + else { + if (!active[0].timer) active[0].last = (new Date()).getDate(); + if (!active[0].rp) active[0].on = false; + if (active[0].timer) active[0].timer = active[0].data.ot; + require("sched").setAlarms(alarms); + load(); + } +} + +function startHM() { + //between 5-8 random swipes + hardMode(0, Math.abs(E.hwRand()%4)+5); +} + +function showAlarm(alarm) { + const settings = require("sched").getSettings(); + + let msg = ""; + if (alarm.timer) msg += require("time_utils").formatTime(alarm.timer); + if (alarm.msg) { + msg += "\n"+alarm.msg; + } + else msg = atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==")+" "+msg; + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + + let buzzCount = settings.buzzCount; + + if (alarm.data.hm && alarm.data.hm == true) { + //hard mode extends auto-snooze time + buzzCount = buzzCount * 3; + startHM(); + } + + else { + E.showPrompt(msg,{ + title: "TIMER!", + buttons : {"Snooze":true,"Ok":false} // default is sleep so it'll come back in 10 mins + }).then(function(sleep) { + buzzCount = 0; + if (sleep) { + if(alarm.ot===undefined) alarm.ot = alarm.t; + alarm.t += settings.defaultSnoozeMillis; + } else { + if (!alarm.timer) alarm.last = (new Date()).getDate(); + if (alarm.ot!==undefined) { + alarm.t = alarm.ot; + delete alarm.ot; + } + if (!alarm.rp) alarm.on = false; + } + //reset timer value + if (alarm.timer) alarm.timer = alarm.data.ot; + // alarm is still a member of 'alarms', so writing to array writes changes back directly + require("sched").setAlarms(alarms); + load(); + }); + } + + function buzz() { + if (settings.unlockAtBuzz) { + Bangle.setLocked(false); + } + + require("buzz").pattern(alarm.vibrate === undefined ? "::" : alarm.vibrate).then(() => { + if (buzzCount--) { + setTimeout(buzz, settings.buzzIntervalMillis); + } else if (alarm.as) { // auto-snooze + buzzCount = settings.buzzCount; + setTimeout(buzz, settings.defaultSnoozeMillis); + } + }); + } + + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) + return; + + buzz(); +} + +// Check for alarms +let alarms = require("sched").getAlarms(); +let active = require("sched").getActiveAlarms(alarms); +if (active.length) { + // if there's an alarm, show it + showAlarm(active[0]); +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/multitimer/app-icon.js b/apps/multitimer/app-icon.js new file mode 100644 index 000000000..693f9f3f1 --- /dev/null +++ b/apps/multitimer/app-icon.js @@ -0,0 +1 @@ +atob("MDABf/////+A///////AwAAAAADAwAAAAADAwAAAAADAwAAAAADAwAAAAADA///////A///////AwAAAAADAwAAAAADAwAAAAADAwAAAAADAwAAAAADAwcP//+DAwef///DAwAAAAADAwAAAAADAwAAAAADAwcP//+DAwef///DAwAAAAADAwAAAAADAwAAAAADAwef///DAwef//+DAwAAAAABAwAAAAAAAwAAAAAAAwef//gfAwcP//AfwwAAAAIZ4wAAAAYIcwAAAA4AOwAAAAzAGwAAABjgHwAAABhwD////xg8D////5gcDAAAABgcDAAAABgADAAAABgAGAAAAAwAGAAAAA4AMAAAAAcAcAAAAAPB4AAAAAH/gAAAAAB+A") diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js new file mode 100644 index 000000000..8832d1a25 --- /dev/null +++ b/apps/multitimer/app.js @@ -0,0 +1,673 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var R = Bangle.appRect; +var layer; +var drag; +var timerInt1 = []; +var timerInt2 = []; + +function getCurrentTime() { + let time = new Date(); + return ( + time.getHours() * 3600000 + + time.getMinutes() * 60000 + + time.getSeconds() * 1000 + ); +} + +function decodeTime(t) { + let hrs = 0 | Math.floor(t / 3600000); + let mins = 0 | Math.floor(t / 60000 % 60); + let secs = 0 | Math.floor(t / 1000 % 60); + return { hrs: hrs, mins: mins, secs: secs }; +} + +function encodeTime(o) { + return o.hrs * 3600000 + o.mins * 60000 + o.secs * 1000; +} + +function formatTime(t) { + let o = decodeTime(t); + return o.hrs + ":" + ("0" + o.mins).substr(-2) + ":" + ("0" + o.secs).substr(-2); +} + +function decodeTimeDecis(t) { + let hrs = 0 | Math.floor(t / 3600000); + let mins = 0 | Math.floor(t / 60000 % 60); + let secs = 0 | Math.floor(t / 1000 % 60); + let decis = 0 | Math.floor(t / 100 % 100); + return { hrs: hrs, mins: mins, secs: secs, decis: decis }; +} + +function formatTimeDecis(t) { + let o = decodeTimeDecis(t); + return o.hrs + ":" + ("0" + o.mins).substr(-2) + ":" + ("0" + o.secs).substr(-2) + "." + ("0" + o.decis).substr(-1); +} + +function clearInt() { + for (let i = 0; i < timerInt1.length; i++) { + if (timerInt1[i]) clearTimeout(timerInt1[i]); + } + for (let i = 0; i < timerInt2.length; i++) { + if (timerInt2[i]) clearInterval(timerInt2[i]); + } + timerInt1 = []; + timerInt2 = []; +} + +function drawTimers() { + layer = 0; + var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer"); + var alarms = require("sched").getAlarms(); + + function updateTimers(idx) { + if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() { + s.drawItem(idx+1); + if (!timerInt2[idx]) timerInt2[idx] = setInterval(function(){ + s.drawItem(idx+1); + }, 1000); + }, 1000 - (timers[idx].t % 1000)); + } + + var s = E.showScroller({ + h : 40, c : timers.length+2, + back : function() {load();}, + draw : (idx, r) => { + function drawMenuItem(a) { + g.setClipRect(R.x,R.y,R.x2,R.y2); + if (idx > 0 && timers[idx-1].msg) msg = "\n"+(timers[idx-1].msg.length > 10 ? + timers[idx-1].msg.substring(0, 10)+"..." : timers[idx-1].msg); + else msg = ""; + return g.setColor(g.theme.bg2).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(a+msg,r.x+12,r.y+(r.h/2)); + } + + if (idx == 0) { + drawMenuItem("+ New Timer"); + } + if (idx == timers.length+1) { + g.setColor(g.theme.bg).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg).setFont("6x8:2").setFontAlign(0,0).drawString("< Swipe >",r.x+(r.w/2),r.y+(r.h/2)); + } + else if (idx > 0 && idx < timers.length+1) { + if (timers[idx-1].on == true) { + drawMenuItem(formatTime(timers[idx-1].t-getCurrentTime())); + updateTimers(idx-1); + } + else drawMenuItem(formatTime(timers[idx-1].timer)); + } + }, + select : (idx) => { + clearInt(); + if (idx == 0) editTimer(-1); + else if (idx > 0 && idx < timers.length+1) timerMenu(idx-1); + } + }); +} + +function timerMenu(idx) { + layer = -1; + var timers = require("sched").getAlarms(); + var timerIdx = []; + var j = 0; + for (let i = 0; i < timers.length; i++) { + if (timers[i].timer && timers[i].appid == "multitimer") { + a = i; + timerIdx.push(a); + j++; + } + } + var a = timers[timerIdx[idx]]; + var msg = ""; + + function updateTimer() { + if (timerInt1[0] == undefined) timerInt1[0] = setTimeout(function() { + s.drawItem(0); + if (timerInt2[0] == undefined) timerInt2[0] = setInterval(function(){ + s.drawItem(0); + }, 1000); + }, 1000 - (a.t % 1000)); + } + + var s = E.showScroller({ + h : 40, c : 5, + back : function() { + clearInt(); + drawTimers(); + }, + draw : (i, r) => { + + function drawMenuItem(b) { + return g.setClipRect(R.x,R.y,R.x2,R.y2).setColor(g.theme.bg2) + .fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(b,r.x+12,r.y+(r.h/2)); + } + + if (i == 0) { + if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg); + if (a.on == true) { + drawMenuItem(formatTime(a.t-getCurrentTime())+msg); + updateTimer(); + } + else { + clearInt(); + drawMenuItem(formatTime(a.timer)+msg); + } + } + if (i == 1) { + if (a.on == true) drawMenuItem("Pause"); + else drawMenuItem("Start"); + } + if (i == 2) drawMenuItem("Reset"); + if (i == 3) drawMenuItem("Edit"); + if (i == 4) drawMenuItem("Delete"); + }, + select : (i) => { + + function saveAndReload() { + require("sched").setAlarms(timers); + require("sched").reload(); + s.draw(); + } + + //pause/start + if (i == 1) { + if (a.on == true) { + clearInt(); + a.timer = a.t-getCurrentTime(); + a.on = false; + timers[timerIdx[idx]] = a; + saveAndReload(); + } + else { + a.t = a.timer+getCurrentTime(); + a.on = true; + timers[timerIdx[idx]] = a; + saveAndReload(); + } + } + //reset + if (i == 2) { + clearInt(); + a.timer = a.data.ot; + if (a.on == true) a.on = false; + saveAndReload(); + } + //edit + if (i == 3) { + clearInt(); + editTimer(idx); + } + //delete + if (i == 4) { + clearInt(); + timers.splice(timerIdx[idx], 1); + saveAndReload(); + drawTimers(); + } + } + }); +} + +function editTimer(idx, a) { + layer = -1; + var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer"); + var alarms = require("sched").getAlarms(); + var timerIdx = []; + var j = 0; + for (let i = 0; i < alarms.length; i++) { + if (alarms[i].timer && alarms[i].appid == "multitimer") { + b = i; + timerIdx.push(b); + j++; + } + } + if (!a) { + if (idx < 0) a = require("sched").newDefaultTimer(); + else a = timers[idx]; + } + if (!a.data) { + a.data = {}; + a.data.hm = false; + } + var t = decodeTime(a.timer); + + function editMsg(idx, a) { + g.clear(); + idx < 0 ? msg = "" : msg = a.msg; + require("textinput").input({text:msg}).then(result => { + if (result != "") { + a.msg = result; + } + else delete a.msg; + editTimer(idx, a); + }); + } + + function kbAlert() { + E.showAlert("Must install keyboard app").then(function() { + editTimer(idx, a); + }); + } + + var menu = { + "": { "title": "Timer" }, + "< Back": () => { + a.t = getCurrentTime() + a.timer; + a.last = 0; + a.data.ot = a.timer; + a.appid = "multitimer"; + a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')"; + if (idx < 0) alarms.push(a); + else alarms[timerIdx[idx]] = a; + require("sched").setAlarms(alarms); + require("sched").reload(); + drawTimers(); + }, + "Enabled": { + value: a.on, + onchange: v => a.on = v + }, + "Hours": { + value: t.hrs, min: 0, max: 23, wrap: true, + onchange: v => { + t.hrs = v; + a.timer = encodeTime(t); + } + }, + "Minutes": { + value: t.mins, min: 0, max: 59, wrap: true, + onchange: v => { + t.mins = v; + a.timer = encodeTime(t); + } + }, + "Seconds": { + value: t.secs, min: 0, max: 59, wrap: true, + onchange: v => { + t.secs = v; + a.timer = encodeTime(t); + } + }, + "Hard Mode": { + value: a.data.hm, + onchange: v => a.data.hm = v + }, + "Vibrate": require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), + "Msg": { + value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg, + //menu glitch? setTimeout required here + onchange: () => { + var kbapp = require("Storage").read("textinput"); + if (kbapp != undefined) setTimeout(editMsg, 0, idx, a); + else setTimeout(kbAlert, 0); + } + }, + "Cancel": () => { + if (idx >= 0) timerMenu(idx); + else drawTimers(); + }, + }; + + E.showMenu(menu); +} + +function drawSw() { + layer = 1; + var sw = require("Storage").readJSON("multitimer.json", true) || []; + + function updateTimers(idx) { + if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() { + s.drawItem(idx+1); + if (!timerInt2[idx]) timerInt2[idx] = setInterval(function(){ + s.drawItem(idx+1); + }, 1000); + }, 1000 - (sw[idx].t % 1000)); + } + + var s = E.showScroller({ + h : 40, c : sw.length+2, + back : function() {load();}, + draw : (idx, r) => { + + function drawMenuItem(a) { + g.setClipRect(R.x,R.y,R.x2,R.y2); + if (idx > 0 && sw[idx-1].msg) msg = "\n"+(sw[idx-1].msg.length > 10 ? + sw[idx-1].msg.substring(0, 10)+"..." : sw[idx-1].msg); + else msg = ""; + return g.setColor(g.theme.bg2).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(a+msg,r.x+12,r.y+(r.h/2)); + } + + if (idx == 0) { + drawMenuItem("+ New Chrono"); + } + if (idx == sw.length+1) { + g.setColor(g.theme.bg).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg).setFont("6x8:2").setFontAlign(0,0).drawString("< Swipe >",r.x+(r.w/2),r.y+(r.h/2)); + } + else if (idx > 0 && idx < sw.length+1) { + if (sw[idx-1].on == true) { + drawMenuItem(formatTime(Date.now()-sw[idx-1].t)); + updateTimers(idx-1); + } + else drawMenuItem(formatTime(sw[idx-1].t)); + } + }, + select : (idx) => { + clearInt(); + if (idx == 0) swMenu(sw.length); + else if (idx > 0 && idx < sw.length+1) swMenu(idx-1); + } + }); +} + +function swMenu(idx, a) { + layer = -1; + var sw = require("Storage").readJSON("multitimer.json", true) || []; + if (sw[idx]) a = sw[idx]; + else { + a = {"t" : 0, "on" : false, "msg" : ""}; + sw[idx] = a; + require("Storage").writeJSON("multitimer.json", sw); + } + + function updateTimer() { + if (timerInt1[0] == undefined) timerInt1[0] = setTimeout(function() { + s.drawItem(0); + if (timerInt2[0] == undefined) timerInt2[0] = setInterval(function(){ + s.drawItem(0); + }, 100); + }, 100 - (a.t % 100)); + } + + function editMsg(idx, a) { + g.clear(); + msg = a.msg; + require("textinput").input({text:msg}).then(result => { + if (result != "") { + a.msg = result; + } + else delete a.msg; + sw[idx] = a; + require("Storage").writeJSON("multitimer.json", sw); + swMenu(idx, a); + }); + } + + function kbAlert() { + E.showAlert("Must install keyboard app").then(function() { + swMenu(idx, a); + }); + } + + var s = E.showScroller({ + h : 40, c : 5, + back : function() { + clearInt(); + drawSw(); + }, + draw : (i, r) => { + + function drawMenuItem(b) { + return g.setClipRect(R.x,R.y,R.x2,R.y2).setColor(g.theme.bg2) + .fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) + .setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(b,r.x+12,r.y+(r.h/2)); + } + + if (i == 0) { + if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg); + else msg = ""; + if (a.on == true) { + drawMenuItem(formatTimeDecis(Date.now()-a.t)+msg); + updateTimer(); + } + else { + clearInt(); + drawMenuItem(formatTimeDecis(a.t)+msg); + } + } + if (i == 1) { + if (a.on == true) drawMenuItem("Pause"); + else drawMenuItem("Start"); + } + if (i == 2) drawMenuItem("Reset"); + if (i == 3) drawMenuItem("Msg"); + if (i == 4) drawMenuItem("Delete"); + }, + select : (i) => { + + function saveAndReload() { + require("Storage").writeJSON("multitimer.json", sw); + s.draw(); + } + + //pause/start + if (i == 1) { + if (a.on == true) { + clearInt(); + a.t = Date.now()-a.t; + a.on = false; + sw[idx] = a; + saveAndReload(); + } + else { + a.t == 0 ? a.t = Date.now() : a.t = Date.now()-a.t; + a.on = true; + sw[idx] = a; + saveAndReload(); + } + } + //reset + if (i == 2) { + clearInt(); + a.t = 0; + if (a.on == true) a.on = false; + saveAndReload(); + } + //edit message + if (i == 3) { + clearInt(); + var kbapp = require("Storage").read("textinput"); + if (kbapp != undefined) editMsg(idx, a); + else kbAlert(); + } + //delete + if (i == 4) { + clearInt(); + sw.splice(idx, 1); + saveAndReload(); + drawSw(); + } + } + }); +} + +function drawAlarms() { + layer = 2; + var alarms = require("sched").getAlarms().filter(a => !a.timer); + + var s = E.showScroller({ + h : 40, c : alarms.length+2, + back : function() {load();}, + draw : (idx, r) => { + + function drawMenuItem(a) { + g.setClipRect(R.x,R.y,R.x2,R.y2); + var on = ""; + var dow = ""; + if (idx > 0 && alarms[idx-1].on == true) on = " - on"; + else if (idx > 0 && alarms[idx-1].on == false) on = " - off"; + if (idx > 0 && idx < alarms.length+1) dow = "\n"+"SMTWTFS".split("").map((d,n)=>alarms[idx-1].dow&(1<",r.x+(r.w/2),r.y+(r.h/2)); + } + else if (idx > 0 && idx < alarms.length+1){ + var str = formatTime(alarms[idx-1].t); + drawMenuItem(str.slice(0, -3)); + } + }, + select : (idx) => { + clearInt(); + if (idx == 0) editAlarm(-1); + else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1); + } + }); +} + +function editDOW(dow, onchange) { + const menu = { + '': { 'title': 'Days of Week' }, + '< Back' : () => onchange(dow) + }; + for (var i = 0; i < 7; i++) (i => { + var dayOfWeek = require("locale").dow({ getDay: () => i }); + menu[dayOfWeek] = { + value: !!(dow&(1< v ? dow |= 1<= 0) a = alarms[alarmIdx[idx]]; + else a = require("sched").newDefaultAlarm(); + } + if (!a.data) { + a.data = {}; + a.data.hm = false; + } + var t = decodeTime(a.t); + + function editMsg(idx, a) { + g.clear(); + idx < 0 ? msg = "" : msg = a.msg; + require("textinput").input({text:msg}).then(result => { + if (result != "") { + a.msg = result; + } + else delete a.msg; + editAlarm(idx, a); + }); + } + + function kbAlert() { + E.showAlert("Must install keyboard app").then(function() { + editAlarm(idx, a); + }); + } + + var menu = { + "": { "title": "Alarm" }, + "< Back": () => { + if (a.data.hm == true) a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')"; + if (a.data.hm == false && a.js) delete a.js; + if (idx >= 0) alarms[alarmIdx[idx]] = a; + else alarms.push(a); + require("sched").setAlarms(alarms); + require("sched").reload(); + drawAlarms(); + }, + "Enabled": { + value: a.on, + onchange: v => a.on = v + }, + "Hours": { + value: t.hrs, min: 0, max: 23, wrap: true, + onchange: v => { + t.hrs = v; + a.t = encodeTime(t); + } + }, + "Minutes": { + value: t.mins, min: 0, max: 59, wrap: true, + onchange: v => { + t.mins = v; + a.t = encodeTime(t); + } + }, + "Repeat": { + value: a.rp, + onchange: v => a.rp = v + }, + "Days": { + value: "SMTWTFS".split("").map((d,n)=>a.dow&(1< editDOW(a.dow, d=>{a.dow=d;editAlarm(idx,a);}) + }, + "Hard Mode": { + value: a.data.hm, + onchange: v => a.data.hm = v + }, + "Vibrate": require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), + "Auto Snooze": { + value: a.as, + onchange: v => a.as = v + }, + "Msg": { + value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg, + //menu glitch? setTimeout required here + onchange: () => { + var kbapp = require("Storage").read("textinput"); + if (kbapp != undefined) setTimeout(editMsg, 0, idx, a); + else setTimeout(kbAlert, 0); + } + }, + "Delete": () => { + if (idx >= 0) { + alarms.splice(alarmIdx[idx], 1); + require("sched").setAlarms(alarms); + require("sched").reload(); + } + drawAlarms(); + }, + }; + + E.showMenu(menu); +} + +drawTimers(); + +Bangle.on("drag", e=>{ + if (layer < 0) return; + 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 (dx == 0) return; + //horizontal swipes + if (Math.abs(dx)>Math.abs(dy)+10) { + //swipe left + if (dx<0) layer == 2 ? layer = 0 : layer++; + //swipe right + if (dx>0) layer == 0 ? layer = 2 : layer--; + clearInt(); + if (layer == 0) drawTimers(); + else if (layer == 1) drawSw(); + else if (layer == 2) drawAlarms(); + } + } +}); diff --git a/apps/multitimer/app.png b/apps/multitimer/app.png new file mode 100644 index 000000000..3006b0a26 Binary files /dev/null and b/apps/multitimer/app.png differ diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json new file mode 100644 index 000000000..ee77d2ecb --- /dev/null +++ b/apps/multitimer/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "multitimer", + "name": "Multi Timer", + "version": "0.03", + "description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.", + "icon": "app.png", + "screenshots": [ + {"url":"screenshot1.png"}, + {"url":"screenshot2.png"}, + {"url":"screenshot3.png"} + ], + "tags": "tool,alarm", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"multitimer.app.js","url":"app.js"}, + {"name":"multitimer.alarm.js","url":"alarm.js"}, + {"name":"multitimer.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"multitimer.json"}], + "dependencies": {"scheduler":"type"} +} diff --git a/apps/multitimer/screenshot1.png b/apps/multitimer/screenshot1.png new file mode 100644 index 000000000..0226ce495 Binary files /dev/null and b/apps/multitimer/screenshot1.png differ diff --git a/apps/multitimer/screenshot2.png b/apps/multitimer/screenshot2.png new file mode 100644 index 000000000..23a0d4c22 Binary files /dev/null and b/apps/multitimer/screenshot2.png differ diff --git a/apps/multitimer/screenshot3.png b/apps/multitimer/screenshot3.png new file mode 100644 index 000000000..6dc2fdf15 Binary files /dev/null and b/apps/multitimer/screenshot3.png differ diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog index 1239554f0..c14e64ba9 100644 --- a/apps/mylocation/ChangeLog +++ b/apps/mylocation/ChangeLog @@ -4,3 +4,4 @@ 0.04: Fixed issue selecting Frankfurt not saved 0.05: Fixed issue with back option 0.06: renamed source files to match standard +0.07: Move mylocation app into 'Settings -> Apps' diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md index fd597397a..a6a16ce83 100644 --- a/apps/mylocation/README.md +++ b/apps/mylocation/README.md @@ -2,6 +2,8 @@ *Sets and stores GPS lat and lon of your preferred city* +To access, go to `Settings -> Apps -> My Location` + * Select one of the preset Cities or setup through the GPS * Other Apps can read this information to do calculations based on location * When the City shows ??? it means the location has been set through the GPS diff --git a/apps/mylocation/icon.js b/apps/mylocation/icon.js deleted file mode 100644 index b79f5875f..000000000 --- a/apps/mylocation/icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA==")) diff --git a/apps/mylocation/metadata.json b/apps/mylocation/metadata.json index 16549b2ba..4ab9aa37e 100644 --- a/apps/mylocation/metadata.json +++ b/apps/mylocation/metadata.json @@ -2,16 +2,15 @@ "name": "My Location", "shortName":"My Location", "icon": "app.png", - "type": "app", + "type": "settings", "screenshots": [{"url":"screenshot_1.png"}], - "version":"0.06", + "version":"0.07", "description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README", "readme": "README.md", "tags": "tool,utility", "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ - {"name":"mylocation.app.js","url":"app.js"}, - {"name":"mylocation.img","url":"icon.js","evaluate": true } + {"name":"mylocation.settings.js","url":"settings.js"} ], "data": [ {"name":"mylocation.json"} diff --git a/apps/mylocation/app.js b/apps/mylocation/settings.js similarity index 73% rename from apps/mylocation/app.js rename to apps/mylocation/settings.js index fd5c9cc6d..7033500fa 100644 --- a/apps/mylocation/app.js +++ b/apps/mylocation/settings.js @@ -1,5 +1,4 @@ -Bangle.loadWidgets(); -Bangle.drawWidgets(); +(function(back) { const SETTINGS_FILE = "mylocation.json"; let settings; @@ -18,7 +17,7 @@ function loadSettings() { } } -function save() { +function saveSettings() { settings = s; require('Storage').write(SETTINGS_FILE, settings); } @@ -34,29 +33,29 @@ function setFromGPS() { //console.log("fix from GPS"); s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' }; Bangle.buzz(1500); // buzz on first position - Bangle.setGPSPower(0); - save(); + Bangle.setGPSPower(0, "mylocation"); + saveSettings(); Bangle.setUI("updown", ()=>{ load(); }); - E.showPrompt("Location has been saved from the GPS fix",{ - title:"Location Saved", - buttons : {"OK":1} + E.showPrompt(/*LANG*/"Location has been saved from the GPS fix",{ + title:/*LANG*/"Location Saved", + buttons : {/*LANG*/"OK":1} }).then(function(v) { load(); // load default clock }); }); - Bangle.setGPSPower(1); - E.showMessage("Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running"); + Bangle.setGPSPower(1, "mylocation"); + E.showMessage(/*LANG*/"Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running"); Bangle.setUI("updown", undefined); } function showMainMenu() { //console.log("showMainMenu"); const mainmenu = { - '': { 'title': 'My Location' }, - '< Back': ()=>{ load(); }, - 'City': { + '': { 'title': /*LANG*/'My Location' }, + '< Back': ()=>{ back(); }, + /*LANG*/'City': { value: 0 | locations.indexOf(s.location), min: 0, max: locations.length - 1, format: v => locations[v], @@ -65,14 +64,15 @@ function showMainMenu() { s.location = locations[v]; s.lat = lats[v]; s.lon = lons[v]; - save(); + saveSettings(); } } }, - 'Set From GPS': ()=>{ setFromGPS(); } + /*LANG*/'Set From GPS': ()=>{ setFromGPS(); } }; return E.showMenu(mainmenu); } loadSettings(); showMainMenu(); +}) diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog index b486a29a1..cd91abe00 100644 --- a/apps/mysticclock/ChangeLog +++ b/apps/mysticclock/ChangeLog @@ -1,2 +1,3 @@ 1.00: First published version. 1.01: Use Bangle.setUI for Launcher/buttons +1.02: Tell clock widgets to hide. diff --git a/apps/mysticclock/metadata.json b/apps/mysticclock/metadata.json index 571a55ecd..bd2df2f8d 100644 --- a/apps/mysticclock/metadata.json +++ b/apps/mysticclock/metadata.json @@ -1,7 +1,7 @@ { "id": "mysticclock", "name": "Mystic Clock", - "version": "1.01", + "version": "1.02", "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", "icon": "mystic-clock.png", "type": "clock", diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js index 2d95633fe..d7f4ab1c3 100644 --- a/apps/mysticclock/mystic-clock-app.js +++ b/apps/mysticclock/mystic-clock-app.js @@ -189,6 +189,13 @@ Bangle.on('touch', (button) => { if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false); }); +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn<0) prevInfo(); + if (btn>0) nextInfo(); + drawAll(); +}); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -200,9 +207,3 @@ if (Bangle.isLCDOn()) { drawAll(); // draw immediately } -// Show launcher when button pressed -Bangle.setUI("clockupdown", btn=>{ - if (btn<0) prevInfo(); - if (btn>0) nextInfo(); - drawAll(); -}); diff --git a/apps/mysticdock/metadata.json b/apps/mysticdock/metadata.json index 54ebedd93..2775b0b72 100644 --- a/apps/mysticdock/metadata.json +++ b/apps/mysticdock/metadata.json @@ -4,7 +4,7 @@ "version": "0.01", "description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.", "icon": "mystic-dock.png", - "type": "dock", + "type": "app", "tags": "dock", "supports": ["BANGLEJS"], "readme": "README.md", diff --git a/apps/nato/ChangeLog b/apps/nato/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/nato/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/ncrclk/ChangeLog b/apps/ncrclk/ChangeLog index 31e5d42c8..0c326161a 100644 --- a/apps/ncrclk/ChangeLog +++ b/apps/ncrclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: A copy of the analogimgclk to work for NodeConf Remote 0.02: Use Bangle.setUI for button/launcher handling +0.03: Tell clock widgets to hide. diff --git a/apps/ncrclk/app.js b/apps/ncrclk/app.js index 16724fa5e..805ac1b95 100644 --- a/apps/ncrclk/app.js +++ b/apps/ncrclk/app.js @@ -120,10 +120,10 @@ Bangle.on('lcdPower', (on) => { } }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); drawHands(true); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/ncrclk/metadata.json b/apps/ncrclk/metadata.json index b50b554e1..fdab77450 100644 --- a/apps/ncrclk/metadata.json +++ b/apps/ncrclk/metadata.json @@ -2,7 +2,7 @@ "id": "ncrclk", "name": "NCR Clock", "shortName": "NCR Clock", - "version": "0.02", + "version": "0.03", "description": "NodeConf Remote clock", "icon": "app.png", "type": "clock", diff --git a/apps/neonx/ChangeLog b/apps/neonx/ChangeLog index 2e815a449..c1a50ecd7 100644 --- a/apps/neonx/ChangeLog +++ b/apps/neonx/ChangeLog @@ -1,4 +1,5 @@ 0.01: Initial release 0.02: Optional fullscreen mode 0.03: Optional show lock status via color -0.04: Ensure that widgets are always hidden in fullscreen mode \ No newline at end of file +0.04: Ensure that widgets are always hidden in fullscreen mode +0.05: Better lock/unlock animation \ No newline at end of file diff --git a/apps/neonx/README.md b/apps/neonx/README.md index ffb3c3f2c..4caa5e00f 100644 --- a/apps/neonx/README.md +++ b/apps/neonx/README.md @@ -24,4 +24,4 @@ Shows the watchface in fullscreen mode. Note: In fullscreen mode, widgets are hidden, but still loaded. ### Show lock status -If enabled, color changes when unlocked to detect the lock state easily. \ No newline at end of file +If enabled, the lock/unlock event is animated by changing the colors. \ No newline at end of file diff --git a/apps/neonx/metadata.json b/apps/neonx/metadata.json index 840e5b82e..ee99f98b8 100644 --- a/apps/neonx/metadata.json +++ b/apps/neonx/metadata.json @@ -2,7 +2,7 @@ "id": "neonx", "name": "Neon X & IO X Clock", "shortName": "Neon X Clock", - "version": "0.04", + "version": "0.05", "description": "Pebble Neon X & Neon IO X for Bangle.js", "icon": "neonx.png", "type": "clock", diff --git a/apps/neonx/neonx.app.js b/apps/neonx/neonx.app.js index 4b9231b0e..fd30fa30f 100644 --- a/apps/neonx/neonx.app.js +++ b/apps/neonx/neonx.app.js @@ -36,14 +36,8 @@ const digits = { const colors = { - x: [ - ["#FF00FF", "#00FFFF"], - ["#00FF00", "#FFFF00"] - ], - io: [ - ["#FF00FF", "#FFFF00"], - ["#00FF00", "#00FFFF"] - ] + x: ["#FF00FF", "#00FF00", "#00FFFF", "#FFFF00"], + io:["#FF00FF", "#00FF00", "#FFFF00", "#00FFFF"], }; const is12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; const screenWidth = g.getWidth(); @@ -71,7 +65,7 @@ function drawLine(poly, thickness){ } -function drawClock(num){ +function drawClock(num, xc){ let tx, ty; if(settings.fullscreen){ @@ -84,9 +78,8 @@ function drawClock(num){ for (let y = 0; y <= 1; y++) { const current = ((y + 1) * 2 + x - 1); let newScale = scale; - - let xc = settings.showLock && !Bangle.isLocked() ? Math.abs(x-1) : x; - let c = colors[settings.io ? 'io' : 'x'][y][xc]; + let colorArr = colors[settings.io ? 'io' : 'x']; + let c = colorArr[xc]; g.setColor(c); if (!settings.io) { @@ -104,6 +97,8 @@ function drawClock(num){ for (let i = 0; i < digits[num[y][x]].length; i++) { drawLine(g.transformVertices(digits[num[y][x]][i], { x: tx, y: ty, scale: newScale}), settings.thickness); } + + xc = (xc+1) % colorArr.length; } } } @@ -111,7 +106,31 @@ function drawClock(num){ function draw(date){ queueDraw(); + _draw(date, 0); +} + +function drawAnimated(){ + queueDraw(); + + // Animate draw through different colors + speed = 25; + setTimeout(function() { + _draw(false, 1); + setTimeout(function() { + _draw(false, 3); + setTimeout(function() { + _draw(false, 2); + setTimeout(function(){ + _draw(false, 0); + }, speed); + }, speed); + }, speed); + }, speed); +} + + +function _draw(date, xc){ // Depending on the settings, we clear all widgets or draw those. if(settings.fullscreen){ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} @@ -140,7 +159,7 @@ function draw(date){ l2 = ('0' + d.getMinutes()).substr(-2); } - drawClock([l1, l2]); + drawClock([l1, l2], xc); } @@ -173,8 +192,14 @@ Bangle.on('lcdPower', function(on){ } }); + Bangle.on('lock', function(isLocked) { - draw(); + if(!settings.showLock){ + return; + } + + // Animate in case the use selected this setting. + drawAnimated(); }); diff --git a/apps/neonx/neonx.settings.js b/apps/neonx/neonx.settings.js index e01ceb4d3..68e156dae 100644 --- a/apps/neonx/neonx.settings.js +++ b/apps/neonx/neonx.settings.js @@ -19,7 +19,7 @@ if (!neonXSettings) resetSettings(); - let thicknesses = [1, 2, 3, 4, 5, 6]; + let thicknesses = [1, 2, 3, 4, 5, 6, 7]; const menu = { "" : { "title":"Neon X & IO"}, diff --git a/apps/notanalog/ChangeLog b/apps/notanalog/ChangeLog index 6515f787c..07430406a 100644 --- a/apps/notanalog/ChangeLog +++ b/apps/notanalog/ChangeLog @@ -1,4 +1,5 @@ 0.01: Launch app. 0.02: 12k steps are 360 degrees - improves readability of steps. 0.03: Battery improvements through sleep (no minute updates) and partial updates of drawing. -0.04: Use alarm for timer instead of own alarm implementation. \ No newline at end of file +0.04: Use alarm for timer instead of own alarm implementation. +0.05: Use internal step counter if no widget is available. \ No newline at end of file diff --git a/apps/notanalog/metadata.json b/apps/notanalog/metadata.json index 0a291b180..81d79f4f2 100644 --- a/apps/notanalog/metadata.json +++ b/apps/notanalog/metadata.json @@ -3,7 +3,7 @@ "name": "Not Analog", "shortName":"Not Analog", "icon": "notanalog.png", - "version":"0.04", + "version":"0.05", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "An analog watch face for people that can not read analog watch faces.", diff --git a/apps/notanalog/notanalog.app.js b/apps/notanalog/notanalog.app.js index c3dc9308f..3c01a921e 100644 --- a/apps/notanalog/notanalog.app.js +++ b/apps/notanalog/notanalog.app.js @@ -88,20 +88,22 @@ Graphics.prototype.setNormalFont = function(scale) { }; - function getSteps() { + var steps = 0; try{ if (WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom.getSteps(); + steps = WIDGETS.wpedom.getSteps(); } else if (WIDGETS.activepedom !== undefined) { - return WIDGETS.activepedom.getSteps(); + steps = WIDGETS.activepedom.getSteps(); + } else { + steps = Bangle.getHealthStatus("day").steps; } } catch(ex) { // In case we failed, we can only show 0 steps. } - return 0; - } + return steps; +} function drawBackground() { @@ -289,6 +291,9 @@ function drawSleep(){ function draw(fastUpdate){ + // Queue draw in one minute + queueDraw(); + // Execute handlers handleState(fastUpdate); @@ -320,9 +325,6 @@ function draw(fastUpdate){ drawState(); drawTime(); drawData(); - - // Queue draw in one minute - queueDraw(); } diff --git a/apps/noteify/ChangeLog b/apps/noteify/ChangeLog index ec66c5568..d7bc46dcd 100644 --- a/apps/noteify/ChangeLog +++ b/apps/noteify/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Use default Bangle formatter for booleans diff --git a/apps/noteify/README.md b/apps/noteify/README.md index d3868efcf..dbdceb399 100644 --- a/apps/noteify/README.md +++ b/apps/noteify/README.md @@ -1,6 +1,6 @@ # WARNING -This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a keyboard such as [Swipe keyboard](https://banglejs.com/apps/?id=kbswipe). +This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and requires a [keyboard library](https://banglejs.com/apps/?c=textinput#). ## Usage @@ -18,3 +18,6 @@ This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched) and r ![](note.png) ![](timer-alert.png) + +## Web interface +You can also add, edit or delete notes in the web interface, accessible with the download button. diff --git a/apps/noteify/app.js b/apps/noteify/app.js index c19694ea4..02d43c065 100644 --- a/apps/noteify/app.js +++ b/apps/noteify/app.js @@ -5,7 +5,7 @@ var notes = require("Storage").readJSON("noteify.json", true) || []; var alarms = require("sched").getAlarms(); msg = ""; -function startNote(idx) { +function startNote(idx) { idx == undefined ? note = "" : note = notes[idx].note; require("textinput").input({text:note}).then(result => { if (result != "") { @@ -23,20 +23,20 @@ function viewNote(idx) { textY += e.dy; g.setClipRect(0, 30, g.getWidth(), g.getHeight()); if (textY > 30) textY = 30; - if (textY < textBound) textY = textBound; + if (textY < textBound) textY = textBound; g.clearRect(0, 30, g.getWidth(), g.getHeight()).setColor(g.theme.fg).setFont("6x8:2").setFontAlign(-1, -1).drawString(g.wrapString(notes[idx].note, g.getWidth()).join("\n"), 0, textY); },back:()=>{ Bangle.setUI(); showEditMenu(idx); }}); - + } function showMainMenu() { var mainMenu = { "" : { "title" : "Noteify" }, "< Back" : function() { load(); }, - "New note" : function() { + "New note" : function() { E.showMenu(); startNote(); }, @@ -171,7 +171,6 @@ function editDOW(dow, onchange) { var dayOfWeek = require("locale").dow({ getDay: () => i }); menu[dayOfWeek] = { value: !!(dow&(1< v ? "Yes" : "No", onchange: v => v ? dow |= 1< 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"..."; - + const menu = { '': { 'title': alarmTitle }, '< Back' : () => showAlarmMenu(), @@ -213,18 +212,15 @@ function editAlarm(alarmIndex, alarm) { }, 'Enabled': { value: a.on, - format: v=>v?"On":"Off", onchange: v=>a.on=v }, 'Repeat': { value: a.rp, - format: v=>v?"Yes":"No", onchange: v=>a.rp=v }, 'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), 'Auto snooze': { value: a.as, - format: v=>v?"Yes":"No", onchange: v=>a.as=v } }; @@ -264,7 +260,7 @@ function editTimer(alarmIndex, alarm) { var t = decodeTime(a.timer); var timerTitle = (a.msg == undefined) ? 'Timer' : (a.msg.length > 12) ? a.msg.replace(/\n/g, " ").substring(0, 12)+"..." : msg.replace(/\n/g, " ").substring(0, 12)+"..."; - + const menu = { '': { 'title': timerTitle }, '< Back' : () => showMainMenu(), @@ -278,7 +274,6 @@ function editTimer(alarmIndex, alarm) { }, 'Enabled': { value: a.on, - format: v=>v?"On":"Off", onchange: v=>a.on=v }, 'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), diff --git a/apps/noteify/interface.html b/apps/noteify/interface.html new file mode 100644 index 000000000..027c98860 --- /dev/null +++ b/apps/noteify/interface.html @@ -0,0 +1,93 @@ + + + + + +
+
+
+ +
+
+ +
+
+
+ + + + diff --git a/apps/noteify/metadata.json b/apps/noteify/metadata.json index bedff0e5b..fbd5a88f1 100644 --- a/apps/noteify/metadata.json +++ b/apps/noteify/metadata.json @@ -1,11 +1,11 @@ { "id": "noteify", "name": "Noteify", - "version": "0.01", + "version": "0.02", "description": "Write notes using an onscreen keyboard and use them as custom messages for alarms or timers.", "icon": "app.png", "tags": "tool,alarm", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"noteify.app.js","url":"app.js"}, @@ -14,6 +14,7 @@ ], "data": [{"name":"noteify.json"}], "dependencies": {"scheduler":"type","textinput":"type"}, + "interface": "interface.html", "screenshots": [ {"url": "menu.png"}, {"url": "note.png"}, diff --git a/apps/novaclock/ChangeLog b/apps/novaclock/ChangeLog new file mode 100644 index 000000000..8b05ff9ec --- /dev/null +++ b/apps/novaclock/ChangeLog @@ -0,0 +1,3 @@ +... +0.10: First update with ChangeLog Added +0.11: Tell clock widgets to hide. diff --git a/apps/novaclock/README.md b/apps/novaclock/README.md new file mode 100644 index 000000000..b54f44241 --- /dev/null +++ b/apps/novaclock/README.md @@ -0,0 +1,9 @@ +# Nova Clock +A simple clock app that uses a clockwork star, from Kirby. + +*Note: This clock draws slightly into the widget area, but since it's in the middle, it shouln't matter that much (nobody has that many widgets... right?)* + +## Credits +Pixel art by me, [dronesflier](https://github.com/dronesflier) + +The Kirby series belongs to Nintendo/HAL Labs diff --git a/apps/novaclock/app-icon.js b/apps/novaclock/app-icon.js new file mode 100644 index 000000000..cf159ba1f --- /dev/null +++ b/apps/novaclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AE3N5ouuGFovuFwYwrF9wuFGFIqC5nMF9guBF9ReD43GGFJeDF9ReFGFImCFwYvBGAReqMEAdCAAwvKAA4pWFxYwNGhQoJAAouHYQYAEGBwsIFBIANGRBgLFzIwDYhoweFx4xGGC4uSGDYuFFpqTIF1BhXFzBhVXSZhNF6QuWGAgvSFzAvS4wved6KOsSDovJ5gACF9IsCBIQxFF8ItEAAYxEF7qHDFowxGBwZeaFxgPEAwYvYABAONF74PVF64RbF6IThDZYVnDIoXtAH4A/AH4AkA==")) diff --git a/apps/novaclock/app.js b/apps/novaclock/app.js new file mode 100644 index 000000000..52bee0dbd --- /dev/null +++ b/apps/novaclock/app.js @@ -0,0 +1,274 @@ +function nova() { + var nova = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHsAABZM/K5fM5kAv2igGi1EAvFUK/6v/K8ipC0SvCAQhM/K5l+AQ5XG5nM1BrBBgJX/V4WoWQRXHKoIYGBBBX2vxOBKIQFCLoRMD1BXHLPpOBK4gFCK4RIDK5IDFK+4AKIwpYFBYuiKfLtBABAPEK5i9IKtg8BAAUPABHMlYADgBXMAwyqrKQOiKA0OAIRWDqwACLAOoWgZQILFg4B0SoHKIYADiEr5moKwgABW4R0BKowUBK1f+VIkOU4QCBToQACJoRTBIgJWDK4YDBWQRVCWFZEBVZBQEAAxNDlbGBBQgABAoguEWE6IBKoxUCIAr8EUIQIDCQUALgOiAAJnCWQavoVgb8BKoeoKoRBBfQZYEBAa9E5mo1FUK4IOEV9JWBVYyUBSoZCBTAJXCLAfMAohgD0V+WITIDLAivkKw6qFTIV+AQKZCKIagDAAl+qgUBV4pYEK8ZWFVgJQBVISvDLIQCCIQigELwQHB0V4vCuCC4IPEK8ZWGKoSvDAAhdC5pYEq3MWgQOCOgJbD1AGBPoZrCgGoK075CHoQAELQi6C0RXCLAKhEDIwADYgYqCK0xGBKoStGBAKaCLAbxDTgRVC5nNCIN+DIQRDCogSBK8ZWBKopPDAIJZDBgZYFUIS8EvwPCAoKvENgZXeKwpJDABxlFK4ZMBK4ZmCAQXNWoJYDYghWk64AEh0Oh8OhAKFLAmiHwVO0RVBvFUBoQAEBgKyBKwVWLDxXGKwpSBKoQOBLIxYE5oFD0V+VQKyCAIIFBV4IABLIZXCqxXaKwcOVw5VF1gIBA4JXGIoUAACitCWDhXCEgJWGJoIABGYYKELApXBDIpuCh4GB1gACBYIICDIJXFlaubLAJXFHgJWFLASyDWAwZFhCmGaghXKWC5XDVw8OHwI9HK4xYCDIoYHDIZ8BDIhXjdgi4DAAxWDdwrIFK5KwOAoJWYgCvHJAMOHpJZCSwZXGJIIZIB4MHa4JXELIxLJVyBXFFwKVJLIavKhBxJDASvFKwpXJgGoK6GiK4pYBK5QMBK5UHDJgNBK4tWK5cA0SvXdoQuDK8ANCDIZSBK4YOBJI9+K5RWHHoiwBF4OsKxo9B1BXEBoRWJP4hXDAApOGV5gMBK4eiK4I+EK4RYHBQMOhyuE1ByBAA6cDAQQNJK5pCBV547BWBBZBSYsIMYZXDDIQfB1B5BAYIlDAAXN5wECNoSuHK45YCK56vDLApOBAIYCBdIJWILAYABEQRTD5uoqmi5otCTYJWIYJKvUK4vXBwIACKoRWEK5RZHWoRXBAgRXHqxXCZwQADAwKvRFAKwGLIwKFKAZzDWAt+BYJbC5oDBWoIRBK5eivF+AAl4K540BQIQ+CAB5VCIQL7CeoKrFAQS1EB4YeCLQwvJK5kOAYItBbomiJIRgEforfBN4IYBUoIEC5pjDL4K9FBgQsBb4ZYGJw4vBK54uDFQRMFLwIADVgj+DUAV4IoqxEBAmiQgRgFLQZXGgF+V5sPUghKCeALiEAAr3GKoQCBVAKxBBYQZKFYLCBQ4xXJRYJXPIII4BLgRYEABCRDDIZEBUoYECDZd45qGFgA9BAAJLH1BXOS4gBBdwTZBHwwGEHAKuCAoQMELAZgDJQLeDCgKEFK4MPAYJMIK5JYDh0AdAZCCFILlE0QABGgI1CCQiXBJYYRBVw5YDEgYsCKx4AMK4UIUgeoE4StBJYRBDRYiSCKIacCKY5MFCoUAbQKuSK56vBRIpWCKQYADBAZRCKYRKDJILPCOIQABvwDCBYSxBZQRtDK7cQDYKwCFAOiFoL5DAQQAIIgIDEKIZoDAAJxDAYJeBgAHCCQRWCK66wDK4KVDK4JSDGoKYCSwShGfggcBBYahCMAYdCvALBGIOiAoZWZK4qvBJwIhBTIRfCTwb4EdAQOCYoLAKa4QkBvx2BvCDEK8CwCIQQEBGAKbBAARBEewRaCL4JXDXQmoUALGDPAgrCKwpXZLAcIEwZYDKopZEWYgBBTANUgF4vwYEDYqzCNgR7EgEOKzSwELAjyDRIIxBHoiwCWoZWCegRpFCwgUDFIQoCKwZXbLAYhBLAeip0Ac4Y3CK4YGB0V+NIKqB5gCBLoJQFVgQACVoIVB0QRBgBVBKziwEK4ifDWYXNfoKYDUwIACqheDlUrCwZ4DOYQKC1EqvxYBFQKueLBd+qkqqhODAAl4p1OSwXNLAVUAIMqCo4SBp1UB4N4WgKufLBg1BAAIzBAolO1F4TwSkDLAb9BJoN+DIJSCB4ICCVoJWiK4pYF5oyBKgQDDJ4RTCAoRcECQV+BId+CAQjBKIMPhxXiLBWoAASrBAAIEBMgZIBLohICJ4JWBAgN4VwRwDgBVCh+iK0CxMKIQCCVIhbEAAYFCUoQYCCYmoVsxYIh0ALIg+DWoKhBUwRHC0QKBW4IKEAAqmBKwMPiBWmWI6yFAAq1BV4YKGNYYAFKoQACK05YJLJCnBJZBVKKwMOK1hYDLJ2iUwJPH5pVIAAJXBBARWpLAX+LApZCLQqxCLIvNK4YVCDQMIVoXMFIQAsJwZZFLQZbDLAKqFBwYVChEIKoQMCK1qyLLY4AGfwZUBVYSsxWRJaKABy5EK2ZZELRKiCUYZUKKu5aG/y0OKYXMCwYA/WgoAE0QEDB4RV/LRQAJJn4A/AH4A/AFo")); + return nova; +} + +function novaEyesStage1() { + var novaEyesStage1 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5gAFH58rDIXN5movwZRKs+oAIg/OgEr5mi1Go0V+AYIFCLN7oGABIZiK0fXAAkOh0Ph0IBQo+HDI0PhEHhEPDJpWoKQJVCh5ZHHwoZFCgcPg50BLIpYpHohVF1hFChxXODAQACL4hXsHgwABgAACBQg+HDIhuChAZEOYJYtJYkIKwo+EBoJXKJYIABDIyxGK8yUFSYpXKHwTIFKw4NFWFKUGHg0AKwaWGDIrHGWGA9QLIUIK5LJBDJAaDK9o8BhA9JHwSvLK5QQBhEOK9qVLgAMBK5bJJAAMHK9yuBJIRXWh78BDJBWBDIpXnWAIvB1hWNK4xYCOJK7BDIxYoGAZYGBQMOdg6wFBgZWFg7IGK9JYCGQI8FdYpXILAMIDI7IILFZOBAIYCBIoI8KOQ4VDYwRWtHorwCAAQ/DBohXKLAZZCLYQZKLFRZFBQo8HDJLLBDJpYlAB4ZiLE3M5moAQIAFHhgZD5oaCDofNK14/E1Go0QADHaBZDKwQBBKuTyKDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A==")); + return novaEyesStage1; +} + +function novaEyesStage0() { + var novaEyesStage0 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj64AFH58rDLBVrH6EAlYZXK0Y7KH4gZiK1MOh0Ph0IHxoZGh8Ig8Ih5YwHgpSBKoUPLI4+FDIoUDh8HOgJZFLFI9EKousIoUOK5wYCAARfEK9g8GAAMAAAQKEHw4ZENwUIDIhzBLFpLEhBWFHwgNBK5RLBAAIZGWIxXmSgqTFK5Q+CZApWHBoqwpSgw8GgBWDSwwZFY4ywwHqBZChBXJZIIZIDQZXtHgMIHpI+CV5ZXKCAMIhxXtSpcABgJXLZJIABg5XuVwJJCK60PfgIZIKwIZFK86wBF4OsKxpXGLARxJXYIZGLFAwDLAwKBhzsHWAoMDKwsHZAxXpLAQyBHgrrFK5BYBhAZHZBBYrJwIBDAQJFBHhRyHCobGCK1o9FeAQACH4YNEK5RYDLIRbCDJRYqLIoKFHg4ZJZYIZNLFYAIHhIZZLEo/LBgIZkLNw7QDLJZnAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA")); + return novaEyesStage0; +} + +function novaEyesStage2() { + var novaEyesStage2 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCDwZYqKwKoGLowFDHwpWCDIRRBAQPMAwd+vANBWNRXBJASoDeRKWGDIoEBOoQDDDAQeBK9CUBJQiTCAAq2CB4Q+DDITGFCAZ2DOohYngAAVDLhWj64ACh0PhA0G6+sBoMPCQY+BgAKCBYMOhwZHBoYZFK88PAAKMHKwYABK4oZEKw5YEDIxXpHpRZChBXJZIIZIDQZXtHgMIHpI+CV5ZXKCAMIhxXtSpcABgJXLZJIABg5XuVwJJCK60PfgIZIKwIZFK86wBF4OsgErKxZXGLARxJXYIZGLEoAVDLhXk1AAC5mo0QGDABJXEBIgaBDYfMAA4ZELEouEHgIALHgsABAN+5hUB0R1CDJywlRIvNAQYECMgZXGY4ZXBCAIDCKQTIILFAAISYo8IOQYUDAoQDCBQRWrHwQABcoxdFHhJyCC4i3DDoWiK1hYDRgSQCAAgMBDJ3NDQJWE5oZMLM6ZGHaBZDOgQBBKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA")); + return novaEyesStage2; +} + +function novaEyesStage3() { + var novaEyesStage3 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9A8BGIY2EABQ+DZARRBUoJVCDYzTELE49CVgo7KHosABoocFZYgBDK8yuGABJfGHwIZQLYQcDLEsAAC4ZaK8nXAAMPh8OGhHX1gPBhATCK4QZDh0PJ5IaCh4ZEK848BhCOKHwI9FOIpXKCAMIhxXpdrAZZK8mo1Gi0V+AIYABBAOiBoIAEBAJXCAwQYDDIwAFDYRXoJAozCAYRDCAAN4I4RXCJAoFELBIdBK8o+DGoyzDKQKcDHgpyBYwquECoYMCAQJWmWAgvCUYadHdgzKF5jMCAITTD5gIDK84+B5gABG4SZDd5A8FZQZLDLQIWGDJCwlLAQAC5oCDAgQABVwoZEKIQeCAYTGFDI5YmcogEEAAg8IOQYUDAoQDCBQRWrHwQABJoV+SQ48KOQRpEW4YdC0RWsLAaMCAIQAEBgIZO5oaCKwfNDJhZnTIw7QLIZ0EKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA==")); + return novaEyesStage3; +} + +function novaEyesStage4() { + var novaEyesStage4 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9EAJQg2EBAoFEHwauBUIN4p1UKoQbGaYhYnK4KvCKoQFBHYi6EHosrBQQaCYwRbBDQd4XYSwoKwIuB5oACIAJBBAYpMDAAI+BKwIKDDAJKBvC2BEgIUDEwawmdgIvDGIhVDLYYAEHoIZBVIQPBKwJeBZoLJEXgYHBK8wzBHISMF5xdFK45GCMwQDBNYonBBAN+XAV+K8wAPlYHGDKIAHV8ztCqiHBWo6gEdgOoK4SpEAgQODDwIjBAAIjBCIRXm5g1CHQQCDHgJWCAgIAD0RXDDIPM5oRC5oECDYR+CK9aVES4ZQFHQShEK4yvEAAbJGAARXlHwI9BJYIAEAwLsEql4vwBBHgZYCCQRrEEYgMDAQJWmWAgvCJQJaDUQQ8DVwYZFVAS9CAITED5gIDK84+BcIQ3CTIa5HKwjKFJYZaBCwwZIWEpYCAAXNAQYECAAJkBK4ysCYYR2DAQIICAAZXpd4zqFAAg8IOQYUDAoQDCBQRWrHwQABJoV+SQ48KOQRpEW4YdC0RWsLAaMCAIQAEBgIZO5oaCKwfNDJhZnTIw7QLIZ0EKuQ/FAAgZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA==")); + return novaEyesStage4; + +} + +function novaEyesWhiteStage0() { + var novaEyesWhiteStage0 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj64AFH58rDLBVrH6EAlYZXK0Y7KH4gZiK2Q+JDLJW0Hw4ZZK/5X6HiY+FDLJX/K/Q8VHwYZZK/5X/K/5X/K/5X/K/5X/K+Y+WHgYZZK/5X7Hyg8FDLJX/K/Y+SHg4ZZLGg8JDLJYlH5YMBDMhZuHaAZZLM4AEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A=")); + return novaEyesWhiteStage0; +} + +function novaEyesTransStage1() { + var novaEyesTransStage1 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5gAFH58rDIXN5movwZRKs+oAIg/OgEr5mi1Go0V+AYIFCLN7oGABIZiK0YRYDLJW0CY4ZZK/5X6FCoVDDLJX/K/QmXC4IZZK/5X/K/5X/K/5X/K/5X/K+YmWCoYZZK/5X7FCgTFDLJX/K/YqSCI4ZZLEoAPDMRYm5nM1ACBAAo8MDIfNDQQdD5pWvH4mo1GiAAY7QLIZWCAIJVyeRQZsAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AfA")); + return novaEyesTransStage1; +} + +function novaEyesTransStage2() { + var novaEyesTransStage2 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCDwZYqKwKoGLowFDHwpWCDIRRBAQPMAwd+vANBWNRXBJASoDeRKWGDIoEBOoQDDDAQeBK9CUBJQiTCAAq2CB4Q+DDITGFCAZ2DOohYngAAVDLhWjC7AZZK/5X/K/5X/K/5X/K/5X/K+YmBACoZcK8moAAXM1GiAwYAJK4gJEDQIbD5gAHDIhYlFwg8BABY8FgAIBv3MKgOiOoQZOWEqJF5oCDAgRkDK4zHDK4IQBAYRSCZBBYoABCTFHhByDCgYFCAYQKCK1Y+CAALlGLoo8JOQQXEW4YdC0RWsLAaMCSAQAEBgIZO5oaBKwnNDJhZnTIw7QLIZ0CAIJVyH4oAEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4A==")); + return novaEyesTransStage2; +} + +function novaEyesTransStage3() { + var novaEyesTransStage3 = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ArgAAFDNhVj5nM1ABC5g/PlYQB1AYCvwZRKsxWE0RZPBgOiv2iAQNUAYIACLN6RBHYQAETQKcDHxMAvwACLIYaEOYRWtGQYAD1BAELBKsBBgRRCWIQBCBQV+LFRWBVAxdGAoY+FKwQZCJgICB5hZCvF+qgFBWNRXBJASoDeROiK4ytDCIOoC4R5DAYQIBK9A8BGIY2EABQ+DZARRBUoJVCDYzTELE49CVgo7KHosABoocFZYgBDK8yuGABJfGHwIZQLYQcDLEsAAC4ZaK8gXYDLJX/K/jtYDLJXk1Go0WivwBDAAIIB0QNBAAgIBK4QGCDAYZGAAobCK9BIFGYQDCIYQABvBHCK4RIFAohYJDoJXlHwY1GWYZSBTgY8FOQLGFVwgVDBgQCBK0ywEF4SjDTo7sGZQvMZgQBCaYfMBAZXnHwPMAAI3CTIbvIHgrKDJYZaBCwwZIWEpYCAAXNAQYECAAKuFDIhRCDwQDCYwoZHLEzlEAggAEHhByDCgYFCAYQKCK1Y+CAAJNCvySHHhRyCNIi3DDoWiK1hYDRgQBCAAgMBDJ3NDQRWD5oZMLM6ZGHaBZDOghVyH4oAEDNgA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AD4=")); + return novaEyesTransStage3; +} + +function novaTopRedraw() { + var novaTopRedraw = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHsAABZM/K5fM5kAv2igGi1EAvFUK/6v/K8isDAQV+BAK4BJn5XL1BOB1GoLod4LQIRE0XM5vM1HN5pX/5hXBIwJdGKwgYGA45X3TgJRDLQZXFAH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AB8AIH4A/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/AH4AP")); + return novaTopRedraw; +} + +function star() { + var backgroundstar = require("heatshrink").decompress(atob("rFYxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A9qxA/K/5W/AH5XGLH6w/K34A/AA2BwJB/K62sIP5XWIH4AW1iv/K/5X/AHFWqwCCq2BJ4ICB1mBAAJZBAQIQBlYUCAYMrAgJZ8KQICBU4QCBLIJVBK4QOCNAJSCOQQA/XpQA/K6q3CAH5XUwJB/ACqu/V7BA/AC2BwJB/LC5A/V/4AtqxX/AC2slZB/ACuBqxB/LC5A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ao")); + return backgroundstar; +} + +function novaOpenEyes(speed, white, animation) { + if (!white) { + g.drawImage(novaEyesStage4(), -10, -10, { + scale: 2.2 + }); + setTimeout(function() { + g.drawImage(novaEyesStage3(), -10, -10, { + scale: 2.2 + }); + }, speed * 2); + setTimeout(function() { + g.drawImage(novaEyesStage2(), -10, -10, { + scale: 2.2 + }); + }, speed * 3); + setTimeout(function() { + g.drawImage(novaEyesStage1(), -10, -10, { + scale: 2.2 + }); + }, speed * 4); + if (animation) { + setTimeout(function() { + g.drawImage(novaEyesStage0(), -10, -10, { + scale: 2.2 + }); + }, speed * 5); + } else {} + } else { + + g.drawImage(novaEyesStage4(), -10, -10, { + scale: 2.2 + }); + setTimeout(function() { + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + timedraw(true); + g.drawImage(novaEyesTransStage3(), -10, -10, { + scale: 2.2 + }); + }, speed * 2); + setTimeout(function() { + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + timedraw(true); + g.drawImage(novaEyesTransStage2(), -10, -10, { + scale: 2.2 + }); + }, speed * 3); + setTimeout(function() { + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + timedraw(true); + g.drawImage(novaEyesTransStage1(), -10, -10, { + scale: 2.2 + }); + open = true; + }, speed * 4); + if (animation) { + setTimeout(function() { + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + open = true; + }, speed * 5); + } else {} + } +} + +function novaCloseEyes(speed, white, animation) { + if (!white) { // for other + if (animation) { + g.drawImage(novaEyesStage0(), -10, -10, { + scale: 2.2 + }); + } else {} + setTimeout(function() { + g.drawImage(novaEyesStage1(), -10, -10, { + scale: 2.2 + }); + }, speed * 2); + setTimeout(function() { + g.drawImage(novaEyesStage2(), -10, -10, { + scale: 2.2 + }); + }, speed * 3); + setTimeout(function() { + g.drawImage(novaEyesStage3(), -10, -10, { + scale: 2.2 + }); + }, speed * 4); + setTimeout(function() { + g.drawImage(novaEyesStage4(), -10, -10, { + scale: 2.2 + }); + }, speed * 5); + } else { // for time + + if (animation) { + timedraw(true); + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + } else {} + setTimeout(function() { + timedraw(true); + g.drawImage(novaEyesTransStage1(), -10, -10, { + scale: 2.2 + }); + }, speed * 2); + setTimeout(function() { + timedraw(true); + g.drawImage(novaEyesTransStage2(), -10, -10, { + scale: 2.2 + }); + }, speed * 3); + setTimeout(function() { + timedraw(true); + g.drawImage(novaEyesTransStage3(), -10, -10, { + scale: 2.2 + }); + }, speed * 4); + setTimeout(function() { + g.drawImage(novaEyesStage4(), -10, -10, { + scale: 2.2 + }); + }, speed * 5); + open = false; + } +} + +function timedraw(animation) { + if (open && timemode || animation) { + g.setFont("6x8", 4); + g.setColor("#00F"); + var d = new Date(); + var h = d.getHours(), + m = d.getMinutes(); + g.drawImage(novaEyesWhiteStage0(), -10, -10, { + scale: 2.2 + }); + g.drawImage(novaEyesTransStage1(), -10, -10, { + scale: 2.2 + }); + // Check if single digit + var hourDigits = h.toString(); + if (hourDigits.length === 1) { // if hour digits only one, render in middle + g.drawString(h, 50, 66); + } else { + g.drawString(h, 38, 66); + } + var minutes = m.toString(); + if (minutes.length === 1) { // same for mins + g.drawString(m, 107, 66); + } else { + g.drawString(m, 94, 66); + } + } +} + +function main() { + Bangle.on("lock", function(lock) { + g.drawImage(novaTopRedraw(), -10, novaYPos, { + scale: 2.2 + }); + if (lock) { + novaCloseEyes(200, true, false); + setTimeout(function() { + novaOpenEyes(100, false, false); + timemode = false; + }, 1200); + }else{ + novaCloseEyes(100, false, false); + setTimeout(function() { + timemode = true; + novaOpenEyes(200, true, false); + }, 600); + } + }); +} + + +g.setFont("6x8", 4); +g.setColor("#FFF"); +var open = false; +var timemode = true; +var clockmode; +var novaYPos = -7; +Bangle.setUI("clock"); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +g.drawImage(nova(), -10, -10, { + scale: 2.2 +}); + +g.drawImage(star(), 5, -5, {scale:0.8}); +g.drawImage(star(), -10, 120, {scale:0.8}); +g.drawImage(star(), 120, -5, {scale:0.8}); + + + +var secondInterval = setInterval(function() { + timedraw(); + g.drawImage(novaTopRedraw(), -10, novaYPos, { + scale: 2.2 + }); +}, 1000); + +novaOpenEyes(300, true, false); +main(); diff --git a/apps/novaclock/app.png b/apps/novaclock/app.png new file mode 100644 index 000000000..6270be5d3 Binary files /dev/null and b/apps/novaclock/app.png differ diff --git a/apps/novaclock/metadata.json b/apps/novaclock/metadata.json new file mode 100644 index 000000000..69b7627f8 --- /dev/null +++ b/apps/novaclock/metadata.json @@ -0,0 +1,15 @@ +{ "id": "novaclock", + "name": "Nova Clock", + "shortName":"Nova Clock", + "icon": "app.png", + "type": "clock", + "version":"0.11", + "description": "A clock inspired by the Kirby series", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme":"README.md", + "storage": [ + {"name":"novaclock.app.js","url":"app.js"}, + {"name":"novaclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 6cb9d061e..2a6c15a09 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -10,3 +10,5 @@ 0.10: Improve scale factor calculation to fix scaling issues (#984) 0.11: Add slight offset to OSM data to align it properly (fix #984) Fix alignment of satellite info text +0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow) +0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device) diff --git a/apps/openstmap/custom.html b/apps/openstmap/custom.html index 6e79a6e9a..6f611dd86 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/custom.html @@ -64,9 +64,10 @@ TODO: var OSMTILECOUNT = 3; // how many tiles do we download in each direction (Math.floor(MAPSIZE / OSMTILESIZE)+1) /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ - var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast - var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + //var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW //var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white + var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; // Create map and try and set the location to where the browser thinks we are var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); @@ -128,7 +129,7 @@ TODO: var min = Math.min(rgba[i+0],rgba[i+1],rgba[i+2]); var max = Math.max(rgba[i+0],rgba[i+1],rgba[i+2]); var d = max-min; - if (max<120) { // black + if (max<120 || (d<8 && max<215)) { // black, or a darker grey rgba[i+0]=0; rgba[i+1]=0; rgba[i+2]=0; @@ -147,7 +148,7 @@ TODO: console.log("Compression options", options); var w = Math.round(width / TILESIZE); var h = Math.round(height / TILESIZE); - var tiles = []; + var tiledImage; for (var y=0;y { document.getElementById("uploadbuttons").style.display=""; mapFiles = tilesLoaded(ctx, canvas.width, canvas.height); - mapFiles.unshift({name:"openstmap.json",content:JSON.stringify({ + mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({ imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, scale : scale, // how much of Bangle.project(latlon) does one pixel equate to? lat : centerlatlon.lat, - lon : centerlatlon.lng + lon : centerlatlon.lng, + w : Math.round(canvas.width / TILESIZE), // width in tiles + h : Math.round(canvas.height / TILESIZE), // height in tiles + fn : "openstmap.0.img" })}); console.log(mapFiles); }); diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 2dc9bd427..32093f70e 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.11", + "version": "0.13", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "icon": "app.png", "tags": "outdoors,gps,osm", diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index d995aca25..2bd7d2e2e 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -22,7 +22,7 @@ function center() { */ -var map = require("Storage").readJSON("openstmap.json"); +var map = require("Storage").readJSON("openstmap.0.json"); map.center = Bangle.project({lat:map.lat,lon:map.lon}); exports.map = map; exports.lat = map.lat; // actual position of middle of screen @@ -30,7 +30,7 @@ exports.lon = map.lon; // actual position of middle of screen var m = exports; exports.draw = function() { - var s = require("Storage"); + var img = require("Storage").read(map.fn); var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); @@ -41,13 +41,11 @@ exports.draw = function() { var ty = 0|(iy/map.tilesize); var ox = (tx*map.tilesize)-ix; var oy = (ty*map.tilesize)-iy; - for (var x=ox,ttx=tx;x=0 && ttx=0 && tty=-1; y-=0.1) { function wind_updated(ev) { if (ev.target.uuid == "0xcc91") { - awa = settings.mount_angle-ev.target.value.getInt16(1, true)*0.1; + awa = settings.mount_angle+ev.target.value.getInt16(1, true)*0.1; + if (awa<0) awa += 360; aws = ev.target.value.getInt16(3, true)*0.01; -// console.log(awa, aws); + //console.log(awa, aws); if (gps_course.spd > 0) { - wv = { // wind vector (in fixed reference frame) - lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws, - lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws + wv = { // wind vector (in "earth" reference frame) + vlon: Math.sin(Math.PI*(gps_course.course+(awa+180))/180)*aws, + vlat: Math.cos(Math.PI*(gps_course.course+(awa+180))/180)*aws }; - twv = { lon: wv.lon+gps_course.lon, lat: wv.lat+gps_course.lat }; - tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2)); - twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course; + twv = { vlon: wv.vlon+gps_course.vlon, vlat: wv.vlat+gps_course.vlat }; + tws = Math.sqrt(Math.pow(twv.vlon,2)+Math.pow(twv.vlat, 2)); + twa = 180+Math.atan2(twv.vlon, twv.vlat)*180/Math.PI-gps_course.course; if (twa<0) twa += 360; if (twa>360) twa -=360; } - else { + else { tws = -1; twa = 0; } @@ -57,34 +62,37 @@ function draw_compass(awa, aws, twa, tws) { a = i*Math.PI/2+Math.PI/4; g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99); } - g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy+Math.cos(Math.PI*awa/180)*w*0.9, w*0.1); + g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy-Math.cos(Math.PI*awa/180)*w*0.9, w*0.1); if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1); g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06); g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w); - if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w); - if (settings.truewind && typeof gps_course.spd!=='undefined') { - spd = gps_course.spd/1.852; - g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1); + if (!pause_gps) { + if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w); + if (settings.truewind && gps_course.spd!=-1) { + spd = gps_course.spd/1.852; + g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1); + } } + if (pause_gps) g.setColor("#f00").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),g.getWidth()-15, g.getHeight()-15); } function parseDevice(d) { device = d; console.log("Found device"); - device.gatt.connect().then(function(ga) { - console.log("Connected"); - gatt = ga; - return ga.getPrimaryService("cc90"); -}).then(function(s) { - return s.getCharacteristic("cc91"); -}).then(function(c) { - c.on('characteristicvaluechanged', (event)=>wind_updated(event)); - return c.startNotifications(); -}).then(function() { - console.log("Done!"); -}).catch(function(e) { - console.log("ERROR"+e); -});} + device.gatt.connect().then(function(ga) { + console.log("Connected"); + gatt = ga; + return ga.getPrimaryService("cc90"); + }).then(function(s) { + return s.getCharacteristic("cc91"); + }).then(function(c) { + c.on('characteristicvaluechanged', (event)=>wind_updated(event)); + return c.startNotifications(); + }).then(function() { + console.log("Done!"); + }).catch(function(e) { + console.log("ERROR"+e); + });} function connection_setup() { NRF.setScan(); @@ -96,8 +104,10 @@ if (settings.truewind) { Bangle.on('GPS',function(fix) { if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph gps_course = - { lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852, - lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852, + { vlon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852, + vlat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852, + lat: fix.lat, + lon: fix.lon, spd: fix.speed, course: fix.course }; @@ -107,6 +117,20 @@ if (settings.truewind) { Bangle.setGPSPower(1, "app"); } +if (settings.truewind) { + Bangle.on("swipe", (d)=>{ + if (d==-1 && !pause_gps) { + pause_gps = true; + Bangle.setGPSPower(0); + draw_compass(0, 0, 0, 0); + } + else if (d==1 && pause_gps) { + pause_gps = false; + Bangle.setGPSPower(1, "app"); + draw_compass(0, 0, 0, 0); + } + }); +} Bangle.loadWidgets(); Bangle.drawWidgets(); draw_compass(0, 0, 0, 0); diff --git a/apps/openwind/metadata.json b/apps/openwind/metadata.json index 9229f7f25..01d7ca124 100644 --- a/apps/openwind/metadata.json +++ b/apps/openwind/metadata.json @@ -1,7 +1,7 @@ { "id": "openwind", "name": "OpenWind", "shortName":"OpenWind", - "version":"0.01", + "version":"0.03", "description": "OpenWind", "icon": "openwind.png", "readme": "README.md", diff --git a/apps/openwind/settings.js b/apps/openwind/settings.js index a7e3a1abe..dd6736811 100644 --- a/apps/openwind/settings.js +++ b/apps/openwind/settings.js @@ -2,7 +2,6 @@ /** * @param {function} back Use back() to return to settings menu */ -const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; (function(back) { const SETTINGS_FILE = 'openwindsettings.json' // initialize with default settings... @@ -29,7 +28,6 @@ const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; '< Back': back, 'True wind': { value: settings.truewind, - format: boolFormat, onchange: save('truewind'), }, 'Mounting angle': { diff --git a/apps/owmweather/ChangeLog b/apps/owmweather/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/owmweather/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/owmweather/README.md b/apps/owmweather/README.md new file mode 100644 index 000000000..ba0427455 --- /dev/null +++ b/apps/owmweather/README.md @@ -0,0 +1,13 @@ +# OpenWeatherMap weather provider + +This updates [Weather](https://banglejs.com/apps/#weather) with data from the OpenWeatherMap API + +## Usage + +Just install and configure the app. This needs an internet-enabled Gadgetbridge version. +Install [My Location](https://banglejs.com/apps/#mylocation) to change the location for the weather requests. +Install one of the text input libraries to set the API key in the app settings or use the web interface. + +## Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/owmweather/app.png b/apps/owmweather/app.png new file mode 100644 index 000000000..bbfc0ace0 Binary files /dev/null and b/apps/owmweather/app.png differ diff --git a/apps/owmweather/boot.js b/apps/owmweather/boot.js new file mode 100644 index 000000000..64d2df3e9 --- /dev/null +++ b/apps/owmweather/boot.js @@ -0,0 +1,28 @@ +(function() { + let waiting = false; + let settings = require("Storage").readJSON("owmweather.json", 1) || { + enabled: false + }; + + function completion(){ + waiting = false; + } + + if (settings.enabled) { + let weather = require("Storage").readJSON('weather.json') || {}; + let lastUpdate; + if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time; + if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){ + if (!waiting){ + waiting = true; + require("owmweather").pull(completion); + } + } + setInterval(() => { + if (!waiting && NRF.getSecurityStatus().connected){ + waiting = true; + require("owmweather").pull(completion); + } + }, settings.refresh * 1000 * 60); + } +})(); diff --git a/apps/owmweather/default.json b/apps/owmweather/default.json new file mode 100644 index 000000000..9d8998867 --- /dev/null +++ b/apps/owmweather/default.json @@ -0,0 +1 @@ +{"enabled":false,"refresh":180} diff --git a/apps/owmweather/interface.html b/apps/owmweather/interface.html new file mode 100644 index 000000000..3f9467a83 --- /dev/null +++ b/apps/owmweather/interface.html @@ -0,0 +1,63 @@ + + + + + +

Set OpenWeatherMap (OWM) API key

+

+ +

Where to get your personal API key?

+

Go to https://home.openweathermap.org/users/sign_up and sign up for a free account.
+ After registration you can login and optain your personal API key.

+ + + + + + + diff --git a/apps/owmweather/lib.js b/apps/owmweather/lib.js new file mode 100644 index 000000000..6ba52b498 --- /dev/null +++ b/apps/owmweather/lib.js @@ -0,0 +1,53 @@ +function parseWeather(response) { + let owmData = JSON.parse(response); + + let isOwmData = owmData.coord && owmData.weather && owmData.main; + + if (isOwmData) { + let json = require("Storage").readJSON('weather.json') || {}; + let weather = {}; + weather.time = Date.now(); + weather.hum = owmData.main.humidity; + weather.temp = owmData.main.temp; + weather.code = owmData.weather[0].id; + weather.wdir = owmData.wind.deg; + weather.wind = owmData.wind.speed; + weather.loc = owmData.name; + weather.txt = owmData.weather[0].main; + + if (weather.wdir != null) { + let deg = weather.wdir; + while (deg < 0 || deg > 360) { + deg = (deg + 360) % 360; + } + weather.wrose = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'n'][Math.floor((deg + 22.5) / 45)]; + } + + json.weather = weather; + require("Storage").writeJSON('weather.json', json); + require("weather").emit("update", json.weather); + return undefined; + } else { + return /*LANG*/"Not OWM data"; + } +} + +exports.pull = function(completionCallback) { + let location = require("Storage").readJSON("mylocation.json", 1) || { + "lat": 51.50, + "lon": 0.12, + "location": "London" + }; + let settings = require("Storage").readJSON("owmweather.json", 1); + let uri = "https://api.openweathermap.org/data/2.5/weather?lat=" + location.lat.toFixed(2) + "&lon=" + location.lon.toFixed(2) + "&exclude=hourly,daily&appid=" + settings.apikey; + if (Bangle.http){ + Bangle.http(uri, {timeout:10000}).then(event => { + let result = parseWeather(event.resp); + if (completionCallback) completionCallback(result); + }).catch((e)=>{ + if (completionCallback) completionCallback(e); + }); + } else { + if (completionCallback) completionCallback(/*LANG*/"No http method found"); + } +}; diff --git a/apps/owmweather/metadata.json b/apps/owmweather/metadata.json new file mode 100644 index 000000000..013d345a5 --- /dev/null +++ b/apps/owmweather/metadata.json @@ -0,0 +1,22 @@ +{ "id": "owmweather", + "name": "OpenWeatherMap weather provider", + "shortName":"OWM Weather", + "version":"0.01", + "description": "Pulls weather from OpenWeatherMap (OWM) API", + "icon": "app.png", + "type": "bootloader", + "tags": "boot,tool,weather", + "supports" : ["BANGLEJS2"], + "interface": "interface.html", + "readme": "README.md", + "data": [ + {"name":"owmweather.json"}, + {"name":"weather.json"} + ], + "storage": [ + {"name":"owmweather.default.json","url":"default.json"}, + {"name":"owmweather.boot.js","url":"boot.js"}, + {"name":"owmweather","url":"lib.js"}, + {"name":"owmweather.settings.js","url":"settings.js"} + ] +} diff --git a/apps/owmweather/settings.js b/apps/owmweather/settings.js new file mode 100644 index 000000000..a4d21dd7c --- /dev/null +++ b/apps/owmweather/settings.js @@ -0,0 +1,84 @@ +(function(back) { + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = Object.assign( + require('Storage').readJSON("owmweather.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {} + ); + } + + var FILE="owmweather.json"; + var settings; + readSettings(); + + function buildMainMenu(){ + var mainmenu = { + '': { 'title': 'OWM weather' }, + '< Back': back, + "Enabled": { + value: !!settings.enabled, + onchange: v => { + writeSettings("enabled", v); + } + }, + "Refresh every": { + value: settings.refresh / 60, + min: 1, + max: 48, + step: 1, + format: v=>v+"h", + onchange: v => { + writeSettings("refresh",Math.round(v * 60)); + } + }, + "Force refresh": ()=>{ + if (!settings.apikey){ + E.showAlert("API key is needed","Hint").then(()=>{ + E.showMenu(buildMainMenu()); + }); + } else { + E.showMessage("Reloading weather"); + require("owmweather").pull((e)=>{ + if (e) { + E.showAlert(e,"Error").then(()=>{ + E.showMenu(buildMainMenu()); + }); + } else { + E.showAlert("Success").then(()=>{ + E.showMenu(buildMainMenu()); + }); + } + }); + } + } + }; + + mainmenu["API key"] = function (){ + if (require("textinput")){ + require("textinput").input({text:settings.apikey}).then(result => { + if (result != "") { + print("Result is", result); + settings.apikey = result; + writeSettings("apikey",result); + } + E.showMenu(buildMainMenu()); + }); + } else { + E.showPrompt("Install a text input lib"),then(()=>{ + E.showMenu(buildMainMenu()); + }); + } + }; + + + return mainmenu; + } + + E.showMenu(buildMainMenu()); +}); diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index a77fa758f..f4640426b 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -17,3 +17,4 @@ 0.15: fixed tendancy for mylocation to default to London added setting to enable/disable idle timer warning 0.16: make check_idle boolean setting work properly with new B2 menu +0.17: Use default Bangle formatter for booleans diff --git a/apps/pastel/metadata.json b/apps/pastel/metadata.json index f04a7ae54..1fe176d5f 100644 --- a/apps/pastel/metadata.json +++ b/apps/pastel/metadata.json @@ -2,7 +2,7 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.16", + "version": "0.17", "description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", "dependencies": {"mylocation":"app","weather":"app"}, diff --git a/apps/pastel/pastel.settings.js b/apps/pastel/pastel.settings.js index afe461f15..14b3d2037 100644 --- a/apps/pastel/pastel.settings.js +++ b/apps/pastel/pastel.settings.js @@ -39,7 +39,6 @@ }, 'Show Grid': { value: !!s.grid, - format: v => v ? /*LANG*/"Yes":/*LANG*/"No", onchange: v => { s.grid = v; save(); @@ -47,7 +46,6 @@ }, 'Show Weather': { value: !!s.weather, - format: v => v ? /*LANG*/"Yes":/*LANG*/"No", onchange: v => { s.weather = v; save(); @@ -55,7 +53,6 @@ }, 'Idle Warning': { value: !!s.idle_check, - format: v => v ? /*LANG*/"Yes":/*LANG*/"No", onchange: v => { s.idle_check = v; save(); diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog index 274c34a34..ac894fc00 100644 --- a/apps/pebble/ChangeLog +++ b/apps/pebble/ChangeLog @@ -8,3 +8,4 @@ 0.08: Add theme options and optional lock symbol 0.09: Add support for internationalization (LANG placeholders + "locale" module) Get steps from built-in step counter (widpedom no more needed, fix #1697) +0.10: Tell clock widgets to hide. diff --git a/apps/pebble/metadata.json b/apps/pebble/metadata.json index f3c1fcc12..c5faa8857 100644 --- a/apps/pebble/metadata.json +++ b/apps/pebble/metadata.json @@ -2,7 +2,7 @@ "id": "pebble", "name": "Pebble Clock", "shortName": "Pebble", - "version": "0.09", + "version": "0.10", "description": "A pebble style clock to keep the rebellion going", "readme": "README.md", "icon": "pebble.png", diff --git a/apps/pebble/pebble.app.js b/apps/pebble/pebble.app.js index 774b24c3f..729d0a896 100644 --- a/apps/pebble/pebble.app.js +++ b/apps/pebble/pebble.app.js @@ -133,6 +133,8 @@ Bangle.on('lock', function(on) { drawLock(); }); +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); @@ -149,4 +151,3 @@ loadThemeColors(); setInterval(draw, 15000); // refresh every 15s draw(); -Bangle.setUI("clock"); diff --git a/apps/pebbled/ChangeLog b/apps/pebbled/ChangeLog index 9db0e26c5..8fe3a0b2c 100644 --- a/apps/pebbled/ChangeLog +++ b/apps/pebbled/ChangeLog @@ -1 +1,2 @@ 0.01: first release +0.02: Tell clock widgets to hide. diff --git a/apps/pebbled/metadata.json b/apps/pebbled/metadata.json index c16025f6f..70820f2c8 100644 --- a/apps/pebbled/metadata.json +++ b/apps/pebbled/metadata.json @@ -2,7 +2,7 @@ "id": "pebbled", "name": "Pebble Clock with distance", "shortName": "Pebble + distance", - "version": "0.01", + "version": "0.02", "description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).", "readme": "README.md", "icon": "pebbled.png", diff --git a/apps/pebbled/pebbled.app.js b/apps/pebbled/pebbled.app.js index bbe98823f..472ac8fff 100644 --- a/apps/pebbled/pebbled.app.js +++ b/apps/pebbled/pebbled.app.js @@ -115,6 +115,7 @@ function getSteps() { return '0'; } +Bangle.setUI("clock"); g.clear(); Bangle.loadWidgets(); /* @@ -126,4 +127,3 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} loadSettings(); setInterval(draw, 15000); // refresh every 15s draw(); -Bangle.setUI("clock"); diff --git a/apps/pie/app.js b/apps/pie/app.js index 69b67d3bd..74f4b4575 100644 --- a/apps/pie/app.js +++ b/apps/pie/app.js @@ -11,7 +11,7 @@ function scrollX(){ gfx.clearRect(0,gfx.getHeight()*(1/4),gfx.getWidth(),0); gfx.scroll(0,gfx.getHeight()/4); score++; - if(typeof(m) != undefined && score>0){ + if(typeof m !== 'undefined' && score>0){ clearInterval(m); m = setInterval(scrollY,Math.abs(100/score+15-0.1*score));} gfx.setColor(1,1,1); diff --git a/apps/planetarium/ChangeLog b/apps/planetarium/ChangeLog index 78288f646..9eedad602 100644 --- a/apps/planetarium/ChangeLog +++ b/apps/planetarium/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Major speed improvement. Added more stars. Up to 500! -0.03: Added more stars and constellations. Now it shows 20 constellations. \ No newline at end of file +0.03: Added more stars and constellations. Now it shows 20 constellations. +0.04: Use default Bangle formatter for booleans diff --git a/apps/planetarium/metadata.json b/apps/planetarium/metadata.json index 00e998634..78add2cea 100644 --- a/apps/planetarium/metadata.json +++ b/apps/planetarium/metadata.json @@ -2,7 +2,7 @@ "id": "planetarium", "name": "Planetarium", "shortName": "Planetarium", - "version": "0.03", + "version": "0.04", "description": "Planetarium showing up to 500 stars using the watch location and time", "icon": "planetarium.png", "tags": "", diff --git a/apps/planetarium/settings.js b/apps/planetarium/settings.js index caadb4016..524901a09 100644 --- a/apps/planetarium/settings.js +++ b/apps/planetarium/settings.js @@ -10,19 +10,16 @@ '< Back': back, 'Star names': { value: !!settings.starnames, - format: v =>v?'On':'Off', onchange: v => { save('starnames',v); }}, 'Constellations': { value: !!settings.constellations, - format: v =>v?'On':'Off', onchange: v => { save('constellations',v); }}, 'Const. names': { value: !!settings.consnames, - format: v =>v?'On':'Off', onchange: v => { save('consnames',v); }}, diff --git a/apps/pokeclk/ChangeLog b/apps/pokeclk/ChangeLog new file mode 100644 index 000000000..5838e596d --- /dev/null +++ b/apps/pokeclk/ChangeLog @@ -0,0 +1,3 @@ +0.01: New face :) +0.02: Color image compressed +0.03: Improved clock diff --git a/apps/pokeclk/README.md b/apps/pokeclk/README.md new file mode 100644 index 000000000..a7b3ea6b1 --- /dev/null +++ b/apps/pokeclk/README.md @@ -0,0 +1,17 @@ +# Poketch Clock + +A clock based on the Poketch electronic device found in Sinnoh + +![](https://user-images.githubusercontent.com/44651387/157491789-1b608c11-8af2-4519-a90f-41b8a58a9a14.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/pokeclk/app-icon.js b/apps/pokeclk/app-icon.js new file mode 100644 index 000000000..4b948799c --- /dev/null +++ b/apps/pokeclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIWTgfAAocH8AFDh/wAp08AoM8AoN+AoN+AoP/AoP/AoWP+IFDAAQFHv4EB/wFBn4FB/gFBj4FB/A6CAoI8CApUHDYIfB8AKB/AfB+ACB+fPBAZ3BApP774FBDoopB/xPBRYJBQLIxlFOIqDLSoyhEVoq5FaKLpGeooAP")) diff --git a/apps/pokeclk/app.js b/apps/pokeclk/app.js new file mode 100644 index 000000000..7e495f7d2 --- /dev/null +++ b/apps/pokeclk/app.js @@ -0,0 +1,75 @@ +Modules.addCached("Font4x5",function(){exports.add=function(a){a.prototype.setFont4x5=function(){this.setFontCustom(atob("AAAAdBgGAfV8CfyBIiQKrcAMAA6IARcAFXVARxAAwABCEAAIAAGTAPx+BHwAvXoK1+DhPg7W4P1uCEPg/X4O1+ACgACoAIqIBSlAIqIIVQC9VAfR4P1UB0VA/FwP1qD9KAdGYPk+AHwAEHwPk2D4Qg+j4PweB0XA/RAHTeD9FgTWQIfgD4fg8HwPi+DZNgwfAJ1yD8QAwQYI/ABEEACEIIIAB9Hg/VQHRUD8XA/WoP0oB0Zg+T4AfAAQfA+TYPhCD6Pg/B4HRcD9EAdN4P0WBNZAh+APh+DwfA+L4Nk2DB8AnXICfiAGwAj8gIYQAA=="),32,4,5)}}}); + +const offset = 25; +const width = g.getWidth(); +const height = g.getHeight(); +const font = "Vector:12"; + +var drawTimeout; + +var img = { + width : 176, height : 149, bpp : 4, + transparent : -1, + palette : new Uint16Array([25804,806,0,21514]), + buffer : require("heatshrink").decompress((atob("iIA/AH4A/AH4AGgAA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/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/AH4ATgUiIP5IVK/5IVgMSkRX/K5MiiJXJiMSJ/5YJiJX/K7BYHKwJjKAH5MKK/5YWBAZX/K65M/LB4AHJf5XWJX5X/K98RiBM/K/5WtK/5XQgEiKokCkBN/K/5XmkRXEiUiK/5WOK5EjJf75BBZJUBKoshAYRX/K/77aiMQBYYHCK4kSAoRYBK/5X/K9IAFK/5XXiJX/K45QIK35XcK3pXHLASu/LipXPKX5X/AChHKK54A/AH4AUXKgaPAHhXQiBN/AH4A/AH4A/AAY"))) +}; + +var night= { + width : 89, height : 76, bpp : 4, + transparent : 2, + buffer : (atob("ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERABEREREREREAEREREREREREREREREREREREREREREREREREREREREREREREQABERERERHwABEREREREREREREREREREREREREREREREREREREREREREREREQ/xERERER/wERERERERERERERERERERERERERERERERERERERERERERERERH//xERERH//xERERERERERERERERERERERERERERERERERERERERERERERERH//xEREf/xERERERERERERERERERERERERERERERERERERERERERERERERERH///////ERERERERERERERERERERERERERERERERERERERERERERERERERER////////8RERERERERERERERERERERERERERERERERERERERERERERERERH/////////ERERERERERERERERERERERERERERERERERERERERERERERERER////D///DxERERERERERERERERERERERERERERERERERERERERERERERERH////w///w8RERERERERERERERERERERERERERERERERERERERER//ERERER//AA///w/w8REREREREREREREREREREREREREREREREREREREREf////EREf/wAP/wAA8PERERERERERERERERERERERERERERERERERERERERH////xERH/8AD/////DxERERERERERERERERERERERERERERERERERERERER////8REf//////////ERERERERERERERERERERERERERERERERERERERERERERH/8R/////////xERERERERERERERERERERERERERERERERERERERER////////Ef////////////////////////////////////////////////////////////////////////////////////////////////////////////8RERERH////////////xERERERERERERERERERERERERERERERERERERERERERERERH///////////EREREREREREREREREREREREREREREREREREREREREf/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w==")) +}; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function isDark(){ + if (g.theme.dark==true){ + g.setColor(0xFFFF); + } + else { + g.setColor(0x0000); + } +} + +function time() { + 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,1); + + require("Font4x5").add(Graphics); // time + isDark(); + g.setFontAlign(0,0); + g.setFont("4x5",7.5).drawString(time, width/2, height/2); + + g.setFontAlign(1,1); + g.setFont("4x5",3).drawString(mo+" "+day, width-15, height-35); +} + +function draw() { //poketch background + if (g.theme.dark==true){ + g.drawImage(night, 0, 25, {scale:2}); //poketch is life + } + else { + g.drawImage(img, 0, 25); //poketch is life + } + time(); + queueDraw(); +} + +//program start +g.clear(); +draw(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/pokeclk/app.png b/apps/pokeclk/app.png new file mode 100644 index 000000000..56789ab22 Binary files /dev/null and b/apps/pokeclk/app.png differ diff --git a/apps/pokeclk/metadata.json b/apps/pokeclk/metadata.json new file mode 100644 index 000000000..c022868ec --- /dev/null +++ b/apps/pokeclk/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "pokeclk", + "name": "Poketch Clock", + "shortName":"Poketch Clock", + "version": "0.03", + "description": "A clock based on the Poketch electronic device found in Sinnoh", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme":"README.md", + "storage": [ + {"name":"pokeclk.app.js","url":"app.js"}, + {"name":"pokeclk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/pokeclk/screenshot (1).png b/apps/pokeclk/screenshot (1).png new file mode 100644 index 000000000..1e17a632b Binary files /dev/null and b/apps/pokeclk/screenshot (1).png differ diff --git a/apps/pongclock/ChangeLog b/apps/pongclock/ChangeLog new file mode 100644 index 000000000..93217e76f --- /dev/null +++ b/apps/pongclock/ChangeLog @@ -0,0 +1,3 @@ +0.01: First release +0.02: added missing type i metadata +0.03: Use default Bangle formatter for booleans diff --git a/apps/pongclock/README.md b/apps/pongclock/README.md new file mode 100644 index 000000000..894070b73 --- /dev/null +++ b/apps/pongclock/README.md @@ -0,0 +1,15 @@ +# Pong Clock + +A clock which is playing Pong while showing the current time as score +* Settings + * Show or hide widgets (auto detecting the used widgets areas) + * Use inverted or standard theme colors for the play area + * Optionally pause while locked (saving battery) +* Loosely based on [https://codepen.io/Rabrennie/pen/WxNEoe](https://codepen.io/Rabrennie/pen/WxNEoe) + +![](screenshot.png) +![](screenshot_settings.png) +![](screenshot_invers_full.png) + +## Creator +[@pidajo](https://github.com/pidajo) diff --git a/apps/pongclock/app.js b/apps/pongclock/app.js new file mode 100644 index 000000000..2a10bf6ed --- /dev/null +++ b/apps/pongclock/app.js @@ -0,0 +1,310 @@ +class Ball { + constructor(collision) { + this.collision = collision; + this.w = 4; + this.h = this.w; + this.y = height / 2 - this.h / 2; + this.x = width / 2 - this.w / 2; + this.oldX = this.x; + this.oldY = this.y; + this.velX = 6; + this.velY = 3.5 + Math.random(); + } + + reset() { + this.y = height / 2 - this.h / 2; + this.x = width / 2 - this.w / 2; + this.velX = 6; + this.velY = 3.5 + Math.random(); + } + + checkCollision(that, isLeft) { + let test = false; + if (isLeft) { + test = this.x <= that.w + this.w && this.y > that.y && this.y < that.y + that.h; + } else { + test = this.x >= that.x + this.w && this.y > that.y && this.y < that.y + that.h; + } + if (test) { + this.velX = -this.velX; + this.velY = (3.5 + 2 * Math.random()) * this.velY / Math.abs(this.velY); + + if (isLeft) { + right.follow = this; + left.follow = null; + } else { + left.follow = this; + right.follow = null; + } + } + } + + move() { + if (this.velX > 0) { + this.checkCollision(right, false); + } else { + this.checkCollision(left, true); + } + + this.x += this.velX; + this.y += this.velY; + + if (this.y <= this.h) { + this.y = this.h; + this.velY = -this.velY; + } + + if (this.y >= height - this.h) { + this.y = height - this.h; + this.velY = -this.velY; + } + + if (this.x >= width) { + left.scored(); + restart(); + } else if (this.x < 0) { + right.scored(); + restart(); + } + + } +} + +class Paddle { + constructor(side) { + this.side = side; + this.w = 4; //15; + this.h = 30; //80; + this.y = height / 2 - this.h / 2; + this.follow = null; + this.target = height / 2 - this.h / 2; + this.score = 99; + this.hasLost = false; + } + + reset() { + this.follow = null; + this.hasLost = false; + this.target = height / 2 - this.h / 2; + this.y = height / 2 - this.h / 2; + this.move(); + } + + scored() { + let d = new Date(); + let value = 0; + if (this.side == "left") { + value = d.getHours(); + } else { + value = d.getMinutes(); + } + if (this.score < value) { + this.score++; + } else { + this.score = value; + } + } + + move() { + + if (this.follow && !this.hasLost) { + var dy = this.follow.y - this.y - this.h / 2; + this.y += dy / 2; + } else { + this.y += (this.target - this.y) / 10; + } + if (this.y < 0) { + this.y = 0; + } + if (this.y > height - this.h) { + this.y = height - this.h; + } + } +} + +var updateTimeout = null; + +function update() { + var d = new Date(); + var lastStep = Date.now(); + left.move(); + right.move(); + if (d.getHours() != left.score) { + right.follow = null; + right.hasLost = true; + } + if (d.getMinutes() != right.score) { + left.follow = null; + left.hasLost = true; + } + + ball.move(); + redraw(); + var nextStep = 40 - (Date.now() - lastStep); + //console.log(nextStep); + updateTimeout = setTimeout(update, nextStep > 0 ? nextStep : 0); + return lastStep; +} + +function redraw() { + let fontHeight = width / 3.6; + let fontTop = top + height / 11; + let topHeight = top + height; + g.reset(); + + if (settings.isInvers) { + g.setColor(g.theme.bg); + g.setBgColor(g.theme.fg); + } + + g.clearRect(0, top + left.oldY, left.w, top + left.oldY + left.h); + g.clearRect(width - right.w, top + right.oldY, width, top + right.oldY + right.h); + //g.clearRect(width / 2 - fontHeight * 1.4, fontTop, width / 2 + fontHeight * 1.4, fontTop + fontHeight); + g.clearRect(ball.oldX - ball.w, top + ball.oldY - ball.h, ball.oldX + ball.w, top + ball.oldY + ball.h); + + g.setFontVector(fontHeight); + /**/ + g.setFontAlign(1, -1); + g.drawString(("0" + left.score).substr(-2), 5 * width / 11, fontTop, true); + g.setFontAlign(-1, -1); + g.drawString(("0" + right.score).substr(-2), 6 * width / 11, fontTop, true); + /**/ + + g.drawLine(width / 2, top, width / 2, topHeight); + g.fillRect(0, top + left.y, left.w, top + left.y + left.h); + left.oldY = left.y; + g.fillRect(width - right.w, top + right.y, width, top + right.y + right.h); + right.oldY = right.y; + g.fillCircle(ball.x, top + ball.y, ball.w); + ball.oldX = ball.x; + ball.oldY = ball.y; +} + +function restart() { + g.reset(); + if (settings.isInvers) { + g.setColor(g.theme.bg); + g.setBgColor(g.theme.fg); + } + g.clearRect(0, top, width, top + height); + ball.reset(); + left.reset(); + right.reset(); + right.follow = ball; + left.move(); + right.move(); + if (settings.withWidgets) { + Bangle.drawWidgets(); + } +} + +function stop() { + if (updateTimeout) { + clearTimeout(updateTimeout); + } + updateTimeout = null; + if (pauseTimeout) { + clearTimeout(pauseTimeout); + } + pauseTimeout = null; +} + +var pauseTimeout = null; + +function pause() { + stop(); + left.scored(); + right.scored(); + redraw(); + pauseTimeout = setTimeout(pause, Date.now() % 60000); +} + +//load settings +const SETTINGS_FILE = "pongclock.json"; +var settings = Object.assign({ + // default values + withWidgets: true, + isInvers: false, + playLocked: true, +}, require('Storage').readJSON(SETTINGS_FILE, true) || {}); +require('Storage').writeJSON(SETTINGS_FILE, settings); + +//make clock +Bangle.setUI("clock"); + +//setup play area +var height = g.getHeight(), + width = g.getWidth(); +var top = 0; + +g.reset(); +g.clearRect(0, top, width, height); + +if (settings.withWidgets) { + Bangle.loadWidgets(); + Bangle.drawWidgets(); + //console.log(WIDGETS); + if (global.WIDGETS) { + let bottom = 0; + for (var i in WIDGETS) { + var w = WIDGETS[i]; + if (w.area) { + if (w.area.indexOf("t") >= 0) { + top = Bangle.appRect.y; + } + if (w.area.indexOf("b") >= 0) { + bottom = height - Bangle.appRect.y2; + } + } + } + height -= top + bottom; + } +} + +if (settings.isInvers) { + g.setColor(g.theme.bg); + g.setBgColor(g.theme.fg); +} +g.clearRect(0, top, width, top + height); + +//setup game +var left = new Paddle("left"); +var right = new Paddle("right"); +var ball = new Ball(true); + +left.x = 20; +right.x = width - 20; + +left.scored(); +right.scored(); + +Bangle.on("lock", (on) => { + //console.log(on); + if (!settings.playLocked) { + if (on) { + pause(); + } else { + stop(); + update(); + } + } +}); + +//start clock +restart(); +if (!settings.playLocked && Bangle.isLocked()) { + pause(); +} else { + update(); +} + +/* +//local testing +require("Storage").write("pongclock.info",{ + "id":"pongclock", + "name":"Pong Clock", + "type":"clock", + "src":"pongclock.app.js", + "icon":"pongclock.img" +}); +*/ diff --git a/apps/pongclock/metadata.json b/apps/pongclock/metadata.json new file mode 100644 index 000000000..3447893ab --- /dev/null +++ b/apps/pongclock/metadata.json @@ -0,0 +1,21 @@ +{ "id": "pongclock", + "name": "Pong Clock", + "shortName":"Pong Clock", + "icon": "pongclock.png", + "version":"0.03", + "description": "A Pong playing clock", + "type": "clock", + "tags": "", + "allow_emulator":true, + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme":"README.md", + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot_settings.png" }, { "url":"screenshot_invers_full.png" } ], + "storage": [ + {"name":"pongclock.app.js","url":"app.js"}, + {"name":"pongclock.img","url":"pongclock-icon.js","evaluate":true}, + {"name":"pongclock.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"pongclock.json"} + ] +} diff --git a/apps/pongclock/pongclock-icon.js b/apps/pongclock/pongclock-icon.js new file mode 100644 index 000000000..22e472af4 --- /dev/null +++ b/apps/pongclock/pongclock-icon.js @@ -0,0 +1 @@ +atob("MDCEBAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAIiAAAAAAAAAAEAAAAAAAABP/EAE//yAAAAAAAD8wAAAT//IAAAAAAv//EA////IAAAAAAP8gAAH///8gAAAAAvP/EB/xA/MAIiAAAf8QAAL/ED8wAAAAAxD/EC8wAf8ALyAAAv8AAAPyAC/wAAAAAAD/ED/wAf8ALyAAD/MAAA/zAC8wAAAAAAD/EAAAAv8ALyAAH/EAAAAAAD8wAAAAAAD/EAAAA/IALyAAL/AAAAAAAP8gAAAAAAD/EAAAH/EALyAAPzD/IAAAAv8AAAAAAAD/EAAAPzAALyAA/yD/IAAAD/IAAAAAAAD/EAAB/xAALyAB/xD/IAAAL/AAAAAAAAD/EAAP8wAALyAC/wD/IAAB/yAAAAAAAAD/EAAv8QAAIiAP8wD/IAAD/wAAAAAAAAD/EAD/MAAAAAAf/zP/MwAf8gAAAA//AAD/EAL/AAAAAAAf////8wA/8AAAAA//AAD/EB/zAAAAIiACIiL/MgL/IAAAAA//AAD/EC////8ALyAAAAD/IAP////wAA//AAD/EC////8ALyAAAAD/IAP////wAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAALyAAAAAAAAAAAAAAAA//AAAAAAAAAAAAIiAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAAAAAAAAD/8A//AAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAIiAAAAAAAAAAAAD/8AAAAAAAASEAAAAALyAAAAAAAAAAAAD/8AAAAAAAP/8wAAAALyAAAAAAAAAAAAD/8AAAAAAB///xAAAALyAAAAAAAAAAAAD/8AAAAAAC///yAAAALyAAAAAAAAAAAAD/8AAAAAAB///xAAAALyAAAAAAAAAAAAD/8AAAAAAAP/8wAAAALyAAAAAAAAAAAAD/8AAAAAAAASEAAAAALyAAAAAAAAAAAAD/8AAAAAAAAAAAAAAALyAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAIiAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALyAAAAAAAAAAAAAAAA==") diff --git a/apps/pongclock/pongclock.png b/apps/pongclock/pongclock.png new file mode 100644 index 000000000..78934d7c6 Binary files /dev/null and b/apps/pongclock/pongclock.png differ diff --git a/apps/pongclock/screenshot.png b/apps/pongclock/screenshot.png new file mode 100644 index 000000000..0750105f0 Binary files /dev/null and b/apps/pongclock/screenshot.png differ diff --git a/apps/pongclock/screenshot_invers_full.png b/apps/pongclock/screenshot_invers_full.png new file mode 100644 index 000000000..434e24872 Binary files /dev/null and b/apps/pongclock/screenshot_invers_full.png differ diff --git a/apps/pongclock/screenshot_settings.png b/apps/pongclock/screenshot_settings.png new file mode 100644 index 000000000..0b27286ea Binary files /dev/null and b/apps/pongclock/screenshot_settings.png differ diff --git a/apps/pongclock/settings.js b/apps/pongclock/settings.js new file mode 100644 index 000000000..7e8511da7 --- /dev/null +++ b/apps/pongclock/settings.js @@ -0,0 +1,42 @@ +(function(back) { + var FILE = "pongclock.json"; + // Load settings + var settings = Object.assign({ + // default values + withWidgets: true, + isInvers: false, + playLocked: true, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Pong Clock" }, + "< Back" : () => back(), + 'Show Widgets': { + value: !!settings.withWidgets, + onchange: v => { + settings.withWidgets = v; + writeSettings(); + } + }, + 'Inverted?': { + value: !!settings.isInvers, + onchange: v => { + settings.isInvers = v; + writeSettings(); + } + }, + 'On Lock?': { + value: !!settings.playLocked, + format: v => v?"Play":"Pause", + onchange: v => { + settings.playLocked = v; + writeSettings(); + } + } + }); +})/*(load)/**/ diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog index 8ccf678de..f0b60a45a 100644 --- a/apps/powermanager/ChangeLog +++ b/apps/powermanager/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Allow forcing monotonic battery voltage/percentage +0.03: Use default Bangle formatter for booleans diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json index 2bb531099..dd1727657 100644 --- a/apps/powermanager/metadata.json +++ b/apps/powermanager/metadata.json @@ -2,7 +2,7 @@ "id": "powermanager", "name": "Power Manager", "shortName": "Power Manager", - "version": "0.02", + "version": "0.03", "description": "Allow configuration of warnings and thresholds for battery charging and display.", "icon": "app.png", "type": "bootloader", diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js index 8af873e5f..7cc683024 100644 --- a/apps/powermanager/settings.js +++ b/apps/powermanager/settings.js @@ -26,14 +26,12 @@ '< Back': back, 'Monotonic percentage': { value: !!settings.forceMonoPercentage, - format: v => settings.forceMonoPercentage ? "On" : "Off", onchange: v => { writeSettings("forceMonoPercentage", v); } }, 'Monotonic voltage': { value: !!settings.forceMonoVoltage, - format: v => settings.forceMonoVoltage ? "On" : "Off", onchange: v => { writeSettings("forceMonoVoltage", v); } diff --git a/apps/presentation_timer/ChangeLog b/apps/presentation_timer/ChangeLog new file mode 100644 index 000000000..2ed460931 --- /dev/null +++ b/apps/presentation_timer/ChangeLog @@ -0,0 +1,2 @@ +0.01: first release +0.02: added interface for configuration from app loader diff --git a/apps/presentation_timer/README.md b/apps/presentation_timer/README.md new file mode 100644 index 000000000..4539fc2f9 --- /dev/null +++ b/apps/presentation_timer/README.md @@ -0,0 +1,47 @@ +# Presentation Timer + +*Forked from Stopwatch Touch* + +Simple application to keep track of slides and +time during a presentation. Useful for conferences, +lectures or any presentation with a somewhat strict timing. + +The interface is pretty simple, it shows a stopwatch +and the number of the current slide (based on the time), +when the time for the last slide is approaching, +the button becomes red, when it passed, +the time will go on for another half a minute and stop automatically. + +You can set personalized timings from the web interface +by uploading a CSV to the bangle (floppy disk button in the app loader). + +Each line in the file (`presentation_timer.csv`) +contains the time in minutes at which the slide +is supposed to finish and the slide number, +separated by a semicolon. +For instance the line `1.5;1` means that slide 1 +is lasting until 1 minutes 30 seconds (yes it's decimal), +after another slide will start. +The only requirement is that timings are increasing, +so slides number don't have to be consecutive, +some can be skipped and they can even be short texts. + +At the moment the app is just quick and dirty +but it should do its job. + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) +![](screenshot4.png) + +## Example configuration file + +_presentation_timer.csv_ +```csv +1.5;1 +2;2 +2.5;3 +3;4 +``` diff --git a/apps/presentation_timer/interface.html b/apps/presentation_timer/interface.html new file mode 100644 index 000000000..137ea8475 --- /dev/null +++ b/apps/presentation_timer/interface.html @@ -0,0 +1,136 @@ + + + + + + + +
+ + + + + +

+
+    
+    
+  
+
diff --git a/apps/presentation_timer/metadata.json b/apps/presentation_timer/metadata.json
new file mode 100644
index 000000000..8790d6208
--- /dev/null
+++ b/apps/presentation_timer/metadata.json
@@ -0,0 +1,17 @@
+{
+  "id": "presentation_timer",
+  "name": "Presentation Timer",
+  "version": "0.02",
+  "description": "A touch based presentation timer for Bangle JS 2",
+  "icon": "presentation_timer.png",
+  "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
+  "tags": "tools,app",
+  "supports": ["BANGLEJS2"],
+  "readme": "README.md",
+  "interface": "interface.html",
+  "storage": [
+    {"name":"presentation_timer.app.js","url":"presentation_timer.app.js"},
+    {"name":"presentation_timer.img","url":"presentation_timer.icon.js","evaluate":true}
+  ],
+  "data": [{ "name": "presentation_timer.csv" }]
+}
diff --git a/apps/presentation_timer/presentation_timer.app.js b/apps/presentation_timer/presentation_timer.app.js
new file mode 100644
index 000000000..1d0e5945d
--- /dev/null
+++ b/apps/presentation_timer/presentation_timer.app.js
@@ -0,0 +1,272 @@
+let w = g.getWidth();
+let h = g.getHeight();
+let tTotal = Date.now();
+let tStart = tTotal;
+let tCurrent = tTotal;
+let running = false;
+let timeY = 2*h/5;
+let displayInterval;
+let redrawButtons = true;
+const iconScale = g.getWidth() / 178; // scale up/down based on Bangle 2 size
+
+// 24 pixel images, scale to watch
+// 1 bit optimal, image string, no E.toArrayBuffer()
+const pause_img = atob("GBiBAf////////////////wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP/wYP////////////////w==");
+const play_img = atob("GBjBAP//AAAAAAAAAAAIAAAOAAAPgAAP4AAP+AAP/AAP/wAP/8AP//AP//gP//gP//AP/8AP/wAP/AAP+AAP4AAPgAAOAAAIAAAAAAAAAAA=");
+const reset_img = atob("GBiBAf////////////AAD+AAB+f/5+f/5+f/5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+cA5+f/5+f/5+f/5+AAB/AAD////////////w==");
+
+const margin = 0.5; //half a minute tolerance
+
+//dummy default values
+var slides = [
+  [1.5, 1],
+  [2, 2],
+  [2.5, 3],
+  [3,4]
+];
+
+function log_debug(o) {
+  //console.log(o);
+}
+
+//first must be a number
+function readSlides() {
+  let csv = require("Storage").read("presentation_timer.csv");
+  if(!csv) return;
+  let lines = csv.split("\n").filter(e=>e);
+  log_debug("Loading "+lines.length+" slides");
+  slides = lines.map(line=>{let s=line.split(";");return [+s[0],s[1]];});
+}
+
+
+function timeToText(t) {
+  let hrs = Math.floor(t/3600000);
+  let mins = Math.floor(t/60000)%60;
+  let secs = Math.floor(t/1000)%60;
+  let tnth = Math.floor(t/100)%10;
+  let text;
+
+  if (hrs === 0)
+    text = ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2) + "." + tnth;
+  else
+    text = ("0"+hrs) + ":" + ("0"+mins).substr(-2) + ":" + ("0"+secs).substr(-2);
+
+  //log_debug(text);
+  return text;
+}
+
+function drawButtons() {
+  log_debug("drawButtons()");
+  if (!running && tCurrent == tTotal) {
+    bigPlayPauseBtn.draw();
+  } else if (!running && tCurrent != tTotal) {
+    resetBtn.draw();
+    smallPlayPauseBtn.draw();
+  } else {
+    bigPlayPauseBtn.draw();
+  }
+
+  redrawButtons = false;
+}
+
+//not efficient but damn easy
+function findSlide(time) {
+  time /= 60000;
+  //change colour for the last 30 seconds
+  if(time > slides[slides.length-1][0] - margin && bigPlayPauseBtn.color!="#f00") {
+    bigPlayPauseBtn.color="#f00";
+    drawButtons();
+  }
+  for(let i=0; i time)
+      return slides[i][1];
+  }
+  //stop automatically
+  if(time > slides[slides.length-1][0] + margin) {
+    bigPlayPauseBtn.color="#0ff"; //restore
+    stopTimer();
+  }
+  return /*LANG*/"end!";
+}
+
+function drawTime() {
+  log_debug("drawTime()");
+  let Tt = tCurrent-tTotal;
+  let Ttxt = timeToText(Tt);
+
+  Ttxt += "\n"+findSlide(Tt);
+  // total time
+  g.setFont("Vector",38);  // check
+  g.setFontAlign(0,0);
+  g.clearRect(0, timeY - 42, w, timeY + 42);
+  g.setColor(g.theme.fg);
+  g.drawString(Ttxt, w/2, timeY);
+}
+
+function draw() {
+  let last = tCurrent;
+  if (running) tCurrent = Date.now();
+  g.setColor(g.theme.fg);
+  if (redrawButtons) drawButtons();
+  drawTime();
+}
+
+function startTimer() {
+  log_debug("startTimer()");
+  draw();
+  displayInterval = setInterval(draw, 100);
+}
+
+function stopTimer() {
+  log_debug("stopTimer()");
+  if (displayInterval) {
+    clearInterval(displayInterval);
+    displayInterval = undefined;
+  }
+}
+
+// BTN stop start
+function stopStart() {
+  log_debug("stopStart()");
+
+  if (running)
+    stopTimer();
+
+  running = !running;
+  Bangle.buzz();
+
+  if (running)
+    tStart = Date.now() + tStart- tCurrent;  
+  tTotal = Date.now() + tTotal - tCurrent;
+  tCurrent = Date.now();
+
+  setButtonImages();
+  redrawButtons = true;
+  if (running) {
+    startTimer();
+  } else {
+    draw();
+  }
+}
+
+function setButtonImages() {
+  if (running) {
+    bigPlayPauseBtn.setImage(pause_img);
+    smallPlayPauseBtn.setImage(pause_img);
+    resetBtn.setImage(reset_img);
+  } else {
+    bigPlayPauseBtn.setImage(play_img);
+    smallPlayPauseBtn.setImage(play_img);
+    resetBtn.setImage(reset_img);
+  }
+}
+
+// lap or reset
+function lapReset() {
+  log_debug("lapReset()");
+  if (!running && tStart != tCurrent) {
+    redrawButtons = true;
+    Bangle.buzz();
+    tStart = tCurrent = tTotal = Date.now();
+    g.clearRect(0,24,w,h);
+    draw();
+  }
+}
+
+// simple on screen button class
+function BUTTON(name,x,y,w,h,c,f,i) {
+  this.name = name;
+  this.x = x;
+  this.y = y;
+  this.w = w;
+  this.h = h;
+  this.color = c;
+  this.callback = f;
+  this.img = i;
+}
+
+BUTTON.prototype.setImage = function(i) {
+  this.img = i;
+}
+
+// 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();
+    return true;
+  }
+  return false;
+};
+
+BUTTON.prototype.draw = function() {
+  g.setColor(this.color);
+  g.fillRect(this.x, this.y, this.x + this.w, this.y + this.h);
+  g.setColor("#000"); // the icons and boxes are drawn black
+  if (this.img != undefined) {
+    let iw = iconScale * 24;  // the images were loaded as 24 pixels, we will scale
+    let ix = this.x + ((this.w - iw) /2);
+    let iy = this.y + ((this.h - iw) /2);
+    log_debug("g.drawImage(" + ix + "," + iy + "{scale: " + iconScale + "})");
+    g.drawImage(this.img, ix, iy, {scale: iconScale}); 
+  }
+  g.drawRect(this.x, this.y, this.x + this.w, this.y + this.h);
+};
+
+
+var bigPlayPauseBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", stopStart, play_img);
+var smallPlayPauseBtn = new BUTTON("small",w/2, 3*h/4 ,w/2, h/4, "#0ff", stopStart, play_img);
+var resetBtn = new BUTTON("rst",0, 3*h/4, w/2, h/4, "#ff0", lapReset, pause_img);
+
+bigPlayPauseBtn.setImage(play_img);
+smallPlayPauseBtn.setImage(play_img);
+resetBtn.setImage(pause_img);
+
+
+Bangle.on('touch', function(button, xy) {
+  var x = xy.x;
+  var y = xy.y;
+
+  // adjust for outside the dimension of the screen
+  // http://forum.espruino.com/conversations/371867/#comment16406025
+  if (y > h) y = h;
+  if (y < 0) y = 0;
+  if (x > w) x = w;
+  if (x < 0) x = 0;
+
+  // not running, and reset
+  if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(x, y)) return;
+
+  // paused and hit play
+  if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(x, y)) return;
+
+  // paused and press reset
+  if (!running && tCurrent != tTotal && resetBtn.check(x, y)) return;
+
+  // must be running
+  if (running && bigPlayPauseBtn.check(x, y)) return;
+});
+
+// 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;
+  }
+});
+
+// Clear the screen once, at startup
+g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
+// above not working, hence using next 2 lines
+g.setColor("#000");
+g.fillRect(0,0,w,h);
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+readSlides();
+draw();
+setWatch(() => load(), BTN, { repeat: false, edge: "falling" });
diff --git a/apps/presentation_timer/presentation_timer.icon.js b/apps/presentation_timer/presentation_timer.icon.js
new file mode 100644
index 000000000..f18768b2b
--- /dev/null
+++ b/apps/presentation_timer/presentation_timer.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AC1WwIZXACmBF7FWAH4Ae/0WAFiQBF9+sAFgv/AAvXAAgvmFgoyWF6IuLGCIvPFpoxRF5wlIwIKJF8lWwIvjQpIvKGBgv8cpWBF5QwLF/4vrEJQvNGBQv/F5FWSCq/XGAVWB5DviEgRiJF8gxDF9q+SF5owWEJYv9GCggMF5wwSD5ovPGCAeOF6AwODp4vRGJYbRF6YAbF/4v/F8eBAYYECAYYvRACFWqwEGwNWwIeSF7IEFAD5VBGhpekAo6QiEYo1LR0QpGBgyOhAxCQfKIIhFGxpegA44+HF85gRA=="))
diff --git a/apps/presentation_timer/presentation_timer.png b/apps/presentation_timer/presentation_timer.png
new file mode 100644
index 000000000..7db9866d7
Binary files /dev/null and b/apps/presentation_timer/presentation_timer.png differ
diff --git a/apps/presentation_timer/screenshot1.png b/apps/presentation_timer/screenshot1.png
new file mode 100644
index 000000000..d26720d7e
Binary files /dev/null and b/apps/presentation_timer/screenshot1.png differ
diff --git a/apps/presentation_timer/screenshot2.png b/apps/presentation_timer/screenshot2.png
new file mode 100644
index 000000000..cbd6f0bd1
Binary files /dev/null and b/apps/presentation_timer/screenshot2.png differ
diff --git a/apps/presentation_timer/screenshot3.png b/apps/presentation_timer/screenshot3.png
new file mode 100644
index 000000000..40b375b37
Binary files /dev/null and b/apps/presentation_timer/screenshot3.png differ
diff --git a/apps/presentation_timer/screenshot4.png b/apps/presentation_timer/screenshot4.png
new file mode 100644
index 000000000..7c43cf91f
Binary files /dev/null and b/apps/presentation_timer/screenshot4.png differ
diff --git a/apps/promenu/metadata.json b/apps/promenu/metadata.json
index 443809004..e0124467a 100644
--- a/apps/promenu/metadata.json
+++ b/apps/promenu/metadata.json
@@ -4,7 +4,7 @@
   "version": "0.02",
   "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
   "icon": "icon.png",
-  "type": "boot",
+  "type": "bootloader",
   "tags": "system",
   "supports": ["BANGLEJS","BANGLEJS2"],
   "readme": "README.md",
diff --git a/apps/ptlaunch/ChangeLog b/apps/ptlaunch/ChangeLog
index eec3610ed..5871b1fdc 100644
--- a/apps/ptlaunch/ChangeLog
+++ b/apps/ptlaunch/ChangeLog
@@ -6,3 +6,4 @@
 0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
 0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/
 0.14: Update setUI to work with new Bangle.js 2v13 menu style
+0.15: Update to support clocks in custom setUI mode
diff --git a/apps/ptlaunch/boot.js b/apps/ptlaunch/boot.js
index 748d564f3..885962761 100644
--- a/apps/ptlaunch/boot.js
+++ b/apps/ptlaunch/boot.js
@@ -76,13 +76,8 @@
   var sui = Bangle.setUI;
   Bangle.setUI = function (mode, cb) {
     sui(mode, cb);
-    if ("object"==typeof mode) mode = mode.mode;
-    if (!mode) {
-      Bangle.removeListener("drag", dragHandler);
-      storedPatterns = {};
-      return;
-    }
-    if (!mode.startsWith("clock")) {
+    if (typeof mode === "object") mode = (mode.clock ? "clock" : "") + mode.mode;
+    if (!mode || !mode.startsWith("clock")) {
       storedPatterns = {};
       Bangle.removeListener("drag", dragHandler);
       return;
diff --git a/apps/ptlaunch/metadata.json b/apps/ptlaunch/metadata.json
index 0b6dce3d1..6f8a9e16f 100644
--- a/apps/ptlaunch/metadata.json
+++ b/apps/ptlaunch/metadata.json
@@ -2,7 +2,7 @@
   "id": "ptlaunch",
   "name": "Pattern Launcher",
   "shortName": "Pattern Launcher",
-  "version": "0.14",
+  "version": "0.15",
   "description": "Directly launch apps from the clock screen with custom patterns.",
   "icon": "app.png",
   "screenshots": [{"url":"manage_patterns_light.png"}],
diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog
index b9be6039d..173765967 100644
--- a/apps/qalarm/ChangeLog
+++ b/apps/qalarm/ChangeLog
@@ -4,3 +4,4 @@
       Fix app icon
       Change menu order so 'back' is at the top
 0.04: Fix alarm not activating sometimes.
+0.05: Use default Bangle formatter for booleans
diff --git a/apps/qalarm/app.js b/apps/qalarm/app.js
index ad071adf0..b04a635b5 100644
--- a/apps/qalarm/app.js
+++ b/apps/qalarm/app.js
@@ -115,22 +115,18 @@ function showEditAlarmMenu(alarmIndex, alarm) {
     },
     Enabled: {
       value: alarm.on,
-      format: (v) => (v ? "On" : "Off"),
       onchange: (v) => (alarm.on = v),
     },
     Repeat: {
       value: alarm.rp,
-      format: (v) => (v ? "Yes" : "No"),
       onchange: (v) => (alarm.rp = v),
     },
     "Auto snooze": {
       value: alarm.as,
-      format: (v) => (v ? "Yes" : "No"),
       onchange: (v) => (alarm.as = v),
     },
     Hard: {
       value: alarm.hard,
-      format: (v) => (v ? "Yes" : "No"),
       onchange: (v) => (alarm.hard = v),
     },
     "Days of week": () => showDaysMenu(alarmIndex, getAlarm()),
@@ -175,7 +171,6 @@ function showDaysMenu(alarmIndex, alarm) {
     let dayOfWeek = require("locale").dow({ getDay: () => i });
     menu[dayOfWeek] = {
       value: alarm.daysOfWeek[i],
-      format: (v) => (v ? "Yes" : "No"),
       onchange: (v) => (alarm.daysOfWeek[i] = v),
     };
   }
@@ -235,12 +230,10 @@ function showEditTimerMenu(timerIndex) {
     },
     Enabled: {
       value: alarm.on,
-      format: (v) => (v ? "On" : "Off"),
       onchange: (v) => (alarm.on = v),
     },
     Hard: {
       value: alarm.hard,
-      format: (v) => (v ? "On" : "Off"),
       onchange: (v) => (alarm.hard = v),
     },
   };
diff --git a/apps/qalarm/metadata.json b/apps/qalarm/metadata.json
index 2039af4bf..841c10e6f 100644
--- a/apps/qalarm/metadata.json
+++ b/apps/qalarm/metadata.json
@@ -3,7 +3,7 @@
   "name": "Q Alarm and Timer",
   "shortName": "Q Alarm",
   "icon": "app.png",
-  "version": "0.04",
+  "version": "0.05",
   "description": "[Not recommended - use 'Alarm & Timer' app] Alarm and timer app with days of week and 'hard' option.",
   "tags": "tool,alarm,widget",
   "supports": ["BANGLEJS", "BANGLEJS2"],
diff --git a/apps/qmsched/ChangeLog b/apps/qmsched/ChangeLog
index 94fcffe1a..88185f337 100644
--- a/apps/qmsched/ChangeLog
+++ b/apps/qmsched/ChangeLog
@@ -6,4 +6,5 @@
 0.06: Fix: don't try to redraw widget when widgets not loaded
 0.07: Option to switch theme
       Changed time selection to 5-minute intervals
-0.08: Support new Bangle.js 2 menu
\ No newline at end of file
+0.08: Support new Bangle.js 2 menu
+0.09: Use default Bangle formatter for booleans
diff --git a/apps/qmsched/app.js b/apps/qmsched/app.js
index 8cd0fa8d9..da43dd7d6 100644
--- a/apps/qmsched/app.js
+++ b/apps/qmsched/app.js
@@ -125,7 +125,6 @@ function showMainMenu() {
   menu[/*LANG*/"Add Schedule"] = () => showEditMenu(-1);
   menu[/*LANG*/"Switch Theme"] = {
     value: !!get("switchTheme"),
-    format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
     onchange: v => v ? set("switchTheme", v) : unset("switchTheme"),
   };
   menu[/*LANG*/"LCD Settings"] = () => showOptionsMenu();
diff --git a/apps/qmsched/metadata.json b/apps/qmsched/metadata.json
index 326a8fc4f..23bdbd2e4 100644
--- a/apps/qmsched/metadata.json
+++ b/apps/qmsched/metadata.json
@@ -2,7 +2,7 @@
   "id": "qmsched",
   "name": "Quiet Mode Schedule and Widget",
   "shortName": "Quiet Mode",
-  "version": "0.08",
+  "version": "0.09",
   "description": "Automatically turn Quiet Mode on or off at set times, change theme and LCD options while Quiet Mode is active.",
   "icon": "app.png",
   "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"},
diff --git a/apps/qrcode/ChangeLog b/apps/qrcode/ChangeLog
index 6d9cc0569..52eadbcf9 100644
--- a/apps/qrcode/ChangeLog
+++ b/apps/qrcode/ChangeLog
@@ -3,3 +3,4 @@
 0.03: Forces integer scaling and adds more configuration (error correction, description, display)
 0.04: Allow scanning of QR codes from camera or file
 0.05: Change brightness on touch
+0.06: Add ability to generate contact info (MeCard format) QR code
diff --git a/apps/qrcode/custom.html b/apps/qrcode/custom.html
index 7ae3eb3af..9955ea6c9 100644
--- a/apps/qrcode/custom.html
+++ b/apps/qrcode/custom.html
@@ -8,6 +8,8 @@
     

+ +

@@ -64,6 +66,14 @@ +
+

First Name:

+

Last Name:

+

Phone Number:

+

Email:

+

Website:

+
+

Try your QR Code:

@@ -156,7 +166,7 @@ function toggleVis(id){ console.info("Got id", id); - ["srcScanFile", "srcText", "srcWifi", "srcScanCam"].forEach(function (item){ + ["srcScanFile", "srcText", "srcWifi", "srcScanCam", "srcMeCard"].forEach(function (item){ document.getElementById(item).style.display = "none"; }); if (id != undefined && id != null) document.getElementById(id).style.display = "block"; @@ -188,6 +198,37 @@ } return qrstring; } + + function generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite){ + var meCardStringOutput = 'MECARD:'; + + //first & Last name part of string, can have one or both + if (meNameFirst.trim().length != 0 && meNameLast.trim().length != 0) { + meCardStringOutput += 'N:'+meNameLast.trim()+','+meNameFirst.trim()+';'; + } + else if (meNameLast.trim().length != 0) { + meCardStringOutput += 'N:'+meNameLast.trim()+';'; + } + else if (meNameFirst.trim().length != 0) { + meCardStringOutput += 'N:'+meNameFirst.trim()+';'; + } + + if (mePhoneNumber.trim().length != 0) { + meCardStringOutput += 'TEL:'+mePhoneNumber.trim()+';'; + } + + if (meEmail.trim().length != 0) { + meCardStringOutput += 'EMAIL:'+meEmail.trim()+';'; + } + + if (meWebsite.trim().length != 0) { + meCardStringOutput += 'URL:'+meWebsite.trim()+';'; + } + + meCardStringOutput += ';'; + return meCardStringOutput; + } + function refreshQRCode(){ if (qrcode == null){ qrcode = new QRCode("qrcode", { @@ -206,6 +247,14 @@ const hidden = document.getElementById("hidden").checked; const wifiString = generateWifiString(ssid, password, hidden, encryption); qrText= wifiString; + } else if (document.getElementById("useMECARD").checked) { + const meNameFirst = document.getElementById("meNameFirst").value; + const meNameLast = document.getElementById("meNameLast").value; + const mePhoneNumber = document.getElementById("mePhoneNumber").value; + const meEmail = document.getElementById("meEmail").value; + const meWebsite = document.getElementById("meWebsite").value; + const meCardString = generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite); + qrText = meCardString; } else if (document.getElementById("useCAM").checked) { qrText= document.getElementById("camQrResult").innerText; } else if (document.getElementById("useFILE").checked) { @@ -258,6 +307,14 @@ } document.getElementById("useTEXT").addEventListener("change",function(){toggleVis("srcText");}); + + document.getElementById("useMECARD").addEventListener("change",function(){toggleVis("srcMeCard");}); + document.getElementById("meNameFirst").addEventListener("change",refreshQRCode); + document.getElementById("meNameLast").addEventListener("change",refreshQRCode); + document.getElementById("mePhoneNumber").addEventListener("change",refreshQRCode); + document.getElementById("meEmail").addEventListener("change",refreshQRCode); + document.getElementById("meWebsite").addEventListener("change",refreshQRCode); + document.getElementById("useCAM").addEventListener("change",function(){ initQrScanner(); initQrCam(); @@ -314,7 +371,6 @@ g.setColor(1,1,1); }); - document.getElementById('camList').addEventListener('change', event => { scanner.setCamera(event.target.value).then(updateFlashAvailability); }); diff --git a/apps/qrcode/metadata.json b/apps/qrcode/metadata.json index 22f8f7b53..24af7b813 100644 --- a/apps/qrcode/metadata.json +++ b/apps/qrcode/metadata.json @@ -1,7 +1,7 @@ { "id": "qrcode", "name": "Custom QR Code", - "version": "0.05", + "version": "0.06", "description": "Use this to upload a customised QR code to Bangle.js", "icon": "app.png", "tags": "qrcode", diff --git a/apps/quicklaunch/ChangeLog b/apps/quicklaunch/ChangeLog index ec66c5568..ae1d4a848 100644 --- a/apps/quicklaunch/ChangeLog +++ b/apps/quicklaunch/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Moved settings from launcher to settings->apps menu diff --git a/apps/quicklaunch/app-icon.js b/apps/quicklaunch/app-icon.js deleted file mode 100644 index 14ae94823..000000000 --- a/apps/quicklaunch/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("kMigILIgPAAYMD/ADBwcGhkAwM5wcA/+2//Av/Rn/giFoyFggkUrFggEKlAkCiApCx+AAYNGoADBkU4AYMQj4DBvEICANkAoIPBgE2B4MAiMAH4MAwECAYNALYUgBIISCHYMYAoQWBAIMEgAYBAIMBwEDDQNgDwUf/4eBg4DCAA4")) diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json index 6411d1a5f..49eafdd35 100644 --- a/apps/quicklaunch/metadata.json +++ b/apps/quicklaunch/metadata.json @@ -1,14 +1,15 @@ -{ "id": "quicklaunch", +{ + "id": "quicklaunch", "name": "Quick Launch", "icon": "app.png", - "version":"0.01", - "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice.", + "version":"0.02", + "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.", + "type": "bootloader", "tags": "tools, system", "supports": ["BANGLEJS2"], "storage": [ - {"name":"quicklaunch.app.js","url":"app.js"}, - {"name":"quicklaunch.boot.js","url":"boot.js"}, - {"name":"quicklaunch.img","url":"app-icon.js","evaluate":true} + {"name":"quicklaunch.settings.js","url":"settings.js"}, + {"name":"quicklaunch.boot.js","url":"boot.js"} ], "data": [{"name":"quicklaunch.json"}] } diff --git a/apps/quicklaunch/app.js b/apps/quicklaunch/settings.js similarity index 99% rename from apps/quicklaunch/app.js rename to apps/quicklaunch/settings.js index f2b749e3e..ac4cc5805 100644 --- a/apps/quicklaunch/app.js +++ b/apps/quicklaunch/settings.js @@ -1,3 +1,4 @@ +(function(back) { var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {}); var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type)); @@ -118,3 +119,4 @@ apps.forEach((a)=>{ }); showMainMenu(); +}); \ No newline at end of file diff --git a/apps/r2d2clk/ChangeLog b/apps/r2d2clk/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/r2d2clk/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/r2d2clk/README.md b/apps/r2d2clk/README.md new file mode 100644 index 000000000..cd98b9170 --- /dev/null +++ b/apps/r2d2clk/README.md @@ -0,0 +1,11 @@ +# R2D2 Clock + +A clock with R2D2's shiny metal face on it. :) + +![](screenshot.png) + +## Creator + +Made by [Noah Howard](https://github.com/nh-99) + +Based on [Interlaced Clock](https://github.com/espruino/BangleApps/tree/master/apps/intclock) \ No newline at end of file diff --git a/apps/r2d2clk/app-icon.js b/apps/r2d2clk/app-icon.js new file mode 100644 index 000000000..246df9376 --- /dev/null +++ b/apps/r2d2clk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4AKqn5ol4416737nlXqnYAIJN/ABMsm80m8j60b60Lu0T22ty2920j+8DBIQPBlkXN4JZ9VIJLBI4IBBLoIHBJoIDBAIO16/Gi4JDBon2jZlCAINEzBbzqn5HoSrB+xJBAI5fNAI8r+7LBAIM8ZN0TywzBIpZfZDIrJDdoJblttagd1W5ZfhDoqTBpnYLsMDq0DmsL28T24DBAKGXgeX1pfCDYIHBDqQzCHYUVLruNzNMq1k69sAKgXBAIK/DBIYXNBZNUu1s+5fb3u271X6wBXm/mu4dD92Y+2YAYIBFBIYTCm4jH71Y4vW51ZLq+12wBB3vX3v3AKvOm69BYYapE+4BCA4Vc2+Fm/Oi/FE5mtuvW3S7U++1ywDB62a404LqZDBUoNEu8Dq0L20L/MMjUL/EMjIFC68Ly0Dy+VaoQrNMIJdR73bCoK7B51588cAYLDTXoOt28b60j+8svUr6sipkp9EylUriEsrIBBMILFBL5+12wDBL567BCoK5B737bYK9Ta4VXU4MT68kvMrqUYwMo0MgAIOAnMjmUJlk4if4qmXbILdBYJ6vBLpazBXoRDB3QVB40YXqYBBUYONq8UjMrykYoFi9QlB51ZoUokMEmHBlVrjlank3DoPGi4vOIYPXXpjRB64zBXoIDBLqpfDvs3hkZkPImMD/4AECIMIsMxssxokjucsvO1HYJfPYIStBLo/WzQNBW4IPBAIJbVL4tk/EL28hw0pgf3zxfD1tVL4WGmMCkWukl61u35zDCYLIJBX4K9d4v3IINMzET68x9EYsFS1SLB2uWmOmkMkL4cq98crWdu/Wq4zREYJfHBIINBYYPe7Y3BL67/Dmk3JIMqxk5kUY8kQ0QBBLoXHmNEmOFlf2hfYvsXboJfT515LoYFBBIJZBXoJhBLq5fCm+t28r+8knEsi6zDmNlAoQBBokw4MriEszUL21c26/TJoJVBL4a1F51ZXrLdCm+ly8b+8j+0srKvBmUJmMjAIRlClXwB4JxBie2pl3P4PFG6fXL4YZDAYRbC40XA4IBFDoZvBAI5fCq+Vq8Ly8jMIUkrIBBldSlXvLYMj6skvUki4RBhe3nk350XHYJfR2uW88bL4XWDoJ/BAJphD404aYIBFBII9DIoJJBjfXjfWY4McnMcrQBCjALC68T64VBvsXb4KRGFIIDDnBhG23vjd9rM8m1My4BMu9Eu+l3HWrF759jhlrpoBBrXrxlx4xlBm5jBzt3zuXAYQFDAJN31u3LoJ/DKoeU2WEiAtB3vXSIajBrm3JoMLyyBBie3AJaPBgY1BzHevFS1Uxw1ChABBlNFueO62aF4JFB71XAIIFBA4oLEvHOzInBA4JdG66RBLYOtuuc6dz121y3OrAVBlkXgd2ifWmgHCAJcj+71BX4a9BoUoqWKAIM58979/m3V0+4ZBnk3bYJLDolXnlXolWnl4mmYpk2mmZml4BYO165PBwkwLIP/AAmtqppBE4PGjAnBXYMb+xfj93aqmXRYMLy8bU4MX2v3GoLjBif4AoOClXBodytkD7AJB2vY5v3WoPe7ZfF+++GIO1uvOnBfp83ashTCC4IrBX4YjDhl6uXu+MA5OD9MgqcxBYJfF626L42eL4WVL/sUvVbqPhkBdB7NindzilZ1vXF4OMuIBBLIJfDznzwkQ40440YL4b1BGoI5BAJmXgd3zuY714LIMpspbBAIMhklzxvu/VUCoNWC4IvBL4O1+4jBgeXAYX4pXQxPoncRgfYie31u3L4J3BKoOEmBbBMoN75+123OrApBQ4IzBsn4rmWrm3AJpLB1vYboItBueOeYIBBtdNymy715ws3pl3C4LFBX4YnHosaAINNjIJD2oXCWIZdCqOc6YhBBYPF+4BBtnXqm3//c70V92YAJv2XoNX60382593a93ZAIX682ZBoV3+24C4IbB61XDYIhDE4WXFo4DBF4QXCeYIpB82YAYIHBB4vmq3/nWN3N8+ybBAJ99AKQZJF8oBCi+NvEDqsb+8T24B/AK3XgeWhYBD24B/AKxbCgEAqlVtnXtn3AP4BS69cu2N3RfB/9b4vV5v260XAP4BOi3N6vu/RdBAAvOrIBCvIB/AJVZ404LY4A/AH4A/AH4A/AH4A/AH4A9")) \ No newline at end of file diff --git a/apps/r2d2clk/app.js b/apps/r2d2clk/app.js new file mode 100644 index 000000000..a7ead76f1 --- /dev/null +++ b/apps/r2d2clk/app.js @@ -0,0 +1,67 @@ +Graphics.prototype.setFontUndo = function(scale) { + // Actual height 19 (20 - 2) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqKAqooCqigKqKAAAACgAAKAAAoAACgAAAAAAAAACgAAKAAAoAACgAAAAAAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAKqqAqqoCqqgKqqAKCgAoKAAAAAKgoAqCgKqKAqooKiioqKKiooqKiioKKqAoqoCgqAKCoAAAACoCgKgKAiCoCIKgKioAqKgACoAAKgACoAAKgACoqAKioCoIgKgiAoCoCgKgAAAAKqgAqqAKqqAqqoKiioqKKiooqKiioKAKAoAoCgCgKAKAAAACgAAKAAAoAACgAAAAAAKqgAqqAKqqAqqoCgCgKAKAAAACgCgKAKAqqoCqqgCqoAKqgAAAACigAKKAAqoACqgAqoACqgAKqAAqoAAqoACqgAKKAAooAAAAAAoAACgAAKAAAoAAqqACqoAKqgAqqAAKAAAoAACgAAKAAAAAAACoAAKgAAqAACoAAAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAAAAACgAAKAAAoAACgAAAAAAoAACgAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAqAACoAAKAAAoAAAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoAAAAKCqAoKoCiqgKKqAoooCiigKKKAoooCqigKqKAKgoAqCgAAAAoAoCgCgKAKAoAoCiigKKKAoooCiigKqqAqqoAqqACqoAAAACqAAKoAAqoACqgAAKAAAoAACgAAKAAqqoCqqgKqqAqqoAAAAKoKAqgoCqigKqKAoooCiigKKKAoooCiqgKKqAoKgCgqAAAAAKqgAqqAKqqAqqoCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKAAAoAACgAAKAAAqqoCqqgCqqAKqoAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCqqgKqqAKqgAqqAAAAAKgAAqAAKqAAqoACigAKKAAooACigAKqqAqqoAqqgCqqAAAACgCgKAKAoAoCgCgAAAAoAqCgCoKAKgoAqAAAAAKAAAoAAKoAAqgAKqgAqqAKgqAqCoCgCgKAKAAAAAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAoKACgoAKCgAAAAKAKAoAoCoKgKgqAKqgAqqAAqgACqAACgAAKAAAAACgAAKAAAoAACgAAKKKAoooCiigKKKAqoACqgACoAAKgAAAAACqqAKqoCqqgKqqAoAACgAAKCoAoKgCiqgKKqAoooCiigKqqAqqoAqqACqoAAAAAqqgCqqAqqoCqqgKKAAooACigAKKAAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAoooCiigKKKAoooCqqgKqqAKCgAoKAAAAAKqgAqqAKqqAqqoCgCgKAKAoAoCgCgKAKAoAoCgCgKAKAAAACqqgKqqAqqoCqqgKAKAoAoCoKgKgqAKqgAqqAAqgACqAAAAACqoAKqgCqqgKqqAoooCiigKKKAoooCgCgKAKAoAoCgCgAAAAKqoAqqgKqqAqqoCigAKKAAooACigAKAAAoAACgAAKAAAAAAAqqACqoAqqoCqqgKAKAoAoCiigKKKAoqoCiqgKKqAoqoAAAAKqqAqqoCqqgKqqAAoAACgAAKAAAoACqqgKqqAqqoCqqgAAAAoAoCgCgKAKAoAoCqqgKqqAqqoCqqgKAKAoAoCgCgKAKAAAAAAKAAAoAACoAAKgAAKAAAoAACgAAKAqqoCqqgKqoAqqgAAAAKqqAqqoCqqgKqqACqAAKoACqoAKqgCoKgKgqAoAoCgCgAAAAqqgCqqAKqqAqqoAACgAAKAAAoAACgAAKAAAoAACgAAKAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAKgAAqAAKqqAqqoCqqgKqqAAAACqqgKqqAqqoCqqgCoAAKgAAKgAAqAAqqoCqqgKqqAqqoAAAACqoAKqgCqqgKqqAoAoCgCgKAKAoAoCqqgKqqAKqgAqqAAAAAqqoCqqgKqqAqqoCigAKKAAooACigAKqAAqoAAqAACoAAAAAAqqACqoAqqoCqqgKAKAoAoCgKgKAqAqqoCqqgCqqAKqoAAAAKqqAqqoCqqgKqqAooACigAKKgAoqACqqgKqqAKioAqKgAAAAKgoAqCgKqKAqooCiigKKKAoooCiigKKqAoqoCgqAKCoAAAACgAAKAAAoAACgAAKqqAqqoCqqgKqqAoAACgAAKAAAoAAAAAAKqoAqqgCqqgKqqAAAoAACgAAKAAAoCqqgKqqAqqgCqqAAAAAqqACqoAKqoAqqgAAKgAAqAACoAAKgKqoAqqgCqoAKqgAAAACqqgKqqAqqoCqqgACoAAKgACoAAKgAAKgAAqAKqqAqqoCqqgKqqAAAACoKgKgqAqqoCqqgAqgACqAAKoAAqgAqqoCqqgKgqAqCoAAAAKoAAqgACqgAKqAAAqoACqgAKqAAqoCqgAKqAAqgACqAAAAAAoCoCgKgKCqAoKoCiqgKKqAqooCqigKoKAqgoCoCgKgKAAAACqqgKqqAqqoCqqgKAKAoAoAAAAKAAAoAACoAAKgAAKgAAqAAAqAACoAACoAAKgAAKgAAqAAAqAACoAACgAAKAAAACgCgKAKAqqoCqqgKqqAqqoAAA"), 32, atob("DQULDw0RDQUHBw0NBQ0FEQ0FDQ0NDQ0NDQ0FBQsNCw0RDQ0NDQ0NDQ0NDQ0NDw0NDQ0NDQ0NDQ8NDQ0HEQc="), 22+(scale<<8)+(1<<16)); + return this; +}; + +var IMAGEWIDTH = 117; +var IMAGEHEIGHT = 60; +var r2d2 = require("heatshrink").decompress(atob("us8yEB+++++eAIQFB33/74LEA4gLLEI4LLAIw5NIqAPE/3/AAgJB63c30b1ubzs5yn6ysazs60tbBYO9/Xe/ghCAA/eM6Jz0HLHnngBBC4JqD999BIPvrp2FA4ILCvoKE74hDE4ILKTog5LBYY5SAAP+78d2u7ynauf4qcXmU1kO0kO1AIW0jOUAIYHBlP1nV1C4Nj2+EvO9eIJHFvpFLOaW+ObQ5XBYjpDDIIjCdJKjGBZXfEIY7HBYh1GHJJ1MVof+BIe+jbhBoW2bIV0b4LVBmVWnV2AImWAIQJFuwTBlUVkPVjOVD4NC610jAvBLIpnF889Odq5IdJQLJdIRBBG4PfIIV9CIQJCBYWeAIYLHC4QhEC4wtGBZ4tIvxXGz2U7VK+zDCykymzbGALt2eIIrBF4NTi+dnRBE/xzpBbQ5PTIj7Bf4IJDAAIHBBZq7FE4IJBBY41BBYY5JBY/vvo3F0taoWVjN0W4K/BoTjhAJArCuwzBG4IHBzsZS4RzE99cOZoXFOZq5OvqtPBYzpTV4QvGdJgVCrppG3wLDHI4LJ//+AQO17dC6ytBlP1cNIBPHYI/BpXW4zJBP4hzNP6KtMBZKtFXJgBCzwRCGoIBCBIYLJGYIXMMYIJDFogLHEJhhCCIN8nClBkPVcvIBHIYMh2hLBZYhzOP7C5NEIi5P888AIL7E/4hBBIL9BBY1dBYS9CAAYhDI4QAD74LEHYIACHJYRC/3OvlCyznBnV2AoLn/AIJDCq0RqlC23fjp/IOYu+OYi5BP4ytMBZStGXJTpDDIIjCHYwLCbowHBBZHfEIbpHBYh1GHJIAB0ualPVkO1cv7rLAIMZysp+m+jhpCzpzTVqwLEVpjpHIIo1BvoRCIIffBZABCBYw1FIIILBN44LJzwHC72EzMA6aXBmUVlP1AP4BLJ4MRqkI+eU7JzQBYihEXKatJcYw5GEYYvC888foIjE/4HBBZo7G/4JBBY41BBYY5Hc4OtzU6utTm9ju9bAP4BQseXqcXnVV30cNAXeP4jpFVoR/CUIt9XJ19Vp6nBHIyvGEoIjCBYgHBBYgjEBYYvHBYhpF3whCLoItEQIPW7u1/nOvvW3vOzvWAP4BTK4N92vc89eS4p/KVoy5GeoytOBY6tCGYI1wN5YrB73nnu+jnGrqNBAP4Bb41c2v8dYbjUBYiVDXIytYCYPnngBBF44JBDILpGroLCHYwhDHY4LEHYppBF4LnDniJBAP4Bf308AIP3UYR5BVpK5DVpgLKVpgLEdIW+e4TRBHYwJBC4QLD74LGF4ghDF44LEdIYlB7wJBcoKBBYv7rmjnGvhzFP4ytEXIytHBYitJBYwtFAoLjHro1CAAgdCBZg1ICoILBGowhF73+3v841dYf4BmrprB52dSop/Erq5VVpi5NF43nng7CF4tdBZovJBY41BBIWd88930cX/7npdYvvvytKXIjdBXJz1FVpQLBBIILC3zrCHYghBC4Q7D74LIAIQLGHYt9BYQtFGoOdCoJ7B308YP4Bt30c5zHBUYR/BRYahEXKatJXJgdCroBBFIn/CIQLBIIIAEBZQpBEIQ1IBYbvC78d3v841dXf4BurrrB73d++dP4bjEVqYLEVpgLEdIfnngBBdI4JBeoQvFroLCHYwhDHY4tHO4U8UNE8519629AIWdAKW9419JNYDBUI65DVpgLKVoy5KdITrCBYS7BAIQHBbYILVAIW+BIgtFBAMe3v841dT9ChC30cAK69CJNQvB739XASLEUJy5LVogLLAojxDzz7B99dBIYABDYIJBAYILGroXBbIZbDCoVdF4QtD3ydCAIKdo73+zsamU1oW2AKcyq2dnYfBdNfW3rjCRYS5FVpgLJVoq5NdKLdCBZYvG/4JBBY/nr29/nGrrpryn6kO0dYMymwBGqwJImsh2udjbprO4O+Q4Md++dRYLdHXJz1GVpLhBBYm+AIYLBAoWeBYoBEBYQTEBZoJBBYfeBoPW3u+jibqdIWVjUqis6u06ywDBoW3pX6AIYHBB4oXBzs7dNlcPYJ/BQoKvFXI4HDBZatIBZQDBd4PvroRBfgfvvoLCvoJDBZnfD4ILBE4ILGC4M940841dAITpp/rpGc4PZoW2kNtjMJkONdIILBnV1dOZ9CriKDXIytPBYKtLBZDpFAILpKHY4jCdKf3vq3B3zp2oXYbYMIskAAAkIwgPC3Dp0rm+jnvvzpOVoYLKVo7bDdJDrCAIQHCYoQLDA4gLLEI4LG33e7ppBTNrpHpX5iHHc4oADjHopX6dO4xBUI65PV4y5MEo7xEzz7B99dBIYAB999BYV9BY1dBYIbBBQnfBYgvB//W3rp1oWXoWWgFCdJMIoQRBnWXdOk852dUJCtLBZKtFXIwJBXIjpREYQLLF5f/7wIBNYU8dObVBoW3boLpKsdC3Dp14094yXBnrdKXJb1GbojpMANpNCrznCrqRmE4IBDdI92pX6kNLdJMhtgPBCYLpeNKoVBaIN+RYK9uzzxDfYjFBvoJB99dBY1dBYV9BYohDE4IKE/y1B5296wBp7vOzqVBZILpFoWXoXYjMpc40qoXZB4LperpXXKoP37//7ytOBZStGXJTpFEYQ3BdI7dGBZXfHZe9/dTjFjzABnqc3Y4LtB739dIrrC29K/MyiUp58ymQHBBYIRDdLLNB3v8IINbvBXTpX3408Y4bpMXI6tJXJTpCAIQRBeIYLFAIgLCCYgLNBIIzC1t7hMUkP1lIBmiM0wma78edJGWdYWWoXZpX6AYILDAYbpQroHH6292vcIIMh2pXThHzyn6Xo65HYo6tJBZ7xEzz7Bf4IJDAAIHBBZrfDAAYJBAILpC3kh2ijEAMd2SYOM3THBdJQhQdJfOvrdBAIIFBBorpDEIMyqxZTjO0xmZUI19XJ19VpK5G74LEdKd9BIIvHdJXfCoQhC0t7dP7pX52d4191u71ub408cYLpfkO1wl5VqQLJVoi5LdIIBBDYIRCd4IBCA4ILXAIW+BYn/zs6MoLp/dKbnB30dvlZtfYtfZukZ1u8dYbpbjO0K4KLBS4ahKXJatFXJTpCz3nnjxBYYYABEIILEAAgHBBYV9BQnfBIIBBI4QAEzsadP4ZFYIzpG519419c4Nj3F8nIBBdYNz/G+jj5BdLmUyn6UYVcVpgLKVo3fBZLpDBoLpMbooLLF4QBBdP7pJoW2mU3lP2mU2lP3oTpIaoW8tfYujnCdYm4ztbCILpe7TTDbpF9BYS5HdK++coXfAoInBCIQJCBYWeAIYLHC4QhEC4oxB0tbdP7jBpXV51y73yukUdYLpJ1u7dJk7dL21xmZZIatPBZzbGBZDxDzz7Bf4IJDAAIHBBZrfDAAYJBAILpCzkh2jp8u0Zu21+n/+BaB++ylUWeoLpF51941cc4LrBc4dz/IHBcYPOzrpdwl5UI19XJ19VpK5G74LEdJAlBEYQLEA4ILEEYgLDF44VCKIWlvbp+y0h2+M2v/+LrB52TlP2Y4LpE/rnB62d2vbuf4te4se4c4OlrbnBCITpdzLRFXIz1GVpoLM3wBBD4TjGGoV9DoQLD74LGd44LH/+dnUh2rp9A4Mqi2M6mlydC6sym4LCdItdAILZB30dzsbyn6b4IJB518dL2UxnabIi5FVo4LEVpQLF3zbEdIOe888AILFFCoIJBfoILGroLCI4IAEEIbpIjTp/oQXCkO3jOXmU2oW2dI3+a4IBD52dboLlCzoNEfITpbyn6S4atMBZStHXJLpC3wZBaIXfBomeBIIjHBZXfBIIjBC45hBhMUX4IBniNUwl6dIeM7UZughVC4JRB63e408AKLpB1u7D4Mh2g1TgHzK4LTCUIOdXI2dVpILE3y5IBYwFCcY7vFAIXvvwLCvwLGcZI1EAwOt3lCy1bnFbrABkq9K62dnffjzrB0t7pX3qdXEKU3C4IbBD4POvoBR63d30cIINTi4DBK6M6y2VjSTDa4S5DUIK5IVoQVD365GC4YLFdJHnn3nr3vAIgHBBZv3eoLpECoU+CYRNBv4BsvxDCJYYhaM4ZvFAJYTCr41aEoo9Drn3vv3z5BDXJ55Ev7jBc4PnnjpNAKjpJB4hZBKoItBAJPvAIQPLAJxlBD4KLBAIQ1CG5ghKD4gBVKqxJCDYKbDaIIJCc4IRCX6whBdId9dYIFBeIYrC74RBdLGe//e/+9AYIhCJqxnB/4A/ADnvY7NeEIqjBEKydD3whEdIIjBnjrB/7pYMoPW7u13e17e+jgLCvwfSNYPfDYNrzF0jN8AP4BUK4Nr3HGnn376bUR4M+DYO+nm1/fW3qnBdMG+999++dAoTFSAIf33/GnuEvWM3WVjQzBEapFBzsagHzkO1AP4BXhHz0tbMYLpUzyTByn6wmawl541dU4LpWYILpGAoYADGYLpYvpNBc4OlvbpZDYMh6s6u06ywB/AKl2LYOtzbpWR4M+0tbTYKfB519dKyzBv7cC/wBC//fjvGvnOAIQTCdP7T/dP4hTEYTbB30b2va889xnZhMTjN0oW2aYWfdP4B/dP69TEIM6usZyjjBzsaAILnBlPVpX3dPlbJYMyq06AP4BUK4Mh2rp9oXXlP1jOVdIU6UoMqitTi7p9iN0mU1AP4BXjN0PYLp7pX3b4MZ2rpCjTp/CoPe/u+nnGrgB/AK++jne/x5XdNU6E4Mh2jp9C4I5B/4A/ADiZCnzp+yi/BEoLp/dP7p/dM+djUhyjp9G4POzudnQfBAP4BXzs7629TarpsrSjBAoLp9///RYMI+ch6oB/AK8JibNBMYLp+2udjQ")); + +// 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() { + //reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); + // draw r2d2 + g.drawImage(r2d2, (g.getWidth()/2)-(IMAGEWIDTH/2), g.getHeight()-IMAGEHEIGHT); + + var x = g.getWidth()/2; + var y = g.getHeight()/2 - 30; + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1).trim(); + var dateStr = require("locale").date(date).toUpperCase(); + + + // draw time + g.setFontAlign(0,0).setFont("Undo:3"); + g.clearRect(0,y-30,g.getWidth(),y+30); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 40; + g.setFontAlign(0,0).setFont("Undo"); + g.clearRect(0,y-10,g.getWidth(),y+20); // 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(); \ No newline at end of file diff --git a/apps/r2d2clk/app.png b/apps/r2d2clk/app.png new file mode 100644 index 000000000..94502501d Binary files /dev/null and b/apps/r2d2clk/app.png differ diff --git a/apps/r2d2clk/metadata.json b/apps/r2d2clk/metadata.json new file mode 100644 index 000000000..249180ac8 --- /dev/null +++ b/apps/r2d2clk/metadata.json @@ -0,0 +1,16 @@ +{ "id": "r2d2clk", + "name": "R2D2 Clock", + "shortName":"R2D2 Clock", + "version":"0.01", + "description": "A clock with R2D2's shiny metal face on it. :)", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"r2d2clk.app.js","url":"app.js"}, + {"name":"r2d2clk.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/r2d2clk/screenshot.png b/apps/r2d2clk/screenshot.png new file mode 100644 index 000000000..f0b3ca84f Binary files /dev/null and b/apps/r2d2clk/screenshot.png differ diff --git a/apps/ratchet_launch/metadata.json b/apps/ratchet_launch/metadata.json index 14ffec34a..45057b0b9 100644 --- a/apps/ratchet_launch/metadata.json +++ b/apps/ratchet_launch/metadata.json @@ -11,6 +11,5 @@ "storage": [ {"name":"ratchet_launch.app.js","url":"app.js"} ], - "sortorder": -10, "readme":"README.md" } diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog index 915fbc5d7..0a86ee3e3 100644 --- a/apps/rclock/ChangeLog +++ b/apps/rclock/ChangeLog @@ -5,3 +5,4 @@ 0.05: Changes which circle show minutes and seconds 0.06: Avoid function wrapper, use setUI for launcher Clock face smaller so no longer breaks widgets +0.07: Tell clock widgets to hide. diff --git a/apps/rclock/metadata.json b/apps/rclock/metadata.json index 77a036481..e5478aa6a 100644 --- a/apps/rclock/metadata.json +++ b/apps/rclock/metadata.json @@ -2,7 +2,7 @@ "id": "rclock", "name": "Round clock with seconds, minutes and date", "shortName": "Round Clock", - "version": "0.06", + "version": "0.07", "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", "icon": "app.png", "type": "clock", diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js index 9c219ab3d..2d6f84dc4 100644 --- a/apps/rclock/rclock.app.js +++ b/apps/rclock/rclock.app.js @@ -196,6 +196,9 @@ const drawHR = function () { } }; +// Show launcher when button pressed +Bangle.setUI("clock"); + // clean app screen g.clear(); Bangle.loadWidgets(); @@ -222,5 +225,3 @@ Bangle.on('HRM', function (d) { // draw now drawClock(); -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index b80dfef94..cb2108e8b 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -2,4 +2,9 @@ 0.02: Fix typo to Purple 0.03: Added dependancy on Pedometer Widget 0.04: Fixed icon and png to 48x48 pixels -0.05: added charging icon \ No newline at end of file +0.05: added charging icon +0.06: Add 12h support and autocycle control +0.07: added localization, removed deprecated code +0.08: removed unused font, fix autocycle, imported suncalc and trimmed, removed pedometer dependency, "tap to cycle" setting +0.09: fix battery icon size +0.10: Tell clock widgets to hide. diff --git a/apps/rebble/KdamThmor.ttf b/apps/rebble/KdamThmor.ttf new file mode 100644 index 000000000..ca484ccbd Binary files /dev/null and b/apps/rebble/KdamThmor.ttf differ diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json index b26fb6a27..91d66df3d 100644 --- a/apps/rebble/metadata.json +++ b/apps/rebble/metadata.json @@ -2,11 +2,11 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.05", + "version": "0.10", "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", "readme": "README.md", "icon": "rebble.png", - "dependencies": {"mylocation":"app", "widpedom":"app"}, + "dependencies": {"mylocation":"app"}, "screenshots": [{"url":"screenshot_rebble.png"}], "type": "clock", "tags": "clock", @@ -14,6 +14,7 @@ "storage": [ {"name":"rebble.app.js","url":"rebble.app.js"}, {"name":"rebble.settings.js","url":"rebble.settings.js"}, - {"name":"rebble.img","url":"rebble.icon.js","evaluate":true} + {"name":"rebble.img","url":"rebble.icon.js","evaluate":true}, + {"name":"suncalc","url":"suncalc.js"} ] } diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js index 7c7d57939..1102aa93f 100644 --- a/apps/rebble/rebble.app.js +++ b/apps/rebble/rebble.app.js @@ -1,17 +1,20 @@ -var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +var SunCalc = require("suncalc"); const SETTINGS_FILE = "rebble.json"; const LOCATION_FILE = "mylocation.json"; +const GLOBAL_SETTINGS = "setting.json"; let settings; let location; - -Graphics.prototype.setFontLECO1976Regular22 = function(scale) { - // Actual height 22 (21 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/nA/+cD/5wP/nAAAAAAAAPwAA/gAD+AAPwAAAAAD+AAP4AA/gAAAAAAAAAAAAAcOAP//A//8D//wP//AHDgAcOAP//A//8D//wP//AHDgAAAAAAAAH/jgf+OB/44H/jj8OP/w4//Dj/8OPxw/4HD/gcP+Bw/4AAAAAAAP+AA/8AD/wQOHHA4c8D//wP/8A//gAD4AAfAAH/8A//wP//A84cDjhwIP/AA/8AB/wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8ABwAAAAAAAAD8AAP4AA/gAD8AAAAAAAAAAAEAAD+AB//A///v/D//gB/wABwAAAAAADgAA/wAf/4P8///wf/4AP8AAOAAAAAAAAAyAAHcAAPwAD/gAP/AA/8AA/AAH8AAMwAAAAAAAAAAAAADgAAOAAA4AAf8AD/wAP/AA/8AAOAAA4AADgAAAAAAAAAAD8AAfwAB/AAD8AAAAAAAADgAAOAAA4AADgAAOAAA4AADgAAAAAAAAAADgAAOAAA4AADgAAAAAAAAABwAB/AA/8A//gP/gA/wADwAAIAAAAAAD//wP//A//8D//wOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA4AcDgBwOAHA//8D//wP//A//8AABwAAHAAAcAAAAAAAA+f8D5/wPn/A+f8DhxwOHHA4ccDhxwP/HA/8cD/xwP/HAAAAAAAAOAHA4AcDhxwOHHA4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/wAP/AA/8AD/wAAHAAAcAABwAAHAA//8D//wP//A//8AAAAAAAA/98D/3wP/fA/98DhxwOHHA4ccDhxwOH/A4f8Dh/wOH/AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccDh/wOH/A4f8Dh/wAAAAAAAD4AAPgAA+AADgAAOAAA4AADgAAP//A//8D//wP//AAAAAAAAP//A//8D//wP//A4ccDhxwOHHA4ccD//wP//A//8D//wAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA//8D//wP//A//8AAAAAAAAOA4A4DgDgOAOA4AAAAAAAAOA/A4H8DgfwOA/AAAAAAAAB4AAPwAA/AAD8AAf4ABzgAPPAA8cAHh4AAAAAAAAAAAAHHAAccABxwAHHAAccABxwAHHAAccABxwAHHAAAAAAAAAOHAA4cADzwAPPAAf4AB/gAD8AAPwAAeAAB4AAAAAAAAA+AAD4AAPgAA+ecDh9wOH3A4fcDhwAP/AA/8AD/wAP/AAAAAAAAAP//4///j//+P//44ADjn/OOf845/zjnHOP8c4//zj//OP/84AAAAAAAP//A//8D//wP//A4cADhwAOHAA4cAD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA//8D//wP9/A/j8AAAAAAAA//8D//wP//A//8DgBwOAHA4AcDgBwOAHA4AcDgBwOAHAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA8A8D//wH/+AP/wAf+AAAAAAAAD//wP//A//8D//wOHHA4ccDhxwOHHA4ccDhxwOAHA4AcAAAAAAAA//8D//wP//A//8DhwAOHAA4cADhwAOHAA4cADgAAOAAAAAAD//wP//A//8D//wOAHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA//8D//wP//A//8ABwAAHAAAcAABwAP//A//8D//wP//AAAAAAAAP//A//8D//wP//AAAAAAAAOAHA4AcDgBwOAHA4AcDgBwOAHA//8D//wP//A//8AAAAAAAA//8D//wP//A//8AHwAA/AAP8AB/wAPn/A8f8DB/wIH/AAAAAAAAP//A//8D//wP//AAAcAABwAAHAAAcAABwAAHAAAAAAAAP//A//8D//wP//Af8AAP+AAH/AAD8AAHwAD/AB/wAf8AP+AA//8D//wP//AAAAAAAAP//A//8D//wP//AfwAAfwAAfwAAfwAAfwP//A//8D//wAAAAAAAAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//wP//A//8D//wAAAAAAAD//wP//A//8D//wOHAA4cADhwAOHAA/8AD/wAP/AA/8AAAAAP//A//8D//wP//A4AcDgBwOAHA4AcD//+P//4///j//+AAA4AADgAAAP//A//8D//wP//A4eADh+AOH8A4f4D/3wP/HA/8MD/wQAAAAAAAD/xwP/HA/8cD/xwOHHA4ccDhxwOHHA4f8Dh/wOH/A4f8AAAAAAAA4AADgAAOAAA//8D//wP//A//8DgAAOAAA4AADgAAAAAA//8D//wP//A//8AABwAAHAAAcAABwP//A//8D//wP//AAAADAAAPgAA/wAD/4AB/8AA/8AAfwAB/AA/8Af+AP/AA/wAD4AAMAAA4AAD+AAP/gA//8AH/wAB/AAf8Af/wP/4A/4AD/gAP/4AH/8AB/wAB/AB/8D//wP/gA/gADgAAIABA4AcDwDwPw/Afn4Af+AA/wAD/AA//AH5+A/D8DwDwOAHAgAEAAAAP/AA/8AD/wAP/AAAf8AB/wAH/AAf8D/wAP/AA/8AD/wAAAAAAAADh/wOH/A4f8Dh/wOHHA4ccDhxwOHHA/8cD/xwP/HA/8cAAAAAAAAf//9///3///f//9wAA3AADcAAMAAAOAAA/gAD/wAH/8AB/8AA/wAAPAAAEAAAAHAADcAANwAB3///f//9///wAA"), 32, atob("BwYLDg4UDwYJCQwMBgkGCQ4MDg4ODg4NDg4GBgwMDA4PDg4ODg4NDg4GDQ4MEg8ODQ8ODgwODhQODg4ICQg="), 22+(scale<<8)+(1<<16)); -} +let is12Hour; Graphics.prototype.setFontKdamThmor = function(scale) { - // Actual height 72 (71 - 0) - g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAH+AAAAAAAAAAAAAP/AAAAAAAAAAAAAf/gAAAAAAAAAAAA//gAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//wAAAAAAAAAAAA//gAAAAAAAAAAAAf/gAAAAAAAAAAAAf/AAAAAAAAAAAAAP+AAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAAAAAAAAAAAB/AAAAAAAAAAAAAP/AAAAAAAAAAAAA//AAAAAAAAAAAAH/+AAAAAAAAAAAAf/+AAAAAAAAAAAD//+AAAAAAAAAAAf//8AAAAAAAAAAB///4AAAAAAAAAAP///wAAAAAAAAAA////AAAAAAAAAAH///4AAAAAAAAAAf///gAAAAAAAAAD///8AAAAAAAAAAf///wAAAAAAAAAB///+AAAAAAAAAAP///4AAAAAAAAAA////AAAAAAAAAAH///4AAAAAAAAAAf///gAAAAAAAAAD///8AAAAAAAAAAP///wAAAAAAAAAB///+AAAAAAAAAAP///4AAAAAAAAAA////AAAAAAAAAAD///4AAAAAAAAAAP///gAAAAAAAAAAf//8AAAAAAAAAAA///wAAAAAAAAAAA//+AAAAAAAAAAAA//4AAAAAAAAAAAA//AAAAAAAAAAAAA/4AAAAAAAAAAAAA/gAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//AAAAAAAAAAAP///+AAAAAAAAAD/////wAAAAAAAAP/////+AAAAAAAA///////gAAAAAAD///////4AAAAAAH///////8AAAAAAf///////+AAAAAA/////////gAAAAB/////////wAAAAB/////////wAAAAD/////////4AAAAH///AAA///8AAAAH//wAAAB//8AAAAP/+AAAAAP/+AAAAP/4AAAAAD/+AAAAf/gAAAAAA//AAAAf/AAAAAAAf/AAAAf+AAAAAAAP/AAAA/+AAAAAAAP/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/8AAAAAAAH/gAAA/+AAAAAAAP/gAAAf+AAAAAAAP/AAAAf/AAAAAAAf/AAAAf/gAAAAAA//AAAAP/4AAAAAD/+AAAAP/+AAAAAP/+AAAAH//gAAAA//8AAAAH///AAAf//8AAAAD/////////4AAAAD/////////wAAAAB/////////wAAAAA/////////gAAAAAf////////AAAAAAH///////8AAAAAAD///////4AAAAAAA///////gAAAAAAAP/////+AAAAAAAAD/////4AAAAAAAAAf////AAAAAAAAAAA///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAADwAAAAAAAAAAAAAD4AAAAAAAAAAAAAH8AAAAAAAAAAAAAP+AAAAAAAAAAAAAf/AAAAAH/AAAAAA//AAAAAH/AAAAAB//AAAAAH/AAAAAB//AAAAAH/AAAAAD/+AAAAAH/AAAAAH/8AAAAAH/AAAAAP/4AAAAAH/AAAAAf/wAAAAAH/AAAAA//gAAAAAH/AAAAB//gAAAAAH/AAAAB//AAAAAAH/AAAAD/+AAAAAAH/AAAAH/8AAAAAAH/AAAAP//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/AAAAAAHgAAAAAD/AAAAAA/wAAAAAH/AAAAAD/wAAAAAP/AAAAAH/wAAAAAf/AAAAAP/wAAAAA//AAAAA//wAAAAB//AAAAB//wAAAAD//AAAAB//wAAAAH//AAAAD//wAAAAP//AAAAH//wAAAAf//AAAAH//gAAAA///AAAAP/+AAAAB///AAAAP/4AAAAD///AAAAf/gAAAAH///AAAAf/AAAAAP///AAAAf+AAAAAf///AAAAf+AAAAA//v/AAAA/8AAAAB//P/AAAA/8AAAAD/+P/AAAA/8AAAAH/8P/AAAA/8AAAAP/4P/AAAA/8AAAAf/wf/AAAA/8AAAA//gf/AAAA/8AAAD//Af/AAAA/8AAAH/+Af/AAAA/+AAAP/8Af/AAAA/+AAAf/4Af/AAAAf/AAB//wAf/AAAAf/gAD//gAf/AAAAf/wAf//AAf/AAAAf/+D//+AAf/AAAAP/////8AAf/AAAAP/////4AAf/AAAAH/////wAAf/AAAAH/////gAAf/AAAAD/////AAAf/AAAAB////8AAAf/AAAAA////4AAAf/AAAAAf///wAAAf/AAAAAP///AAAAf/AAAAAD//8AAAAf/AAAAAA//gAAAAP/AAAAAABwAAAAAH/AAAAAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAfAAAAAAAAHgAAAA/wAAAAAAA/wAAAA/8AAAAAAD/wAAAB/+AAAAAAH/wAAAB//AAAAAAP/wAAAB//gAAAAA//wAAAB//wAAAAB//wAAAB//4AAAAB//wAAAB//8AAAAD//wAAAA//8AAAAH//wAAAAf/+AAAAH//gAAAAH/+AAAAP/+AAAAAB//AAAAP/4AAAAAA//AAAAf/gAAAAAAf/AAAAf/AAAAAAAf/AAAAf/AAAAAAAP/gAAAf+AAAAAAAP/gAAA/+AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAD/gAAH/gAAA/8AAH/wAAH/gAAA/8AAH/wAAP/gAAA/+AAH/wAAP/AAAAf/AAP/4AAf/AAAAf/AAf/4AA//AAAAf/wA//8AB//AAAAf/+H//+AD/+AAAAP//////4f/+AAAAP////v////8AAAAH////v////8AAAAH////H////4AAAAD////H////wAAAAB///+D////wAAAAA///8D////gAAAAAf//4B////AAAAAAP//wA///+AAAAAAD//AAf//4AAAAAAAf4AAH//gAAAAAAAAAAAB/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAD/gAAAAAAAAAAAAP/wAAAAAAAAAAAAf/wAAAAAAAAAAAA//wAAAAAAAAAAAD//wAAAAAAAAAAAH//wAAAAAAAAAAAP//wAAAAAAAAAAA///wAAAAAAAAAAB///wAAAAAAAAAAD///wAAAAAAAAAAP///wAAAAAAAAAAf///wAAAAAAAAAA//9/wAAAAAAAAAD//x/wAAAAAAAAAH//h/wAAAAAAAAAP/+B/wAAAAAAAAA//8B/wAAAAAAAAB//4B/wAAAAAAAAD//gB/wAAAAAAAAP//AB/wAAAAAAAAf/+AB/wAAAAAAAA//4AB/wAAAAAAAD//wAB/wAAAAAAAH//AAB/wAAAAAAAP/+AAB/wAAAAAAA//8AAB/wAAAAAAB//wAAB/wAAAAAAD//gAAB/wAAAAAAP/+AAAB/wAAAAAAf/8AAAB/wAAAAAAf/5////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAf//////////AAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/wAAAAAAAAAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAPgAAAAAAAAAAAAA/wAAAAAAAAD4AAB/4AAAAAAAD/4AAB/4AAAAAAD//4AAB/8AAAAAD///8AAB/8AAAAD////8AAB/+AAAAf////8AAA/+AAAAf////+AAA/+AAAAf////+AAAf/AAAAf////8AAAf/AAAAf////8AAAP/AAAAf////8AAAP/AAAAf//4/4AAAH/gAAAf/8A/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AA/4AAAH/gAAAf+AB/4AAAH/gAAAf+AB/4AAAH/gAAAf+AA/8AAAP/gAAAf+AA/8AAAP/AAAAf+AA/8AAAP/AAAAf+AA/+AAAf/AAAAf+AA/+AAA//AAAAf+AA//AAB/+AAAAf+AAf/gAD/+AAAAf+AAf/4Af/8AAAAf+AAf/////8AAAAf+AAP/////4AAAAf+AAP/////wAAAAf+AAH/////wAAAAf+AAD/////gAAAAf+AAD/////AAAAAf+AAB////+AAAAAf8AAA////8AAAAAf4AAAP///wAAAAAfgAAAH///AAAAAAAAAAAA//8AAAAAAAAAAAAH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/4AAAAAAAAAAAB///AAAAAAAAAAAP///wAAAAAAAAAA////8AAAAAAAAAB////+AAAAAAAAAH/////AAAAAAAAAP/////gAAAAAAAA//////wAAAAAAAB//////4AAAAAAAH//////8AAAAAAAP//////8AAAAAAAf//+AP/+AAAAAAB///4AB/+AAAAAAD///wAA/+AAAAAAH///gAAf/AAAAAAf///AAAP/AAAAAA///+AAAP/AAAAAB///+AAAH/gAAAAH//3+AAAH/gAAAAP//v8AAAH/gAAAAf//P8AAAH/gAAAB//+P8AAAH/gAAAD//4f8AAAH/gAAAH//wf8AAAH/gAAAP//gf8AAAH/gAAAf//Af8AAAH/gAAAf/8Af+AAAH/gAAAf/4Af+AAAP/AAAAf/wAf+AAAP/AAAAf/gAf/AAAf/AAAAf/AAP/gAA//AAAAf8AAP/wAB/+AAAAf4AAP/4AD/+AAAAfwAAP//Af/8AAAAfgAAH/////8AAAAeAAAH/////4AAAAcAAAD/////4AAAAYAAAD/////wAAAAQAAAB/////gAAAAAAAAA/////AAAAAAAAAAf///+AAAAAAAAAAP///8AAAAAAAAAAD///wAAAAAAAAAAB///AAAAAAAAAAAAP/8AAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf8AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAAAAAAAf+AAAAAAAABAAAAf+AAAAAAAAHAAAAf+AAAAAAAAfAAAAf+AAAAAAAB/AAAAf+AAAAAAAH/AAAAf+AAAAAAAf/AAAAf+AAAAAAB//AAAAf+AAAAAAH//AAAAf+AAAAAAf//AAAAf+AAAAAB///AAAAf+AAAAAH///AAAAf+AAAAAf///AAAAf+AAAAB///+AAAAf+AAAAH///8AAAAf+AAAAf///wAAAAf+AAAA////AAAAAf+AAAD///8AAAAAf+AAAP///wAAAAAf+AAA////AAAAAAf+AAD///8AAAAAAf+AAP///wAAAAAAf+AA////AAAAAAAf+AD///8AAAAAAAf+AP///wAAAAAAAf+A////AAAAAAAAf+D///8AAAAAAAAf+P///wAAAAAAAAf+f//+AAAAAAAAAf////4AAAAAAAAAf////gAAAAAAAAAf///+AAAAAAAAAAf///4AAAAAAAAAAf///gAAAAAAAAAAf//+AAAAAAAAAAAf//4AAAAAAAAAAAf//gAAAAAAAAAAAf/+AAAAAAAAAAAAf/4AAAAAAAAAAAAf/gAAAAAAAAAAAAf+AAAAAAAAAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4AAAAAAAAAAAAB//gAAAAAAADwAAH//4AAAAAAA//AAf//8AAAAAAD//wA////AAAAAAP//4B////gAAAAAf//+B////wAAAAA////D////wAAAAB////H////4AAAAD////n////8AAAAH/////////8AAAAH/////////+AAAAP//////wP/+AAAAP//////AB/+AAAAf/wD//8AA//AAAAf/AA//4AAf/AAAAf+AAf/4AAP/AAAAf8AAP/wAAH/AAAA/8AAH/wAAH/gAAA/4AAH/gAAH/gAAA/4AAH/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAD/gAAD/gAAA/4AAH/gAAH/gAAA/8AAH/wAAH/gAAAf8AAP/wAAH/gAAAf+AAP/wAAP/AAAAf/AAf/4AAf/AAAAf/wB//8AAf/AAAAP/////+AB//AAAAP//////wH/+AAAAH/////////+AAAAH/////////8AAAAD////n////8AAAAB////H////4AAAAA////D////wAAAAAf//+B////wAAAAAP//8B////gAAAAAH//wA////AAAAAAB//AAf//8AAAAAAAH4AAH//4AAAAAAAAAAAB//gAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/8AAAAAAAAAAAAf//AAAAAAAAAAAB///wAAAAAAAAAAH///4AAAAAAAAAAP///+AAAAAAAAAAf////AAAAAAAAAA/////AAAADAAAAB/////gAAAHAAAAD/////wAAAPAAAAH/////wAAAfAAAAH/////4AAB/AAAAP//A//4AAD/AAAAP/4AH/8AAH/AAAAf/gAD/8AAP/AAAAf/AAB/8AA//AAAAf+AAA/8AB//AAAAf+AAA/8AD//AAAA/8AAAf8AH//AAAA/8AAAf8Af//AAAA/8AAAf8A//+AAAA/8AAAf8B//+AAAA/8AAAf8D//8AAAA/8AAAf8P//wAAAA/8AAAf8f//gAAAA/8AAAf4//+AAAAA/8AAAf5//8AAAAA/8AAAf3//4AAAAAf+AAA////gAAAAAf+AAB////AAAAAAf/AAB///8AAAAAAf/gAD///4AAAAAAP/4AP///gAAAAAAP//B////AAAAAAAH//////+AAAAAAAH//////4AAAAAAAD//////wAAAAAAAB//////AAAAAAAAA/////+AAAAAAAAAf////4AAAAAAAAAP////wAAAAAAAAAH////AAAAAAAAAAB///8AAAAAAAAAAAf//gAAAAAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAD8AAAAAAAA/4AAAP+AAAAAAAB/8AAAf/AAAAAAAB/+AAAf/gAAAAAAD/+AAA//gAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD//AAA//wAAAAAAD/+AAA//gAAAAAAB/+AAAf/gAAAAAAA/8AAAP/AAAAAAAAf4AAAH+AAAAAAAAHgAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("FCM0NDQ0NDQ0NDQ0GA=="), 90+(scale<<8)+(1<<16)); + // Actual height 70 (69 - 0) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('AH4AMgfABZM/BZMB/4WJg/+BZMf/ALJ//gIpP/wAugLpUAvyBKsDC/ACKYJQIKYJgaYKv6YJh7HJeoP8VxLSJg//+D0JIhMf/7RIf4JPJv//LX5a6CwLvJn5aJLYIKJgY4IADn/KpKvBAAKvIAARiGBQanGOwILJBQgLFFogvGIgZHGWAIAEdwg5FNYreBAAjvDeoIAFYQcfBYy3DEQRKEKQQiCAoRiCIogoDCIJGDEQLlEIwZoBCwYLCHQQoBQwgGEj7aFGoKuDKwYSFE4LZFv41Ch6dEIITICn5FEDwQuDeAwuEBQgeEB4b8EFwbADNIZdaHQoSBFwUfNIoGEv5GFXYpGEIoJBCZgjZGHQILDCwIpDj//GgQoBMggcBAApkDBQwiDDoQAEEQY0BERJGBERBGCERC8BBYrYFBQj8FLwrBGBQbkFEYoKFBYgtFL4jLFZ4gKJAH4AciALKRA73DbIgAFj/ABZLOGEQjDEj40En6tEv4oDgLPEAoLRFCIcHDgouJDgP4FxAiFFwt//xXEFwcDEQouEj4iEFwv/EQguEEQJ6EFwgiBS4guE/5uEFwiiBAAyiDBQwdDCw4uCIoIAGFwSLBF34unAAy7EAAy7EAAzqEAArqEF34ukAH4AGgfgNJWAAod8Cwn+SQn4RggFEv4oE/4FDg//FAYFFn4oEAoidBFAYFFh//YIYFBFwd//7BDAoIuCgf/YIYFBFwcfFAgFFDgIoDDgIFCEQpcBFwZFFn4uEAoJcEFwYFBLgouDQoo/BAwcf/hcEFwgiELgPfFwQRBEQYVBFwcPDYYzB+YSDn55DKwOPFwgbCKwP8CQYuBXIouEKIZcBIIgbF/BBEDYZcB4ASFDYI5BCgIuEHQSzCFwo6CeYQuEv4nBOYIPBFwa7Ddoa7FJoLtCFwhNBAAQfBFwiTBAAXAT4oKDCYSfFAAQ9BFwg6BAAQHBFwhDCLgQuFIwY5BFwhGDDwT9FOQI5CFwpSDDoYuDBYQWCFwoLCAgQuFCIsHFwgAFh4uEAH4AWjgLKvwGFj6LDP4sBcgjhCCwaGDn4LEgKjDAgKXEh61Dg7LEdQIuDj7AEZgIpDfYPACIgdCFwLjDdIQRCFwIoDEQJdEFAgiBJgYoEEQoLCAoRFFBYRjCFAIWDQII0Dv6SFv40CRYg1DHQRXBBQg1BFISpDBwQSEEQTQDj4SCDYJKBh42Cv4uCh4TCn4aBIIIuDCYIHBDQIeBFwYPBg4aCe4YPDfAYuHv4uNLo6bBLpJ4EFwYTBEQIHBCQYbBHQIqBEwIGCXYl/IQTwDD4P+CwIfBFILCCBAQACwACBEQQQBAArlDn4LGcoY3BGAIlEHQYAB+YiGMQIAB54DCOgRGD/0fEQpGD+A+CEQZ6BLYhFEKQX8HwYKDBYXgHwQ5DBYQpBBYQ5DHYRWDUQQAGgK5DADsBBZUfb4IAIOYoAETgJcFAAbLBBRBoBUQg5FRYxQDRYJGIZQQ5KFxDtCFxDpCFw7dIfAouICwQuHHIP+FxBQB8YuHf4UPFw6KCn4uGKAWAFw6KB/glBHJHAFw5QCQQIuGRQLzBFww5CKgRQH/A9BFwxQCFw45BCYQuGKAI5BFwwGBKAIuHRQRVCFwhQDFw6KBKAIuHfwQAEGAYKGGgbQCAAowCFwIAGF34ugAAjqHTojqFfQrqFcYoWJF0f+CxMH8ALJAEkCBZU8BRMB/CCKOw0DA4V/OwqhBA4IDBwAKFVoTlBBQytCn6xDBQX/IQQDDAgIACSwIRBTQQWDGwUHHQYzBAAK5CHQk/Fwo6EFwppBNoQuGgIPDFwYeCOoguC34eCh74DEASMCCQI+CDYQCBCQYuDDYMPFwQ6BFwYbBn4uCg4uE8ASBFwUfFwqIBCQV/FwsfLpAbBPgZdFFwpdGFwhdHDwQPELoYeCHwYbD/46CAYaMEBwLqFFwRGCv5RDFYUfBYIWBGQQuDv7iDMIQuCNIIADCwQuCfIgiDFwT5DEQYuDHQIiFVAc/EQyJDIwYiDc4RGDNAYuBCAJGDRYQHBCAQLDCwcPCAR+BHIgAEBYQKHEYQtDAH4Ak/gKJZALMBRhLGDAAjSGWYgLCEY7qDBYwtCXhBEBewzpF/5fGj4LDdYwKD//gKBBeHKAZGGHIX+gJGGKAQfBHQoSBCYQEB+A5GA4InBHQiJEQgKKGOIUPHQg5CFQU/HQaKDVgR1ERQQeCIwK8DBQPvDwUHFwZQB/0/DwUfFwaKB+IeDv4PCHIWHFw45B/geDFwjBCDwYPDEQKsCLoxFB+CIDCQIPCP4OAj6MCj4uEBAN/FQV/SAS0CFwIqBXYioCA4ZYBVwYbBHoIaCQAY+CHoPACwKADGwa+CEQcPFQIfBAARVCgE+dgiGCBYRVCHQLiFganEEQsIZQgiFAAZFGAAZGDNAYADcQSLDAAhSCVwYLHHI4LCCxC5FAH4AIJhRYBXgQAGh5vJgE/VI4uDSRAuJoAuJg4uKvguJg/wFxN/OAQuGaoIuJv/8FxAWBFxN/T4YuFCwIuJCwIuICwQuICwIuICwQGDFwgWCEQQuECwQpDFwk/BQIdDFwYPBCwguECwwuDCw4uDCw4uCCw4uDCw4uCCxAuCCxAuBCwYKEFwQWCRIYuD8YWIEAO/CxEPCoQWGLQYWHFwIWJJ4YWHFwYKGFwYWHFwYKHFwQWIFwQKHFwQWIFwQKIFwIWJdQQuJ8ALJAH8f/BuK/gIFv6RDBYqlBwEBSIIjFA4OAWgSSEA4WAv4LGA4TXC//Ab4v+j4LCwBYDAwP8DQTNEAwXzAYTCDFQfvAYRSDFQYADIwYqDAAZGCEQYAB8A6ENARHCDoI6DAgKKCD4N/HQQIB8ACBCYQGBAYMHE4IxBIQIPBHQU/DYIOBA4ISCDYQHBh4iCh7ICD4IaEAYJpCB4d/GwQuEGwasBDwYPBA4MHFw4HCj4uHA4QuULqyUDRgxCCRhC0Cn46CEwYbB+DhCYQa7DAAQyBcoIaBdQoLBawYrCAApRCHQILGKIT/C//7Eoh1DAAPvAYRRCIwkfEQpGD/AyDBQSBBCQQiGKQX+HwYiDKQXwGQRFDBYYyDNAYLCAwILCBQg+FHIgAEC4IKIQwKtCAH4AWnwKJPoKrEOAi3GaY4WJ/6KHW4ShIfwTbFAAMDCwX8A4UYHIrQE8AiFeYcHHwQiDKQZ6DEQZSCgYmDEQZGCj4uCEQQZBCYRtDNAPAg46Cg5hDv5aBBYI6Bn4aCRYInBDQIpCFwQTBGwQaBGQIuCn59Cn4uBSAgbDHoYuCE4JlCEwJjBCQUPEQUH/hjCFwaUCj/wHIKzDSgd/4AWBQAhhDcYTpDFwg5BUYYuE8Y5ELoufHIhdFaoguBYYbJESgjWDGgQHCH4IiDBQZZBCIIiCKAa7CIwIWCKAbPC8AWCKAZpCCgRQFIQhQGHQQADKAhOEKApGDAARQEIwZQHIwpQFBYpQFKQgWHPwYWHBYQWIEYREGL4YKJAH4AegIEDsCxGPIfgCwr/Dn6nFh6jCgKcGn/wEQQbDXgYqCn/4BQkDDwYPDFzV/JoUfB4RdOgI1DnjG/ACoA='))), + 46, + atob("GBo2NjY2NjY2NjY2Gg=="), + 94+(scale<<8)+(1<<16) + ); + return this; } var boot_img = require("heatshrink").decompress(atob("oFAwkEogA/AH4A/AH4A/AH4A/AE8AAAoeXoAfeDQUBmcyD7A+Dh///8QD649CiAfaHwUvD4sEHy0DDYIfEICg+Cn4fHICY+DD4nxcgojOHwgfEIAYfRCIQaDD4ZAFD5r7DH4//kAfRCIZ/GAAnwD5p9DX44fTHgYSBf4ofVDAQEBl4fFUAgfOXoQzBgIfFBAIfPP4RAEAoYAB+cRiK/SG4h/WIBAfXIA7CBAAswD55AHn6fUIBMCD65AHl4gCmcziAfQQJqfQQJpiDgk0IDXxQLRAEECaBM+QgRYRYgUIA0CD4ggSQJiDCiAKBICszAAswD55AHABKBVD7BAFABIqBD5pAFABPxD55AOD6BADiIAJQAyxLABwf/gaAPAH4A/AH4ARA==")); @@ -25,6 +28,7 @@ var sunSet = "00:00"; function log_debug(o) { //console.log(o); + } // requires the myLocation app @@ -33,12 +37,35 @@ function loadLocation() { } function loadSettings() { - settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green'}; + settings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true,'sideTap':0}; + //sideTap 0 = on | 1 = sidebar1... + + let tmp = require('Storage').readJSON(SETTINGS_FILE, 1) || settings; + for (const key in tmp) { + settings[key] = tmp[key] + } + + if(settings.sideTap!=0) + sideBar=parseInt(settings.sideTap)-1; //tab to show + is12Hour = (require("Storage").readJSON(GLOBAL_SETTINGS, 1) || {})["12hour"] || false; +} + +const zeroPad = (num, places) => String(num).padStart(places, '0') + +function formatHours(h) { + if (is12Hour) { + if (h == 0) { + h = 12; + } else if (h > 12) { + h -= 12; + } + } + return zeroPad(h, 2); } function extractTime(d){ var h = d.getHours(), m = d.getMinutes(); - return(("0"+h).substr(-2) + ":" + ("0"+m).substr(-2)); + return(formatHours(h) + ":" + zeroPad(m, 2)); } function updateSunRiseSunSet(lat, lon){ @@ -78,9 +105,12 @@ const wb = 40; // battery width function draw() { log_debug("draw()"); let date = new Date(); - let da = date.toString().split(" "); - let hh = da[4].substr(0,2); - let mm = da[4].substr(3,2); + let hh = date.getHours(); + let mm = date.getMinutes(); + + hh = formatHours(hh); + mm = zeroPad(mm,2); + //const t = 6; if (drawCount % 60 == 0) @@ -117,8 +147,11 @@ function draw() { function drawSideBar1() { let date = new Date(); - let da = date.toString().split(" "); + let dy=require("date_utils").dow(date.getDay(),1).toUpperCase(); + let dd=date.getDate(); + let mm=require("date_utils").month(date.getMonth()+1,1).toUpperCase(); + drawBattery(w2 + (w-w2-wb)/2, h/10, wb, 17); setTextColor(); @@ -126,7 +159,7 @@ function drawSideBar1() { g.setFontAlign(0, -1); g.drawString(E.getBattery() + '%', w3, (h/10) + 17 + 7); - drawDateAndCalendar(w3, h/2, da[0], da[2], da[1]); + drawDateAndCalendar(w3, h/2, dy, dd, mm); } function drawSideBar2() { @@ -203,7 +236,7 @@ function drawBattery(x,y,wi,hi) { g.clearRect(x+2,y+2+2,x+wi-4-2,y+2+hi-2); // centre g.setColor(g.theme.fg); g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact - g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level + g.fillRect(x+3, y+5, x +3 + E.getBattery()*(wi-10)/100, y+hi-1); // the level if( Bangle.isCharging() ) { @@ -214,20 +247,11 @@ function drawBattery(x,y,wi,hi) { } -function getSteps() { - if (WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom.getSteps(); - } - return '????'; -} - // format steps so they fit in the place function formatSteps() { - var s = getSteps(); + var s = Bangle.getHealthStatus("day").steps; - if ( s == '????') { - return s; - } else if (s < 1000) { + if (s < 1000) { return s + ''; } else if (s < 10000) { return '' + (s/1000).toFixed(1) + 'K'; @@ -236,20 +260,19 @@ function formatSteps() { } function nextSidebar() { + if (++sideBar > 2) sideBar = 0; log_debug("next: " + sideBar); + } function prevSidebar() { + if (--sideBar < 0) sideBar = 2; log_debug("prev: " + sideBar); + } -Bangle.setUI("clockupdown", btn=> { - if (btn<0) prevSidebar(); - if (btn>0) nextSidebar(); - draw(); -}); // timeout used to update every minute @@ -260,13 +283,29 @@ function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; - nextSidebar(); + if (settings.autoCycle) { + nextSidebar(); + } draw(); }, 60000 - (Date.now() % 60000)); } log_debug("starting.."); + +if(settings.autoCycle || settings.sideTap==0) +{ + Bangle.setUI("clockupdown", btn=> { + if (btn<0) prevSidebar(); + if (btn>0) nextSidebar(); + draw(); + }); +} +else{ + Bangle.setUI("clock"); +} + + g.clear(); Bangle.loadWidgets(); /* @@ -277,6 +316,11 @@ Bangle.loadWidgets(); for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} loadSettings(); loadLocation(); + + + + + draw(); // queues the next draw for a minutes time Bangle.on('charging', function(charging) { //redraw the sidebar ( with the battery ) @@ -288,4 +332,4 @@ Bangle.on('charging', function(charging) { drawSideBar2(); break; } -}); \ No newline at end of file +}); diff --git a/apps/rebble/rebble.settings.js b/apps/rebble/rebble.settings.js index db3bab878..37b7be3a1 100644 --- a/apps/rebble/rebble.settings.js +++ b/apps/rebble/rebble.settings.js @@ -2,37 +2,76 @@ const SETTINGS_FILE = "rebble.json"; // initialize with default settings... - let s = {'bg': '#0f0', 'color': 'Green'} + let localSettings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true, 'sideTap':0}; + //sideTap 0 = on| 1= sideBar1 | 2 = ... // ...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) || s; + let settings = storage.readJSON(SETTINGS_FILE, 1) || localSettings; + const saved = settings || {} for (const key in saved) { - s[key] = saved[key] + localSettings[key] = saved[key] } function save() { - settings = s + settings = localSettings storage.write(SETTINGS_FILE, settings) } var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; - E.showMenu({ - '': { 'title': 'Rebble 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.bg = bg_code[v]; - save(); + function showMenu() + { + const menu={ + '': { 'title': 'Rebble Clock' }, + '< Back': back, + 'Colour': { + value: 0 | color_options.indexOf(localSettings.color), + min: 0, max: 5, + format: v => color_options[v], + onchange: v => { + localSettings.color = color_options[v]; + localSettings.bg = bg_code[v]; + save(); + }, }, + 'Auto Cycle': { + value: localSettings.autoCycle, + onchange: (v) => { + localSettings.autoCycle = v; + save(); + showMenu(); + } + } + }; + + if( !localSettings.autoCycle) + { + menu['Tap to Cycle']= { + value: localSettings.sideTap, + min: 0, + max: 3, + step: 1, + format: v => NumberToSideTap(v), + onchange: v => { + localSettings.sideTap=v + save(); + setTimeout(showMenu, 10); + } + }; } - }); -}) + E.showMenu(menu); + } + + function NumberToSideTap(Number) + { + if(Number==0) + return 'on'; + return Number+""; + } + + showMenu(); +}) \ No newline at end of file diff --git a/apps/rebble/suncalc.js b/apps/rebble/suncalc.js new file mode 100644 index 000000000..d86f039c5 --- /dev/null +++ b/apps/rebble/suncalc.js @@ -0,0 +1,143 @@ +/* + (c) 2011-2015, Vladimir Agafonkin + SunCalc is a JavaScript library for calculating sun/moon position and light phases. + https://github.com/mourner/suncalc + + edit for banglejs +*/ + +(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 new Date((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 declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } + + +// 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; +} + +var SunCalc = {}; + + +// 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' ] +]; + + + +// 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: fromJulian(Jnoon), + nadir: 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]] = fromJulian(Jrise); + result[time[2]] = fromJulian(Jset); + } + + 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 window.SunCalc = SunCalc; + + +}()); \ No newline at end of file diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 140567068..1941a435b 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -20,3 +20,4 @@ 0.14: Remove unneeded variable assignment 0.15: Show distance more accurately in conjunction with new locale app (fix #1523) 0.16: Ability to append to existing track (fix #1712) +0.17: Use default Bangle formatter for booleans diff --git a/apps/recorder/app.js b/apps/recorder/app.js index fb3dfab4f..9006d2236 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -40,11 +40,9 @@ function getTrackNumber(filename) { } function showMainMenu() { - function boolFormat(v) { return v?"Yes":"No"; } function menuRecord(id) { return { value: settings.record.includes(id), - format: boolFormat, onchange: v => { settings.recording = false; // stop recording if we change anything settings.record = settings.record.filter(r=>r!=id); diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 8cf339e85..cc9762d20 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -12,9 +12,22 @@ + + + \ No newline at end of file diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js index 7b35d8a85..1919e7483 100644 --- a/apps/sleeplog/lib.js +++ b/apps/sleeplog/lib.js @@ -1,196 +1,465 @@ +// define accessable functions exports = { // define en-/disable function, restarts the service to make changes take effect - setEnabled: function(enable, logfile, powersaving) { - // check if sleeplog is available - if (typeof global.sleeplog !== "object") return; - - // set default logfile - if ((typeof logfile !== "string" || !logfile.endsWith(".log")) && - logfile !== false) logfile = "sleeplog.log"; - + setEnabled: function(enable) { // stop if enabled - if (global.sleeplog.enabled) global.sleeplog.stop(); + if (global.sleeplog && sleeplog.enabled) sleeplog.stop(); - // define storage and filename - var storage = require("Storage"); - var filename = "sleeplog.json"; + // define settings filename + var settings = "sleeplog.json"; // change enabled value in settings - storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, { - enabled: enable, - logfile: logfile, - powersaving: powersaving || false - })); + require("Storage").writeJSON(settings, Object.assign( + require("Storage").readJSON(settings, true) || {}, { + enabled: enable + } + )); // force changes to take effect by executing the boot script - eval(storage.read("sleeplog.boot.js")); + eval(require("Storage").read("sleeplog.boot.js")); - // clear variables - storage = undefined; - filename = undefined; return true; }, - // define read log function - // sorting: latest first, format: - // [[number, int, float, string], [...], ... ] - // - number // timestamp in ms - // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping - // - float // internal temperature - // - string // additional information - readLog: function(logfile, since, until) { - // check/set logfile - if (typeof logfile !== "string" || !logfile.endsWith(".log")) { - logfile = (global.sleeplog || {}).logfile || "sleeplog.log"; + // define read log function, returns log array + // sorting: ascending (latest first), format: + // [[number, int, int], [...], ... ] + // - number // timestamp in 10min + // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep + // - int // consecutive: 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep + readLog: function(since, until) { + // set now and check if now is before since + var now = Date.now(); + if (now < since) return []; + + // set defaults and convert since, until and now to 10min steps + since = Math.floor((since || 0) / 6E5); + until = Math.ceil((until || now) / 6E5); + now = Math.ceil(now / 6E5); + + // define output log + var log = []; + + // open StorageFile + var file = require("Storage").open("sleeplog.log", "r"); + // cache StorageFile size + var storageFileSize = file.getLength(); + // check if a Storage File needs to be read + if (storageFileSize) { + // define previous line cache + var prevLine; + // loop through StorageFile entries + while (true) { + // cache new line + var line = file.readLine(); + // exit loop if all lines are read + if (!line) break; + // skip lines starting with "," + if (line.startsWith(",")) continue; + // parse line + line = line.trim().split(",").map(e => parseInt(e)); + // exit loop if new line timestamp is not before until + if (line[0] >= until) break; + // check if new line timestamp is 24h before since or not after since + if (line[0] + 144 < since) { + // skip roughly the next 10 lines + file.read(118); + file.readLine(); + } else if (line[0] <= since) { + // cache line for next cycle + prevLine = line; + } else { + // add previous line if it was cached + if (prevLine) log.push(prevLine); + // add new line at the end of log + log.push(line); + // clear previous line cache + prevLine = undefined; + } + } + // add previous line if it was cached + if (prevLine) log.push(prevLine); + // set unknown consecutive statuses + log = log.reverse().map((entry, index) => { + if (entry[2] === 0) entry[2] = (log[index - 1] || [])[2] || 0; + return entry; + }).reverse(); + // remove duplicates + log = log.filter((entry, index) => + !(index > 0 && entry[1] === log[index - 1][1] && entry[2] === log[index - 1][2]) + ); } - // check if since is in the future - if (since > Date()) return []; + // check if log empty or first entry is after since + if (!log[0] || log[0][0] > since) { + // look for all needed storage files + var files = require("Storage").list(/^sleeplog_\d\d\d\d\.log$/, { + sf: false + }); - // read logfile - var log = require("Storage").read(logfile); - // return empty log - if (!log) return []; - // decode data if needed - if (log[0] !== "[") log = atob(log); - // do a simple check before parsing - if (!log.startsWith("[[") || !log.endsWith("]]")) return []; - log = JSON.parse(log) || []; + // check if any file available + if (files.length) { + // generate start and end times in 10min steps + files = files.map(file => { + var start = this.fnToMs(parseInt(file.substr(9, 4))) / 6E5; + return { + name: file, + start: start, + end: start + 2016 + }; + }).sort((a, b) => b.start - a.start); - // check if filtering is needed - if (since || until) { - // search for latest entry befor since - if (since) since = (log.find(element => element[0] <= since) || [0])[0]; - // filter selected time period - log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14))); + // read all neccessary files + var filesLog = []; + files.some(file => { + // exit loop if since after end + if (since >= file.end) return true; + // read file if until after start and since before end + if (until > file.start || since < file.end) { + var thisLog = require("Storage").readJSON(file.name, 1) || []; + if (thisLog.length) filesLog = thisLog.concat(filesLog); + } + }); + // free ram + files = undefined; + + // check if log from files is available + if (filesLog.length) { + // remove unwanted entries + filesLog = filesLog.filter((entry, index, filesLog) => ( + (filesLog[index + 1] || [now])[0] >= since && entry[0] <= until + )); + // add to log as previous entries + log = filesLog.concat(log); + } + // free ram + filesLog = undefined; + } } - // output log + // define last index + var lastIndex = log.length - 1; + // set timestamp of first entry to since if first entry before since + if (log[0] && log[0][0] < since) log[0][0] = since; + // add timestamp at now with unknown status if until after now + if (until > now) log.push([now, 0, 0]); + return log; }, - // define write log function, append or replace log depending on input - // append input if array length >1 and element[0] >9E11 - // replace log with input if at least one entry like above is inside another array - writeLog: function(logfile, input) { - // check/set logfile - if (typeof logfile !== "string" || !logfile.endsWith(".log")) { - if (!global.sleeplog || sleeplog.logfile === false) return; - logfile = sleeplog.logfile || "sleeplog.log"; + // define move log function, move StorageFile content into files seperated by fortnights + moveLog: function(force) { + /** convert old logfile (< v0.10) if present **/ + if (require("Storage").list("sleeplog.log", { + sf: false + }).length) { + convertOldLog(); } + /** may be removed in later versions **/ - // check if input is an array - if (typeof input !== "object" || typeof input.length !== "number") return; + // first day of this fortnight period + var thisFirstDay = this.fnToMs(this.msToFn(Date.now())); - // check for entry plausibility - if (input.length > 1 && input[0] * 1 > 9E11) { - // read log - var log = this.readLog(logfile); + // read timestamp of the first StorageFile entry + var firstDay = (require("Storage").open("sleeplog.log", "r").read(47) || "").match(/\n\d*/); + // calculate the first day of the fortnight period + if (firstDay) firstDay = this.fnToMs(this.msToFn(parseInt(firstDay[0].trim()) * 6E5)); - // remove last state if it was unknown and less then 5min ago - if (log.length > 0 && log[0][1] === 0 && - Math.floor(Date.now()) - log[0][0] < 3E5) log.shift(); + // check if moving is neccessary or forced + if (force || firstDay && firstDay < thisFirstDay) { + // read log for each fortnight period + while (firstDay) { + // calculate last day + var lastDay = firstDay + 12096E5; + // read log of the fortnight period + var log = require("sleeplog").readLog(firstDay, lastDay); - // add entry at the first position if it has changed - if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input); + // check if before this fortnight period + if (firstDay < thisFirstDay) { + // write log in seperate file + require("Storage").writeJSON("sleeplog_" + this.msToFn(firstDay) + ".log", log); + // set last day as first + firstDay = lastDay; + } else { + // rewrite StorageFile + require("Storage").open("sleeplog.log", "w").write(log.map(e => e.join(",")).join("\n")); + // clear first day to exit loop + firstDay = undefined; + } - // map log as input - input = log; - } - - // simple check for log plausibility - if (input[0].length > 1 && input[0][0] * 1 > 9E11) { - // write log to storage - require("Storage").write(logfile, btoa(JSON.stringify(input))); - return true; - } - }, - - // define log to humanreadable string function - // sorting: latest last, format: - // "{substring of ISO date} - {status} for {duration}min\n..." - getReadableLog: function(printLog, since, until, logfile) { - // read log and check - var log = this.readLog(logfile, since, until); - if (!log.length) return; - // reverse array to set last timestamp to the end - log.reverse(); - - // define status description and log string - var statusText = ["unknown ", "not worn", "awake ", "sleeping"]; - var logString = []; - - // rewrite each entry - log.forEach((element, index) => { - logString[index] = "" + - Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " + - statusText[element[1]] + - (index === log.length - 1 ? - element.length < 3 ? "" : " ".repeat(12) : - " for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min" - ) + - (element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") + - (element[3] ? " | " + element[3] : ""); - }); - logString = logString.join("\n"); - - // if set print and return string - if (printLog) { - print(logString); - print("- first", Date(log[0][0])); - print("- last", Date(log[log.length - 1][0])); - var period = log[log.length - 1][0] - log[0][0]; - print("- period= " + Math.floor(period / 864E5) + "d " + Math.floor(period % 864E5 / 36E5) + "h " + Math.floor(period % 36E5 / 6E4) + "min"); - } - return logString; - }, - - // define function to eliminate some errors inside the log - restoreLog: function(logfile) { - // read log and check - var log = this.readLog(logfile); - if (!log.length) return; - - // define output variable to show number of changes - var output = log.length; - - // remove non decremental entries - log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]); - - // write log - this.writeLog(logfile, log); - - // return difference in length - return output - log.length; - }, - - // define function to reinterpret worn status based on given temperature threshold - reinterpretTemp: function(logfile, tempthresh) { - // read log and check - var log = this.readLog(logfile); - if (!log.length) return; - - // set default tempthresh - tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27); - - // define output variable to show number of changes - var output = 0; - - // remove non decremental entries - log = log.map(element => { - if (element[2]) { - var tmp = element[1]; - element[1] = element[2] > tempthresh ? 3 : 1; - if (tmp !== element[1]) output++; + // free ram + log = undefined; } - return element; + } + }, + + // define function to return stats from the last date [ms] for a specific duration [ms] or for the complete log + getStats: function(until, duration, log) { + // define stats variable + var stats = { + calculatedAt: // [date] timestamp of the calculation + Math.round(Date.now()), + deepSleep: 0, // [min] deep sleep duration + lightSleep: 0, // [min] light sleep duration + awakeSleep: 0, // [min] awake duration inside consecutive sleep + consecSleep: 0, // [min] consecutive sleep duration + awakeTime: 0, // [min] awake duration outside consecutive sleep + notWornTime: 0, // [min] duration of not worn status + unknownTime: 0, // [min] duration of unknown status + logDuration: 0, // [min] duration of all entries taken into account + firstDate: undefined, // [date] first entry taken into account + lastDate: undefined // [date] last entry taken into account + }; + + // set default inputs + until = until || stats.calculatedAt; + if (!duration) duration = 864E5; + + // read log for the specified duration or complete log if not handed over + if (!log) log = this.readLog(duration ? until - duration : 0, until); + + // check if log not empty or corrupted + if (log && log.length && log[0] && log[0].length === 3) { + // calculate and set first log date from 10min steps + stats.firstDate = log[0][0] * 6E5; + stats.lastDate = log[log.length - 1][0] * 6E5; + + // cycle through log to calculate sums til end or duration is exceeded + log.forEach((entry, index, log) => { + // calculate duration of this entry from 10min steps to minutes + var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10; + + // check if duration greater 0 + if (duration) { + // calculate sums + if (entry[1] === 4) stats.deepSleep += duration; + else if (entry[1] === 3) stats.lightSleep += duration; + else if (entry[1] === 2) { + if (entry[2] === 2) stats.awakeSleep += duration; + else if (entry[2] === 1) stats.awakeTime += duration; + } + if (entry[2] === 2) stats.consecSleep += duration; + if (entry[1] === 1) stats.notWornTime += duration; + if (entry[1] === 0) stats.unknownTime += duration; + stats.logDuration += duration; + } + }); + } + + // free ram + log = undefined; + + // return stats of the last day + return stats; + }, + + // define function to return last break time of day from date or now (default: 12 o'clock) + getLastBreak: function(date, ToD) { + // set default date or correct date type if needed + if (!date || !date.getDay) date = date ? new Date(date) : new Date(); + // set default ToD as set in sleeplog.conf or settings if available + if (ToD === undefined) ToD = (global.sleeplog && sleeplog.conf ? sleeplog.conf.breakToD : + (require("Storage").readJSON("sleeplog.json", true) || {}).breakToD) || 12; + // calculate last break time and return + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), ToD); + }, + + // define functions to convert ms to the number of fortnights since the first Sunday at noon: 1970-01-04T12:00 + fnToMs: function(no) { + return (no + 0.25) * 12096E5; + }, + msToFn: function(ms) { + return (ms / 12096E5 - 0.25) | 0; + }, + + // define set debug function, options: + // enable as boolean, start/stop debugging + // duration in hours, generate csv log if set, max: 96h + setDebug: function(enable, duration) { + // check if global variable accessable + if (!global.sleeplog) return new Error("sleeplog: Can't set debugging, global object missing!"); + + // check if nothing has to be changed + if (!duration && + (enable && sleeplog.debug === true) || + (!enable && !sleeplog.debug)) return; + + // check if en- or disable debugging + if (enable) { + // define debug object + sleeplog.debug = {}; + + // check if a file should be generated + if (typeof duration === "number") { + // check duration boundaries, 0 => 8 + duration = duration > 96 ? 96 : duration || 12; + // calculate and set writeUntil in 10min steps + sleeplog.debug.writeUntil = ((Date.now() / 6E5 | 0) + duration * 6) * 6E5; + // set fileid to "{hours since 1970}" + sleeplog.debug.fileid = Date.now() / 36E5 | 0; + // write csv header on empty file + var file = require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a"); + if (!file.getLength()) file.write( + "timestamp,movement,status,consecutive,asleepSince,awakeSince,bpm,bpmConfidence\n" + ); + // free ram + file = undefined; + } else { + // set debug as active + sleeplog.debug = true; + } + } else { + // disable debugging + delete sleeplog.debug; + } + + // save status forced + sleeplog.saveStatus(true); + }, + + // define debugging function, called after logging if debug is set + debug: function(data) { + // check if global variable accessable and debug active + if (!global.sleeplog || !sleeplog.debug) return; + + // set functions to convert timestamps + function localTime(timestamp) { + return timestamp ? Date(timestamp).toString().split(" ")[4].substr(0, 5) : "- - -"; + } + function officeTime(timestamp) { + // days since 30.12.1899 + return timestamp / 864E5 + 25569; + } + + // generate console output + var console = "sleeplog: " + + localTime(data.timestamp) + " > " + + "movement: " + ("" + data.movement).padStart(4) + ", " + + "unknown ,non consec.,consecutive".split(",")[sleeplog.consecutive] + " " + + "unknown,not worn,awake,light sleep,deep sleep".split(",")[data.status].padEnd(12) + ", " + + "asleep since: " + localTime(sleeplog.info.asleepSince) + ", " + + "awake since: " + localTime(sleeplog.info.awakeSince); + // add bpm if set + if (data.bpm) console += ", " + + "bpm: " + ("" + data.bpm).padStart(3) + ", " + + "confidence: " + data.bpmConfidence; + // output to console + print(console); + + // check if debug is set as object with a file id and it is not past writeUntil + if (typeof sleeplog.debug === "object" && sleeplog.debug.fileid && + Date.now() < sleeplog.debug.writeUntil) { + // generate next csv line + var csv = [ + officeTime(data.timestamp), + data.movement, + data.status, + sleeplog.consecutive, + sleeplog.info.asleepSince ? officeTime(sleeplog.info.asleepSince) : "", + sleeplog.info.awakeSince ? officeTime(sleeplog.info.awakeSince) : "", + data.bpm || "", + data.bpmConfidence || "" + ].join(","); + // write next line to log if set + require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a").write(csv + "\n"); + } else { + // clear file setting in debug + sleeplog.debug = true; + } + + }, + + // print log as humanreadable output similar to debug output + printLog: function(since, until) { + // set default until + until = until || Date.now(); + // print each entry inside log + this.readLog(since, until).forEach((entry, index, log) => { + // calculate duration of this entry from 10min steps to minutes + var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10; + // print this entry + print((index + ")").padStart(4) + " " + + Date(entry[0] * 6E5).toString().substr(0, 21) + " > " + + "unknown ,non consec.,consecutive".split(",")[entry[2]] + " " + + "unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) + + "for" + (duration + "min").padStart(8)); }); + }, - // write log - this.writeLog(logfile, log); + /** convert old (< v0.10) to new logfile data **/ + convertOldLog: function() { + // read old logfile + var oldLog = require("Storage").read("sleeplog.log") || ""; + // decode data if needed + if (!oldLog.startsWith("[")) oldLog = atob(oldLog); + // delete old logfile and return if it is empty or corrupted + if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) { + require("Storage").erase("sleeplog.log"); + return; + } - // return output - return output; + // transform into StorageFile and clear oldLog to have more free ram accessable + require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n")); + oldLog = undefined; + + // calculate fortnight from now + var fnOfNow = this.msToFn(Date.now()); + + // open StorageFile with old log data + var file = require("Storage").open("sleeplog_old.log", "r"); + // define active fortnight and file cache + var activeFn = true; + var fileCache = []; + // loop through StorageFile entries + while (activeFn) { + // define fortnight for this entry + var thisFn = false; + // cache new line + var line = file.readLine(); + // check if line is filled + if (line) { + // parse line + line = line.substr(0, 15).split(",").map(e => parseInt(e)); + // calculate fortnight for this entry + thisFn = this.msToFn(line[0]); + // convert timestamp into 10min steps + line[0] = line[0] / 6E5 | 0; + // set consecutive to unknown + line.push(0); + } + // check if active fortnight and file cache is set, fortnight has changed and + // active fortnight is not fortnight from now + if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) { + // write file cache into new file according to fortnight + require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache); + // clear file cache + fileCache = []; + } + // add line to file cache if it is filled + if (line) fileCache.push(line); + // set active fortnight + activeFn = thisFn; + } + // check if entries are leftover + if (fileCache.length) { + // format fileCache entries into a string + fileCache = fileCache.map(e => e.join(",")).join("\n"); + // read complete new log StorageFile as string + file = require("Storage").open("sleeplog.log", "r"); + var newLogString = file.read(file.getLength()); + // add entries at the beginning of the new log string + newLogString = fileCache + "\n" + newLogString; + // rewrite new log StorageFile + require("Storage").open("sleeplog.log", "w").write(newLogString); + } + + // free ram + file = undefined; + fileCache = undefined; + + // clean up old files + require("Storage").erase("sleeplog.log"); + require("Storage").open("sleeplog_old.log", "w").erase(); } - + /** may be removed in later versions **/ }; diff --git a/apps/sleeplog/metadata.json b/apps/sleeplog/metadata.json index 8cf6979d6..f6ce661e8 100644 --- a/apps/sleeplog/metadata.json +++ b/apps/sleeplog/metadata.json @@ -2,27 +2,32 @@ "id":"sleeplog", "name":"Sleep Log", "shortName": "SleepLog", - "version": "0.04", - "description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.", + "version": "0.11", + "description": "Log and view your sleeping habits. This app is using the built in movement calculation.", "icon": "app.png", "type": "app", "tags": "tool,boot", "supports": ["BANGLEJS2"], "readme": "README.md", + "interface": "interface.html", "storage": [ {"name": "sleeplog.app.js", "url": "app.js"}, - {"name": "sleeplog.img", "url": "app-icon.js", "evaluate":true}, + {"name": "sleeplog.img", "url": "app-icon.js", "evaluate": true}, {"name": "sleeplog.boot.js", "url": "boot.js"}, {"name": "sleeplog", "url": "lib.js"}, {"name": "sleeplog.settings.js", "url": "settings.js"} ], "data": [ - {"name": "sleeplog.json"}, - {"name": "sleeplog.log"} + {"name": "sleeplog.json"} ], "screenshots": [ - {"url": "screenshot1.png"}, - {"url": "screenshot2.png"}, - {"url": "screenshot3.png"} - ] + {"url": "screenshot-1_app_light.png"}, + {"url": "screenshot-2_day_light.png"}, + {"url": "screenshot-3_graph_light.png"}, + {"url": "screenshot-4_graph2_light.png"}, + {"url": "screenshot-5_app_dark.png"}, + {"url": "screenshot-6_day_dark.png"}, + {"url": "screenshot-7_graph_dark.png"}, + {"url": "screenshot-8_graph2_dark.png"} + ] } diff --git a/apps/sleeplog/nolog.png b/apps/sleeplog/nolog.png deleted file mode 100644 index b153b5769..000000000 Binary files a/apps/sleeplog/nolog.png and /dev/null differ diff --git a/apps/sleeplog/off_20x20.png b/apps/sleeplog/off_20x20.png new file mode 100644 index 000000000..abf3d3bfc Binary files /dev/null and b/apps/sleeplog/off_20x20.png differ diff --git a/apps/sleeplog/powersaving.png b/apps/sleeplog/powersaving.png deleted file mode 100644 index ea487b48c..000000000 Binary files a/apps/sleeplog/powersaving.png and /dev/null differ diff --git a/apps/sleeplog/screenshot-1_app_light.png b/apps/sleeplog/screenshot-1_app_light.png new file mode 100644 index 000000000..f4c01773c Binary files /dev/null and b/apps/sleeplog/screenshot-1_app_light.png differ diff --git a/apps/sleeplog/screenshot-2_day_light.png b/apps/sleeplog/screenshot-2_day_light.png new file mode 100644 index 000000000..61e0a60f6 Binary files /dev/null and b/apps/sleeplog/screenshot-2_day_light.png differ diff --git a/apps/sleeplog/screenshot-3_graph_light.png b/apps/sleeplog/screenshot-3_graph_light.png new file mode 100644 index 000000000..4b74afd25 Binary files /dev/null and b/apps/sleeplog/screenshot-3_graph_light.png differ diff --git a/apps/sleeplog/screenshot-4_graph2_light.png b/apps/sleeplog/screenshot-4_graph2_light.png new file mode 100644 index 000000000..300be5d05 Binary files /dev/null and b/apps/sleeplog/screenshot-4_graph2_light.png differ diff --git a/apps/sleeplog/screenshot-5_app_dark.png b/apps/sleeplog/screenshot-5_app_dark.png new file mode 100644 index 000000000..82e1f8c2f Binary files /dev/null and b/apps/sleeplog/screenshot-5_app_dark.png differ diff --git a/apps/sleeplog/screenshot-6_day_dark.png b/apps/sleeplog/screenshot-6_day_dark.png new file mode 100644 index 000000000..a727f73e0 Binary files /dev/null and b/apps/sleeplog/screenshot-6_day_dark.png differ diff --git a/apps/sleeplog/screenshot-7_graph_dark.png b/apps/sleeplog/screenshot-7_graph_dark.png new file mode 100644 index 000000000..71612fa8e Binary files /dev/null and b/apps/sleeplog/screenshot-7_graph_dark.png differ diff --git a/apps/sleeplog/screenshot-8_graph2_dark.png b/apps/sleeplog/screenshot-8_graph2_dark.png new file mode 100644 index 000000000..09c19e95c Binary files /dev/null and b/apps/sleeplog/screenshot-8_graph2_dark.png differ diff --git a/apps/sleeplog/screenshot1.png b/apps/sleeplog/screenshot1.png deleted file mode 100644 index 200a305c4..000000000 Binary files a/apps/sleeplog/screenshot1.png and /dev/null differ diff --git a/apps/sleeplog/screenshot2.png b/apps/sleeplog/screenshot2.png deleted file mode 100644 index 61f580336..000000000 Binary files a/apps/sleeplog/screenshot2.png and /dev/null differ diff --git a/apps/sleeplog/screenshot3.png b/apps/sleeplog/screenshot3.png deleted file mode 100644 index 4a29b5008..000000000 Binary files a/apps/sleeplog/screenshot3.png and /dev/null differ diff --git a/apps/sleeplog/settings.js b/apps/sleeplog/settings.js index 11c7c0adb..9bf37ed69 100644 --- a/apps/sleeplog/settings.js +++ b/apps/sleeplog/settings.js @@ -1,144 +1,431 @@ (function(back) { + // define settings filename var filename = "sleeplog.json"; + // define logging prompt display status + var thresholdsPrompt = true; - // set storage and load settings - var storage = require("Storage"); - var settings = Object.assign({ - breaktod: 10, // time of day when to start/end graphs - maxawake: 36E5, // 60min in ms - minconsec: 18E5, // 30min in ms - tempthresh: 27, // every temperature above ist registered as worn - powersaving: false, // disables ESS and uses build in movement detection - maxmove: 100, // movement threshold on power saving mode - nomothresh: 0.012, // values lower than 0.008 getting triggert by noise - sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min - winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s - enabled: true, // en-/disable completely - logfile: "sleeplog.log", // logfile - }, storage.readJSON(filename, true) || {}); + // define default vaules + var defaults = { + // main settings + enabled: true, // en-/disable completely + // threshold settings + maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep + minConsec: 18E5, // [ms] minimal time to count for consecutive sleep + deepTh: 100, // threshold for deep sleep + lightTh: 200, // threshold for light sleep + // app settings + breakToD: 12, // [h] time of day when to start/end graphs + appTimeout: 0 // lock and backlight timeouts for the app + }; - // write change to global.sleeplog and storage - function writeSetting(key, value) { - // change key in global.sleeplog - if (typeof global.sleeplog === "object") global.sleeplog[key] = value; - // reread settings to only change key - settings = Object.assign(settings, storage.readJSON(filename, true) || {}); - // change the value of key - settings[key] = value; - // write to storage - storage.writeJSON(filename, settings); + // assign loaded settings to default values + var settings = Object.assign(defaults, require("Storage").readJSON(filename, true) || {}); + + // write change to storage + function writeSetting() { + require("Storage").writeJSON(filename, settings); } - // define function to change values that need a restart of the service - function changeRestart() { - require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving); + // plot a debug file + function plotDebug(filename) { + // handle swipe events + function swipeHandler(x, y) { + if (x) { + start -= x; + if (start < 0 || maxStart && start > maxStart) { + start = start < 0 ? 0 : maxStart; + } else { + drawGraph(); + } + } else { + minMove += y * 10; + if (minMove < 0 || minMove > 300) { + minMove = minMove < 0 ? 0 : 300; + } else { + drawGraph(); + } + } + } + // handle touch events + function touchHandler() { + invert = !invert; + drawGraph(); + } + + // read required entries + function readEntries(count) { + // extract usabble data from line + function extract(line) { + if (!line) return; + line = line.trim().split(","); + return [Math.round((parseFloat(line[0]) - 25569) * 144), parseInt(line[1])]; + } + + // open debug file + var file = require("Storage").open(filename, "r"); + // skip title + file.readLine(); + // skip past entries + for (var i = 0; i < start * count; i++) { file.readLine(); } + // define data with first entry + var data = [extract(file.readLine())]; + // get start time in 10min steps + var start10min = data[0][0]; + // read first required entry + var line = extract(file.readLine()); + + // read next count entries from file + while (data.length < count) { + // check if line is immediately after the last entry + if (line[0] === start10min + data.length) { + // add line to data + data.push(line); + // read new line + line = extract(file.readLine()); + // stop if no more data available + if (!line) break; + } else { + // add line with unknown movement + data.push([start10min + data.length, 0]); + } + } + + // free ram + file = undefined + // set this start as max, if less entries than expected + if (data.length < count) maxStart = start; + return data; + } + + // draw graph at starting point + function drawGraph() { + // set correct or inverted drawing + function rect(fill, x0, y0, x1, y1) { + if (fill ^ invert) { + g.fillRect(x0, y0, x1, y1); + } else { + g.clearRect(x0, y0, x1, y1); + } + } + + // set witdh + var width = g.getWidth(); + // calculate entries to display (+ set width zero based) + var count = (width--) / 4; + // read required entries + var data = readEntries(count); + + // clear app area + g.reset().clearRect(0, width - 13, width, width); + rect(false, 0, 24, width, width - 14); + // draw x axis + g.drawLine(0, width - 13, width, width - 13); + // draw x label + data.forEach((e, i) => { + var startTime = new Date(e[0] * 6E5); + if (startTime.getMinutes() === 0) { + g.fillRect(4 * i, width - 12, 4 * i, width - 9); + g.setFontAlign(-1, -1).setFont("6x8") + .drawString(startTime.getHours(), 4 * i + 1, width - 8); + } else if (startTime.getMinutes() === 30) { + g.fillRect(4 * i, width - 12, 4 * i, width - 11); + } + }); + + // calculate max height + var height = width - 38; + // cycle through entries + data.forEach((e, i) => { + // check if movement available + if (e[1]) { + // set color depending on recognised status + var color = e[1] < deepTh ? 31 : e[1] < lightTh ? 2047 : 2016; + // correct according to min movement + e[1] -= minMove; + // keep movement in bounderies + e[1] = e[1] < 0 ? 0 : e[1] > height ? height : e[1]; + // draw line and rectangle + g.reset(); + rect(true, 4 * i, width - 14, 4 * i, width - 14 - e[1]); + g.setColor(color).fillRect(4 * i + 1, width - 14, 4 * i + 3, width - 14 - e[1]); + } else { + // draw error in red + g.setColor(63488).fillRect(4 * i, width - 14, 4 * i, width - 14 - height); + } + }); + // draw threshold lines + [deepTh, lightTh].forEach(th => { + th -= minMove; + if (th > 0 && th < height) { + // draw line + g.reset(); + rect(true, 0, width - 14 - th, width, width - 14 - th); + // draw value above or below line + var yAlign = th < height / 2 ? -1 : 1; + if (invert) g.setColor(1); + g.setFontAlign(1, yAlign).setFont("6x8") + .drawString(th + minMove, width - 2, width - 13 - th + 10 * yAlign); + } + }); + + // free ram + data = undefined; + } + + // get thresholds + var deepTh = global.sleeplog ? sleeplog.conf.deepTh : defaults.deepTh; + var lightTh = global.sleeplog ? sleeplog.conf.lightTh : defaults.lightTh; + // set lowest movement displayed + var minMove = deepTh - 20; + // set start point + var start = 0; + // define max start point value + var maxStart = 0; + // define inverted color status + var invert = false; + + // setup UI + Bangle.setUI({ + mode: "custom", + back: selectDebug, + touch: touchHandler, + swipe: swipeHandler + }); + + // first draw + drawGraph(start); } - // calculate sleepthresh factor - var stFactor = settings.winwidth / 12.5 / 60; + // select a debug logfile + function selectDebug() { + // load debug files + var files = require("Storage").list(/^sleeplog_\d\d\d\d\d\d\.csv$/, {sf:true}); + + // check if no files found + if (!files.length) { + // show prompt + E.showPrompt( /*LANG*/"No debug files found.", { + title: /*LANG*/"Debug log", + buttons: { + /*LANG*/"Back": 0 + } + }).then(showDebug); + } else { + // prepare scroller + const H = 40; + var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF"; + // show scroller + E.showScroller({ + h: H, c: files.length, + back: showDebug, + scrollMin : -24, scroll : -24, // title is 24px, rendered at -1 + draw : (idx, r) => { + if (idx < 0) { + return g.setFont("12x20").setFontAlign(-1,0).drawString(menuIcon + " Select file", r.x + 12, r.y + H - 12); + } else { + g.setColor(g.theme.bg2).fillRect({x: r.x + 4, y: r.y + 2, w: r.w - 8, h: r.h - 4, r: 5}); + var name = new Date(parseInt(files[idx].match(/\d\d\d\d\d\d/)[0]) * 36E5); + name = name.toString().slice(0, -12).split(" ").filter((e, i) => i !== 3).join(" "); + g.setColor(g.theme.fg2).setFont("12x20").setFontAlign(-1, 0).drawString(name, r.x + 12, r.y + H / 2); + } + }, + select: (idx) => plotDebug(files[idx]) + }); + } + } + + // show menu or promt to change debugging + function showDebug() { + // check if sleeplog is available + if (global.sleeplog) { + // get debug status, file and duration + var enabled = !!sleeplog.debug; + var file = typeof sleeplog.debug === "object"; + var duration = 0; + // setup debugging menu + var debugMenu = { + "": { + title: /*LANG*/"Debugging" + }, + /*LANG*/"< Back": () => { + // check if some value has changed + if (enabled !== !!sleeplog.debug || file !== (typeof sleeplog.debug === "object") || duration) + require("sleeplog").setDebug(enabled, file ? duration || 12 : undefined); + // redraw main menu + showMain(7); + }, + /*LANG*/"View log": () => selectDebug(), + /*LANG*/"Enable": { + value: enabled, + onchange: v => enabled = v + }, + /*LANG*/"write File": { + value: file, + onchange: v => file = v + }, + /*LANG*/"Duration": { + value: file ? (sleeplog.debug.writeUntil - Date.now()) / 36E5 | 0 : 12, + min: 1, + max: 96, + wrap: true, + format: v => v + /*LANG*/ "h", + onchange: v => duration = v + }, + /*LANG*/"Cancel": () => showMain(7), + }; + // show menu + var menu = E.showMenu(debugMenu); + } else { + // show error prompt + E.showPrompt("Sleeplog" + /*LANG*/"not enabled!", { + title: /*LANG*/"Debugging", + buttons: { + /*LANG*/"Back": 7 + } + }).then(showMain); + } + } + + // show menu to change thresholds + function showThresholds() { + // setup logging menu + var menu; + var thresholdsMenu = { + "": { + title: /*LANG*/"Thresholds" + }, + /*LANG*/"< Back": () => showMain(2), + /*LANG*/"Max Awake": { + value: settings.maxAwake / 6E4, + step: 10, + min: 10, + max: 120, + wrap: true, + noList: true, + format: v => v + /*LANG*/"min", + onchange: v => { + settings.maxAwake = v * 6E4; + writeSetting(); + } + }, + /*LANG*/"Min Consecutive": { + value: settings.minConsec / 6E4, + step: 10, + min: 10, + max: 120, + wrap: true, + noList: true, + format: v => v + /*LANG*/"min", + onchange: v => { + settings.minConsec = v * 6E4; + writeSetting(); + } + }, + /*LANG*/"Deep Sleep": { + value: settings.deepTh, + step: 1, + min: 30, + max: 200, + wrap: true, + noList: true, + onchange: v => { + settings.deepTh = v; + writeSetting(); + } + }, + /*LANG*/"Light Sleep": { + value: settings.lightTh, + step: 10, + min: 100, + max: 400, + wrap: true, + noList: true, + onchange: v => { + settings.lightTh = v; + writeSetting(); + } + }, + /*LANG*/"Reset to Default": () => { + settings.maxAwake = defaults.maxAwake; + settings.minConsec = defaults.minConsec; + settings.deepTh = defaults.deepTh; + settings.lightTh = defaults.lightTh; + writeSetting(); + showThresholds(); + } + }; + + // display info/warning prompt or menu + if (thresholdsPrompt) { + thresholdsPrompt = false; + E.showPrompt("Changes take effect from now on, not retrospective", { + title: /*LANG*/"Thresholds", + buttons: { + /*LANG*/"Ok": 0 + } + }).then(() => menu = E.showMenu(thresholdsMenu)); + } else { + menu = E.showMenu(thresholdsMenu); + } + } // show main menu function showMain(selected) { + // set debug image + var debugImg = !global.sleeplog ? + "FBSBAOAAfwAP+AH3wD4+B8Hw+A+fAH/gA/wAH4AB+AA/wAf+APnwHw+D4Hx8A++AH/AA/gAH" : // X + typeof sleeplog.debug === "object" ? + "FBSBAB/4AQDAF+4BfvAX74F+CBf+gX/oFJKBf+gUkoF/6BSSgX/oFJ6Bf+gX/oF/6BAAgf/4" : // file + sleeplog.debug ? + "FBSBAP//+f/V///4AAGAABkAAZgAGcABjgAYcAGDgBhwAY4AGcABmH+ZB/mAABgAAYAAH///" : // console + 0; // off + debugImg = debugImg ? "\0" + atob(debugImg) : false; + // set menu var mainMenu = { "": { title: "Sleep Log", selected: selected }, - "Exit": () => load(), - "< Back": () => back(), - "Break Tod": { - value: settings.breaktod, + /*LANG*/"< Back": () => back(), + /*LANG*/"Thresholds": () => showThresholds(), + /*LANG*/"Break ToD": { + value: settings.breakToD, step: 1, min: 0, max: 23, wrap: true, - onchange: v => writeSetting("breaktod", v), - }, - "Max Awake": { - value: settings.maxawake / 6E4, - step: 5, - min: 15, - max: 120, - wrap: true, - format: v => v + "min", - onchange: v => writeSetting("maxawake", v * 6E4), - }, - "Min Consec": { - value: settings.minconsec / 6E4, - step: 5, - min: 15, - max: 120, - wrap: true, - format: v => v + "min", - onchange: v => writeSetting("minconsec", v * 6E4), - }, - "Temp Thresh": { - value: settings.tempthresh, - step: 0.5, - min: 20, - max: 40, - wrap: true, - format: v => v + "°C", - onchange: v => writeSetting("tempthresh", v), - }, - "Power Saving": { - value: settings.powersaving, - format: v => v ? "on" : "off", - onchange: function(v) { - settings.powersaving = v; - changeRestart(); - // redraw menu with changed entries subsequent to onchange - // https://github.com/espruino/Espruino/issues/2149 - setTimeout(showMain, 1, 6); + noList: true, + format: v => v + ":00", + onchange: v => { + settings.breakToD = v; + writeSetting(); } }, - "Max Move": { - value: settings.maxmove, - step: 1, - min: 50, - max: 200, + /*LANG*/"App Timeout": { + value: settings.appTimeout / 1E3, + step: 10, + min: 0, + max: 120, wrap: true, - onchange: v => writeSetting("maxmove", v), + noList: true, + format: v => v ? v + "s" : "-", + onchange: v => { + settings.appTimeout = v * 1E3; + writeSetting(); + } }, - "NoMo Thresh": { - value: settings.nomothresh, - step: 0.001, - min: 0.006, - max: 0.02, - wrap: true, - format: v => ("" + v).padEnd(5, "0"), - onchange: v => writeSetting("nomothresh", v), - }, - "Min Duration": { - value: Math.floor(settings.sleepthresh * stFactor), - step: 1, - min: 5, - max: 15, - wrap: true, - format: v => v + "min", - onchange: v => writeSetting("sleepthresh", Math.ceil(v / stFactor)), - }, - "Enabled": { + /*LANG*/"Enabled": { value: settings.enabled, - format: v => v ? "on" : "off", - onchange: function(v) { + onchange: v => { settings.enabled = v; - changeRestart(); + require("sleeplog").setEnabled(v); } }, - "Logfile ": { - value: settings.logfile === "sleeplog.log" ? true : (settings.logfile || "").endsWith(".log") ? "custom" : false, - format: v => v === true ? "default" : v ? "custom" : "off", - onchange: function(v) { - if (v !== "custom") { - settings.logfile = v ? "sleeplog.log" : false; - changeRestart(); - } - } + /*LANG*/"Debugging": { + value: debugImg, + onchange: () => setTimeout(showDebug, 10) } }; - // check power saving mode to delete unused entries - (settings.powersaving ? ["NoMo Thresh", "Min Duration"] : ["Max Move"]).forEach(property => delete mainMenu[property]); var menu = E.showMenu(mainMenu); } diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog index 875b3c1da..6bf296342 100644 --- a/apps/sleepphasealarm/ChangeLog +++ b/apps/sleepphasealarm/ChangeLog @@ -3,3 +3,11 @@ 0.03: Add compatibility for Bangle.js 2 and new firmware, added "Alarm at " for the alarm time 0.04: Read alarms from new scheduling library, account for higher acceleration sensor noise on Bangle.js 2 0.05: Refactor decodeTime() to scheduling library +0.06: Add logging + use Layout library and display ETA +0.07: Add check for day of week +0.08: Update to new time_utils module +0.09: Vibrate with configured pattern + Add setting to defer start of algorithm + Add setting to disable scheduler alarm + diff --git a/apps/sleepphasealarm/README.md b/apps/sleepphasealarm/README.md new file mode 100644 index 000000000..ecb3feb06 --- /dev/null +++ b/apps/sleepphasealarm/README.md @@ -0,0 +1,26 @@ +# Sleep Phase Alarm + +The alarm must be in the next 24h. + +The display shows: + +- The current time. +- Time of the next alarm or timer. +- Time difference between current time and alarm time (ETA). +- Current state of the ESS algorithm, "Sleep" or "Awake", useful for debugging. State can also be "Deferred", see the "Run before alarm"-option. + +## Settings + +* **Keep alarm enabled** + - Yes: (default) Alert will stay enabled, e.g. for an alarm at 7:00 the clock will buzz at the calculated time from the ESS algorithm (for example 6:45) and again at 7:00. + - No: No action at configured alarm time from scheduler. +* **Run before alarm** + - disabled: (default) The ESS algorithm starts immediately when the application starts. + - 1..23: The ESS algorithm starts the configured time before the alarm. E.g. when set to 1h for an alarm at 7:00 the ESS algorithm will start at 6:00. This improves battery life. + +## Logging + +For each day of month (1..31) the ESS states are logged. An entry will be overwritten in the next month, e.g. an entry on the 4th May will overwrite an entry on the 4th April. +The logs can be viewed with the download button: + +![](screenshot.jpg) diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 236b71c0b..b19799c4b 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -1,6 +1,19 @@ -const BANGLEJS2 = process.env.HWVERSION == 2; //# check for bangle 2 -const alarms = require("Storage").readJSON("sched.json",1)||[]; +const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2 +const CONFIGFILE = "sleepphasealarm.json"; +const Layout = require("Layout"); +const locale = require('locale'); +const alarms = require("Storage").readJSON("sched.json",1) || []; +const config = Object.assign({ + logs: [], // array of length 31 with one entry for each day of month + settings: { + startBeforeAlarm: 0, // 0 = start immediately, 1..23 = start 1h..23h before alarm time + disableAlarm: false, + } +}, require("Storage").readJSON(CONFIGFILE,1) || {}); const active = alarms.filter(a=>a.on); +const schedSettings = require("sched").getSettings(); +let buzzCount = schedSettings.buzzCount; +let logs = []; // Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS): // Marko Borazio, Eugen Berlin, Nagihan Kücükyildiz, Philipp M. Scholl and Kristof Van Laerhoven, "Towards a Benchmark for Wearable Sleep Analysis with Inertial Wrist-worn Sensing Units", ICHI 2014, Verona, Italy, IEEE Press, 2014. @@ -39,108 +52,149 @@ function calc_ess(acc_magn) { } // locate next alarm -var nextAlarm; +var nextAlarmDate; +var nextAlarmConfig; active.forEach(alarm => { const now = new Date(); - const t = require("sched").decodeTime(alarm.t); - var dateAlarm = new Date(now.getFullYear(), now.getMonth(), now.getDate(), t.hrs, t.mins); + const time = require("time_utils").decodeTime(alarm.t); + var dateAlarm = new Date(now.getFullYear(), now.getMonth(), now.getDate(), time.h, time.m); if (dateAlarm < now) { // dateAlarm in the past, add 24h dateAlarm.setTime(dateAlarm.getTime() + (24*60*60*1000)); } - if (nextAlarm === undefined || dateAlarm < nextAlarm) { - nextAlarm = dateAlarm; + if ((alarm.dow >> dateAlarm.getDay()) & 1) { // check valid day of week + if (nextAlarmDate === undefined || dateAlarm < nextAlarmDate) { + nextAlarmDate = dateAlarm; + nextAlarmConfig = alarm; + } } }); -function drawString(s, y) { //# replaced x: always centered - g.reset(); //# moved up to prevent blue background - g.clearRect(0, y - 12, 239, y + 8); //# minimized upper+lower clearing - g.setFont("Vector", 20); - g.setFontAlign(0, 0); // align centered - g.drawString(s, g.getWidth() / 2, y); //# set x to center -} +var layout = new Layout({ + type:"v", c: [ + {type:"txt", font:"10%", label:"Sleep Phase Alarm", bgCol:g.theme.bgH, fillx: true, height:Bangle.appRect.h/6}, + {type:"txt", font:"16%", label: ' '.repeat(20), id:"date", height:Bangle.appRect.h/6}, + {type:"txt", font:"12%", label: "", id:"alarm_date", height:Bangle.appRect.h/6}, + {type:"txt", font:"10%", label: ' '.repeat(20), id:"eta", height:Bangle.appRect.h/6}, + {type:"txt", font:"12%", label: ' '.repeat(20), id:"state", height:Bangle.appRect.h/6}, + ] +}, {lazy:true}); function drawApp() { - g.clearRect(0,24,239,215); //# no problem - var alarmHour = nextAlarm.getHours(); - var alarmMinute = nextAlarm.getMinutes(); + var alarmHour = nextAlarmDate.getHours(); + var alarmMinute = nextAlarmDate.getMinutes(); if (alarmHour < 10) alarmHour = "0" + alarmHour; if (alarmMinute < 10) alarmMinute = "0" + alarmMinute; - const s = "Alarm at " + alarmHour + ":" + alarmMinute + "\n\n"; //# make distinct to time - E.showMessage(s, "Sleep Phase Alarm"); + layout.alarm_date.label = "Alarm at " + alarmHour + ":" + alarmMinute; + layout.render(); function drawTime() { if (Bangle.isLCDOn()) { const now = new Date(); - var nowHour = now.getHours(); - var nowMinute = now.getMinutes(); - var nowSecond = now.getSeconds(); - if (nowHour < 10) nowHour = "0" + nowHour; - if (nowMinute < 10) nowMinute = "0" + nowMinute; - if (nowSecond < 10) nowSecond = "0" + nowSecond; - const time = nowHour + ":" + nowMinute + (BANGLEJS2 ? "" : ":" + nowSecond); //# hide seconds on bangle 2 - drawString(time, BANGLEJS2 ? 85 : 105); //# remove x, adjust height for bangle 2 an newer firmware + layout.date.label = locale.time(now, BANGLEJS2 && Bangle.isLocked() ? 1 : 0); // hide seconds on bangle 2 + const diff = nextAlarmDate - now; + const diffHour = Math.floor((diff % 86400000) / 3600000).toString(); + const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString(); + layout.eta.label = "ETA: -"+ diffHour + ":" + diffMinutes.padStart(2, '0'); + layout.render(); } + + setTimeout(()=>{ + drawTime(); + }, 1000 - (Date.now() % 1000)); } - if (BANGLEJS2) { - drawTime(); - setTimeout(_ => { - drawTime(); - setInterval(drawTime, 60000); - }, 60000 - Date.now() % 60000); //# every new minute on bangle 2 - } else { - setInterval(drawTime, 500); // 2Hz - } + drawTime(); } -var buzzCount = 19; function buzz() { if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence Bangle.setLCDPower(1); - Bangle.buzz().then(()=>{ - if (buzzCount--) { - setTimeout(buzz, 500); - } else { - // back to main after finish - setTimeout(load, 1000); - } - }); + require("buzz").pattern(nextAlarmConfig.vibrate || ";"); + if (buzzCount--) { + setTimeout(buzz, schedSettings.buzzIntervalMillis); + } else { + // back to main after finish + setTimeout(load, 1000); + } +} + +function addLog(time, type) { + logs.push({time: time, type: type}); + if (logs.length > 1) { // Do not write if there is only one state + require("Storage").writeJSON(CONFIGFILE, config); + } } // run var minAlarm = new Date(); var measure = true; -if (nextAlarm !== undefined) { - Bangle.loadWidgets(); //# correct widget load draw order +if (nextAlarmDate !== undefined) { + config.logs[nextAlarmDate.getDate()] = []; // overwrite log on each day of month + logs = config.logs[nextAlarmDate.getDate()]; + g.clear(); + Bangle.loadWidgets(); Bangle.drawWidgets(); + let swest_last; // minimum alert 30 minutes early - minAlarm.setTime(nextAlarm.getTime() - (30*60*1000)); - Bangle.on('accel', (accelData) => { // 12.5Hz - const now = new Date(); - const acc = accelData.mag; - const swest = calc_ess(acc); + minAlarm.setTime(nextAlarmDate.getTime() - (30*60*1000)); + run = () => { + layout.state.label = "Start"; + layout.render(); + Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval + Bangle.setPollInterval(80); // 12.5Hz + Bangle.on('accel', (accelData) => { + const now = new Date(); + const acc = accelData.mag; + const swest = calc_ess(acc); - if (swest !== undefined) { - if (Bangle.isLCDOn()) { - drawString(swest ? "Sleep" : "Awake", BANGLEJS2 ? 150 : 180); //# remove x, adjust height + if (swest !== undefined) { + if (Bangle.isLCDOn()) { + layout.state.label = swest ? "Sleep" : "Awake"; + layout.render(); + } + // log + if (swest_last != swest) { + if (swest) { + addLog(new Date(now - sleepthresh*13/12.5*1000), "sleep"); // calculate begin of no motion phase, 13 values/second at 12.5Hz + } else { + addLog(now, "awake"); + } + swest_last = swest; + } } - } - if (now >= nextAlarm) { - // The alarm widget should handle this one - setTimeout(load, 1000); - } else if (measure && now >= minAlarm && swest === false) { - buzz(); - measure = false; - } - }); + if (now >= nextAlarmDate) { + // The alarm widget should handle this one + addLog(now, "alarm"); + setTimeout(load, 1000); + } else if (measure && now >= minAlarm && swest_last === false) { + addLog(now, "alarm"); + buzz(); + measure = false; + if (config.settings.disableAlarm) { + // disable alarm for scheduler + nextAlarmConfig.last = now.getDate(); + require("Storage").writeJSON("sched.json", alarms); + } + } + }); + }; drawApp(); + if (config.settings.startBeforeAlarm === 0) { + // Start immediately + run(); + } else { + // defer start + layout.state.label = "Deferred"; + layout.render(); + const diff = nextAlarmDate - Date.now(); + let timeout = diff-config.settings.startBeforeAlarm*60*60*1000; + if (timeout < 0) timeout = 0; + setTimeout(run, timeout); + } } else { E.showMessage('No Alarm'); setTimeout(load, 1000); } -// BTN2 to menu, BTN3 to main # on bangle 2 only BTN to main -if (!BANGLEJS2) setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); setWatch(() => load(), BANGLEJS2 ? BTN : BTN3, { repeat: false, edge: "falling" }); diff --git a/apps/sleepphasealarm/interface.html b/apps/sleepphasealarm/interface.html new file mode 100644 index 000000000..f45c183e1 --- /dev/null +++ b/apps/sleepphasealarm/interface.html @@ -0,0 +1,107 @@ + + + + + +

Please select a wakeup day:

+
+ +
+
+ +
+ + + + + + + diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json index aecfa36e4..6ec5f4180 100644 --- a/apps/sleepphasealarm/metadata.json +++ b/apps/sleepphasealarm/metadata.json @@ -2,14 +2,18 @@ "id": "sleepphasealarm", "name": "SleepPhaseAlarm", "shortName": "SleepPhaseAlarm", - "version": "0.05", + "version": "0.09", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "icon": "app.png", "tags": "alarm", "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", "dependencies": {"scheduler":"type"}, "storage": [ {"name":"sleepphasealarm.app.js","url":"app.js"}, + {"name":"sleepphasealarm.settings.js","url":"settings.js"}, {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"sleepphasealarm.json","storageFile":true}], + "interface": "interface.html" } diff --git a/apps/sleepphasealarm/screenshot.jpg b/apps/sleepphasealarm/screenshot.jpg new file mode 100644 index 000000000..b1fd05dec Binary files /dev/null and b/apps/sleepphasealarm/screenshot.jpg differ diff --git a/apps/sleepphasealarm/settings.js b/apps/sleepphasealarm/settings.js new file mode 100644 index 000000000..a79abb598 --- /dev/null +++ b/apps/sleepphasealarm/settings.js @@ -0,0 +1,37 @@ +(function(back) { + const CONFIGFILE = "sleepphasealarm.json"; + // Load settings + const config = Object.assign({ + logs: [], // array of length 31 with one entry for each day of month + settings: { + startBeforeAlarm: 0, // 0 = start immediately, 1..23 = start 1h..23h before alarm time + disableAlarm: false, + } + }, require("Storage").readJSON(CONFIGFILE,1) || {}); + + function writeSettings() { + require('Storage').writeJSON(CONFIGFILE, config); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "SleepPhaseAlarm" }, + 'Keep alarm enabled': { + value: !!config.settings.disableAlarm, + format: v => v?"No":"Yes", + onchange: v => { + config.settings.disableAlarm = v; + writeSettings(); + } + }, "< Back" : () => back(), + 'Run before alarm': { + format: v => v === 0 ? 'disabled' : v+'h', + value: config.settings.startBeforeAlarm, + min: 0, max: 23, + onchange: v => { + config.settings.startBeforeAlarm = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/slidingtext/ChangeLog b/apps/slidingtext/ChangeLog index 0327ff387..1b45c36cb 100644 --- a/apps/slidingtext/ChangeLog +++ b/apps/slidingtext/ChangeLog @@ -5,4 +5,6 @@ 0.05: BUGFIX: pedometer widget interfered with the clock Font Alignment 0.06: Use Bangle.setUI for button/launcher handling 0.07: Support for Bangle.js 2 and themes -0.08: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.08: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.09: Added button control toggle and other live controls to new settings screen. +0.10: Tell clock widgets to hide. diff --git a/apps/slidingtext/README.md b/apps/slidingtext/README.md index d2d2fb5b6..d5a561634 100644 --- a/apps/slidingtext/README.md +++ b/apps/slidingtext/README.md @@ -6,22 +6,49 @@ Inspired by the Pebble sliding clock, old times are scrolled off the screen and ## Usage -### Button 1 +### Bangle 2 + +The Bangle 2 has Live Controls switched **off** by default so the colour and language have to be changed from the setting Menu. +Please locate the Sliding Text clock under the setting->apps menu. + +With the Live Controls switched on: +#### Bottom right hand corner press +press the bottom right hand corner of the screen to change the colour + +| White | Black | Gray | Red | +|----------------------|----------------------|----------------------|----------------------| +| ![](b2_color-01.jpg) | ![](b2_color-02.jpg) | ![](b2_color-03.jpg) | ![](b2_color-04.jpg) | + +#### Top right hand corner press +press the top right hand corner of the screen to change the language + +### Bangle 1 + +By Default the Live Controls (The side buttons) are switched on, which means the clock face can be controlled dynamically using the 2 side buttons on the right hand side + +#### Button 1 Use Button 1 (the top right button) to change the language | English | English (Traditional) | French | Japanese (Romanji) | | ---- | ---- | ---- | ---- | -| ![](./format-01.jpg) | ![](format-02.jpg) | ![](format-03.jpg) |![](format-04.jpg) | +| ![](format-01.jpg) | ![](format-02.jpg) | ![](format-03.jpg) |![](format-04.jpg) | | **German** | **Spanish** | | | -| ![](./format-05.jpg) | ![](format-06.jpg) | | | +| ![](format-05.jpg) | ![](format-06.jpg) | | | -### Button 3 +#### Button 3 Button 3 (bottom right button) is used to change the colour | Black | Red | Gray | Purple | | ---- | ---- | ---- | ---- | -| ![](./color-01.jpg) | ![](color-02.jpg) | ![](color-03.jpg) | ![](color-04.jpg) | +| ![](b1_color-01.jpg) | ![](b1_color-02.jpg) | ![](b1_color-03.jpg) | ![](b1_color-04.jpg) | + +#### Settings + +To turn off the Live Controls and change the settings statically please visit the settings menu. The settings menu will allow you to: +- Colour Scheme +- Language +- Live Controls (On or Off) ## Further Details diff --git a/apps/slidingtext/color-01.jpg b/apps/slidingtext/b1_color-01.jpg similarity index 100% rename from apps/slidingtext/color-01.jpg rename to apps/slidingtext/b1_color-01.jpg diff --git a/apps/slidingtext/color-02.jpg b/apps/slidingtext/b1_color-02.jpg similarity index 100% rename from apps/slidingtext/color-02.jpg rename to apps/slidingtext/b1_color-02.jpg diff --git a/apps/slidingtext/color-03.jpg b/apps/slidingtext/b1_color-03.jpg similarity index 100% rename from apps/slidingtext/color-03.jpg rename to apps/slidingtext/b1_color-03.jpg diff --git a/apps/slidingtext/color-04.jpg b/apps/slidingtext/b1_color-04.jpg similarity index 100% rename from apps/slidingtext/color-04.jpg rename to apps/slidingtext/b1_color-04.jpg diff --git a/apps/slidingtext/b2_color-01.jpg b/apps/slidingtext/b2_color-01.jpg new file mode 100644 index 000000000..7428f7623 Binary files /dev/null and b/apps/slidingtext/b2_color-01.jpg differ diff --git a/apps/slidingtext/b2_color-02.jpg b/apps/slidingtext/b2_color-02.jpg new file mode 100644 index 000000000..7e3b8666f Binary files /dev/null and b/apps/slidingtext/b2_color-02.jpg differ diff --git a/apps/slidingtext/b2_color-03.jpg b/apps/slidingtext/b2_color-03.jpg new file mode 100644 index 000000000..96c8655cf Binary files /dev/null and b/apps/slidingtext/b2_color-03.jpg differ diff --git a/apps/slidingtext/b2_color-04.jpg b/apps/slidingtext/b2_color-04.jpg new file mode 100644 index 000000000..dac36365a Binary files /dev/null and b/apps/slidingtext/b2_color-04.jpg differ diff --git a/apps/slidingtext/metadata.json b/apps/slidingtext/metadata.json index 2937a618b..49ffaf16f 100644 --- a/apps/slidingtext/metadata.json +++ b/apps/slidingtext/metadata.json @@ -1,7 +1,7 @@ { "id": "slidingtext", "name": "Sliding Clock", - "version": "0.08", + "version": "0.10", "description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported", "icon": "slidingtext.png", "type": "clock", @@ -12,6 +12,7 @@ "allow_emulator": false, "storage": [ {"name":"slidingtext.app.js","url":"slidingtext.js"}, + {"name":"slidingtext.settings.js","url":"slidingtext.settings.js"}, {"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true}, {"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"}, {"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"}, @@ -21,5 +22,6 @@ {"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"}, {"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"}, {"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"} - ] + ], + "data": [{"name": "slidingtext.settings.json"}] } diff --git a/apps/slidingtext/slidingtext.dtfmt.js b/apps/slidingtext/slidingtext.dtfmt.js index 865ea47e6..266ed0b35 100644 --- a/apps/slidingtext/slidingtext.dtfmt.js +++ b/apps/slidingtext/slidingtext.dtfmt.js @@ -7,6 +7,7 @@ class DateFormatter { * to the lines of text on the screen */ name(){return "no name";} + shortName(){return "no short name"} formatDate(date){ return ["no","date","defined"]; } diff --git a/apps/slidingtext/slidingtext.js b/apps/slidingtext/slidingtext.js index 7b56af1a1..2f24e5596 100644 --- a/apps/slidingtext/slidingtext.js +++ b/apps/slidingtext/slidingtext.js @@ -5,11 +5,17 @@ */ const color_schemes = [ + { + name: "white", + background : [1.0,1.0,1.0], + main_bar: [0.0,0.0,0.0], + other_bars: [0.1,0.1,0.1], + }, { name: "black", background : [0.0,0.0,0.0], main_bar: [1.0,1.0,1.0], - other_bars: [0.85,0.85,0.85], + other_bars: [0.9,0.9,0.9], }, { name: "red", @@ -81,7 +87,7 @@ function reset_commands(){ function has_commands(){ return command_stack_high_priority.length > 0 || - command_stack_low_priority.lenth > 0; + command_stack_low_priority.length > 0; } class ShiftText { @@ -244,15 +250,19 @@ function setRowDisplays(y, heights) { y += heights[i]; } } -if (g.getHeight()>200) + +function bangleVersion(){ + return (g.getHeight()>200)? 1 : 2; +} + +if (bangleVersion()<2) setRowDisplays(50, [40,30,30,30,40]); else setRowDisplays(34, [35,25,25,25,35]); function nextColorTheme(){ - //console.log("next color theme"); color_scheme_index += 1; - if(color_scheme_index >= row_displays.length){ + if(color_scheme_index > row_displays.length){ color_scheme_index = 0; } setColorScheme(color_schemes[color_scheme_index]); @@ -411,8 +421,7 @@ function draw_clock(){ reset_commands(); date = display_time(date); console.log("draw_clock:" + last_draw_time.toISOString() + " display:" + date.toISOString()); - // for debugging only - //date.setMinutes(37); + var rows = date_formatter.formatDate(date); var display; for (var i = 0; i < rows.length; i++) { @@ -495,7 +504,7 @@ function set_colorscheme(colorscheme_name){ function set_dateformat(dateformat_name){ console.log("setting date format:" + dateformat_name); for (var i=0; i < date_formatters.length; i++) { - if(date_formatters[i].name() == dateformat_name){ + if(date_formatters[i].shortName() == dateformat_name){ date_formatter_idx = i; date_formatter = date_formatters[date_formatter_idx]; console.log("match"); @@ -503,6 +512,7 @@ function set_dateformat(dateformat_name){ } } +var enable_live_controls = false; const PREFERENCE_FILE = "slidingtext.settings.json"; /** * Called on startup to set the watch to the last preference settings @@ -510,7 +520,7 @@ const PREFERENCE_FILE = "slidingtext.settings.json"; function load_settings(){ var setScheme = false; try{ - settings = require("Storage").readJSON(PREFERENCE_FILE); + var settings = require("Storage").readJSON(PREFERENCE_FILE); if(settings != null){ console.log("loaded:" + JSON.stringify(settings)); if(settings.color_scheme != null){ @@ -520,9 +530,15 @@ function load_settings(){ if(settings.date_format != null){ set_dateformat(settings.date_format); } + if(settings.enable_live_controls == null){ + settings.enable_live_controls = (bangleVersion() <= 1); + } + enable_live_controls = settings.enable_live_controls; } else { console.log("no settings to load"); + enable_live_controls = (bangleVersion() <= 1); } + console.log("enable_live_controls=" + enable_live_controls); } catch(e){ console.log("failed to load settings:" + e); } @@ -536,24 +552,30 @@ function load_settings(){ */ function save_settings(){ var settings = { - date_format : date_formatter.name(), + date_format : date_formatter.shortName(), color_scheme : color_schemes[color_scheme_index].name, + enable_live_controls: enable_live_controls }; console.log("saving:" + JSON.stringify(settings)); require("Storage").writeJSON(PREFERENCE_FILE,settings); } function button1pressed() { - changeFormatter(); - save_settings(); + console.log("button1pressed"); + if (enable_live_controls) { + changeFormatter(); + save_settings(); + } } function button3pressed() { console.log("button3pressed"); - nextColorTheme(); - reset_clock(true); - draw_clock(); - save_settings(); + if (enable_live_controls) { + nextColorTheme(); + reset_clock(true); + draw_clock(); + save_settings(); + } } // The interval reference for updating the clock @@ -625,12 +647,13 @@ Bangle.on('lcdPower', (on) => { g.clear(); load_settings(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); - -startTimers(); // Show launcher when button pressed Bangle.setUI("clockupdown", d=>{ if (d<0) button1pressed(); if (d>0) button3pressed(); }); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +startTimers(); + diff --git a/apps/slidingtext/slidingtext.locale.de.js b/apps/slidingtext/slidingtext.locale.de.js index 3cb178232..da5c2f01d 100644 --- a/apps/slidingtext/slidingtext.locale.de.js +++ b/apps/slidingtext/slidingtext.locale.de.js @@ -66,6 +66,7 @@ function germanMinsToText(mins) { class GermanDateFormatter extends DateFormatter { constructor() { super();} name(){return "German";} + shortName(){return "de"} formatDate(date){ var mins = date.getMinutes(); var hourOfDay = date.getHours(); diff --git a/apps/slidingtext/slidingtext.locale.en.js b/apps/slidingtext/slidingtext.locale.en.js index 7d37fcae1..6414ef7a9 100644 --- a/apps/slidingtext/slidingtext.locale.en.js +++ b/apps/slidingtext/slidingtext.locale.en.js @@ -5,6 +5,7 @@ const numberToText = require("slidingtext.utils.en.js").numberToText; class EnglishDateFormatter extends DateFormatter { constructor() { super();} name(){return "English";} + shortName(){return "en"} formatDate(date){ var hours_txt = hoursToText(date.getHours()); var mins_txt = numberToText(date.getMinutes()); diff --git a/apps/slidingtext/slidingtext.locale.en2.js b/apps/slidingtext/slidingtext.locale.en2.js index cd07e8848..d7d7ff6a8 100644 --- a/apps/slidingtext/slidingtext.locale.en2.js +++ b/apps/slidingtext/slidingtext.locale.en2.js @@ -7,6 +7,7 @@ class EnglishTraditionalDateFormatter extends DateFormatter { super(); } name(){return "English (Traditional)";} + shortName(){return "en2"} formatDate(date){ var mins = date.getMinutes(); var hourOfDay = date.getHours(); diff --git a/apps/slidingtext/slidingtext.locale.es.js b/apps/slidingtext/slidingtext.locale.es.js index 1b6f6d11b..62c68b64d 100644 --- a/apps/slidingtext/slidingtext.locale.es.js +++ b/apps/slidingtext/slidingtext.locale.es.js @@ -47,6 +47,7 @@ function spanishMinsToText(mins){ class SpanishDateFormatter extends DateFormatter { constructor() { super();} name(){return "Spanish";} + shortName(){return "es"} formatDate(date){ var mins = date.getMinutes(); var hourOfDay = date.getHours(); diff --git a/apps/slidingtext/slidingtext.locale.fr.js b/apps/slidingtext/slidingtext.locale.fr.js index 5844c1a4e..d4c1dc9d6 100644 --- a/apps/slidingtext/slidingtext.locale.fr.js +++ b/apps/slidingtext/slidingtext.locale.fr.js @@ -31,6 +31,7 @@ function frenchHeures(hours){ class FrenchDateFormatter extends DateFormatter { constructor() { super(); } name(){return "French";} + shortName(){return "fr"} formatDate(date){ var hours = frenchHoursToText(date.getHours()); var heures = frenchHeures(date.getHours()); diff --git a/apps/slidingtext/slidingtext.locale.jp.js b/apps/slidingtext/slidingtext.locale.jp.js index c28780e88..0f6e46a21 100644 --- a/apps/slidingtext/slidingtext.locale.jp.js +++ b/apps/slidingtext/slidingtext.locale.jp.js @@ -61,6 +61,7 @@ function japaneseMinsToText(mins){ class JapaneseDateFormatter extends DateFormatter { constructor() { super(); } name(){return "Japanese (Romanji)";} + shortName(){return "jp"} formatDate(date){ var hours_txt = japaneseHoursToText(date.getHours()); var mins_txt = japaneseMinsToText(date.getMinutes()); diff --git a/apps/slidingtext/slidingtext.settings.js b/apps/slidingtext/slidingtext.settings.js new file mode 100644 index 000000000..d1006990e --- /dev/null +++ b/apps/slidingtext/slidingtext.settings.js @@ -0,0 +1,65 @@ +(function(back) { + const PREFERENCE_FILE = "slidingtext.settings.json"; + var settings = Object.assign({}, + require('Storage').readJSON(PREFERENCE_FILE, true) || {}); + // the screen controls are defaulted on for a bangle 1 and off for a bangle 2 + if(settings.enable_live_controls == null){ + settings.enable_live_controls = (g.getHeight()> 200); + } + console.log("loaded:" + JSON.stringify(settings)); + + const LANGUAGES_FILE = "slidingtext.languages.json"; + const LANGUAGES_DEFAULT = ["en","en2"]; + var locales = null; + try { + locales = require("Storage").readJSON(LANGUAGES_FILE); + } catch(e) { + console.log("failed to load languages:" + e); + } + if(locales == null || locales.length == 0){ + locales = LANGUAGES_DEFAULT; + console.log("defaulting languages to locale:" + locales); + } + + function writeSettings() { + console.log("saving:" + JSON.stringify(settings)); + require('Storage').writeJSON(PREFERENCE_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 + E.showMenu({ + "" : { "title" : "Sliding Text" }, + "< Back" : () => back(), + "Colour": stringInSettings("color_scheme", ["white", "black", "red","grey","purple","blue"]), + "Languages": stringInSettings("date_format", locales), + "Live Control": { + value: (settings.enable_live_controls !== undefined ? settings.enable_live_controls : true), + format: v => v ? "On" : "Off", + onchange: v => { + settings.enable_live_controls = v; + writeSettings(); + } + }, + }); +}) \ No newline at end of file diff --git a/apps/smclock/ChangeLog b/apps/smclock/ChangeLog index 2a3874d34..42d50d3b9 100644 --- a/apps/smclock/ChangeLog +++ b/apps/smclock/ChangeLog @@ -4,3 +4,4 @@ 0.04: Add support for settings 0.05: Add ability to change background (3bit or 4bit) 0.06: Replace battery text with image +0.07: Use default Bangle formatter for booleans diff --git a/apps/smclock/metadata.json b/apps/smclock/metadata.json index ca40193a2..6790c2030 100644 --- a/apps/smclock/metadata.json +++ b/apps/smclock/metadata.json @@ -4,7 +4,7 @@ "shortName": "MonoClock", "icon": "app.png", "screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }], - "version": "0.06", + "version": "0.07", "description": "A simple watchface based on my stylised monogram.", "type": "clock", "tags": "clock", diff --git a/apps/smclock/settings.js b/apps/smclock/settings.js index ee4a35a26..30119e48d 100644 --- a/apps/smclock/settings.js +++ b/apps/smclock/settings.js @@ -46,7 +46,6 @@ "Analog Face": { value: settings.showAnalogFace !== undefined ? settings.showAnalogFace : false, - format: v => v ? "On" : "Off", onchange: v => { settings.showAnalogFace = v; writeSettings(); @@ -71,7 +70,6 @@ "Week Info": { value: settings.showWeekInfo !== undefined ? settings.showWeekInfo : false, - format: v => v ? "On" : "Off", onchange: v => { settings.showWeekInfo = v; writeSettings(); @@ -80,7 +78,6 @@ "Vector Font": { value: settings.useVectorFont !== undefined ? settings.useVectorFont : false, - format: v => v ? "On" : "Off", onchange: v => { settings.useVectorFont = v; writeSettings(); diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 07afedd21..bf128e2fb 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1 +1,2 @@ -0.01: Release \ No newline at end of file +0.01: Release +0.02: Rewrite with new interface \ No newline at end of file diff --git a/apps/smpltmr/README.md b/apps/smpltmr/README.md index 1296166e2..eeb48d338 100644 --- a/apps/smpltmr/README.md +++ b/apps/smpltmr/README.md @@ -1,21 +1,18 @@ # Simple Timer -A simple app to set a timer quickly. Simply tab on top/bottom/left/right -to select the minutes and tab in the middle of the screen to start/stop -the timer. Note that this timer depends on qalarm. +A simple app to set a timer quickly. Drag or tap on the up and down buttons over the hour, minute or second to set the time. -# Overview -If you open the app, you can simply control the timer -by clicking on top, bottom, left or right of the screen. -If you tab at the middle of the screen, the timer is -started / stopped. +This app uses the `sched` library, which allows the timer to continue to run in the background when this app is closed. -![](description.png) +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) +![](screenshot_4.png) - -# Creator +# Creators [David Peer](https://github.com/peerdavid) +[Sir Indy](https://github.com/sir-indy) # Thanks to... Time icon created by CreativeCons - Flaticon \ No newline at end of file diff --git a/apps/smpltmr/app.js b/apps/smpltmr/app.js index eb01e27d0..4e95d3a30 100644 --- a/apps/smpltmr/app.js +++ b/apps/smpltmr/app.js @@ -3,122 +3,188 @@ * * Creator: David Peer * Date: 02/2022 + * + * Modified: Sir Indy + * Date: 05/2022 */ -Bangle.loadWidgets(); - - -const alarm = require("sched"); - +const Layout = require("Layout"); +const alarm = require("sched") const TIMER_IDX = "smpltmr"; -const screenWidth = g.getWidth(); -const screenHeight = g.getHeight(); -const cx = parseInt(screenWidth/2); -const cy = parseInt(screenHeight/2)-12; -var minutes = 5; -var interval; //used for the 1 second interval timer - -function isTimerEnabled(){ - var alarmObj = alarm.getAlarm(TIMER_IDX); - if(alarmObj===undefined || !alarmObj.on){ - return false; +const secondsToTime = (s) => new Object({h:Math.floor((s/3600) % 24), m:Math.floor((s/60) % 60), s:Math.floor(s % 60)}); +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); +function formatTime(s) { + var t = secondsToTime(s); + if (t.h) { + return t.h + ':' + ("0" + t.m).substr(-2) + ':' + ("0" + t.s).substr(-2); + } else { + return t.m + ':' + ("0" + t.s).substr(-2); } - - return true; } -function getTimerMin(){ - var alarmObj = alarm.getAlarm(TIMER_IDX); - return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); +var seconds = 5 * 60; // Default to 5 minutes +var drawTimeout; +//var timerRunning = false; +function timerRunning() { + return (alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX)) != undefined) +} +const imgArrow = atob("CQmBAAgOBwfD47ndx+OA"); +const imgPause = atob("GBiBAP+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B/w=="); +const imgPlay = atob("GBiBAIAAAOAAAPgAAP4AAP+AAP/gAP/4AP/+AP//gP//4P//+P///v///v//+P//4P//gP/+AP/4AP/gAP+AAP4AAPgAAOAAAIAAAA=="); + +function onDrag(event) { + if (!timerRunning()) { + Bangle.buzz(40, 0.3); + var diff = -Math.round(event.dy/5); + if (event.x < timePickerLayout.hours.w) { + diff *= 3600; + } else if (event.x > timePickerLayout.mins.x && event.x < timePickerLayout.secs.x) { + diff *= 60; + } + updateTimePicker(diff); + } } -function setTimer(minutes){ +function onTouch(button, xy) { + if (xy.y > (timePickerLayout.btnStart.y||timerLayout.btnStart.y)) { + Bangle.buzz(40, 0.3); + onButton(); + return; + } + if (!timerRunning()) { + var touchMidpoint = timePickerLayout.hours.y + timePickerLayout.hours.h/2; + var diff = 0; + Bangle.buzz(40, 0.3); + if (xy.y > 24 && xy.y < touchMidpoint - 10) { + diff = 1; + } else if (xy.y > touchMidpoint + 10 && xy.y < timePickerLayout.btnStart.y) { + diff = -1; + } + if (xy.x < timePickerLayout.hours.w) { + diff *= 3600; + } else if (xy.x > timePickerLayout.mins.x && xy.x < timePickerLayout.secs.x) { + diff *= 60; + } + updateTimePicker(diff); + } + +} + +function onButton() { + g.clearRect(Bangle.appRect); + if (timerRunning()) { + timerStop(); + } else { + if (seconds > 0) { + timerRun(); + } + } +} + +function updateTimePicker(diff) { + seconds = clamp(seconds + (diff || 0), 0, 24 * 3600 - 1); + var set_time = secondsToTime(seconds); + updateLayoutField(timePickerLayout, 'hours', set_time.h); + updateLayoutField(timePickerLayout, 'mins', set_time.m); + updateLayoutField(timePickerLayout, 'secs', set_time.s); +} + +function updateTimer() { + var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX)); + updateLayoutField(timerLayout, 'timer', formatTime(timeToNext / 1000)); + queueDraw(1000); +} + +function queueDraw(millisecs) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + updateTimer(); + }, millisecs - (Date.now() % millisecs)); +} + +function timerRun() { alarm.setAlarm(TIMER_IDX, { - // msg : "Simple Timer", - timer : minutes*60*1000, + vibrate : ".-.-", + hidden: true, + timer : seconds * 1000 }); alarm.reload(); + g.clearRect(Bangle.appRect); + timerLayout.render(); + updateTimer(); } -function deleteTimer(){ +function timerStop() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX)); + if (timeToNext != undefined) { + seconds = timeToNext / 1000; + } alarm.setAlarm(TIMER_IDX, undefined); alarm.reload(); + g.clearRect(Bangle.appRect); + timePickerLayout.render(); + updateTimePicker(); } -setWatch(_=>load(), BTN1); -function draw(){ - g.clear(1); - Bangle.drawWidgets(); - - if (interval) { - clearInterval(interval); - } - interval = undefined; - - // Write time - g.setFontAlign(0, 0, 0); - g.setFont("Vector", 32).setFontAlign(0,-1); - - var started = isTimerEnabled(); - var text = minutes + " min."; - if(started){ - var min = getTimerMin(); - text = min + " min."; - } - - var rectWidth = parseInt(g.stringWidth(text) / 2); - - if(started){ - interval = setInterval(draw, 1000); - g.setColor("#ff0000"); - } else { - g.setColor(g.theme.fg); - } - g.fillRect(cx-rectWidth-5, cy-5, cx+rectWidth, cy+30); - - g.setColor(g.theme.bg); - g.drawString(text, cx, cy); -} - - -Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.25); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.25); - var lower = g.getHeight() - upper; - - var isLeft = e.x < left; - var isRight = e.x > right; - var isUpper = e.y < upper; - var isLower = e.y > lower; - var isMiddle = !isLeft && !isRight && !isUpper && !isLower; - var started = isTimerEnabled(); - - if(isRight && !started){ - minutes += 1; - Bangle.buzz(40, 0.3); - } else if(isLeft && !started){ - minutes -= 1; - Bangle.buzz(40, 0.3); - } else if(isUpper && !started){ - minutes += 5; - Bangle.buzz(40, 0.3); - } else if(isLower && !started){ - minutes -= 5; - Bangle.buzz(40, 0.3); - } else if(isMiddle) { - if(!started){ - setTimer(minutes); - } else { - deleteTimer(); - } - Bangle.buzz(80, 0.6); - } - minutes = Math.max(0, minutes); - - draw(); +var timePickerLayout = new Layout({ + type:"v", c: [ + {type:undefined, height:2}, + {type:"h", c: [ + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Hours"}, + {type:"img", pad:8, src:imgArrow}, + {type:"txt", font:"20%", label:"00", id:"hours", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrow, r:2} + ]}, + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Minutes"}, + {type:"img", pad:8, src:imgArrow}, + {type:"txt", font:"20%", label:"00", id:"mins", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrow, r:2} + ]}, + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Seconds"}, + {type:"img", pad:8, src:imgArrow}, + {type:"txt", font:"20%", label:"00", id:"secs", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrow, r:2} + ]}, + ]}, + {type:"btn", src:imgPlay, id:"btnStart", fillx:1 } + ], filly:1 }); -g.reset(); -draw(); \ No newline at end of file +var timerLayout = new Layout({ + type:"v", c: [ + {type:"txt", font:"22%", label:"0:00", id:"timer", fillx:1, filly:1 }, + {type:"btn", src:imgPause, id:"btnStart", cb: l=>timerStop(), fillx:1 } + ], filly:1 +}); + +function updateLayoutField(layout, field, value) { + layout.clear(layout[field]); + layout[field].label = value; + layout.render(layout[field]); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +Bangle.setUI({ + mode : "custom", + touch : function(n,e) {onTouch(n,e);}, + drag : function(e) {onDrag(e);}, + btn : function(n) {onButton();}, +}); + +g.clearRect(Bangle.appRect); +if (timerRunning()) { + timerLayout.render(); + updateTimer(); +} else { + timePickerLayout.render(); + updateTimePicker(); +} diff --git a/apps/smpltmr/description.png b/apps/smpltmr/description.png deleted file mode 100644 index 1286d1ab9..000000000 Binary files a/apps/smpltmr/description.png and /dev/null differ diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 06bad962d..cb1ef6eab 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,13 +2,13 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.01", + "version": "0.02", "description": "A very simple app to start a timer.", "icon": "app.png", - "tags": "tool", + "tags": "tool,alarm,timer", "dependencies": {"scheduler":"type"}, "supports": ["BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}, {"url": "screenshot_2.png"}], + "screenshots": [{"url":"screenshot_1.png"}, {"url": "screenshot_2.png"}, {"url": "screenshot_3.png"}, {"url": "screenshot_4.png"}], "readme": "README.md", "storage": [ {"name":"smpltmr.app.js","url":"app.js"}, diff --git a/apps/smpltmr/screenshot.png b/apps/smpltmr/screenshot.png deleted file mode 100644 index eff94475c..000000000 Binary files a/apps/smpltmr/screenshot.png and /dev/null differ diff --git a/apps/smpltmr/screenshot_1.png b/apps/smpltmr/screenshot_1.png new file mode 100644 index 000000000..54eb9d20c Binary files /dev/null and b/apps/smpltmr/screenshot_1.png differ diff --git a/apps/smpltmr/screenshot_2.png b/apps/smpltmr/screenshot_2.png index 7b5dc9a3d..fb0145f17 100644 Binary files a/apps/smpltmr/screenshot_2.png and b/apps/smpltmr/screenshot_2.png differ diff --git a/apps/smpltmr/screenshot_3.png b/apps/smpltmr/screenshot_3.png new file mode 100644 index 000000000..efa10d9c1 Binary files /dev/null and b/apps/smpltmr/screenshot_3.png differ diff --git a/apps/smpltmr/screenshot_4.png b/apps/smpltmr/screenshot_4.png new file mode 100644 index 000000000..c0f984378 Binary files /dev/null and b/apps/smpltmr/screenshot_4.png differ diff --git a/apps/sonicclk/ChangeLog b/apps/sonicclk/ChangeLog new file mode 100644 index 000000000..5cdaa3764 --- /dev/null +++ b/apps/sonicclk/ChangeLog @@ -0,0 +1,6 @@ +0.01: [MAJOR] Added sonic clock app +0.02: [PATCH] Fixed text alignment issue; Increased acceleration required to activate twist; +0.03: [MINOR] Added settings menu to control twist threshold and LCD Activity +0.04: [PATCH] Call `Bangle.setUI` when exiting settings menu, settings tap moved to top +0.05: [PATCH] Firmware 2v11 - use `wakeOnTwist` rather than manual `setLCDPower`; Reset sonic on `fullReset` +0.06: Use default Bangle formatter for booleans diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog deleted file mode 100644 index d78fe291f..000000000 --- a/apps/sonicclk/Changelog +++ /dev/null @@ -1,5 +0,0 @@ -0.01 [MAJOR] Added sonic clock app -0.02 [PATCH] Fixed text alignment issue; Increased acceleration required to activate twist; -0.03 [MINOR] Added settings menu to control twist threshold and LCD Activity -0.04 [PATCH] Call `Bangle.setUI` when exiting settings menu, settings tap moved to top -0.05 [PATCH] Firmware 2v11 - use `wakeOnTwist` rather than manual `setLCDPower`; Reset sonic on `fullReset` diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js index eddb971f8..2d72de68e 100644 --- a/apps/sonicclk/app.js +++ b/apps/sonicclk/app.js @@ -268,7 +268,6 @@ const settingsMenu = { "": { title: "Settings" }, "Active Mode": { value: settings.activeMode, - format: (v) => (v ? "On" : "Off"), onchange: (v) => (settings.activeMode = v), }, "Twist Thresh": { diff --git a/apps/sonicclk/metadata.json b/apps/sonicclk/metadata.json index 5a2d64db1..ad3e52fdb 100644 --- a/apps/sonicclk/metadata.json +++ b/apps/sonicclk/metadata.json @@ -1,7 +1,7 @@ { "id": "sonicclk", "name": "Sonic Clock", - "version": "0.05", + "version": "0.06", "description": "A classic sonic clock featuring run, stop and wait animations.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/speedalt/ChangeLog b/apps/speedalt/ChangeLog index 78c14594b..224d2711d 100644 --- a/apps/speedalt/ChangeLog +++ b/apps/speedalt/ChangeLog @@ -10,3 +10,4 @@ 0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. 0.11: Now also runs on Bangle.js 2 with basic functionality 0.12: Full functionality on Bangle.js 2: Bangle.js 1 buttons mapped to touch areas. +0.13: Use default Bangle formatter for booleans diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md index 6f0d4efe5..e629f94bb 100644 --- a/apps/speedalt/README.md +++ b/apps/speedalt/README.md @@ -2,7 +2,7 @@ You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint. -Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. +Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information. @@ -36,7 +36,7 @@ BTN4 : Left Display Tap : Swaps which figure is in the large display. You can ha ## App Settings -Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). +Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). ## Kalman Filter @@ -67,13 +67,11 @@ This app will work quite happily on its own but will use the [GPS Setup App](htt When using the GPS Setup App this app switches the GPS to SuperE (default) mode while the display is lit and showing fix information. This ensures that that fixes are updated every second or so. 10 seconds after the display is blanked by the watch this app will switch the GPS to PSMOO mode and will only attempt to get a fix every two minutes. This improves power saving while the display is off and the delay gives an opportunity to restore the display before the GPS power mode is switched. -The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. +The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. ## Waypoints -Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode. - -The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.) +Waypoints are used in Distance and VMG modes. See the `Waypoints` app for information on how to create/edit waypoints. The first 6 characters of the name are displayed in Speed+[D]istance mode. Sample waypoints.json (My sailing waypoints) @@ -149,5 +147,3 @@ Developed for my use in sailing, cycling and motorcycling. If you find this soft Many thanks to Gordon Williams. Awesome job. Special thanks also to @jeffmer, for the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app and @hughbarney for the Low power GPS code development and Wouter Bulten for the Kalman filter code. - - diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js index 79db932db..4587252c2 100644 --- a/apps/speedalt/app.js +++ b/apps/speedalt/app.js @@ -209,7 +209,7 @@ function nxtWp(inc){ } function loadWp() { - var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}]; + var w = require("waypoints").load(); if (cfg.wp>=w.length) cfg.wp=0; if (cfg.wp<0) cfg.wp = w.length-1; savSettings(); diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json index e03d23c8b..89bfd4a57 100644 --- a/apps/speedalt/metadata.json +++ b/apps/speedalt/metadata.json @@ -2,18 +2,18 @@ "id": "speedalt", "name": "GPS Adventure Sports", "shortName": "GPS Adv Sport", - "version": "0.12", + "version": "0.13", "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", "icon": "app.png", "type": "app", "tags": "tool,outdoors", "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies" : { "waypoints":"type" }, "readme": "README.md", "allow_emulator": true, "storage": [ {"name":"speedalt.app.js","url":"app.js"}, {"name":"speedalt.img","url":"app-icon.js","evaluate":true}, {"name":"speedalt.settings.js","url":"settings.js"} - ], - "data": [{"name":"speedalt.json"}] + ] } diff --git a/apps/speedalt/settings.js b/apps/speedalt/settings.js index 8906e2e2c..aeaa84f2c 100644 --- a/apps/speedalt/settings.js +++ b/apps/speedalt/settings.js @@ -37,12 +37,7 @@ '< Load GPS Adv Sport': ()=>{load('speedalt.app.js');}, 'Units' : function() { E.showMenu(unitsMenu); }, 'Colours' : function() { E.showMenu(colMenu); }, - 'Kalman Filter' : function() { E.showMenu(kalMenu); }/*, - 'Vibrate' : { - value : settings.buzz, - format : v => v?"On":"Off", - onchange : () => { settings.buzz = !settings.buzz; writeSettings(); } - }*/ + 'Kalman Filter' : function() { E.showMenu(kalMenu); } }; const unitsMenu = { @@ -73,12 +68,10 @@ '< 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(); } } }; diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog index 73e9bfc40..ec76c6a16 100644 --- a/apps/speedalt2/ChangeLog +++ b/apps/speedalt2/ChangeLog @@ -8,8 +8,11 @@ 0.08: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted. 0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. 0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. -1.06: Misc memory and screen optimisations. -1.10: Adds Kalman filter. -1.14: Add VMG and coordinates screens -1.43: Adds mirroring of the watch face to an Android device. See README.md -1.49: Droidscript mirroring prog automatically uses last connection address. Auto connects when run. +0.11: Misc memory and screen optimisations. +0.12: Adds Kalman filter. +0.13: Add VMG and coordinates screens +0.14: Adds mirroring of the watch face to an Android device. See README.md +0.15: Droidscript mirroring prog automatically uses last connection address. Auto connects when run. +0.16: Add configuration item Wpt File Suffix. A one character suffix to append to the waypoints.json file. A number of other apps also use this file name. Using the file name suffix allows the speedalt2 waypoints to be retained if one of these other apps is installed for a different use. +0.17: Use default Bangle formatter for booleans +0.18: Move waypoints.json to 'waypoints' app (with editor) diff --git a/apps/speedalt2/README.md b/apps/speedalt2/README.md index e1c6b0a5a..d6a5e1d71 100644 --- a/apps/speedalt2/README.md +++ b/apps/speedalt2/README.md @@ -25,7 +25,7 @@ Touch functions as BTN1 short press. ## App Settings -Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). +Select the desired display units. Speed can be as per the default locale, kph, knots, mph or m/s. Distance can be km, miles or nautical miles. Altitude can be feet or metres. Select one of three colour schemes. Default (three colours), high contrast (all white on black) or night ( all red on black ). ## Kalman Filter @@ -37,13 +37,13 @@ When the GPS obtains a fix the number of satellites is displayed as 'Sats:nn'. W ## Power Saving -The The GPS Adv Sport app obeys the watch screen off timeouts as a power saving measure. Restore the screen as per any of the colck/watch apps. Use BTN2 to lock the screen on but doing this will use more battery. +The The GPS Adv Sport app obeys the watch screen off timeouts as a power saving measure. Restore the screen as per any of the clock/watch apps. Use BTN2 to lock the screen on but doing this will use more battery. This app will work quite happily on its own but will use the [GPS Setup App](https://banglejs.com/apps/#gps%20setup) if it is installed. You may choose to use the GPS Setup App to gain significantly longer battery life while the GPS is on. Please read the Low Power GPS Setup App Readme to understand what this does. When using the GPS Setup App this app switches the GPS to SuperE (default) mode while the display is lit and showing fix information. This ensures that that fixes are updated every second or so. 10 seconds after the display is blanked by the watch this app will switch the GPS to PSMOO mode and will only attempt to get a fix every two minutes. This improves power saving while the display is off and the delay gives an opportunity to restore the display before the GPS power mode is switched. -The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. +The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode. ## Velocity Made Good - VMG @@ -63,9 +63,9 @@ The Droidscript script file is called : **GPS Adv Sports II.js** **Important Gotcha :** For the BLE comms to find and connect to the Bangle.js it must be paired with the Android device but **NOT** connected. The Bangle.js must have been set in the Bluetooth settings as connectable. -Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep. +Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep. -When runnig a blue 'led' will flash each time a data packet is recieved to refresh the android display. +When running a blue 'led' will flash each time a data packet is recieved to refresh the android display. An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. ) @@ -74,9 +74,11 @@ Android Screen Mirroring:
## Waypoints -Waypoints are used in Distance and VMG modes. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode. +Waypoints are used in Distance and VMG modes. See the `Waypoints` app for information on how to create/edit waypoints. The first 6 characters of the name are displayed in Speed+[D]istance mode. -The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.) +By default the waypoints file is called waypoints.json + +**Note** : The waypoints.json file is used by a number of different gps apps. The setting 'Wpt File Suffix' allows one of waypoints.1.json, waypoints.2.json or waypoints.3.json to be used instead. This allows the other apps to be used with a different set of waypoints without losing the speedalt2 waypoint set. Sample waypoints.json (My sailing waypoints) @@ -152,4 +154,3 @@ Developed for my use in sailing, cycling and motorcycling. If you find this soft Many thanks to Gordon Williams. Awesome job. Special thanks also to @jeffmer, for the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app and @hughbarney for the Low power GPS code development and Wouter Bulten for the Kalman filter code. - diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js index ed16131a4..839ac63eb 100644 --- a/apps/speedalt2/app.js +++ b/apps/speedalt2/app.js @@ -5,8 +5,9 @@ Mike Bennett mike[at]kereru.com 1.14 : Add VMG screen 1.34 : Add bluetooth data stream for Droidscript 1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring +1.50 : Add cfg.wptSfx one char suffix to append to waypoints.json filename. Protects speedalt2 waypoints from other apps that use the same file name for waypoints. */ -var v = '1.49'; +var v = '1.50'; var vDroid = '1.50'; // Required DroidScript program version /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ @@ -184,7 +185,7 @@ let LED = // LED as minimal and only definition (as instance / singleton) , reset: function() { this.set(false); } // turn off , write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off , toggle: function() { this.set( ! this.isOn); } // toggle the LED -}, LED1 = LED; // LED1 as 'synonym' for LED +}, LED1 = LED; // LED1 as 'synonym' for LED var lf = {fix:0,satellites:0}; @@ -209,7 +210,7 @@ function nxtWp(){ } function loadWp() { - var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}]; + var w = require("waypoints").load(cfg.wptSfx); if (cfg.wp>=w.length) cfg.wp=0; if (cfg.wp<0) cfg.wp = w.length-1; savSettings(); @@ -238,7 +239,7 @@ function bearing(a,b){ 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 metres var d = Math.sqrt(x*x + y*y) * 6371000; return d; @@ -250,38 +251,38 @@ function drawScrn(dat) { buf.clear(); buf.setBgColor(0); - + var n; n = dat.val.toString(); - + var s=50; // Font size var l=n.length; - + if ( l <= 7 ) s=55; if ( l <= 6 ) s=60; if ( l <= 5 ) s=80; if ( l <= 4 ) s=100; if ( l <= 3 ) s=120; - - buf.setFontAlign(0,0); //Centre - buf.setColor(1); + + buf.setFontAlign(0,0); //Centre + buf.setColor(1); buf.setFontVector(s); buf.drawString(n,126,52); - + // Primary Units buf.setFontAlign(-1,1); //left, bottom - buf.setColor(2); + buf.setColor(2); buf.setFontVector(35); - buf.drawString(dat.unit,5,164); - + buf.drawString(dat.unit,5,164); + drawMax(dat.max); // MAX display indicator drawWP(dat.wp); // Waypoint name drawSats(dat.sats); - + g.reset(); g.drawImage(img,0,40); - + LED1.write(!pwrSav); } @@ -294,7 +295,7 @@ function drawPosn(dat) { var x, y; x=210; y=0; - buf.setFontAlign(1,-1); + buf.setFontAlign(1,-1); buf.setFontVector(60); buf.setColor(1); @@ -319,45 +320,45 @@ function drawPosn(dat) { function drawClock() { if (!canDraw) return; - + buf.clear(); buf.setBgColor(0); - + var x, y; x=185; y=0; - buf.setFontAlign(1,-1); + buf.setFontAlign(1,-1); buf.setFontVector(94); time = require("locale").time(new Date(),1); - + buf.setColor(1); - + buf.drawString(time.substring(0,2),x,y); buf.drawString(time.substring(3,5),x,y+80); - + g.reset(); g.drawImage(img,0,40); - + LED1.write(!pwrSav); } function drawWP(wp) { - buf.setColor(3); + buf.setColor(3); buf.setFontAlign(0,1); //left, bottom buf.setFontVector(40); - buf.drawString(wp,120,132); + buf.drawString(wp,120,132); } function drawSats(sats) { - buf.setColor(3); + buf.setColor(3); buf.setFont("6x8", 2); buf.setFontAlign(1,1); //right, bottom - buf.drawString(sats,240,160); + buf.drawString(sats,240,160); } function drawMax(max) { buf.setFontVector(30); - buf.setColor(2); + buf.setColor(2); buf.setFontAlign(0,1); //centre, bottom buf.drawString(max,120,164); } @@ -368,7 +369,7 @@ if ( emulator ) { fix.speed = 10 + (Math.random()*5); fix.alt = 354 + (Math.random()*50); fix.lat = -38.92; - fix.lon = 175.7613350; + fix.lon = 175.7613350; fix.course = 245; fix.satellites = 12; fix.time = new Date(); @@ -377,7 +378,7 @@ if ( emulator ) { var m; - var sp = '---'; + var sp = '---'; var al = sp; var di = sp; var brg = ''; // bearing @@ -389,15 +390,15 @@ if ( emulator ) { var lon = '---.--'; var sats = sp; var vmg = sp; - - + + // Waypoint name var wpName = wp.name; if ( wpName == undefined || wpName == 'NONE' ) wpName = ''; - wpName = wpName.substring(0,8); + wpName = wpName.substring(0,8); if (fix.fix) lf = fix; - + if (lf.fix) { // Smooth data @@ -407,10 +408,10 @@ if ( emulator ) { lf.smoothed = 1; if ( maxN <= 15 ) maxN++; } - + // Bearing to waypoint brg = bearing(lf,wp); - + // Current course crs = lf.course; @@ -446,19 +447,19 @@ if ( emulator ) { if ( di >= 100 ) di = parseFloat(di).toFixed(1); if ( di >= 1000 ) di = parseFloat(di).toFixed(0); if (isNaN(di)) di = '------'; - + // Age of last fix (secs) age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); // Lat / Lon ns = 'N'; if ( lf.lat < 0 ) ns = 'S'; - lat = Math.abs(lf.lat.toFixed(2)); + lat = Math.abs(lf.lat.toFixed(2)); ew = 'E'; if ( lf.lon < 0 ) ew = 'W'; - lon = Math.abs(lf.lon.toFixed(2)); - + lon = Math.abs(lf.lon.toFixed(2)); + // Sats if ( age > 10 ) { sats = 'Age:'+Math.round(age); @@ -572,7 +573,7 @@ if ( emulator ) { ew:ew }); } - + if ( cfg.modeA == 5 ) { // Large clock drawClock(); @@ -584,14 +585,14 @@ function prevScrn() { cfg.modeA = cfg.modeA-1; if ( cfg.modeA < 0 ) cfg.modeA = 5; savSettings(); - onGPS(lf); + onGPS(lf); } function nextScrn() { cfg.modeA = cfg.modeA+1; if ( cfg.modeA > 5 ) cfg.modeA = 0; savSettings(); - onGPS(lf); + onGPS(lf); } // Next function on a screen @@ -609,7 +610,7 @@ function nextFunc(dur) { function updateClock() { if (!canDraw) return; if ( cfg.modeA != 5 ) return; - drawClock(); + drawClock(); if ( emulator ) {maxSpd++;maxAlt++;} } @@ -660,10 +661,10 @@ function setButtons(){ var dur = e.time - e.lastTime; nextFunc(dur); }, BTN1, { edge:"falling",repeat:true}); - - // Power saving on/off + + // Power saving on/off setWatch(function(e){ - pwrSav=!pwrSav; + pwrSav=!pwrSav; if ( pwrSav ) { var s = require('Storage').readJSON('setting.json',1)||{}; var t = s.timeout||10; @@ -675,7 +676,7 @@ function setButtons(){ } LED1.write(!pwrSav); }, BTN2, {repeat:true,edge:"falling"}); - + // BTN3 - next screen setWatch(function(e){ nextScrn(); @@ -684,7 +685,7 @@ function setButtons(){ Bangle.on('lcdPower',function(on) { if (!SCREENACCESS.withApp) return; - if (on) startDraw(); + if (on) startDraw(); else stopDraw(); }); @@ -701,7 +702,7 @@ Bangle.on('touch', function(button){ // == Main Prog -// Read settings. +// Read settings. let cfg = require('Storage').readJSON('speedalt2.json',1)||{}; cfg.spd = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed @@ -712,12 +713,13 @@ cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions. cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units cfg.colour = cfg.colour||0; // Colour scheme. cfg.wp = cfg.wp||0; // Last selected waypoint for dist -cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock +cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary -cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; +cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt; cfg.touch = cfg.touch==undefined?true:cfg.touch; +cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx; if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 }); if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 }); @@ -747,7 +749,7 @@ var SCREENACCESS = { withApp:true, request:function(){this.withApp=false;stopDraw();}, release:function(){this.withApp=true;startDraw();} -}; +}; var gpssetup; try { diff --git a/apps/speedalt2/metadata.json b/apps/speedalt2/metadata.json index 4ace46854..2c8d37f79 100644 --- a/apps/speedalt2/metadata.json +++ b/apps/speedalt2/metadata.json @@ -2,12 +2,13 @@ "id": "speedalt2", "name": "GPS Adventure Sports II", "shortName":"GPS Adv Sport II", - "version":"1.49", + "version":"0.18", "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", "icon": "app.png", "type": "app", "tags": "tool,outdoors", "supports": ["BANGLEJS"], + "dependencies" : { "waypoints":"type" }, "readme": "README.md", "allow_emulator": true, "storage": [ @@ -15,5 +16,7 @@ {"name":"speedalt2.img","url":"app-icon.js","evaluate":true}, {"name":"speedalt2.settings.js","url":"settings.js"} ], - "data": [{"name":"speedalt2.json"}] + "data": [ + {"name":"speedalt2.json"} + ] } diff --git a/apps/speedalt2/settings.js b/apps/speedalt2/settings.js index babb03061..63fa424ba 100644 --- a/apps/speedalt2/settings.js +++ b/apps/speedalt2/settings.js @@ -30,6 +30,11 @@ writeSettings(); } + function setSfx(s) { + settings.wptSfx = s; + writeSettings(); + } + const appMenu = { '': {'title': 'GPS Adv Sprt II'}, @@ -38,9 +43,9 @@ 'Units' : function() { E.showMenu(unitsMenu); }, 'Colours' : function() { E.showMenu(colMenu); }, 'Kalman Filter' : function() { E.showMenu(kalMenu); }, + 'Wpt File Suffix' : function() { E.showMenu(sfxMenu); }, 'Touch' : { value : settings.touch, - format : v => v?"On":"Off", onchange : () => { settings.touch = !settings.touch; writeSettings(); } } }; @@ -69,17 +74,24 @@ 'Inverted' : function() { setColour(3); } }; + const sfxMenu = { + '': {'title': 'Wpt File Suffix'}, + '< Back': function() { E.showMenu(appMenu); }, + 'Default' : function() { setSfx(''); }, + '1' : function() { setSfx('1'); }, + '2' : function() { setSfx('2'); }, + '3' : function() { setSfx('3'); } + }; + const kalMenu = { '': {'title': 'Kalman Filter'}, '< 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(); } } }; diff --git a/apps/stardateclock/ChangeLog b/apps/stardateclock/ChangeLog index 431463bc8..bb6430b65 100644 --- a/apps/stardateclock/ChangeLog +++ b/apps/stardateclock/ChangeLog @@ -1 +1,3 @@ 0.01: Initial release on the app repository for Bangle.js 1 and 2 +0.02: Fixed let/const usage while using firmware version >=2v14 +0.03: Tell clock widgets to hide. diff --git a/apps/stardateclock/app.js b/apps/stardateclock/app.js index 70f1070fc..adf2c14c7 100644 --- a/apps/stardateclock/app.js +++ b/apps/stardateclock/app.js @@ -1,10 +1,10 @@ // Stardate clock face, by KaiRo.at, 2021-2022 -var redrawClock = true; -var clockface = "digital"; +let redrawClock = true; +let clockface = "digital"; // note: Bangle.js 1 has 240x240x16, 2 has 176x176x3 screen -var bpp = g.getBPP ? g.getBPP() : 16; +const bpp = g.getBPP ? g.getBPP() : 16; // Load fonts Graphics.prototype.setFontAntonio27 = function(scale) { @@ -22,16 +22,15 @@ const fontSizeLarge = 1; const fontHeightLarge = 42 * fontSizeLarge; // LCARS dimensions +let baseUnit1 = 5; +let baseUnit2 = 3; +let baseUnit3 = 10; if (g.getWidth() < 200) { // Bangle.js 2 - const baseUnit1 = 3; - const baseUnit2 = 2; - const baseUnit3 = 7; -} -else { - const baseUnit1 = 5; - const baseUnit2 = 3; - const baseUnit3 = 10; + baseUnit1 = 3; + baseUnit2 = 2; + baseUnit3 = 7; } + const widgetsHeight = 24; const sbarWid = baseUnit3 * 5; const hbarHt = baseUnit1; @@ -86,19 +85,19 @@ const colorLCARSPurple = "#A06060"; const colorLCARSBrown = "#C09070"; // More colors: teal #008484, yellow FFCF00, purple #6050B0 -var lastSDateString; -var lastTimeStringToMin; -var lastTimeStringSec; -var lastDateString; -var lastAnalogDate; +let lastSDateString; +let lastTimeStringToMin; +let lastTimeStringSec; +let lastDateString; +let lastAnalogDate; function updateStardate() { - var curDate = new Date(); + const curDate = new Date(); // Note that the millisecond division and the 1000-unit multiplier cancel each other out. - var sdateval = (curDate - gSDBase) / secondsPerYear; + const sdateval = (curDate - gSDBase) / secondsPerYear; - var sdatestring = (Math.floor(sdateval * sdateDecFactor) / sdateDecFactor).toFixed(sdateDecimals); + const sdatestring = (Math.floor(sdateval * sdateDecFactor) / sdateDecFactor).toFixed(sdateDecimals); // Reset the state of the graphics library. g.reset(); @@ -120,34 +119,33 @@ function updateStardate() { lastSDateString = sdatestring; // Schedule next when an update to the last decimal is due. - var mstonextUpdate = (Math.ceil(sdateval * sdateDecFactor) / sdateDecFactor - sdateval) * secondsPerYear; + const mstonextUpdate = (Math.ceil(sdateval * sdateDecFactor) / sdateDecFactor - sdateval) * secondsPerYear; if (redrawClock) { setTimeout(updateStardate, mstonextUpdate); } } function updateConventionalTime() { - var curDate = new Date(); + const curDate = new Date(); if (clockface == "digital") { drawDigitalClock(curDate); - } - else { + } else { drawAnalogClock(curDate); } // Schedule next when an update to the last second is due. - var mstonextUpdate = Math.ceil(curDate / 1000) * 1000 - curDate; + const mstonextUpdate = Math.ceil(curDate / 1000) * 1000 - curDate; if (redrawClock) { setTimeout(updateConventionalTime, mstonextUpdate); } } function drawDigitalClock(curDate) { - var timestringToMin = ("0" + curDate.getHours()).substr(-2) + ":" + const timestringToMin = ("0" + curDate.getHours()).substr(-2) + ":" + ("0" + curDate.getMinutes()).substr(-2) + ":"; - var timestringSec = ("0" + curDate.getSeconds()).substr(-2); - var datestring = "" + curDate.getFullYear() + "-" + const timestringSec = ("0" + curDate.getSeconds()).substr(-2); + const datestring = "" + curDate.getFullYear() + "-" + ("0" + (curDate.getMonth() + 1)).substr(-2) + "-" + ("0" + curDate.getDate()).substr(-2); @@ -156,7 +154,7 @@ function drawDigitalClock(curDate) { g.setBgColor(colorBg); // Set Font g.setFont(fontNameLarge, fontSizeLarge); - var ctimePosLeft = ctimePosCenter - g.stringWidth("12:34:56") / 2; + let ctimePosLeft = ctimePosCenter - g.stringWidth("12:34:56") / 2; if (ctimePosLeft + g.stringWidth("00:00:00") > g.getWidth()) { ctimePosLeft = g.getWidth() - g.stringWidth("00:00:00"); } @@ -174,7 +172,7 @@ function drawDigitalClock(curDate) { g.drawString(timestringToMin, ctimePosLeft, ctimePosTop); lastTimeStringToMin = timestringToMin; } - var ctimePosLeftSec = ctimePosLeft + g.stringWidth(timestringToMin); + const ctimePosLeftSec = ctimePosLeft + g.stringWidth(timestringToMin); if (lastTimeStringSec) { // Clear the area where we want to draw the seconds. //g.setBgColor("#FF6600"); // for debugging @@ -190,7 +188,7 @@ function drawDigitalClock(curDate) { if (datestring != lastDateString) { // Set Font g.setFont(fontName, fontSize); - var cdatePosLeft = cdatePosCenter - g.stringWidth("1234-56-78") / 2; + const cdatePosLeft = cdatePosCenter - g.stringWidth("1234-56-78") / 2; if (lastDateString) { // Clear the area where we want to draw the time. //g.setBgColor("#FF6600"); // for debugging @@ -212,8 +210,7 @@ function drawLine(x1, y1, x2, y2, color) { // On high-bpp devices, use anti-aliasing. Low-bpp (Bangle.js 2) doesn't clear nicely with AA. if (bpp > 3 && g.drawLineAA) { g.drawLineAA(x1, y1, x2, y2); - } - else { + } else { g.drawLine(x1, y1, x2, y2); } } @@ -228,17 +225,17 @@ function drawAnalogClock(curDate) { g.setBgColor(colorBg); // Init variables for drawing any seconds we have not drawn. // If minute changed, we'll set for the full wheel below. - var firstDrawSecond = lastAnalogDate ? lastAnalogDate.getSeconds() + 1 : curDate.getSeconds(); - var lastDrawSecond = curDate.getSeconds(); + let firstDrawSecond = lastAnalogDate ? lastAnalogDate.getSeconds() + 1 : curDate.getSeconds(); + let lastDrawSecond = curDate.getSeconds(); if (!lastAnalogDate || curDate.getMinutes() != lastAnalogDate.getMinutes()) { // Draw the main hour lines. //g.setColor("#9C9CFF"); //g.drawCircle(clockCtrX, clockCtrY, analogRad); for (let i = 0; i < 60; i = i + 15) { - let edgeX = clockCtrX + analogRad * Math.sin(i * Math.PI / 30); - let edgeY = clockCtrY - analogRad * Math.cos(i * Math.PI / 30); - let innerX = clockCtrX + (analogRad - analogMainLineLength) * Math.sin(i * Math.PI / 30); - let innerY = clockCtrY - (analogRad - analogMainLineLength) * Math.cos(i * Math.PI / 30); + const edgeX = clockCtrX + analogRad * Math.sin(i * Math.PI / 30); + const edgeY = clockCtrY - analogRad * Math.cos(i * Math.PI / 30); + const innerX = clockCtrX + (analogRad - analogMainLineLength) * Math.sin(i * Math.PI / 30); + const innerY = clockCtrY - (analogRad - analogMainLineLength) * Math.cos(i * Math.PI / 30); drawLine(edgeX, edgeY, innerX, innerY, colorHours); } // Set for drawing the full second wheel. @@ -247,17 +244,15 @@ function drawAnalogClock(curDate) { } // Draw the second wheel, or the parts of it that we haven't done yet. for (let i = firstDrawSecond; i <= lastDrawSecond; i++) { - let edgeX = clockCtrX + analogRad * Math.sin(i * Math.PI / 30); - let edgeY = clockCtrY - analogRad * Math.cos(i * Math.PI / 30); - let innerX = clockCtrX + (analogRad - analogSubLineLength) * Math.sin(i * Math.PI / 30); - let innerY = clockCtrY - (analogRad - analogSubLineLength) * Math.cos(i * Math.PI / 30); + const edgeX = clockCtrX + analogRad * Math.sin(i * Math.PI / 30); + const edgeY = clockCtrY - analogRad * Math.cos(i * Math.PI / 30); + const innerX = clockCtrX + (analogRad - analogSubLineLength) * Math.sin(i * Math.PI / 30); + const innerY = clockCtrY - (analogRad - analogSubLineLength) * Math.cos(i * Math.PI / 30); if (i <= curDate.getSeconds()) { drawLine(edgeX, edgeY, innerX, innerY, colorSeconds); - } - else if (i % 5 == 0) { + } else if (i % 5 == 0) { drawLine(edgeX, edgeY, innerX, innerY, colorHours); - } - else { + } else { clearLine(edgeX, edgeY, innerX, innerY); } } @@ -265,25 +260,25 @@ function drawAnalogClock(curDate) { // Clear previous hands. if (curDate.getMinutes() != lastAnalogDate.getMinutes()) { // Clear hour hand. - let HhAngle = (lastAnalogDate.getHours() + lastAnalogDate.getMinutes() / 60) * Math.PI / 6; - let HhEdgeX = clockCtrX + analogHourHandLength * Math.sin(HhAngle); - let HhEdgeY = clockCtrY - analogHourHandLength * Math.cos(HhAngle); + const HhAngle = (lastAnalogDate.getHours() + lastAnalogDate.getMinutes() / 60) * Math.PI / 6; + const HhEdgeX = clockCtrX + analogHourHandLength * Math.sin(HhAngle); + const HhEdgeY = clockCtrY - analogHourHandLength * Math.cos(HhAngle); clearLine(HhEdgeX, HhEdgeY, clockCtrX, clockCtrY); // Clear minute hand. - let MhEdgeX = clockCtrX + analogMinuteHandLength * Math.sin(lastAnalogDate.getMinutes() * Math.PI / 30); - let MhEdgeY = clockCtrY - analogMinuteHandLength * Math.cos(lastAnalogDate.getMinutes() * Math.PI / 30); + const MhEdgeX = clockCtrX + analogMinuteHandLength * Math.sin(lastAnalogDate.getMinutes() * Math.PI / 30); + const MhEdgeY = clockCtrY - analogMinuteHandLength * Math.cos(lastAnalogDate.getMinutes() * Math.PI / 30); clearLine(MhEdgeX, MhEdgeY, clockCtrX, clockCtrY); } } if (!lastAnalogDate || curDate.getMinutes() != lastAnalogDate.getMinutes()) { // Draw hour hand. - let HhAngle = (curDate.getHours() + curDate.getMinutes() / 60) * Math.PI / 6; - let HhEdgeX = clockCtrX + analogHourHandLength * Math.sin(HhAngle); - let HhEdgeY = clockCtrY - analogHourHandLength * Math.cos(HhAngle); + const HhAngle = (curDate.getHours() + curDate.getMinutes() / 60) * Math.PI / 6; + const HhEdgeX = clockCtrX + analogHourHandLength * Math.sin(HhAngle); + const HhEdgeY = clockCtrY - analogHourHandLength * Math.cos(HhAngle); drawLine(HhEdgeX, HhEdgeY, clockCtrX, clockCtrY, colorHands); // Draw minute hand. - let MhEdgeX = clockCtrX + analogMinuteHandLength * Math.sin(curDate.getMinutes() * Math.PI / 30); - let MhEdgeY = clockCtrY - analogMinuteHandLength * Math.cos(curDate.getMinutes() * Math.PI / 30); + const MhEdgeX = clockCtrX + analogMinuteHandLength * Math.sin(curDate.getMinutes() * Math.PI / 30); + const MhEdgeY = clockCtrY - analogMinuteHandLength * Math.cos(curDate.getMinutes() * Math.PI / 30); drawLine(MhEdgeX, MhEdgeY, clockCtrX, clockCtrY, colorHands); } lastAnalogDate = curDate; @@ -292,8 +287,7 @@ function drawAnalogClock(curDate) { function switchClockface() { if (clockface == "digital") { clockface = "analog"; - } - else { + } else { clockface = "digital"; } // Clear whole lower area. @@ -340,10 +334,10 @@ updateStardate(); updateConventionalTime(); // Make sure widgets can be shown. //g.setColor("#FF0000"); g.fillRect(0, 0, g.getWidth(), widgetsHeight); // debug -Bangle.loadWidgets(); -Bangle.drawWidgets(); // Show launcher on button press as usual for a clock face Bangle.setUI("clock", Bangle.showLauncher); +Bangle.loadWidgets(); +Bangle.drawWidgets(); // Stop updates when LCD is off, restart when on Bangle.on('lcdPower', on => { if (on) { @@ -351,8 +345,7 @@ Bangle.on('lcdPower', on => { // Draw immediately to kick things off. updateStardate(); updateConventionalTime(); - } - else { + } else { redrawClock = false; } }); diff --git a/apps/stardateclock/metadata.json b/apps/stardateclock/metadata.json index 2f4f27425..d27b14512 100644 --- a/apps/stardateclock/metadata.json +++ b/apps/stardateclock/metadata.json @@ -3,7 +3,7 @@ "name":"Stardate Clock", "shortName":"Stardate Clock", "description": "A clock displaying a stardate along with a 'standard' digital/analog clock in LCARS design", - "version":"0.01", + "version":"0.03", "icon": "app.png", "type":"clock", "tags": "clock", diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog index 104fce19d..14c84afd5 100644 --- a/apps/stopwatch/ChangeLog +++ b/apps/stopwatch/ChangeLog @@ -1,2 +1,3 @@ 0.01: first release 0.02: Adjust for touch events outside of screen g dimensions +0.03: Do not register as watch, manually start clock on button diff --git a/apps/stopwatch/metadata.json b/apps/stopwatch/metadata.json index cc13ec92f..7840dd9b5 100644 --- a/apps/stopwatch/metadata.json +++ b/apps/stopwatch/metadata.json @@ -1,7 +1,7 @@ { "id": "stopwatch", "name": "Stopwatch Touch", - "version": "0.02", + "version": "0.03", "description": "A touch based stop watch for Bangle JS 2", "icon": "stopwatch.png", "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js index e2be95451..92e7a9977 100644 --- a/apps/stopwatch/stopwatch.app.js +++ b/apps/stopwatch/stopwatch.app.js @@ -227,4 +227,4 @@ g.fillRect(0,0,w,h); Bangle.loadWidgets(); Bangle.drawWidgets(); draw(); -Bangle.setUI("clock"); // Show launcher when button pressed +setWatch(() => load(), BTN, { repeat: false, edge: "falling" }); diff --git a/apps/swp2clk/metadata.json b/apps/swp2clk/metadata.json index aa95a6473..8b0cce2d8 100644 --- a/apps/swp2clk/metadata.json +++ b/apps/swp2clk/metadata.json @@ -5,7 +5,7 @@ "version": "0.01", "description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.", "icon": "app.png", - "type": "boot", + "type": "bootloader", "tags": "tools", "supports": ["BANGLEJS2"], "readme": "README.md", diff --git a/apps/tabanchi/ChangeLog b/apps/tabanchi/ChangeLog new file mode 100644 index 000000000..4e2facf6f --- /dev/null +++ b/apps/tabanchi/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial implementation +0.02: Fix app icon +0.03: Fix clock animation issue and reduce source size diff --git a/apps/tabanchi/README.md b/apps/tabanchi/README.md new file mode 100644 index 000000000..71ad22558 --- /dev/null +++ b/apps/tabanchi/README.md @@ -0,0 +1,47 @@ +たばんち (tabanchi) +=================== + +A Tamagotchi clone watch app for the BangleJS2 smartwatch. + +Author +------ + +Written by pancake in 2022, powered by insomnia + +Source repository: https://github.com/trufae/tabanchi + +Features +-------- + +* [x] 12/24 clock with HH:mm:ss +* [x] Battery level indicator +* [x] Eating meals and snacks +* [x] Refusing to do things +* [x] Getting sick +* [x] Take a shower +* [x] Switch on/off the light +* [x] Status for happy/hunger/discipline +* [ ] Evolutions +* [ ] Hatching eggs +* [x] Playing a game +* [ ] Education +* [x] Medicine +* [ ] Death + + +Resources +--------- + +* Original pixmaps taken from: + - https://www.spriters-resource.com/resources/sheets/141/144400.png +* Espruino Image converter: + - https://www.espruino.com/Image+Converter +* Tamagotchi Essentials + - https://tamagotchi.fandom.com/wiki/Tamagotchi_(1996_Pet) +* Tamagotchi Emulator Source (Java) + - https://gist.github.com/aerospark/80c60e801398fd961e3f + +Screenshots +----------- +![tama on bangle](screenshot.jpg) + diff --git a/apps/tabanchi/app-icon.js b/apps/tabanchi/app-icon.js new file mode 100644 index 000000000..126bbb1a9 --- /dev/null +++ b/apps/tabanchi/app-icon.js @@ -0,0 +1 @@ +atob("MDCI/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v///wAAAP///////////////////wAAAP////////////7+/v7+/v7+/v7+/v7+/v///wAAAP///////////////////wAAAP////////////7+/v7+/v7+/v7+/v7+/v///wAAAP///////////////////wAAAP////////////7+/v7+/v7+/v7+/v///wAAAP///////wAAAP///wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v///wAAAP///////wAAAP///wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v///wAAAP///////wAAAP///wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///////////////////wAAAP///////////////wAAAP////7+/v7+/v///wAAAP///////////////////wAAAP///////////////wAAAP////7+/v7+/v///wAAAP///////////////////wAAAP///////////////wAAAP////7+/v7+/v///wAAAP///wAAAP///////////wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///wAAAP///////////wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///wAAAP///////////wAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///////////////////////wAAAP///////////wAAAP////7+/v7+/v///wAAAP///////////////////////wAAAP///////////wAAAP////7+/v7+/v///wAAAP///////////////////////wAAAP///////////wAAAP////7+/v7+/v///wAAAP///////////////////////wAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///////////////////////wAAAAAAAAAAAAAAAAAAAP////7+/v7+/v///wAAAP///////////////////////wAAAAAAAAAAAAAAAAAAAP////7+/v///wAAAP///////////////////////////////////wAAAP////////7+/v7+/v///wAAAP///////////////////////////////////wAAAP////////7+/v7+/v///wAAAP///////////////////////////////////wAAAP////////7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////7+/v7+/v7+/v7+/v7+/v7+/v////////////////////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v////////////////////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v////////////////////////////////////7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/g==") diff --git a/apps/tabanchi/app.js b/apps/tabanchi/app.js new file mode 100644 index 000000000..f159052b7 --- /dev/null +++ b/apps/tabanchi/app.js @@ -0,0 +1,1585 @@ +// GPL TAMAGOTCHI CLONE FOR THE BANGLEJS2 SMARTWATCH BY pancake 2022 +// TABANCHI -- たばんち + +const scale = 6; +const w = g.getWidth(); +const h = g.getHeight(); +const yy = 34; +const y = 40 - scale; +let tool = -1; +let hd = 1; +let vd = 1; +let x = 20; +let sx = 0; // screen scroll x position +let cacaLevel = 0; +let cacaBirth = null; +let angryState = 0; +let animated = true; +let transition = false; +let caca = null; +let egg = null; +let mode = ''; +let evolution = 1; +let callForAttention = false; // TODO : move into tama{} +let useAmPm = true; +let oldMode = ''; +let gameChoice = 0; +let gameTries = 0; +let gameWins = 0; +let statusMode = 0; +let lightSelect = 0; +let lightMode = 0; // on is zero +let frame = 0; + +const tama = { + age: 0, + weight: 1, + aspect: 6, + discipline: 0, + happy: 3, + sick: false, + hungry: 3, + cacas: 0, + // hidden + sickness: 0, + defenses: 100, + tummy: 100, + awake: 3 +}; + + +g.setBgColor(0); + +const sun = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('773nW9rnvfc=') +}; + +const tama06eat0 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('/////4A/vd+B7+N3g/e714P39/f39/f3++/8H/////8=') +}; +const meal0 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('gXp6tbW1tYE=') +}; +const meal1 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('v19htbW1tYE=') +}; + +const meal2 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////5+htYE=') +}; +const snack0 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('358D08vA+fs=') +}; +const snack1 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('///708vA+fs=') +}; +const snack2 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////+vA+fs=') +}; + +const angry0 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////8/Pv/8=') +}; + +const angry1 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('8/Dg4fn/v/8=') +}; + +const right = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('7+eDgYPn7/8=') +}; + +const left = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('9+fBgcHn9/8=') +}; + +const img_on = { + width: 16, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('//+M73VvdW91r3Wvjc///w==') +}; + +const img_off = { + width: 16, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('//+MIXXvdGN173Xvje///w==') +}; + +const right0 = { + width: 3, + height: 5, + bpp: 1, + transparent: 1, + buffer: atob('d1Y=') +}; + +const right1 = { + width: 3, + height: 5, + bpp: 1, + transparent: 1, + buffer: atob('ZBY=') +}; + +const am = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('w7mBuf+Rqak=') +}; + +const pm = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('g52Dn/+Rqak=') +}; +const numbers = [ + { // 0 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('lmZmnw==') + }, { // 1 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('2d3d3w==') + }, { // 2 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('lu23Dw==') + }, { // 3 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('Hu3uHw==') + }, { // 4 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('lVVQ3w==') + }, { // 5 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('B3HuHw==') + }, { // 6 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('l3Fmnw==') + }, { // 7 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('Bm7d3w==') + }, { // 8 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('lmlmnw==') + }, { // 9 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('lmjunw==') + } +]; + +const snumbers = [ + { // 0 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4qqjw==') + }, { // 1 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/93d3w==') + }, { // 2 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/46Ljw==') + }, { // 3 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/46Ojw==') + }, { // 4 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/6qO7w==') + }, { // 5 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4uOjw==') + }, { // 6 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4uKjw==') + }, { // 7 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4ru7w==') + }, { // 8 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4qKjw==') + }, { // 9 + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/4qOjw==') + } +]; + +const colon = { + width: 4, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/f/9/w==') +}; + +const egg00 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('/////////D/7n/GP8e/n9+537nfvx/OP+Z/wD/////8=') +}; + +const h24 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('ldWxnf/bw9s=') +}; + +const discipline = { + width: 32, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('///v/x//7/9v/i//akqqI27erqtqWiqja1rqrxpK6qM=') +}; + +const linebar = { + width: 32, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////4AAAA9////3f///93////d////3f///94AAAA8=') +}; + +const hungry = { + width: 32, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////2////9qiKr/aqqa/wqquv9qqLz/aK6+/2/4+f8=') +}; + +const happy = { + width: 32, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////2/iP/9v6qv/bGqr/w9iK/9obvP/a277/2yu5/8=') +}; + +const vs = { + width: 16, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('Uf9V/1f/W/9d/7X/sf///w==') +}; + +const egg01 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('///////////8P/uf8Y/x7+P37nfud/PP+Z/gB/////8=') +}; + +const tama06no0 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('//////w/+9/3gey974Hv3e/B7/fv9+/39+/4H/////8=') +}; + +const tama06no1 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('//////w/+9+B7703gfe794P37/fv9+/39+/4H/////8=') +}; + +const caca00 = { + width: 12, + height: 12, + bpp: 1, + transparent: 1, + buffer: atob('/////733v72/+f4vw3wH////') +}; + +const caca01 = { + width: 12, + height: 12, + bpp: 1, + transparent: 1, + buffer: atob('////v/33v7+3+f4v0HwH////') +}; + +const tama00 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('///////////8H/vv9oHvveuB793vwd/34A////////8=') +}; +const tama01 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('/////////AH7vfeB7sfvwevd78Hv7+/v7+/33/g///8=') +}; + +const tool00 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('//////7v4f8zHwP8zHwP8zHwP8zHwP8THwP8DHwP8BHgP8AHgP8AHgP+AHgP+APgP/AfAP/g/AP/x/AP/x/AP/x/AP/w/gP/w/8P/w/8P/gf8P/gf8P/AP8P/AP4P/AP4P/gf4P/wf4P/4/8f/////A=') +}; + +const tool01 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('///////D///fD4/+Pn4/+P/4//P/7/v+Af3n4APjDwAHDjgADP/AGD//DPh/+H/h/+O5x/GOkxxCOkxBOG8xh/GYz//HBz//jBn//xjmPw4/ODx///Dx+AfH/8AP///////+Af//8AP///////////A=') +}; + +const tool02 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('///////////////+D///4B///x8///xUf/hmTf+An/f4AmTfwAmTfgAl8fABx8+AD4B8AH8D4AP//wAf//gA///AB//+AH//8AP//4A///wD//8AP//4A///4H///4H///8H///+P/////////////A=') +}; + +const tool03 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('/////////////x////g////gf//fwP/+P4P/+OMH/+GEH/8DCD/4BjB/wxhB/h4xg/D8Qw8H8Yw4H+M5wH+H/gD/H/gD/D/wB8B/wAwB/wAAz/wAD//gAH//CAf//PB////n//////////////////A=') +}; + +const tool10 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('////////////////wf///AH//yAD/Dkfh8B0/wAA8/wA44vnD545Bgx8ZA4x4H58xznP8RjjH8ZnmP8ZmGPw5gPAB58fAHx8fw/x4///j8///j8f//D8f/8H+D/gf/AAA//gAD//+Af///////////A=') +}; + +const tool11 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('//////////////////////B///wAD/+AAA/8D/wPwfz+Hg/z/Dj7z3xH5znYM5znIM5TmIOdT8YGeL85GOP45neP/xj+P/zz+P/zx+H/nx+H/n4///H4/h/P8QAGP+AAAf/D/g////////////////A=') +}; +const tool12 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('/////////////////////////////z////5////5///94///48/g/8c8AH8M4AGcEwAOEEgAyAgAAwAgAB4YwAH8YwAH+c4AH/c4AAf84gAB/4gAB/5wAB/54AD//8AH///gf/////////////////A=') +}; + +const tool13 = { + width: 30, + height: 30, + bpp: 1, + transparent: 1, + buffer: atob('/////////////////////////+A///8AP//weH//x/jg/j/yAPnPwOHnM4/jnM9/5n8//5k/+fkk/vHEmPPgMjAbgcxxjmc4fD/48AB/4+AQ/x//4fj//+AH///AP/////////////////////////A=') +}; + +const shower = { + width: 8, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('5cuXy+XLl8vly5fL5cuXyw==') +}; + +const tools = [ + tool00, tool01, tool02, tool03, + tool10, tool11, tool12, tool13 +]; + +const tamabg = { + width: 176, + height: 176, + bpp: 8, + buffer: require('heatshrink').decompress(atob('/wAHlUrldPp9VAH4A/AClPlcqlSnIVo1Vq2B1esACOrAAQTSDhIAHHaQkKDrAwXGpgJBwFWWQKtKkkrwGr6wA/AH4AdWQMrVxEqquBJ34A/AEWBlcAVwsAqurHd/X6+y2Quq6AAEWH+rqqvFp+lM7QYVVoOsAAKypV4qwaJQIAC1hGfwMrVwcrwBlcLyisB1utWIav/fxZPiq0qV4VV1ZkcMqReCVwIACMASxlJTJPGJwhPDI7urlauBlWAMbplSLwwADV74/FJJZQRfw5OKCQIACJyVWlX+lerV7XRV6iuJ1usWDymFV5S3IVyGJxOzV4JOEVYWsAAKxTwCvBqqvVMQ6vSL4wACMAKvkWBBWIJyavDJwPXVooQDWQZOO1crklWR5pNHV8iwDCg4AEeza3KV7BQBAAOyAwQAFWCOsqsqqxfSNBiDQ2SuIKYZREK4JbBAAJsDVyvRVxqvNJIyvBAgRCCVxBeEV8hZKV8Gz63X66mB1gPHWCwAOJyyqBVppeCJ5qvXLa6vRMIRjNMBqvtWAQAOV6N6V+OsV5xhaV96wPV75gkV5JeBMB4AB2RiNJjxOCISJNLJhqvBqyvkMpmsWBRsR1hiNKKAdNV4Swc2av4NBOyJ5av/JwSvbJhqvQVzRpJ2WsMDSvB1hQMKSSv/VsmsMxOyWDmsMRL4fAA2zWDSvbVzZlM2RgaV5RKjWAr8jV7XRAAJjd6+zAAJiX2avx66v0LzJiR6+y2SwXDIJQc6IABWCOyV7AnNV8hbOABCwKx+PV5ReKIJAGLKaawQxOJAYOz2b7JV9SwX66wJV5WzL5g/HA5ivT1ivTVpyvOJoi3TV6xlS1usMRw+HJBKvmVgKtBVyCvPMRBTFWF+sAAJiQHYxIKKKpIIIgQADVgKuSV7a5MV7CwGLoYAEFC5IfJIJHE2ezJLqv/MwKvBMQJkHEzKvhI4QADIbavgMsKvBUzpVPJDZFkV/5nBVshKFFUyv0Bg5h/AH6vhWIhY/AH6vsAH4A/V7cGV4WyAAQwmFSIRDACQ6VNEIib6CvCg9W1gAL1oAGCJPQAAoOHJoIWUIihFKCg49FHgwAQI5wgSqyvBLpAAV6IoFCx5MGC6AnQYZ4SEV7InMECGmqCvV2awLWIRHMJhSugE44oMVy4nPD6KvXADLJLAEabGbBqvYAAKvfkqvuAF5rJ1gABCY3RV7QAB6I8JFCGsV4T6NAH6vZWIYUSACRAM6IAFBIQ+B6KvEWFGzx6uw6KJWV9IAM1lWV4L2BQuCv46J6K1ivzmSvCWAKxaKo4cXdjyJh6IABEhxSaxCvEWDZrcLz6uPeqoqPV8KwYfRnRMS5gY1iuObTYyHAAQcY2avHJK3RV5iwBWJwaKV0ivjWAaviOCI2BNp6yEV6wZOV3I1GFSyvJEB6tUSxobRAALoEISytjGh5uKV5qwBWJatXbZgkWJAKxJERivzWIQABV6hPKVroqJE7KAKN4QACcpIAeKK76BAARxBV4R1IY46uhbUKHQNgSu7WY9WgKvBOY6vHV0KOP6PRD5xKHAF53f1lQg9WE4w0JX46vpWR+sV2x5gV4+sMJquxNxiuVaILTIBQfRWGavWLgQ1cV6wAcJSwBBAAixm1lWV4IrDQKQ1aV2ZPbQ4SAN6IABV7iCVIgI0WTT59OCYqvdGCKxU1lQkqvBBIh3VWSaugTYglPV+B/EV6YKGGKxpQ6KukFRBBDfg6tXKTI0PV5SwKx43QV1bgPLBQdTECAAS6IABPgusqyvJRTxnFV2SOS1gABV9izGHASvLRgKNfVsSuRRy/RV1hcGV5gAC1gveV360SV1RdBmSvOHr2sWECuQKP4tNqyvPLzmsD4XRV1yPsPbpKBV6SQbacDQDf9QArfgqvSWLIjLdYKu/V2R1BqCvTMq3REsBJnGaIllV7AYBVySvQAAITB6KudRE5xUK6KvD1iwVH5wlVFhwkRQrQAE6IrgLYIiLqEBqzAMABfRV05qLLwIqLVryuMOaB4SV4MHV4aOXVsyXMWASyH6J9Lx6vTfTh5SV445UHhKtpNBauMACh0cECavJHizlUL7IAtKC7nINSKvKDyRahV3esKTCvmAAPRcygaRaagAExOJV36VCEZCvfSpStNdyquRGCB7JV2RyQV6BGIVyRjSEqj8QWaivYEzavRaYyEZHpatcVxArVVsiwDAAKveNAPRQ7haYV7ZsDWZYcPKbawJV6oAgIAYkgSKSufODivZ1msRb/Q6PRV8CQTGwJaHDiR1cV7KtBHb4AjIgYAULI3RDCBQedQKvVVwg+iV+5YIEJr/fWAqvBgFWeAysGYwgAGDJYANDYInBWLiuhEpquhOoivBkivLVwJcOCALpUaMKvYE6pHYWB2sq0lqxCJQ5KxKVrR/SV1wpCAAiulFoSvDIhCvTWR4jSOCSuXTCauqV4vRBxJlVEBKJYORqurAFivEdZivfRbAjiVv6vFCJpliaajWL6KurFgStoAAKvQNp46WWSyxbVzDpKADgoCV4Msq6MaZ4LrZV6gvJDTStPWEpwE1lQqyvSRgytaEYaxTGJivhDxSteIg6vBqyVURoKtdSSh0R6KOZd6HRMi4oKV4KwBS6qukSRiUTaI6uiH6olOV4MrqzSWV86TI6IcVTARKTVqKxSaiCvBktWA4jVTWFPRVzJNDV1BzNDySvHFBjUIWFIAvVy5yKDyivJFJCtID4msTP6u/V8AiQWX+t6KHLVzRoL1ivfLhSx0NxqdRIhSJUGqqvYFDyygEggYT1iSQCJRdbV7fRbMSugephNTMoyvPLK7XRV44oO6KxvGBD3MfaAdHRBZUZWCivD1gzQV6pdYb5XRBYJHYZhSJHPSKweV4YoSWDB0LKa6xICyw2IVbhCSboIABqyvUWFCtSK4REUdJyukMBSsDV4cHV9x4LVqivXRKCvkAAPRdBeImSvVWDQ/KV6qIIDxhhQV84AMV7CwV1hkOWCREWVqDBSV/4AD6IAHEqrABVrTQKG6qfhE4OsV8xQBFRxRXVrT4HJCI1GMMBVPV42PbSiujKg57iciomb6IiQV4yu8WIgtrVxY5ZaaivBgKvWI4KCrFlquLRxgkW6IVJmUlV6b4cLqjarVxyOKaa/REQ6vSEpKErFVSuPWKfREC2sqyvPExiCmbgiyYfJqtSNRWPVwwABV7IbBfjKEYGKKwXI5pgPWB4yGVxyvH6KvDBxJMSWEBxVABJ5GI46vZETZNI1lQV4gQFJaitd6KOGFbAfJVz6wIQbesqyvEFIpMWe44ARFSHRVzJ1GV7bSXJ5SvFSI6wsFirQaZgivyJQuPx6vHI4hgTWBIABVsrcOEhphbaBJaQKAyvFxEyV4KGjWB6uYFRZ4UV7QgFH4I3MBwQWE2ezV4sHqwPG2QZCFZywWEjBiJJSR4GG7iuFV54AHDYKvCgyvCXYKtBAAiJbGAQjgMhYTQHQJmB1vRGrghBMAYUMTAoAG2esmUqq3XAAPW6wNDAoIA/ADxvBNQY/66+sqyvB6xD8AFatBAAZA7V4gA/6HQFM+y2ez2WyJ/esqqv/LwYADV0gAB1gABWLxOFJ63QV/ZVGV86sC1oAEWIJIVVxav/VqhWFMDiuK1iuC2ezV4iwMHxiuHJ6yvQ6/X2ZNBAAavmKwJfcABKpBVwQAGWAJHRIAquJV7F6V5pXBKQoHBCQ6/FYCCviGpZWFV/+sV55WJKgJVFAwOsAAawPKxBfYVoQ3DBg2sV8BBDV95WLNYuyYIITEWB5ZKL6jmGHYL1G1hHBV777JV9JPE2YABUQ2y64KFAAawdLJ42DHYmzGwRFBewJIJV5r5RVzKvVLQRbGLAJlYMxwQGJB7nFegRTJJL6vrMogAFx+JBZIADBwRmaBxJJRf4qvaWCauVV6xaDV6GPV7ZoQV5arEV/6vbACxmHJoyvc1g9QV5WyV5xKSV8xTJACZmFU6qvgI6CqXWLavP2Sv/V8esV8iyUV6CwbM4yv/I4KBLJkavYWARoZVwKv/exiuiWCKvPWDWzV0ZHJVzGz2ZvLV/6wCKgquSV8JGKVzKvqVx6vUNSusVwxiX65ZPV6xGIWESuQV6SyE1htL2atDVwJiMNSREPIJyuHNh48IVT6vZWIhuKVwRmQMCBiPeZWzAAIJHLAKIWJp6vtWIasINgJlUVx5iPeRKvIVwKtXfr6vgWARwHNoJgbNBJAQWB+sVzSvpqxCYOIhkBMqxXIWDQADfAQABWAZIXVyivzOIhkYV8A+CHgivDXAiuaV/5xNV7prIfGyvMK5av/V64KGRzawCVzj9RV+hhf1ivnJsj9iV/RYENpSu8UoxNgV/htMIP7ylV4MAV/4A/AFivCqusIn4A/AFWrqv+qurIn4A/V9cr/0rwBE/AH4AqwEq/0qqxE/AH4Ap1lVgH+/0r1ZG/AH4AowMrVwP+lVW1hH/AH4Am1dVVwQABleAWH4A/AEusq0qV4iw/AH6unwErVwqw/AH4Al1dWlSuHAAMqquA1ZQ/AH4Ab1mrwErVxQACldVWQIA/AH4AYq1VlcrVpgADgEqAAQXBY4IA/AH4ASgClI')) +}; + +const tama06happy = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('/////4Afvm+Hd+B74duDq7v7g/vv++/79/f4D/////8=') +}; + +const battery = { + width: 32, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/////x/t//9vxP//bG2arx9taa9obQuPa2177xy2i58=') +}; +const snack = { + width: 24, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('z//7t//7vDGbzb1q9aF5ta1qzbKa////') +}; +const meal = { + width: 24, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('////k//fqzjfqt7fqhDfqvbfuxlf////') +}; + +const face = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/8OlgZmBw/8=') +}; + +const year = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/6qpq8vrn/8=') +}; + +const weight = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/34A54G9pQA=') +}; + +const weight_g = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('49vj+cO7x/8=') +}; + +const heart0 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('yba+vt3r9/8=') +}; + +const heart1 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('yaCgoMHj9/8=') +}; + +g.clear(); +g.setColor(1, 1, 1); +g.fillRect(0, 0, 200, 200); + +g.setColor(0); + +g.drawString('Loading...', 10, 10); +egg = egg00; +n = tama00; + +function drawHearts (n) { + for (i = 0; i < 4; i++) { + const himg = (i < n) ? heart1 : heart0; + g.drawImage(himg, 1 + (scale * (8 * i)) - scale - scale, 40 + (scale * 8), { scale: (scale) }); + } +} + +function drawLinebar (n, arrow) { // 0-100 + g.drawImage(linebar, 0, yy + (scale * 8), { scale: scale }); + + let wop = scale * 2; // (frame++%2)? scale*3:scale*2; + if (frame % 2) { + wop += scale; + } + let twelve = 12; + if (arrow) { + twelve = 11; + } + const val = (n * twelve) / 100; + const max = val || twelve; + + for (let i = 0; i < max; i++) { + g.setColor(0, 0, 0); + + if (arrow) { + const x = wop + (i * scale * 2) + ((i % 2) * scale); + const y = yy + (scale * 11); + g.fillRect(x + (scale * 2), y, x + (scale * 3), y + scale); + g.fillRect(x + scale, y + scale, x + (scale * 2), y + (scale * 2)); + g.fillRect(x, y + (scale * 2), x + scale, y + (scale * 3)); + } else { + const x = (i * scale * 2) + (scale * 2); + const y = yy + (scale * 11); + g.fillRect(x, y, x + scale, y + scale * 3); + } + } +} + +function drawStatus () { + switch (statusMode) { + case 0: + g.drawImage(face, scale, yy, { scale: scale }); + g.drawImage(weight, scale, yy + (scale * 8), { scale: scale }); + g.drawImage(numbers[0], w - (scale * 14), yy, { scale: scale }); + g.drawImage(year, w - (scale * 8), yy, { scale: scale }); + g.drawImage(numbers[1], w - (scale * 14), yy + (scale * 9), { scale: scale }); + g.drawImage(weight_g, w - (scale * 8), yy + (scale * 9), { scale: scale }); + break; + case 1: // discipline + g.drawImage(discipline, 0, yy, { scale: scale }); + drawLinebar(tama.discipline, false); + break; + case 2: // hungry + g.drawImage(hungry, scale, yy, { scale: scale }); + drawHearts(tama.hungry); + break; + case 3: // happy + g.drawImage(happy, scale, yy, { scale: scale }); + drawHearts(tama.happy); + break; + case 5: // battery + g.drawImage(battery, scale, yy, { scale: scale }); + drawLinebar(E.getBattery(), true); + break; + default: + statusMode = 0; + drawStatus(); + break; + } +} + +function drawScene () { + if (Bangle.isLocked()) { + tool = -1; + } + g.setColor(0, 0, 0); + g.fillRect(0, 0, 200, 200); + g.drawImage(tamabg, 0, 0, { scale: 1 }); + g.setColor(1, 1, 1); + + if (evolution == 0) { + g.drawImage(egg, w / 4, 32, { scale: scale }); + return; + } + if (callForAttention) { + g.drawImage(tool13, 10 + 30 + 10 + 30 + 10 + 30 + 10, 135); + } + if (mode == 'game') { + drawGame(); + if (!transition) { + if (gameChoice == 2) { + g.drawImage(right, w - (scale * 7), 40 + (scale * 4), { scale: scale }); + } else if (gameChoice == 1) { + g.drawImage(left, 0, 40 + (scale * 4), { scale: scale }); + } + return; + } + } + if (gameTries > 4) { + mode = ''; + oldMode = ''; + const s0 = numbers[gameWins]; + const s1 = numbers[(5 - gameWins)]; + g.drawImage(s0, (scale * 5), 60, { scale: scale }); + g.drawImage(vs, (scale * 12), 60, { scale: scale }); + g.drawImage(s1, (scale * 22), 60, { scale: scale }); + + gameTries++; + if (gameTries > 10) { + const winrar = (gameWins > 2); + gameTries = 0; + gameWins = 0; + oldMode = ''; + mode = ''; + if (winrar) { + tama.happy++; + animateHappy(); + } + } + return; + } + + if (mode == 'clock') { + drawClock(); + if (!transition) { + return; + } + } + + drawTools(); + if (mode == 'status') { + drawStatus(); + return; + } + if (mode == 'food') { + drawFoodMenu(); + return; + } + if (mode == 'light') { + drawLight(); + return; + } + if (mode == 'happy') { + drawHappy(); + return; + } + if (mode == 'angry') { + drawAngry(); + return; + } + if (mode == 'medicine') { + if (tama.sick > 0) { + drawMedicine(); + } else { + animateAngry(); + } + return; + } + if (mode == 'eating') { + if (lightSelect == 0 && tama.hungry > 4) { + drawEatingNo(); + } else { + drawEating(); + } + return; + } + if (lightMode) { + // just dark screen and maybe zZz if its sleeping + g.setColor(0, 0, 0); + g.fillRect(0, 38, w + sx, h - 50); + if (tama.sleep) { + drawCaca(); + } + } else { + // draw tamagotchi + g.drawImage(n, x + sx, y, { scale: scale }); + // draw caca + drawCaca(); + } +} + +function drawAngry () { + const one = angryState % 2; + g.drawImage(one ? tama06no0 : tama06no1, (scale * 5), 40, { scale: scale }); + g.drawImage(one ? angry0 : angry1, (scale * 20), 40, { scale: scale }); +} + +function drawHappy () { + const one = angryState % 2; + g.drawImage(one ? tama06happy : tama06no1, (scale * 5), 40, { scale: scale }); + if (one) { + g.drawImage(sun, (scale * 20), 46, { scale: scale }); + } +} + +function drawEatingNo () { // food eating animation + const one = angryState % 2; + + g.drawImage(lightSelect ? snack0 : meal0, scale, 40 + (scale * 7), { scale: scale }); + + g.drawImage(one ? tama06no0 : tama06no1, (scale * 10), 40, { scale: scale }); +} + +const med0 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('///4P/1//X/9f+AP+7/4P/o/+j/4P/g//H/+//7///8=') +}; +const med1 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('//////g//X/9f+AP+z/7P/o/+D/7P/g//H/+//7///8=') +}; + +const med2 = { + width: 16, + height: 16, + bpp: 1, + transparent: 1, + buffer: atob('////////+D/9f+AP+j/7P/s/+z/7v/g//H/+//7///8=') +}; + +function drawMedicine () { // food eating animation + const med = [med0, med1, med2]; + const img = med[0 | ((frame / 2) % 3)]; + if (img) { + g.drawImage(img, 0, 34, { scale: scale }); + } + g.drawImage(tama06no0, (scale * 10), 40, { scale: scale }); +} + +function drawEating () { // food eating animation + const one = angryState % 2; + const snack = [snack0, snack1, snack2]; + const meal = [meal0, meal1, meal2]; + const img = lightSelect ? snack[0 | (frame / 2)] : meal[0 | (frame / 2)]; + if (img) { + g.drawImage(img, scale, 40 + (scale * 7), { scale: scale }); + } + g.drawImage(one ? tama06no1 : tama06eat0, (scale * 10), 40, { scale: scale }); +} + +function drawFoodMenu () { // food menu + if (lightSelect == 0) { + g.drawImage(right, -scale, 40, { scale: scale }); + } else { + g.drawImage(right, -scale, 40 + (7 * scale), { scale: scale }); + } + g.drawImage(meal, scale * 5, 34, { scale: scale }); + g.drawImage(snack, scale * 5, 40 + (7 * scale), { scale: scale }); +} + +function drawLight () { + if (lightSelect == 0) { + g.drawImage(right, 2, 40, { scale: scale }); + } else { + g.drawImage(right, 2, 40 + (7 * scale), { scale: scale }); + } + g.drawImage(img_on, scale * 8, 34, { scale: scale }); + g.drawImage(img_off, scale * 8, 40 + (7 * scale), { scale: scale }); +} + +function drawTools () { + if (tool >= 0) { + // top actions + if (tool == 0) { g.drawImage(tool00, 10, 2); } + if (tool == 1) { g.drawImage(tool01, 10 + 30 + 10, 2); } + if (tool == 2) { g.drawImage(tool02, 10 + 30 + 10 + 30 + 10, 2); } + if (tool == 3) { g.drawImage(tool03, 10 + 30 + 10 + 30 + 10 + 30 + 10, 2); } + // bottom actions + if (tool == 4) { g.drawImage(tool10, 10, 135); } + if (tool == 5) { g.drawImage(tool11, 10 + 30 + 10, 135); } + if (tool == 6) { g.drawImage(tool12, 10 + 30 + 10 + 30 + 10, 135); } + } +} + +// this function is executed once per second. so the animations look stable and consistent +function updateAnimation () { + frame++; + if (evolution == 0) { + // animate the egg + egg = (egg == egg00) ? egg01 : egg00; + return; + } + if (mode == 'game') { + // console.log("update Animation"); + if (transition) { + const beep = frame % 4; + if (beep == 0) { + Bangle.beep(150, 4000); + } else if (beep == 2) { + Bangle.beep(150, 3200); + } + } else { + Bangle.beep(100); + } + if (gameChoice != 0) { + // do things + gameChoice = 0; + if ((0 | (Math.random() * 3)) > 0) { + animateHappy(); + gameWins++; + } else { + animateAngry(); + } + } + return; + } + if (mode == 'medicine') { + if (frame > 3) { + mode = ''; + tama.sick = 0; + } + } + x += (scale) * hd; + if (x + (tama00.width * scale) >= w) { + hd = -hd; + } + if (x < 0) { + hd = -hd; + } + caca = (caca == caca00) ? caca01 : caca00; + // y += vd * scale; + vd = -vd; + const width = (w / scale); + if (tama.sleep) { + n = tama00; + x = (width / 2); + } else { + n = n == tama00 ? tama01 : tama00; + if (tama.cacas > 0 || tama.sick > 0) { + if (x > (width / 2)) { + hd = -1; + x = (width / 2); + } + } + } +} + +function nextItem () { + tool++; + if (tool > 6) tool = 0; +} + +function prevItem () { + tool--; + if (tool < 0) tool = 7; +} + +function activateItem () { + if (mode != '') { + return; + } + switch (tool) { + case -1: + animateToClock(); + break; + case 0: // food + if (tama.sleep) { + } else { + // evolution = 0; + mode = 'food'; + lightSelect = 0; + } + break; + case 1: // onoff + mode = 'light'; + break; + case 2: // game + if (tama.sleep) { + } else { + animateToGame(); + } + break; + case 3: // vax + if (tama.sleep) { + // cant medicate if sleeping + } else { + mode = 'medicine'; + frame = 0; + angryState = 0; + } + break; + case 4: // shower + if (tama.sleep) { + tama.happy = 0; + } + tama.awake = 10; // time to go to sleep again if in time + tama.sleep = false; + animateShower(); + break; + case 5: // status + mode = 'status'; + statusMode = 0; + break; + case 6: // blame + if (tama.sleep) { + tama.happy = 0; + tama.sleep = false; + } else if (callForAttention) { + if (tama.happy > 0 && tama.hungry > 0 && tama.sick < 1) { + tama.discipline += 2; + callForAttention = false; + } else if (tama.sick > 0) { + tama.discipline--; + } + } + animateAngry(); + break; + } +} + +const skull = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('gwFtARGDq/8=') +}; + +const zz0 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('//H9+/fRf/8=') +}; + +const zz1 = { + width: 8, + height: 8, + bpp: 1, + transparent: 1, + buffer: atob('/8P79+/fw/8=') +}; + +const zz2 = { + width: 8, + height: 8, + bpp: 1, + transparent: 0, + buffer: atob('AA4CBAgugAA=') +}; +const zz3 = { + width: 8, + height: 8, + bpp: 1, + transparent: 0, + buffer: atob('ADwECBAgPAA=') +}; + +function drawCaca () { + if (mode == 'game') { + return; + } + if (!caca) { + caca = caca00; + } + let zz = [zz0, zz1]; + + if (lightMode) { + zz = [zz2, zz3]; + g.setColor(1, 1, 1); + var fi = ((frame) / 2) % 2; + g.drawImage(zz[fi ? 1 : 0], sx + w - (scale * 9), 40, { scale: scale }); + return; + } + g.setColor(0, 0, 0); + if (tama.sleep) { + var fi = ((frame) / 2) % 2; + g.drawImage(zz[fi ? 1 : 0], sx + w - (scale * 9), 34, { scale: scale }); + if (tama.sick > 0) { + g.drawImage(skull, sx + w - (scale * 9), 34 + (scale * 6), { scale: scale }); + } else if (tama.cacas > 0) { + g.drawImage(caca, sx + w - (scale * 11), 32 + (scale * 6), { scale: scale }); + } + } else if (tama.sick > 0) { + g.drawImage(skull, sx + w - (scale * 9), 34 + scale, { scale: scale }); + if (tama.cacas > 0) { + g.drawImage(caca, sx + w - (scale * 11), 32 + (scale * 6), { scale: scale }); + } + } else { + if (tama.cacas > 0) { + g.drawImage(caca, sx + w - (scale * 11), 34 + (scale * 6), { scale: scale }); + } + if (tama.cacas > 1) { + g.drawImage(caca, sx + w - (scale * 11), 24, { scale: scale }); + } + } +} + +function animateHappy () { + if (transition || mode == 'happy') { + return; + } + angryState = 0; + mode = 'happy'; + transition = true; + const width = w / scale; + const cx = w; + var iv = setInterval(function () { + angryState++; + if (angryState > 3) { + clearInterval(iv); + transition = false; + angryState = 0; + mode = oldMode; + if (mode == 'game') { + gameTries++; + } + } + drawScene(); + }, 1000); +} + +function animateAngry () { + if (transition || mode == 'angry') { + return; + } + angryState = 0; + mode = 'angry'; + transition = true; + const width = w / scale; + const cx = w; + var iv = setInterval(function () { + angryState++; + if (angryState > 3) { + clearInterval(iv); + transition = false; + angryState = 0; + mode = oldMode; + if (mode == 'game') { + gameTries++; + } + } + drawScene(); + }, 1000); +} + +function animateFood () { + if (transition || mode == 'eating') { + return; + } + // XXX TODO this is printing the angry state not the eating one + angryState = 0; + mode = 'eating'; + tama.hungry++; + if (lightSelect == 1) { // snack + tama.happy++; + tama.hungry++; + tama.sickness += 2; + } + frame = 0; + transition = true; + const width = w / scale; + const cx = w; + var iv = setInterval(function () { + angryState++; + if (angryState > 3) { + clearInterval(iv); + transition = false; + angryState = 0; + mode = 'food'; + } + drawScene(); + }, 1000); +} + +function animateShower () { + if (transition) { + return; + } + transition = true; + const width = w / scale; + let cx = w; + var iv = setInterval(function () { + sx -= scale * 4; + drawScene(); + cx -= scale * 4; + g.setColor(1, 1, 1); + g.drawImage(shower, cx, 40 - scale, { scale: scale }); + if (cx < 0) { + clearInterval(iv); + mode = ''; + transition = false; + animated = true; + sx += width; + if (sx < 0) sx = 0; + if (tama.cacas > 0) { + // if it was dirty, play the happy animation + } + tama.cacas = 0; + drawScene(); + } + }, 100); +} + +function animateToGame () { + if (transition || mode === 'game') { + return; + } + mode = 'game'; + gameChoice = 0; + transition = true; + let cx = 0; + sx = -w; + animated = false; + var iv = setInterval(function () { + sx += scale * 2; + updateAnimation(); + drawScene(); + cx += scale * 2; + if (cx > w) { + clearInterval(iv); + sx = 0; + animated = true; + transition = false; + drawScene(); + } + }, 100); +} + +function animateToClock () { + if (transition) { + return; + } + if (mode == 'clock') { + return; + } + mode = 'clock'; + transition = true; + const width = w / scale; + let cx = w; + sx = 0; + animated = false; + var iv = setInterval(function () { + sx -= scale * 4; + drawScene(); + cx -= scale * 4; + g.setColor(0, 0, 0); + if (cx < 0) { + clearInterval(iv); + mode = 'clock'; + transition = false; + animated = true; + drawScene(); + } + }, 100); +} + +function animateFromClock () { + if (transition) { + return; + } + if (mode != 'clock') { + return; + } + transition = true; + let cx = 0; + const width = w / scale; + animated = false; + var iv = setInterval(function () { + sx += scale * 4; + drawScene(); + cx += scale * 4; + if (cx > w) { + clearInterval(iv); + mode = ''; + sx = 0; + animated = true; + transition = false; + drawScene(); + } + }, 100); +} + +function button (n) { + if (evolution == 0) { + if (n == 3) { + evolution = 1; + return; + } + } + if (mode == 'happy' || mode == 'angry') { + return; + } + + if (mode == 'game') { + switch (n) { + case 1: + // pick left + gameChoice = 1; + drawScene(); + oldMode = 'game'; + break; + case 2: + // pick right + gameChoice = 2; + drawScene(); + oldMode = 'game'; + break; + case 3: + mode = ''; + // exit game + break; + } + return; + } + if (mode == 'eating') { + Bangle.buzz(); + return; + } + Bangle.beep(150); + + switch (n) { + case 1: + switch (mode) { + case 'clock': + useAmPm = !useAmPm; + drawScene(); + break; + case 'food': + case 'light': + lightSelect = lightSelect ? 0 : 1; + drawScene(); + break; + case 'status': + if (oldMode != 'clock') { + statusMode++; + drawScene(); + } + break; + default: + nextItem(); + drawScene(); + break; + } + break; + case 2: + switch (mode) { + case 'clock': + animateFromClock(); + break; + case 'status': + if (oldMode != 'clock') { + statusMode++; + drawScene(); + } + break; + case 'food': + animateFood(); + break; + case 'light': + mode = ''; + lightMode = lightSelect; + drawScene(); + break; + default: + activateItem(); + tool = -1; + drawScene(); + } + break; + case 3: + switch (mode) { + case 'clock': + animateFromClock(); + break; + case 'light': + case 'food': + mode = ''; + lightState = 0; + drawScene(); + break; + case 'status': + if (oldMode == 'clock') { + mode = 'clock'; + oldMode = ''; + } else { + mode = ''; + statusMode = 0; + drawScene(); + } + break; + default: + mode = ''; + tool = -1; + drawScene(); + break; + } + break; + } +} + +function drawGame () { + g.setColor(0, 0, 0); + + let one = frame % 2; + if (transition) { + one = 0; + g.drawImage(heart1, sx + w + (scale * 6), 40, { scale: scale }); + g.drawImage(heart1, sx + w + (scale * 16), 40, { scale: scale }); + g.drawImage(heart0, sx + w, 40 + (scale * 8), { scale: scale }); + g.drawImage(heart0, sx + w + (scale * 12), 40 + (scale * 8), { scale: scale }); + } else { + if (gameTries > 4) { + if (oldMode != '') { + if (gameWins > 2) { + animateHappy(); + } + } + mode = oldMode; + oldMode = ''; + } else { + g.drawImage(one ? tama06no1 : tama06no0, (scale * 7) + sx, 40, { scale: scale }); + } + } +} + +function drawClock () { + const d = new Date(); + let hh = ''; + if (useAmPm) { + const h = (d.getHours() > 12) ? d.getHours() - 12 : d.getHours(); + hh = (h < 10) ? ' ' + h : '' + h; + } else { + hh = (d.getHours() < 10) ? ' ' + d.getHours() : '' + d.getHours(); + } + const mm = (d.getMinutes() < 10) ? '0' + d.getMinutes() : '' + d.getMinutes(); + const ss = (d.getSeconds() < 10) ? '0' + d.getSeconds() : '' + d.getSeconds(); + const ts = hh + ':' + mm; + const useVector = false; + const wsx = w + sx + ((2.4) * scale); + + if (useVector) { + g.setFont('Vector', 60); + g.setColor(0, 0, 0); + g.drawString(ts, w + sx + 30, 54); + g.setFont('Vector', 24); + g.setColor(0, 0, 0); + g.drawString(ss, w + sx + (w - 20), 104); + } else { + const s0 = numbers[ts[0] - '0']; + const s1 = numbers[ts[1] - '0']; + const s2 = numbers[ts[3] - '0']; + const s3 = numbers[ts[4] - '0']; + // hours + if (s0) { + g.drawImage(s0, wsx, yy, { scale: scale }); + } + g.drawImage(s1, wsx + (5 * scale), yy, { scale: scale }); + g.drawImage(colon, wsx + (scale + scale + scale + (5 * scale)), yy, { scale: scale }); + // minutes + g.drawImage(s2, wsx + (2 * scale) + (5 * 2 * scale), yy, { scale: scale }); + g.drawImage(s3, wsx + (2 * scale) + (5 * 3 * scale), yy, { scale: scale }); + // seconds + const s4 = snumbers[ss[0] - '0']; + const s5 = snumbers[ss[1] - '0']; + g.drawImage(s4, wsx + (3 * scale) + (3 * 6 * scale), yy, { scale: scale }); + g.drawImage(s5, wsx + scale + (4 * 6 * scale), yy, { scale: scale }); + const arrows = [ + '00000', + '10000', + '11000', + '11100', + '11110', + '11111', + '01111', + '00111', + '00011', + '00001' + ]; + // arrow + for (let i = 0; i < 5; i++) { + const n = d.getSeconds() % 10; + const arrow = arrows[n]; + const img = (arrow[i] == '1') ? right1 : right0; + g.drawImage(img, wsx + (3 * i * scale) + (scale * 14), yy + (10 * scale), { scale: scale }); + } + } + if (useAmPm) { + if (d.getHours() < 13) { + g.drawImage(am, wsx, yy + (8 * scale), { scale: scale }); + } else { + g.drawImage(pm, wsx, yy + (8 * scale), { scale: scale }); + } + } else { + g.drawImage(h24, wsx, yy + (8 * scale), { scale: scale }); + // show something from tamagotchi stats + } +} + +setInterval(function () { + updateAnimation(); + drawScene(); +}, 1000); + +function pooMaker() { + if (tama.hungry > 0 && !tama.sleep) { + const a = 0 | (cacaLevel / tama.tummy); + const b = 0 | ((cacaLevel + tama.hungry) / tama.tummy); + cacaLevel += tama.hungry; + if (a != b) { + if (tama.cacas == 0) { + cacaBirth = new Date(); + } + tama.hungry--; + tama.cacas++; + } + } + const d = new Date(); + const h = d.getHours(); + tama.sleep = (h > 22 || h < 8); + if (tama.awake > 0) { + tama.awake--; + tama.sleep = false; + } +} +function sickMaker() { + if (tama.sleep) { + return; + } + callForAttention = false; + + // health check + tama.sickness += tama.cacas; + if (tama.hungry == 0) { + callForAttention = true; + // tama.sickness++; + } + if (tama.hungry == 4) { + // tama.sickness++; + } + if (tama.sickness > tama.defenses) { + tama.sickness = 0; + tama.sick++; + } + if (tama.sick > 0) { + callForAttention = true; + } +} + +setInterval(pooMaker, 5e3); +setInterval(sickMaker, 2e3); +updateAnimation(); + +Bangle.on('touch', function (r, s) { + const w4 = w / 3; + if (s.x > w - w4) { + if (s.y < 50) { + Bangle.beep(150); + if (oldMode == 'clock') { + oldMode = ''; + mode = 'clock'; + } else + if (mode == 'clock') { + mode = 'status'; + oldMode = 'clock'; + statusMode = 5; // battery + } else { + evolution = !evolution; + tool = -1; + } + drawScene(); + } else { + button(3); + } + } else if (s.x < w4) { + button(1); + } else { + button(2); + } +}); diff --git a/apps/tabanchi/app.png b/apps/tabanchi/app.png new file mode 100644 index 000000000..7e653301d Binary files /dev/null and b/apps/tabanchi/app.png differ diff --git a/apps/tabanchi/metadata.json b/apps/tabanchi/metadata.json new file mode 100644 index 000000000..f72147162 --- /dev/null +++ b/apps/tabanchi/metadata.json @@ -0,0 +1,31 @@ +{ + "id": "tabanchi", + "name": "Tabanchi", + "shortName": "Tabanchi", + "version": "0.03", + "type": "app", + "description": "Tamagotchi WatchApp", + "icon": "app.png", + "allow_emulator": true, + "tags": "clock, watch, virtual pet", + "supports": [ + "BANGLEJS2" + ], + "readme": "README.md", + "storage": [ + { + "name": "tabanchi.app.js", + "url": "app.js" + }, + { + "name": "tabanchi.img", + "url": "app-icon.js", + "evaluate": true + } + ], + "screenshots": [ + { + "url": "screenshot.jpg" + } + ] +} diff --git a/apps/tabanchi/screenshot.jpg b/apps/tabanchi/screenshot.jpg new file mode 100644 index 000000000..fcd97df84 Binary files /dev/null and b/apps/tabanchi/screenshot.jpg differ diff --git a/apps/teatimer/ChangeLog b/apps/teatimer/ChangeLog new file mode 100644 index 000000000..db8dd270b --- /dev/null +++ b/apps/teatimer/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Fix issue setting colors after showMessage +0.03: Fix BG/FG Color if e.g. theme background is black diff --git a/apps/teatimer/app.js b/apps/teatimer/app.js index dd7afdadb..c394b5e00 100644 --- a/apps/teatimer/app.js +++ b/apps/teatimer/app.js @@ -67,9 +67,9 @@ function startTimer() { - hint for help in state start */ function showCounter(withHint) { - //g.clear(); + g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier E.showMessage("", appTitle()); - g.setFontAlign(0,0); // center font + g.reset().setFontAlign(0,0); // center font // draw the current counter value g.setBgColor(-1).setColor(0,0,1); // blue g.setFont("Vector",20); // vector font, 20px @@ -123,9 +123,9 @@ function countUp() { outOfTime(); return; } - g.clear(); + g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier E.showMessage("", appTitle()); - g.setFontAlign(0,0); // center font + g.reset().setFontAlign(0,0); // center font g.setBgColor(-1).setColor(0,0,1); // blue g.setFont("Vector",20); // vector font, 20px g.drawString("Timer: " + timeFormated(counterStart),80,55); @@ -216,6 +216,8 @@ function initDragEvents() { function showHelp() { if (state == states.start) { state = states.help; + g.setBgColor(g.theme.bg); + g.setColor(g.theme.fg); E.showMessage("Swipe up/down\n+/- one minute\n\nSwipe left/right\n+/- 15 seconds\n\nPress Btn1 to start","Tea timer help"); } // return to start diff --git a/apps/teatimer/metadata.json b/apps/teatimer/metadata.json index acace0402..b5cdce92e 100644 --- a/apps/teatimer/metadata.json +++ b/apps/teatimer/metadata.json @@ -1,7 +1,7 @@ { "id": "teatimer", "name": "Tea Timer", - "version": "0.01", + "version": "0.03", "description": "A simple timer. You can easyly set up the time.", "icon": "teatimer.png", "type": "app", diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog index 4e53f6f8b..75d1a760e 100644 --- a/apps/terminalclock/ChangeLog +++ b/apps/terminalclock/ChangeLog @@ -2,3 +2,6 @@ 0.02: Rename "Activity" in "Motion" and display the true values for it 0.03: Add Banglejs 1 compatibility 0.04: Fix settings bug +0.05: Add altitude display (only Bangle.js 2) +0.06: Add power related settings to control the HR and pressure(altitude) sensor from the watchface +0.07: Use ClockFace module and rework the settings to be able to personnalize the order of the lines diff --git a/apps/terminalclock/README.md b/apps/terminalclock/README.md index 5a54583d2..93967e8a7 100644 --- a/apps/terminalclock/README.md +++ b/apps/terminalclock/README.md @@ -4,6 +4,12 @@ A clock displayed as a terminal cli. It can display : - time - date +- altitude - hrm - motion - steps + + +"Power saving" setting control the HR and pressure (altitude) sensors. +If "Off" they will always be on. +If "On" the sensors will be turned on every "Power on interval" minutes for 45 secondes diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js index d219b84d8..b60a32094 100644 --- a/apps/terminalclock/app.js +++ b/apps/terminalclock/app.js @@ -1,16 +1,16 @@ -var locale = require("locale"); -var fontColor = g.theme.dark ? "#0f0" : "#000"; +const locale = require("locale"); var heartRate = 0; +var altitude = -9001; -// handling the differents versions of the Banglejs smartwatch +const fontColor = g.theme.dark ? "#0f0" : "#000"; +// handling the differents versions of the Banglejs smartwatch screen sizes if (process.env.HWVERSION == 1){ var paddingY = 3; var font6x8At4Size = 48; var font6x8At2Size = 27; var font6x8FirstTextSize = 6; var font6x8DefaultTextSize = 3; -} -else{ +} else{ var paddingY = 2; var font6x8At4Size = 32; var font6x8At2Size = 18; @@ -18,35 +18,85 @@ else{ var font6x8DefaultTextSize = 2; } -function setFontSize(pos){ +// initialising the clockface +const ClockFace = require("ClockFace"); +const clock = new ClockFace({ + precision: 60, + settingsFile: "terminalclock.json", + + init: function () { + // check settings and set default if needed + this.showHRM = false; + this.showAltitude = false; + this.lock_precision = this.precision; + this.unlock_precision = 1; + if (this.HRMinConfidence === undefined) this.HRMinConfidence = 50; + if (this.PowerOnInterval === undefined) this.PowerOnInterval = 15; + if (this.powerSaving===undefined) this.powerSaving = true; + ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(k => { + if (this[k]===undefined){ + if(k == "L2") this[k] = "Date"; + else if(k == "L3") { + this[k] = "HR"; + this.showHRM = true; + }else if(k == "L4") this[k] = "Motion"; + else if(k == "L5") this[k] = "Steps"; + else if(k == "L6") this[k] = ">"; + else this[k] = "Empty"; + } + else if (this[k]==="HR") this.showHRM = true; + else if (this[k]==="Alt") this.showAltitude = true && process.env.HWVERSION == 2; + }); + + // set the lock and unlock actions + Bangle.on("lock", on => { + if (on) lock(); + else unlock(); + }); + + // set the services (HRM, pressure sensor, etc....) + if(!this.powerSaving){ + turnOnServices(); + } else{ + setInterval(turnOnServices, this.PowerOnInterval*60000); // every PowerOnInterval min + } + // start the clock unlocked + unlock(); + }, + + draw: function (date) { + var curPos = 1; + g.setFontAlign(-1, -1); + g.setColor(fontColor); + drawTime(date, curPos); + curPos++; + + ["L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"].forEach(line => { + if (this[line]==='Date') drawDate(date, curPos); + else if (this[line]==='HR') drawHRM(curPos); + else if (this[line]==='Motion') drawMotion(curPos); + else if (this[line]==='Alt') drawAltitude(curPos); + else if (this[line]==='Steps') drawStepCount(curPos); + else if (this[line]==='>') drawInput(curPos); + curPos++; + }); + }, +}); + + +/* ---------------------------- +Draw related of specific lines +-------------------------------- */ + +function drawLine(line, pos){ if(pos == 1) g.setFont("6x8", font6x8FirstTextSize); else g.setFont("6x8", font6x8DefaultTextSize); -} -function clearField(pos){ - var yStartPos = Bangle.appRect.y + - paddingY * (pos - 1) + - font6x8At4Size * Math.min(1, pos-1) + - font6x8At2Size * Math.max(0, pos-2); - var yEndPos = Bangle.appRect.y + - paddingY * (pos - 1) + - font6x8At4Size * Math.min(1, pos) + - font6x8At2Size * Math.max(0, pos-1); - g.clearRect(Bangle.appRect.x, yStartPos, Bangle.appRect.x2, yEndPos); -} - -function clearWatchIfNeeded(now){ - if(now.getMinutes() % 10 == 0) - g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2); -} - -function drawLine(line, pos){ - setFontSize(pos); - var yPos = Bangle.appRect.y + - paddingY * (pos - 1) + - font6x8At4Size * Math.min(1, pos-1) + + var yPos = Bangle.appRect.y + + paddingY * (pos - 1) + + font6x8At4Size * Math.min(1, pos-1) + font6x8At2Size * Math.max(0, pos-2); g.drawString(line, 5, yPos, true); } @@ -65,8 +115,7 @@ function drawDate(now, pos){ drawLine(locale_date, pos); } -function drawInput(now, pos){ - clearField(pos); +function drawInput(pos){ drawLine(">", pos); } @@ -77,71 +126,85 @@ function drawStepCount(pos){ } function drawHRM(pos){ - clearField(pos); if(heartRate != 0) drawLine(">HR: " + parseInt(heartRate), pos); else drawLine(">HR: unknown", pos); } -function drawActivity(pos){ - clearField(pos); +function drawAltitude(pos){ + if(altitude > 0) + drawLine(">Alt: " + altitude.toFixed(1) + "m", pos); + else + drawLine(">Alt: unknown", pos); +} + +function drawMotion(pos){ var health = Bangle.getHealthStatus('last'); var steps_formated = ">Motion: " + parseInt(health.movement); drawLine(steps_formated, pos); } -function draw(){ - var curPos = 1; - g.reset(); - g.setFontAlign(-1, -1); - g.setColor(fontColor); - var now = new Date(); - clearWatchIfNeeded(now); // mostly to not have issues when changing days - drawTime(now, curPos); - curPos++; - if(settings.showDate){ - drawDate(now, curPos); - curPos++; +/* ----------------------------------------------- +Services functions (HRM, pressure, etc...) +-------------------------------------------------- */ + +function turnOnServices(){ + if(clock.showHRM){ + Bangle.setHRMPower(true, "terminalclock"); } - if(settings.showHRM){ - drawHRM(curPos); - curPos++; + if(clock.showAltitude){ + Bangle.setBarometerPower(true, "terminalclock"); } - if(settings.showActivity){ - drawActivity(curPos); - curPos++; + if(clock.powerSaving){ + setTimeout(function () { + turnOffServices(); + }, 45000); } - if(settings.showStepCount){ - drawStepCount(curPos); - curPos++; +} + +function turnOffServices(){ + if(clock.showHRM){ + Bangle.setHRMPower(false, "terminalclock"); + } + if(clock.showAltitude){ + Bangle.setBarometerPower(false, "terminalclock"); } - drawInput(now, curPos); } Bangle.on('HRM',function(hrmInfo) { - if(hrmInfo.confidence >= settings.HRMinConfidence) + if(hrmInfo.confidence >= clock.HRMinConfidence) heartRate = hrmInfo.bpm; }); +const MEDIANLENGTH = 20; // technical +var avr = [], median; // technical +Bangle.on('pressure', function(e) { + while (avr.length>MEDIANLENGTH) avr.pop(); + avr.unshift(e.altitude); + median = avr.slice().sort(); + if (median.length>10) { + var mid = median.length>>1; + altitude = E.sum(median.slice(mid-4,mid+5)) / 9; + } +}); -// Clear the screen once, at startup -g.clear(); -// load the settings -var settings = Object.assign({ - // default values - HRMinConfidence: 50, - showDate: true, - showHRM: true, - showActivity: true, - showStepCount: true, -}, require('Storage').readJSON("terminalclock.json", true) || {}); -// Show launcher when middle button pressed -Bangle.setUI("clock"); -// Load widgets -Bangle.loadWidgets(); -Bangle.drawWidgets(); -// draw immediately at first -draw(); +/* ------------------------------------------------- +Clock related functions but not in the ClockFace module +---------------------------------------------------- */ -var secondInterval = setInterval(draw, 10000); +function unlock(){ + if(clock.powerSaving){ + turnOnServices(); + } + clock.precision = clock.unlock_precision; + clock.tick(); +} + +function lock(){ + clock.precision = clock.lock_precision; + clock.tick(); +} + +// starting the clock +clock.start(); diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index a34602913..a8682f9a8 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,7 +3,7 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.04", + "version":"0.07", "icon": "app.png", "type": "clock", "tags": "clock", diff --git a/apps/terminalclock/settings.js b/apps/terminalclock/settings.js index 6b686058b..f347e8ee3 100644 --- a/apps/terminalclock/settings.js +++ b/apps/terminalclock/settings.js @@ -2,60 +2,100 @@ var FILE = "terminalclock.json"; // Load settings var settings = Object.assign({ + // ClockFace lib + loadWidgets: true, + // TerminalClock specific HRMinConfidence: 50, - showDate: true, - showHRM: true, - showActivity: true, - showStepCount: true, + powerSaving: true, + PowerOnInterval: 15, + L2: 'Date', + L3: 'HR', + L4: 'Motion', + L5: 'Steps', + L6: '>', + L7: 'Empty', + L8: 'Empty', + L9: 'Empty', }, require('Storage').readJSON(FILE, true) || {}); function writeSettings() { require('Storage').writeJSON(FILE, settings); } - // Show the menu - E.showMenu({ - "" : { "title" : "Terminal Clock" }, - "< Back" : () => back(), - 'HR confidence': { - value: settings.HRMinConfidence, - min: 0, max: 100, + if(process.env.HWVERSION == 2) { + var lineType = ['Date', 'HR', 'Motion', 'Alt', 'Steps', '>', 'Empty']; + } else{ + var lineType = ['Date', 'HR', 'Motion', 'Steps', '>', 'Empty']; + } + function getLineChooser(lineID){ + return { + value: lineType.indexOf(settings[lineID]), + min: 0, max: lineType.length-1, + format: v => lineType[v], onchange: v => { - settings.HRMinConfidence = v; - writeSettings(); - } - }, - 'Show date': { - value: settings.showDate, - format: v => v?"Yes":"No", - onchange: v => { - settings.showDate = v; - writeSettings(); - } - }, - 'Show HRM': { - value: settings.showHRM, - format: v => v?"Yes":"No", - onchange: v => { - settings.showHRM = v; - writeSettings(); - } - }, - 'Show Activity': { - value: settings.showActivity, - format: v => v?"Yes":"No", - onchange: v => { - settings.showActivity = v; - writeSettings(); - } - }, - 'Show Steps': { - value: settings.showStepCount, - format: v => v?"Yes":"No", - onchange: v => { - settings.showStepCount = v; + settings[lineID] = lineType[v]; writeSettings(); + }, + }; + } + + var lineMenu = { + '< Back': function() { E.showMenu(getMainMenu());}, + 'Line 2': getLineChooser('L2'), + 'Line 3': getLineChooser('L3'), + 'Line 4': getLineChooser('L4'), + 'Line 5': getLineChooser('L5'), + 'Line 6': getLineChooser('L6'), + 'Line 7': getLineChooser('L7'), + 'Line 8': getLineChooser('L8'), + 'Line 9': getLineChooser('L9'), + }; + + function getMainMenu(){ + var mainMenu = { + "" : { "title" : "Terminal Clock" }, + "< Back" : () => back(), + 'HR confidence': { + value: settings.HRMinConfidence, + min: 0, max: 100, + onchange: v => { + settings.HRMinConfidence = v; + writeSettings(); + } + }, + 'Show widgets': { + value: settings.loadWidgets, + onchange: v => { + settings.loadWidgets = v; + writeSettings(); + } + }, + 'Power saving': { + value: settings.powerSaving, + onchange: v => { + settings.powerSaving = v; + writeSettings(); + setTimeout(function() { + E.showMenu(getMainMenu()); + },0); + } } + }; + if(settings.powerSaving){ + mainMenu['Power on interval'] = { + value: settings.PowerOnInterval, + min: 3, max: 60, + onchange: v => { + settings.PowerOnInterval = v; + writeSettings(); + }, + format: x => x + "m" + }; } - }); -}) + + mainMenu['Lines'] = function() { E.showMenu(lineMenu);}; + return mainMenu; + } + + E.showMenu(getMainMenu()); +}) \ No newline at end of file diff --git a/apps/themesetter/ChangeLog b/apps/themesetter/ChangeLog new file mode 100644 index 000000000..ddbd06706 --- /dev/null +++ b/apps/themesetter/ChangeLog @@ -0,0 +1,2 @@ +... +0.04: First update with ChangeLog Added diff --git a/apps/thermom/ChangeLog b/apps/thermom/ChangeLog index 6d3a966e3..0b8c325e5 100644 --- a/apps/thermom/ChangeLog +++ b/apps/thermom/ChangeLog @@ -4,3 +4,5 @@ 0.05: Use temperature from current locale Update every 10s, average last 5 readings Changes based on #1092 +0.06: Minor tweaks for stability. Update every 5 seconds +0.07: Add back button diff --git a/apps/thermom/app.js b/apps/thermom/app.js index 0e45ed3e7..3aa99c015 100644 --- a/apps/thermom/app.js +++ b/apps/thermom/app.js @@ -24,7 +24,7 @@ function onTemperature(p) { // Gets the temperature in the most accurate way (pressure sensor or inbuilt thermistor) function drawTemperature() { if (Bangle.getPressure) { - Bangle.getPressure().then(onTemperature); + Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); } else { onTemperature({ temperature : E.getTemperature() @@ -34,8 +34,11 @@ function drawTemperature() { setInterval(function() { drawTemperature(); -}, 10000); +}, 5000); +Bangle.loadWidgets(); +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); E.showMessage("Reading temperature..."); drawTemperature(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); diff --git a/apps/thermom/metadata.json b/apps/thermom/metadata.json index 381f85e17..a215df624 100644 --- a/apps/thermom/metadata.json +++ b/apps/thermom/metadata.json @@ -1,7 +1,7 @@ { "id": "thermom", "name": "Thermometer", - "version": "0.05", + "version": "0.07", "description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.", "icon": "app.png", "tags": "tool", diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog index e17baa27c..7a357b1aa 100644 --- a/apps/timerclk/ChangeLog +++ b/apps/timerclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Add sunrise/sunset. Fix timer bugs. +0.03: Use default Bangle formatter for booleans diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json index 7c6c7c9b3..72f42d8d4 100644 --- a/apps/timerclk/metadata.json +++ b/apps/timerclk/metadata.json @@ -2,7 +2,7 @@ "id": "timerclk", "name": "Timer Clock", "shortName":"Timer Clock", - "version":"0.02", + "version":"0.03", "description": "A clock with stopwatches, timers and alarms build in.", "icon": "app-icon.png", "type": "clock", diff --git a/apps/timerclk/settings.js b/apps/timerclk/settings.js index 992985f52..1a8500add 100644 --- a/apps/timerclk/settings.js +++ b/apps/timerclk/settings.js @@ -1,6 +1,5 @@ (function(back) { const FILE = "timerclk.json"; - const BOOL_FORMAT = v=>v?/*LANG*/"On":/*LANG*/"Off"; // Load settings var settings = require('Storage').readJSON(FILE, true) || {} settings.clock = Object.assign({ @@ -130,7 +129,6 @@ }, "short date": { value: !!settings.clock.shortDate, - format: BOOL_FORMAT, onchange: v => { settings.clock.shortDate = v; writeSettings(); @@ -138,7 +136,6 @@ }, "stopwatches": { value: !!settings.clock.showStopwatches, - format: v=>v?/*LANG*/"Show":/*LANG*/"Hide", onchange: v => { settings.clock.showStopwatches = v; writeSettings(); @@ -146,7 +143,6 @@ }, "timers": { value: !!settings.clock.showTimers, - format: v=>v?/*LANG*/"Show":/*LANG*/"Hide", onchange: v => { settings.clock.showTimers = v; writeSettings(); @@ -154,7 +150,6 @@ }, "sun times": { value: !!settings.clock.showSrss, - format: v=>v?/*LANG*/"Show":/*LANG*/"Hide", onchange: v => { settings.clock.showSrss = v; writeSettings(); diff --git a/apps/tinyVario/ChangeLog b/apps/tinyVario/ChangeLog new file mode 100644 index 000000000..a201ee465 --- /dev/null +++ b/apps/tinyVario/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial Version +0.02: Touch data fields to configure them. +0.03: Changed menu layout, fixed automatic flight time detection. +0.04: flight time detection should work without GPS now. New vario display available. +0.05: some bugs fiexed, no new features. diff --git a/apps/tinyVario/README.md b/apps/tinyVario/README.md new file mode 100644 index 000000000..c375585c2 --- /dev/null +++ b/apps/tinyVario/README.md @@ -0,0 +1,20 @@ +# Turn your Bangle.js2 into a flight computer! + +All settings can be accessed by touching the corresponding data fields. I made all interface objects as big as possible to make it easier to use with gloves. + +## This is a work in progress. Working features so far: +- Altimeter +- Variometer +- Average rate of climb +- Ground speed +- Flying time with automatic take-off detection + + +## Planned features: +- glide slope display +- final glide computer +- waypoint navigation +- flight log (possibly IGC file export) + +Contact me for feedback and suggestions! +tinyVario@dumke.org diff --git a/apps/tinyVario/app-icon.js b/apps/tinyVario/app-icon.js new file mode 100644 index 000000000..c94da4e62 --- /dev/null +++ b/apps/tinyVario/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4P/AAMgFksD4/AAoUGnguDgkEAosQAocAAosYAoX8gEIAgMBwEAh4FCAQMJBAIFCgAFUO4Rraj4hBTwZwDgFgsAFDoAXEoYFEqUAg4fCj0AngfChgfEAoPAYomDD4kiAok2AYU8gEUAoXmgEcR4WBAogaEgKtBAAdMAolIAokoAojJDAAM4AokwRxIA==")) diff --git a/apps/tinyVario/app.js b/apps/tinyVario/app.js new file mode 100644 index 000000000..b9a87c821 --- /dev/null +++ b/apps/tinyVario/app.js @@ -0,0 +1,465 @@ +/* +To do: + -flight log + -statistics page + -navigation +*/ + +getAltitude = (p,baseP) => (44330 * (1.0 - Math.pow(p/baseP, 0.1903))); +getFL = () => (44330 * (1.0 - Math.pow(pressure/1013.25, 0.1903))).toFixed(0); +getTimeString = () => (settings.localTime) ? (require("locale").time(Date(),1)):(Date().toUTCString().slice(Date().toUTCString().length-12,Date().toUTCString().length-7)); + +var fg=g.getColor(); +var bg=g.getBgColor(); +var red="#F00",green="#0F0"; + +const unitsRoc=[ + {name:"m/s", factor:1, precision:1, layoutCode:{type:"v", halign:1, c: [ + {type:"txt", font:"12%", halign:0, filly:0, label:"m"}, + {type:"", height:1,width:"20", bgCol:fg}, + {type:"txt", font:"12%", halign:0, filly:0, label:"s"}]}}, + {name:"ft/m", factor:196.85039370078738, precision:0, layoutCode:{type:"v", halign:1, c: [ + {type:"txt", font:"12%", halign:0, filly:0, label:"ft"}, + {type:"", height:1,width:"30", bgCol:fg}, + {type:"txt", font:"12%", halign:0, filly:0, label:"min"}]}}, + {name:"kt", factor:1.9438444924406, precision:1, layoutCode: + {type:"txt", font:"12%", halign:0, filly:0, label:"kt"}} + ]; +const unitsGs=[ + {name:"km/h", factor:1, precision:1, layoutCode:{type:"v", halign:1, c: [ + {type:"txt", font:"12%", halign:0, filly:0, label:"km"}, + {type:"", height:1,width:"30", bgCol:fg}, + {type:"txt", font:"12%", halign:0, filly:0, label:"h"}]}}, + {name:"kt", factor:0.5399568, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"kt"}}, + {name:"m/s", factor:0.2777777777777778, precision:1, layoutCode:{type:"v", halign:1, c: [ + {type:"txt", font:"12%", halign:0, filly:0, label:"m"}, + {type:"", height:1,width:"20", bgCol:fg}, + {type:"txt", font:"12%", halign:0, filly:0, label:"s"}]}} + ]; +const unitsAlt=[ + {name:"m", factor:1, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"m"}}, + {name:"ft", factor:3.280839895013123, precision:0, layoutCode:{type:"txt", font:"12%", halign:0, filly:0, label:"ft"}} + ]; +const unitROC={type:"v", halign:1, c: [ + {type:"txt", font:"12%", halign:0, filly:0, label:"m"}, + {type:"", height:1,width:"20", bgCol:fg}, + {type:"txt", font:"12%", halign:0, filly:0, label:"s"} + ]}; + +const ground=0, flying=1, landed=2, maybeFlying=3, maybeLanded=4; + +var settings = Object.assign({ + rocU: 0, + altU: 0, + gsU:0, + intTime:10, + localTime:true, + autoDetect:true, + bargraph:false +}, require('Storage').readJSON("tinyVario.json", true) || {}); + +var qnh=Math.floor(Bangle.getOptions().seaLevelPressure) || 1013; +var pfdHandle; +var rawP=0, samples=0; +var altH = []; +var altRaw=-9999, altFast=0, altSlow=0; +var fastGain=0.5, slowGain=0.3; +var roc=0,rocAvg=0, gs; +var lastPressure = Date.now(); +var pressure = 1000; +var state=ground; +var takeoffTime=0, landingTime=0, flyingTime; +var Layout = require("Layout"); +var oldSettings; + +//var delta=0;//TESTING + + +function updateText(t) { + g.reset(); + g.clearRect(t.x,t.y,t.x+t.w-1,t.y+t.h-1); + if (t.col) g.setColor(t.col); + else g.setColor(fg); + if (t.halign==1) + g.setFont(t.font).setFontAlign(1,0,0).drawString(t.label, t.x+t.w, t.y+(t.h>>1)); + else if (t.halign==-1) + g.setFont(t.font).setFontAlign(-1,0,0).drawString(t.label, t.x, t.y+(t.h>>1)); + else + g.setFont(t.font).setFontAlign(0,0,0).drawString(t.label, t.x+(t.w>>1), t.y+(t.h>>1)); +} + +function initPFD() { + Bangle.setUI(); + var pfd = new Layout( + {type:"v",c: [ + /*{type:"h",c: [ + {type:"", fillx:1, height:"1"} + ]},*/ + {type:"h",filly:1, c: [ + {type:"custom", width:"25", render:()=>{ + var p = pfd.vario; + if (roc>0.1) g.setColor(0,1,0); + if (roc<-1) g.setColor(1,0,0); + var y=p.y+p.h/2-roc*(p.h/2)/5; + if (settings.bargraph==false) { + g.clearRect(p.x,p.y,p.x+p.w-1,p.y+p.h-1); + g.fillRect(p.x,p.y+(p.h/2),p.x+p.w-1,Math.clip(y,p.y,p.y+p.h-1)); + } else { + g.setClipRect(p.x,p.y,p.x+p.w-1,p.y+p.h-1); + g.scroll(-1,0); + g.drawLine(p.x+p.w-1,p.y+(p.h/2),p.x+p.w-1,Math.clip(y,p.y,p.y+p.h-1)); + } + g.reset(); + }, id:"vario",filly:1, cb:()=>initVarioMenu()}, + {type:"", filly:1, width:1, bgCol:fg}, + {type:"v",fillx:1, c: [ + {type:"h", halign:1, c:[ + {type:"txt", font:"22%", halign:1, filly:1, fillx:1, label:"9999", id:"alt", cb:()=>initAltMenu()}, + unitsAlt[settings.altU].layoutCode + ]}, + {type:"", fillx:1, height:"1", bgCol:fg}, + {type:"h", halign:1, c:[ + {type:"txt", font:"25%", halign:1, filly:1, fillx:1, label:"-9.9", id:"avg", cb:()=>initAvgMenu()}, + unitsRoc[settings.rocU].layoutCode + ]}, + {type:"", fillx:1, height:"1", bgCol:fg}, + {type:"h", halign:1, c:[ + {type:"txt", font:"25%", halign:1, filly:1, fillx:1, label:"XXX", id:"gs", cb:()=>initGsMenu()}, + unitsGs[settings.gsU].layoutCode + ]} + ]} + ]}, + {type:"", fillx:1, height:"1", bgCol:fg}, + {type:"h",c: [ + {type:"txt",pad:0, halign:0, font:"15%",fillx:1, label:"99:99", id:"time", cb:()=>initTimeMenu()}, + {type:"", width:1,height:g.getHeight()*0.15, bgCol:fg}, + {type:"txt",pad:0, halign:0, font:"15%", fillx:1, label:"--:--", id:"flyingtime", cb:()=>initFlyingTimeMenu() } + ]} + ]},{lazy:true} + ); + g.clear(); + pfd.render(); + //-------testing------ + //rawP=1000; + //samples=1; + //-------------------- + pfdHandle = setInterval(function() { + t1=Date().getTime(); + //process pressure readings + if (samples) { + pressure=rawP/samples; + samples=0; + rawP=0; + if (altRaw==-9999) {//first measurement) + altRaw=getAltitude(pressure,qnh); + altFast=altRaw; + altSlow=altRaw; + for (let i = 0; i < settings.intTime*4+1; i++) altH.push(altRaw); + } + } + //altRaw=altRaw+delta;getAltitude(pressure,qnh);//TESTING + altRaw=getAltitude(pressure,qnh); + altFast=altFast+(altRaw-altFast)*fastGain; + altSlow=altSlow+(altRaw-altSlow)*slowGain; + altH.push(altFast); + while (altH.length>settings.intTime*4) { + rocAvg=(altH[altH.length-1]-altH[0])/settings.intTime; + altH.shift(); + } + roc=(altFast-altSlow)/((0.25/slowGain)-(0.25/fastGain)); + + if (settings.autoDetect==true) switch (state) { + case ground: + if ((gs>=5) || (roc>=1) || (roc<=-1)) { + state=maybeFlying; + takeoffTime=Date().getTime(); + } + break; + case maybeFlying: + if (!(gs>=5) && (roc<1) && (roc>-1)) state=ground; + else if (Date().getTime()-takeoffTime>60000) state=flying; + break; + case flying: + if (!(gs>=5) && (roc<1) && (roc>-1)) { + state=maybeLanded; + landingTime=Date().getTime(); + } + break; + case maybeLanded: + if ((gs>=5) || (roc>=1) || (roc<=-1)) state=flying; + else if (Date().getTime()-landingTime>60000) state=landed; + break; + } + if ((state==flying) || (state==maybeLanded)) { + flyingTime=Date().getTime()-takeoffTime; + pfd.flyingtime.label=(flyingTime / 3600000).toFixed(0)+":"+(flyingTime / 60000 % 60).toFixed(0).padStart(2,'0'); + pfd.flyingtime.col=fg; + } else if (state==landed) { + flyingTime=landingTime-takeoffTime; + pfd.flyingtime.label=(flyingTime / 3600000).toFixed(0)+":"+(flyingTime / 60000 % 60).toFixed(0).padStart(2,'0'); + pfd.flyingtime.col=green; + } + + pfd.alt.label=(altRaw*unitsAlt[settings.altU].factor).toFixed(unitsAlt[settings.altU].precision); + pfd.avg.col=(rocAvg<-1) ? (red):((rocAvg>0.1) ? (green):(fg)); + pfd.avg.label=(rocAvg*unitsRoc[settings.rocU].factor).toFixed(unitsRoc[settings.rocU].precision); + + var gps = Bangle.getGPSFix(); + if (gps!=undefined) { + pfd.gs.label=(gps.speed*unitsGs[settings.gsU].factor).toFixed(unitsGs[settings.gsU].precision); + updateText(pfd.gs); + gs=gps.speed; + } //else gs=0; + + pfd.time.label=getTimeString(); + updateText(pfd.alt); + updateText(pfd.avg); + updateText(pfd.time); + updateText(pfd.flyingtime); + + pfd.vario.render(); + //print(Date().getTime()-t1); + }, 250); + +} + +function initAltMenu() { + var oldQnh=qnh; + function updateAltMenu() { + altMenu.clear(); + altMenu.alt.label= + (getAltitude(pressure,qnh)*unitsAlt[settings.altU].factor).toFixed(unitsAlt[settings.altU].precision) + +unitsAlt[settings.altU].name; + altMenu.qnh.label=qnh; + altMenu.render(); + } + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var altMenu = new Layout ({ + type:"v", c: [{ + type:"v", width:180, c: [ + {type:"h", c: [ + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label:"-", cb:l=>{qnh--; updateAltMenu();}}, + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label:"+", cb:l=>{qnh++; updateAltMenu();}} + ]}, + {type:"v", c: [ + {type:"h", c: [ + {type:"txt", font:"13%", fillx:1, filly:1, label:"QNH: "}, + {type:"txt", font:"13%", fillx:1, filly:1, id:"qnh", label:" "}, + ]}, + {type:"h", c: [ + {type:"txt", font:"13%", fillx:1, filly:1, label:"Alt: "}, + {type:"txt", font:"13%", fillx:1, filly:1, id:"alt", label: " "}, + ]} + ]}, + {type:"btn", font:"12%", id:"units", pad:2, fillx:1, filly:1, label:"Units: "+unitsAlt[settings.altU].name, cb:()=>{ + settings.altU=(settings.altU+1)%unitsAlt.length; + altMenu.units.label="Units: "+unitsAlt[settings.altU].name; + altMenu.render(); + }}, + ]}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + print("old settings restored"); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + Bangle.setOptions({seaLevelPressure:qnh}); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + altMenu.render(); + updateAltMenu(); +} + +function initAvgMenu() { + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var avgMenu = new Layout ({ + type:"v", c: [{ + type:"v", width:180, c: [ + {type:"h", c: [ + {type:"btn", font:"12%", pad:2, fillx:1, filly:1, label:"-", cb:l=>{ + settings.intTime=Math.clip(settings.intTime-1,1,60); + avgMenu.clear(); + avgMenu.interval.label="Interval: "+settings.intTime+"s"; + avgMenu.render(); + }}, + {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"+", cb:l=>{ + settings.intTime=Math.clip(settings.intTime+1,1,60); + avgMenu.clear(); + avgMenu.interval.label="Interval: "+settings.intTime+"s"; + avgMenu.render(); + }} + ]}, + {type:"txt", id:"interval", font:"10%", pad:1, fillx:1, filly:1, label:"Interval: "+settings.intTime+"s"}, + {type:"btn", font:"12%", id:"units", pad:1, fillx:1, filly:1, label:"Units: "+unitsRoc[settings.rocU].name, cb:()=>{ + settings.rocU=(settings.rocU+1)%unitsRoc.length; + avgMenu.units.label="Units: "+unitsRoc[settings.rocU].name; + avgMenu.render(); + }}, + ]}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + avgMenu.render(); +} + +function initGsMenu() { + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var gsMenu = new Layout ({ + type:"v", c: [ + {type:"btn", font:"20%", id:"units", pad:1, fillx:1, filly:1, label:"Units:\n"+unitsGs[settings.gsU].name, cb:()=>{ + settings.gsU=(settings.gsU+1)%unitsGs.length; + gsMenu.units.label="Units:\n"+unitsGs[settings.gsU].name; + gsMenu.render(); + }}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + print("old settings restored"); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + gsMenu.render(); +} + +function initTimeMenu() { + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var timeMenu = new Layout ({ + type:"v", c: [ + {type:"btn", font:"20%", id:"format", pad:1, fillx:1, filly:1, label:"Time:\n"+((settings.localTime==true) ? ("LCL") : ("UTC")), cb:()=>{ + settings.localTime=!settings.localTime; + timeMenu.format.label="Time:\n"+((settings.localTime==true) ? ("LCL") : ("UTC")); + timeMenu.render(); + }}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + timeMenu.render(); +} + +function initVarioMenu() { + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var varioMenu = new Layout ({ + type:"v", c: [ + {type:"btn", font:"20%", id:"format", pad:1, fillx:1, filly:1, label:"Display:\n"+((settings.bargraph==true) ? ("graph") : ("simple")), cb:()=>{ + settings.bargraph=!settings.bargraph; + varioMenu.format.label="Display:\n"+((settings.bargraph==true) ? ("graph") : ("simple")); + varioMenu.render(); + }}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + varioMenu.render(); +} +function initFlyingTimeMenu() { + oldSettings=Object.assign({},settings); + clearInterval(pfdHandle); + var ftMenu = new Layout ( + {type:"v", c: [ + {type:"v", c: [ + {type:"h", c: [ + {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"Toggle\nAuto", cb:()=>{ + settings.autoDetect=!settings.autoDetect; + ftMenu.manual.label= (settings.autoDetect==true)? + ("AUTO"):((state==flying) ? ("LAND") : ("TAKE\nOFF")); + ftMenu.render(); + }}, + {type:"btn", font:"12%", id:"manual", pad:1, fillx:1, filly:1, label:(settings.autoDetect==true)? + ("AUTO"):((state==flying) ? ("LAND") : ("TAKE\nOFF")), cb:()=>{ + if (settings.autoDetect==false) { + if (state!=flying) { + E.showPrompt("Take off now?").then((v)=> { + if (v) { + state=flying; + takeoffTime=Date().getTime(); + initPFD(); + } + }); + } else { + E.showPrompt("Land now?").then((v)=> { + if (v) { + state=landed; + landingTime=Date().getTime(); + initPFD(); + } + }); + } + } + } + } + ]}, + {type:"btn", font:"12%", pad:1, fillx:1, filly:1, label:"Reset", cb:()=>{ + E.showPrompt("Reset Flight?").then((v)=> { + state=ground; + initPFD(); + }); + }} + ]}, + {type:"h", c: [ + {type:"btn", font:"16%", pad:1, fillx:1, label:"BACK", cb: ()=>{ + settings=Object.assign({},oldSettings); + initPFD(); + }}, + {type:"btn", font:"16%", pad:1, fillx:1, label:"SAVE", cb: ()=>{ + require('Storage').writeJSON("tinyVario.json", settings); + initPFD(); + }} + ]} + ], lazy:true}); + g.clear(); + ftMenu.render(); +} + +Bangle.setGPSPower(true, "tinyVario"); +Bangle.setBarometerPower(true, "tinyVario"); + +Bangle.on('pressure', function(e) { + if (samples<10) { //no need to gather more samples when stuck in a menu + rawP+=e.pressure; + samples++; + } +}); + +initPFD(); diff --git a/apps/tinyVario/app.png b/apps/tinyVario/app.png new file mode 100644 index 000000000..1f6c09bc4 Binary files /dev/null and b/apps/tinyVario/app.png differ diff --git a/apps/tinyVario/metadata.json b/apps/tinyVario/metadata.json new file mode 100644 index 000000000..f038e7515 --- /dev/null +++ b/apps/tinyVario/metadata.json @@ -0,0 +1,15 @@ +{ "id": "tinyVario", + "name": "Tiny Vario", + "shortName" : "tinyVario", + "version":"0.05", + "icon": "app.png", + "readme": "README.md", + "description": "A very simple app for gliding / paragliding / hang gliding etc.", + "tags": "outdoors", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"tinyVario.app.js","url":"app.js"}, + {"name":"tinyVario.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"tinyVario.json"}] +} diff --git a/apps/tinydraw/ChangeLog b/apps/tinydraw/ChangeLog index 2ee16e6b5..4bae1b9f8 100644 --- a/apps/tinydraw/ChangeLog +++ b/apps/tinydraw/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial release 0.02: Don't start drawing with white colour on white canvas +0.03: Fix segmented line glitch when drawing, optimize screen lock detection diff --git a/apps/tinydraw/README.md b/apps/tinydraw/README.md index a4acd9a72..f250af920 100644 --- a/apps/tinydraw/README.md +++ b/apps/tinydraw/README.md @@ -1,14 +1,13 @@ TinyDraw ======== -This is a simple drawing application to make sketches -using different brushes and colors for your BangleJS2 watch! +This is a simple drawing application to make sketches using different +brushes and colors for your BangleJS2 watch! * Brush types: dot, brush, circle, square -It is my first BangleJS application, I plan -to continue improving this app over time, but -if you want to contribute or provide feedback +It is my first BangleJS application, I plan to continue improving +this app over time, but if you want to contribute or provide feedback don't hesitate to contact me! --pancake diff --git a/apps/tinydraw/app.js b/apps/tinydraw/app.js index b0b3ef15b..52460bc79 100644 --- a/apps/tinydraw/app.js +++ b/apps/tinydraw/app.js @@ -1,10 +1,10 @@ (function () { - var pen = 'circle'; - var discard = null; - var kule = [0, 255, 255]; // R, G, B - var oldLock = false; + let pen = 'circle'; + let discard = null; + const kule = [0, 255, 255]; // R, G, B + let oldLock = false; - setInterval(() => { +Bangle.on("lock", function() { if (Bangle.isLocked()) { if (oldLock) { return; @@ -19,8 +19,7 @@ oldLock = false; drawUtil(); } - }, 1000); - +}); function nextColor () { kule[0] = Math.random(); kule[1] = Math.random(); @@ -35,10 +34,33 @@ case 'square': pen = 'circle'; break; default: pen = 'pixel'; break; } - console.log('set time'); drawUtil(); - discard = setTimeout(function () { console.log('timeout'); discard = null; }, 500); + discard = setTimeout(function () { oldX = -1; oldY = -1; console.log('timeout'); discard = null; }, 500); + } + + var oldX = -1; + var oldY = -1; + + function drawBrushIcon () { + const w = g.getWidth(); + switch (pen) { + case 'circle': + g.fillCircle(w - 10, 10, 5); + break; + case 'square': + g.fillRect(w - 5, 5, w - 15, 15); + break; + case 'pixel': + g.setPixel(10, 10); + g.fillCircle(w - 10, 10, 2); + break; + case 'crayon': + g.drawLine(w - 10, 5, w - 10, 15); + g.drawLine(w - 14, 6, w - 10, 12); + g.drawLine(w - 6, 6, w - 10, 12); + break; + } } function drawUtil () { @@ -58,35 +80,32 @@ g.setColor('#fff'); g.fillCircle(g.getWidth() - 10, 10, 8); g.setColor('#000'); - - var w = g.getWidth(); - switch (pen) { - case 'circle': - g.fillCircle(w - 10, 10, 5); - break; - case 'square': - g.fillRect(w - 5, 5, w - 15, 15); - break; - case 'pixel': - g.setPixel(10, 10); - g.fillCircle(w - 10, 10, 2); - break; - case 'crayon': - var tap = { x: 10, y: 15, dy: -5, dx: 5 }; - g.drawLine(w - tap.x, tap.y, w - tap.x + tap.dx, tap.y + tap.dy); - g.drawLine(w - tap.x + 1, tap.y + 2, w - tap.x + tap.dx, tap.y + tap.dy - 2); - g.drawLine(w - tap.x + 2, tap.y + 2, w - tap.x + tap.dx, tap.y + tap.dy + 2); - break; - } + drawBrushIcon(); } - var tapTimer = null; + + let tapTimer = null; + let dragTimer = null; Bangle.on('drag', function (tap) { + let from = { x: tap.x, y: tap.y }; + const to = { x: tap.x + tap.dx, y: tap.y + tap.dy }; + if (oldX != -1) { + from = { x: oldX, y: oldY }; + } if (tap.b === 0) { if (tapTimer !== null) { clearTimeout(tapTimer); tapTimer = null; } } + if (dragTimer != null) { + clearTimeout(dragTimer); + dragTimer = null; + } + dragTimer = setTimeout(function () { + oldX = -1; + oldY = -1; + }, 100); + // tap and hold the clear button if (tap.x < 32 && tap.y < 32) { if (tap.b === 1) { @@ -110,6 +129,8 @@ tapTimer = setTimeout(function () { g.clear(); drawUtil(); + oldX = -1; oldY = -1; + tapTimer = null; }, 800); } @@ -127,28 +148,34 @@ drawUtil(); return; } - + oldX = to.x; + oldY = to.y; g.setColor(kule[0], kule[1], kule[2]); switch (pen) { case 'pixel': - g.setPixel(tap.x, tap.y); - g.drawLine(tap.x, tap.y, tap.x + tap.dx, tap.y + tap.dy); + g.drawLine(from.x, from.y, to.x, to.y); break; case 'crayon': - g.drawLine(tap.x, tap.y, tap.x + tap.dx, tap.y + tap.dy); - g.drawLine(tap.x + 1, tap.y + 2, tap.x + tap.dx, tap.y + tap.dy - 2); - g.drawLine(tap.x + 2, tap.y + 2, tap.x + tap.dx, tap.y + tap.dy + 2); + g.drawLine(from.x, from.y, to.x, to.y); + g.drawLine(from.x + 1, from.y + 2, to.x, to.y - 2); + g.drawLine(from.x + 2, from.y + 2, to.x, to.y + 2); break; case 'circle': - var XS = tap.dx / 10; - var YS = tap.dy / 10; - for (i = 0; i < 10; i++) { - g.fillCircle(tap.x + (i * XS), tap.y + (i * YS), 4, 4); + var XS = (to.x - from.x) / 32; + var YS = (to.y - from.y) / 32; + for (i = 0; i < 32; i++) { + g.fillCircle(from.x + (i * XS), from.y + (i * YS), 4, 4); } break; case 'square': - g.fillRect(tap.x - 10, tap.y - 10, tap.x + 10, tap.y + 10); + var XS = (to.x - from.x) / 32; + var YS = (to.y - from.y) / 32; + for (i = 0; i < 32; i++) { + const posX = from.x + (i * XS); + const posY = from.y + (i * YS); + g.fillRect(posX - 10, posY - 10, posX + 10, posY + 10); + } break; } drawUtil(); @@ -157,3 +184,4 @@ g.clear(); drawUtil(); })(); + diff --git a/apps/tinydraw/metadata.json b/apps/tinydraw/metadata.json index 357fcc1d0..35d994ec3 100644 --- a/apps/tinydraw/metadata.json +++ b/apps/tinydraw/metadata.json @@ -1,7 +1,7 @@ { "id": "tinydraw", "name": "TinyDraw", "shortName":"TinyDraw", - "version":"0.02", + "version":"0.03", "type": "app", "description": "Draw stuff in your wrist", "icon": "app.png", diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index 7b5c53de7..e15ffa29b 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -5,3 +5,4 @@ 0.05: Improve perf 0.06: Complete rewrite in 80x80, better perf, add settings 0.07: Added suppport for Bangle 2, added README file +0.08: Use default Bangle formatter for booleans diff --git a/apps/toucher/metadata.json b/apps/toucher/metadata.json index 8b2715f0c..0c7a35773 100644 --- a/apps/toucher/metadata.json +++ b/apps/toucher/metadata.json @@ -2,7 +2,7 @@ "id": "toucher", "name": "Touch Launcher", "shortName": "Toucher", - "version": "0.07", + "version": "0.08", "description": "Touch enable left to right launcher.", "icon": "app.png", "type": "launch", diff --git a/apps/toucher/settings.js b/apps/toucher/settings.js index 51275d846..f3004000a 100644 --- a/apps/toucher/settings.js +++ b/apps/toucher/settings.js @@ -41,7 +41,6 @@ }, "Animation" : { value : settings.animation, - format : v => v?"On":"Off", onchange : saveChange('animation') }, "Frame rate" : { @@ -51,7 +50,6 @@ }, "Debug" : { value : settings.debug, - format : v => v?"On":"Off", onchange : saveChange('debug') }, '< Back': back diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog index f81907152..203360bd0 100644 --- a/apps/touchtimer/ChangeLog +++ b/apps/touchtimer/ChangeLog @@ -3,3 +3,4 @@ 0.03: Add ability to repeat last timer 0.04: Add 5 second count down buzzer 0.05: Fix 5 second count down buzzer to be only in the final 5 seconds +0.06: Use default Bangle formatter for booleans diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json index 9261f3619..8e09a7e34 100644 --- a/apps/touchtimer/metadata.json +++ b/apps/touchtimer/metadata.json @@ -2,7 +2,7 @@ "id": "touchtimer", "name": "Touch Timer", "shortName": "Touch Timer", - "version": "0.05", + "version": "0.06", "description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.", "icon": "app.png", "tags": "tools", diff --git a/apps/touchtimer/settings.js b/apps/touchtimer/settings.js index 79424f250..d3de4e6d3 100644 --- a/apps/touchtimer/settings.js +++ b/apps/touchtimer/settings.js @@ -33,7 +33,6 @@ }, "CountDown Buzz": { value: !!settings.countDownBuzz, - format: value => value?"On":"Off", onchange: (value) => { settings.countDownBuzz = value; writeSettings(settings); diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog index 02831edde..6693f57ec 100644 --- a/apps/vectorclock/ChangeLog +++ b/apps/vectorclock/ChangeLog @@ -6,3 +6,4 @@ 0.06: Redraw widgets when time is updated 0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437 0.08: Redraw widgets only once per minute +0.09: Workaround for issue in 2v14 firmware (fix #1959) diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index 663a4c84f..ee3a4ea53 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -16,7 +16,7 @@ var commands = []; var showSeconds = true; function pushCommand(command) { - let hash = E.CRC32(E.toJS(arguments)); + var hash = E.CRC32(E.toJS(arguments)); if (!delete rectsToClear[hash]) { commands.push({hash: hash, command: Function.apply.bind(command, null, arguments.slice(1))}); } diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json index 541766fa2..245aad044 100644 --- a/apps/vectorclock/metadata.json +++ b/apps/vectorclock/metadata.json @@ -1,7 +1,7 @@ { "id": "vectorclock", "name": "Vector Clock", - "version": "0.08", + "version": "0.09", "description": "A digital clock that uses the built-in vector font.", "icon": "app.png", "type": "clock", diff --git a/apps/verticalface/ChangeLog b/apps/verticalface/ChangeLog index 99ab68ec4..dc6c11eab 100644 --- a/apps/verticalface/ChangeLog +++ b/apps/verticalface/ChangeLog @@ -4,3 +4,4 @@ 0.07: Added leading zero to hours and minutes 0.08: Show step count by calling wpedom.getSteps() or activepedom.getSteps() 0.09: Fix time when minutes<10 and hours>9 (fix #767) +0.10: Tell clock widgets to hide. diff --git a/apps/verticalface/app.js b/apps/verticalface/app.js index 4fcae5642..178481047 100644 --- a/apps/verticalface/app.js +++ b/apps/verticalface/app.js @@ -97,6 +97,28 @@ function drawBattery() { // Clear the screen once, at startup g.clear(); +// Show launcher when button pressed +Bangle.setUI("clockupdown", btn=>{ + if (btn!=0) return; + //HRM Controller. + if(!HRMstate){ + //console.log("Toggled HRM"); + //Turn on. + Bangle.buzz(); + Bangle.setHRMPower(1); + currentHRM = "CALC"; + HRMstate = true; + } else if(HRMstate){ + //console.log("Toggled HRM"); + //Turn off. + Bangle.buzz(); + Bangle.setHRMPower(0); + HRMstate = false; + currentHRM = []; + } + drawBPM(HRMstate); +}); + // Load and draw widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -128,28 +150,6 @@ Bangle.on('lcdPower',on=>{ } }); -// Show launcher when button pressed -Bangle.setUI("clockupdown", btn=>{ - if (btn!=0) return; - //HRM Controller. - if(!HRMstate){ - //console.log("Toggled HRM"); - //Turn on. - Bangle.buzz(); - Bangle.setHRMPower(1); - currentHRM = "CALC"; - HRMstate = true; - } else if(HRMstate){ - //console.log("Toggled HRM"); - //Turn off. - Bangle.buzz(); - Bangle.setHRMPower(0); - HRMstate = false; - currentHRM = []; - } - drawBPM(HRMstate); -}); - Bangle.on('touch', function(button) { if(button == 1 || button == 2){ Bangle.showLauncher(); diff --git a/apps/verticalface/metadata.json b/apps/verticalface/metadata.json index da41b3f0d..273070022 100644 --- a/apps/verticalface/metadata.json +++ b/apps/verticalface/metadata.json @@ -2,7 +2,7 @@ "id": "verticalface", "name": "Vertical watch face", "shortName": "Vertical Face", - "version": "0.09", + "version": "0.10", "description": "A simple vertical watch face with the date. Heart rate monitor is toggled with BTN1", "icon": "app.png", "type": "clock", diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog index 1b584f7dd..292d77a99 100644 --- a/apps/waypointer/ChangeLog +++ b/apps/waypointer/ChangeLog @@ -1,2 +1,4 @@ 0.01: New app! 0.02: Make Bangle.js 2 compatible +0.03: Silently use built in heading when no magnav calibration file is present +0.04: Move waypoints.json (and editor) to 'waypoints' app diff --git a/apps/waypointer/README.md b/apps/waypointer/README.md index c0b4c5125..241d70a65 100644 --- a/apps/waypointer/README.md +++ b/apps/waypointer/README.md @@ -61,58 +61,10 @@ The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location. -## Waypoint JSON file - -When the app is loaded from the app loader, a file named -`waypoints.json` is loaded along with the javascript etc. The file -has the following contents: - - -``` -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] -``` - -The file contains the initial NONE waypoint which is useful if you -just want to display course and speed. The next two entries are -waypoints to No 10 Downing Street and to Stone Henge - obtained from -Google Maps. The last five entries are entries which can be *marked*. - -You add and delete entries using the Web IDE to load and then save -the file from and to watch storage. The app itself does not limit the -number of entries although it does load the entire file into RAM -which will obviously limit this. - - -## Waypoint Editor - -Clicking on the download icon of gpsnav in the app loader invokes the -waypoint editor. The editor downloads and displays the current -`waypoints.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 waypoint` -button. A new markable entry is created by using the `Add name` -button. The edited `waypoints.json` file is uploaded to the Bangle by -clicking the `Upload` button. +## Setting Waypoints +Check out the documentation for the `Waypoints` app. This provides +the ability to set waypoints from your browser. ## Calibration of the Compass diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js index 615fbbc36..9fd288c9a 100644 --- a/apps/waypointer/app.js +++ b/apps/waypointer/app.js @@ -50,7 +50,7 @@ function drawCompass(course) { if(!candraw) return; if (Math.abs(previous.course - course) < 9) return; // reduce number of draws due to compass jitter previous.course = course; - + buf1.setColor(1); buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,79*scale); buf1.setColor(0); @@ -63,10 +63,10 @@ function drawCompass(course) { /***** COMPASS CODE ***********/ var heading = 0; -function newHeading(m,h){ +function newHeading(m,h){ var s = Math.abs(m - h); var delta = (m>h)?1:-1; - if (s>=180){s=360-s; delta = -delta;} + if (s>=180){s=360-s; delta = -delta;} if (s<2) return h; var hd = h + delta*(1 + Math.round(s/5)); if (hd<0) hd+=360; @@ -74,11 +74,14 @@ function newHeading(m,h){ return hd; } -var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; +var CALIBDATA = require("Storage").readJSON("magnav.json",1) || {}; function tiltfixread(O,S){ - var start = Date.now(); var m = Bangle.getCompass(); + if (O === undefined || S === undefined) { + // no valid calibration from magnav, use built in + return 360-m.heading; + } 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; var d = Math.atan2(-m.dx,m.dy)*180/Math.PI; @@ -97,6 +100,7 @@ function tiltfixread(O,S){ // Note actual mag is 360-m, error in firmware function read_compass() { var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + if (isNaN(d)) return; // built in compass heading can return NaN when uncalibrated heading = newHeading(d,heading); direction = wp_bearing - heading; if (direction < 0) direction += 360; @@ -143,9 +147,9 @@ function drawN(){ var bs = wp_bearing.toString(); bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs; var dst = loc.distance(dist); - + // -1=left (default), 0=center, 1=right - + // show distance on the left if (previous.dst !== dst) { previous.dst = dst; @@ -155,7 +159,7 @@ function drawN(){ buf2.drawString(dst,0,0); flip2_bw(0, g.getHeight()-40*scale); } - + // bearing, place in middle at bottom of compass if (previous.bs !== bs) { previous.bs = bs; @@ -188,7 +192,7 @@ function onGPS(fix) { if (fix!==undefined){ satellites = fix.satellites; } - + if (candraw) { if (fix!==undefined && fix.fix==1){ dist = distance(fix,wp); @@ -236,7 +240,7 @@ function setButtons(){ else { doselect(); } }); } - + Bangle.on('lcdPower',function(on) { if (on) { clear_previous(); @@ -246,7 +250,7 @@ Bangle.on('lcdPower',function(on) { } }); -var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +var waypoints = require("waypoints").load(); wp=waypoints[0]; function nextwp(inc){ @@ -262,7 +266,7 @@ function doselect(){ if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) { waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon}; wp = waypoints[wpindex]; - require("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); } selected=!selected; drawN(); diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json index 111259bbc..8b923c604 100644 --- a/apps/waypointer/metadata.json +++ b/apps/waypointer/metadata.json @@ -1,16 +1,15 @@ { "id": "waypointer", "name": "Way Pointer", - "version": "0.02", + "version": "0.04", "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", "icon": "waypointer.png", "tags": "tool,outdoors,gps", "supports": ["BANGLEJS", "BANGLEJS2"], + "dependencies" : { "waypoints":"type" }, "readme": "README.md", - "interface": "waypoints.html", "storage": [ {"name":"waypointer.app.js","url":"app.js"}, {"name":"waypointer.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/waypointer/waypoints.html b/apps/waypointer/waypoints.html deleted file mode 100644 index d02260732..000000000 --- a/apps/waypointer/waypoints.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - -

List of waypoints

- - - - - - - - - - - - -
NameLat.Long.Actions
-
-

Add a new waypoint

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - diff --git a/apps/waypointer/waypoints.json b/apps/waypointer/waypoints.json deleted file mode 100644 index 98a670c0d..000000000 --- a/apps/waypointer/waypoints.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name":"NONE" - }, - { - "name":"No10", - "lat":51.5032, - "lon":-0.1269 - }, - { - "name":"Stone", - "lat":51.1788, - "lon":-1.8260 - }, - { "name":"WP0" }, - { "name":"WP1" }, - { "name":"WP2" }, - { "name":"WP3" }, - { "name":"WP4" } -] \ No newline at end of file diff --git a/apps/waypoints/ChangeLog b/apps/waypoints/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/waypoints/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/waypoints/README.md b/apps/waypoints/README.md new file mode 100644 index 000000000..e252054f2 --- /dev/null +++ b/apps/waypoints/README.md @@ -0,0 +1,56 @@ +# Waypoints + +This app provides a common way to set up the `waypoints.json` file, +which several other apps rely on for navigation. + +## Waypoint JSON file + +When the app is loaded from the app loader, a file named +`waypoints.json` is loaded along with the javascript etc. The file +has the following contents: + + +``` +[ + { + "name":"NONE" + }, + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + }, + { "name":"WP0" }, + { "name":"WP1" }, + { "name":"WP2" }, + { "name":"WP3" }, + { "name":"WP4" } +] +``` + +The file contains the initial NONE waypoint which is useful if you +just want to display course and speed. The next two entries are +waypoints to No 10 Downing Street and to Stone Henge - obtained from +Google Maps. The last five entries are entries which can be *marked*. + +You add and delete entries using the Web IDE to load and then save +the file from and to watch storage. The app itself does not limit the +number of entries although it does load the entire file into RAM +which will obviously limit this. + + +## Waypoint Editor + +Clicking on the download icon of `Waypoints` in the app loader invokes the +waypoint editor. The editor downloads and displays the current +`waypoints.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 waypoint` +button. A new markable entry is created by using the `Add name` +button. The edited `waypoints.json` file is uploaded to the Bangle by +clicking the `Upload` button. diff --git a/apps/waypoints/app-icon.js b/apps/waypoints/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/waypoints/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/waypoints/app.js b/apps/waypoints/app.js new file mode 100644 index 000000000..06c254a36 --- /dev/null +++ b/apps/waypoints/app.js @@ -0,0 +1,34 @@ +// place your const, vars, functions or classes here + +// clear the screen +g.clear(); + +var n = 0; + +// redraw the screen +function draw() { + g.reset().clearRect(Bangle.appRect); + g.setFont("6x8").setFontAlign(0,0).drawString("Up / Down",g.getWidth()/2,g.getHeight()/2 - 20); + g.setFont("Vector",60).setFontAlign(0,0).drawString(n,g.getWidth()/2,g.getHeight()/2 + 30); +} + +// Respond to user input +Bangle.setUI({mode: "updown"}, function(dir) { + if (dir<0) { + n--; + draw(); + } else if (dir>0) { + n++; + draw(); + } else { + n = 0; + draw(); + } +}); + +// First draw... +draw(); + +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/waypoints/app.png b/apps/waypoints/app.png new file mode 100644 index 000000000..0b9344405 Binary files /dev/null and b/apps/waypoints/app.png differ diff --git a/apps/wpmoto/wpmoto.html b/apps/waypoints/interface.html similarity index 60% rename from apps/wpmoto/wpmoto.html rename to apps/waypoints/interface.html index 9966f51f6..48e47df57 100644 --- a/apps/wpmoto/wpmoto.html +++ b/apps/waypoints/interface.html @@ -4,6 +4,7 @@ + @@ -11,6 +12,8 @@ html, body { height: 100% } .flex-col { display:flex; flex-direction:column; height:100% } #map { width:100%; height:100% } + #tab-map { width:100%; height:100% } + #tab-list { width:100%; height:100% } /* https://stackoverflow.com/a/58686215 */ .arrow-icon { @@ -23,6 +26,7 @@ transform-origin: center center; font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; } + @@ -32,8 +36,57 @@ +
+ +
+
+
+
@@ -44,8 +97,23 @@ - + + diff --git a/apps/waypoints/lib.js b/apps/waypoints/lib.js new file mode 100644 index 000000000..88b402a73 --- /dev/null +++ b/apps/waypoints/lib.js @@ -0,0 +1,7 @@ +exports.load = (num) => { + return require("Storage").readJSON(`waypoints${num?"."+num:""}.json`)||[{name:"NONE"}]; +}; + +exports.save = (waypoints,num) => { + require("Storage").writeJSON(`waypoints${num?"."+num:""}.json`, waypoints); +}; diff --git a/apps/waypoints/metadata.json b/apps/waypoints/metadata.json new file mode 100644 index 000000000..d7fa00f7e --- /dev/null +++ b/apps/waypoints/metadata.json @@ -0,0 +1,20 @@ +{ "id": "waypoints", + "name": "Waypoints", + "version":"0.01", + "description": "Provides 'waypoints.json' used by various navigation apps, as well as a way to edit it from the App Loader with maps or a list", + "icon": "app.png", + "tags": "tool,outdoors,gps", + "type": "waypoints", + "supports" : ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"waypoints","url":"lib.js"} + ], + "data": [ + {"name":"waypoints.json","url":"waypoints.json"}, + {"name":"waypoints.1.json"}, + {"name":"waypoints.2.json"}, + {"name":"waypoints.3.json"} + ] +} diff --git a/apps/waypoints/waypoints.json b/apps/waypoints/waypoints.json new file mode 100644 index 000000000..066f05c10 --- /dev/null +++ b/apps/waypoints/waypoints.json @@ -0,0 +1,12 @@ +[ + { + "name":"No10", + "lat":51.5032, + "lon":-0.1269 + }, + { + "name":"Stone", + "lat":51.1788, + "lon":-1.8260 + } +] diff --git a/apps/wclock/ChangeLog b/apps/wclock/ChangeLog index 9a2ebdd5f..e50ee6842 100644 --- a/apps/wclock/ChangeLog +++ b/apps/wclock/ChangeLog @@ -1,2 +1,3 @@ 0.02: Modified for use with new bootloader and firmware 0.03: setUI and support for different screens +0.04: Tell clock widgets to hide. diff --git a/apps/wclock/clock-word.js b/apps/wclock/clock-word.js index aff134273..7ddb7bc35 100644 --- a/apps/wclock/clock-word.js +++ b/apps/wclock/clock-word.js @@ -122,11 +122,11 @@ Bangle.on('lcdPower', function(on) { if (on) drawWordClock(); }); +// Show launcher when button pressed +Bangle.setUI("clock"); + g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); setInterval(drawWordClock, 1E4); drawWordClock(); - -// Show launcher when button pressed -Bangle.setUI("clock"); diff --git a/apps/wclock/metadata.json b/apps/wclock/metadata.json index f22b53dc1..820af7ac0 100644 --- a/apps/wclock/metadata.json +++ b/apps/wclock/metadata.json @@ -1,7 +1,7 @@ { "id": "wclock", "name": "Word Clock", - "version": "0.03", + "version": "0.04", "description": "Display Time as Text", "icon": "clock-word.png", "screenshots": [{"url":"screenshot_word.png"}], diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 101da48e1..49e23e1d6 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -12,3 +12,4 @@ 0.13: Tweak Bangle.js 2 light theme colors 0.14: Use weather condition code for icon selection 0.15: Fix widget icon +0.16: Don't mark app as clock diff --git a/apps/weather/app.js b/apps/weather/app.js index efd9b0209..f63b226b9 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -101,7 +101,11 @@ weather.on("update", update); update(); -// Show launcher when middle button pressed +// We want this app to behave like a clock: +// i.e. show launcher when middle button pressed Bangle.setUI("clock"); +// But the app is not actually a clock +// This matters for widgets that hide themselves for clocks, like widclk or widclose +delete Bangle.CLOCK; Bangle.drawWidgets(); diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index 1d0b6b469..25037de3d 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.15", + "version": "0.16", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/weather/readme.md b/apps/weather/readme.md index 6d0ea04a5..b37d0b38e 100644 --- a/apps/weather/readme.md +++ b/apps/weather/readme.md @@ -11,6 +11,9 @@ You can view the full report through the app: 1. Install [Gadgetbridge for Android](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) on your phone. 2. Set up [Gadgetbridge weather reporting](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather). +If using the `Bangle.js Gadgetbridge` app on your phone (as opposed to the standard F-Droid `Gadgetbridge`) you need to set the package name +to `com.espruino.gadgetbridge.banglejs` in the settings of the weather app (`settings -> gadgetbridge support -> package name`). + ## Settings * Expiration timespan can be set after which the local weather data is considered as invalid diff --git a/apps/widadjust/ChangeLog b/apps/widadjust/ChangeLog new file mode 100644 index 000000000..9b2a8d3c8 --- /dev/null +++ b/apps/widadjust/ChangeLog @@ -0,0 +1,2 @@ +0.01: New widget +0.02: Use default Bangle formatter for booleans diff --git a/apps/widadjust/metadata.json b/apps/widadjust/metadata.json index a308072f5..cef91369f 100644 --- a/apps/widadjust/metadata.json +++ b/apps/widadjust/metadata.json @@ -2,7 +2,7 @@ "id": "widadjust", "name": "Adjust Clock", "icon": "icon.png", - "version": "0.01", + "version": "0.02", "description": "Adjusts clock continually in the background to counter clock drift", "type": "widget", "tags": "widget", diff --git a/apps/widadjust/settings.js b/apps/widadjust/settings.js index 5791d763b..6743c7fc5 100644 --- a/apps/widadjust/settings.js +++ b/apps/widadjust/settings.js @@ -80,9 +80,7 @@ min: 0, max: intervalV.length - 1, format: v => intervalN[v], - onchange: v => { - settings.updateInterval = intervalV[v]; - }, + onchange: v => settings.updateInterval = intervalV[v] , }, 'Threshold': { @@ -97,10 +95,9 @@ 'Save State': { value: settings.saveState, - format: v => v ? 'On' : 'Off', - onchange: () => { - settings.saveState = !settings.saveState; - if (!settings.saveState && !stateFileErased) { + onchange: (v) => { + settings.saveState = v; + if (!v && !stateFileErased) { stateFileErased = true; require("Storage").erase(STATE_FILE); } @@ -109,10 +106,7 @@ 'Debug Log': { value: settings.debugLog, - format: v => v ? 'On' : 'Off', - onchange: () => { - settings.debugLog = !settings.debugLog; - }, + onchange: v => settings.debugLog = v, }, }; diff --git a/apps/widagps/ChangeLog b/apps/widagps/ChangeLog new file mode 100644 index 000000000..6728683c8 --- /dev/null +++ b/apps/widagps/ChangeLog @@ -0,0 +1 @@ +0.01: first version diff --git a/apps/widagps/README.md b/apps/widagps/README.md new file mode 100644 index 000000000..e5e9a8f8a --- /dev/null +++ b/apps/widagps/README.md @@ -0,0 +1,12 @@ +# A-GPS Data + +Load assisted GPS data directly to the watch using the new http requests on Android GadgetBridge. + +Make sure: +* your GadgetBridge version supports http requests +* turn on internet access in GadgetBridge settings + +The widget loads the data in the background every 12 hours. It retries every 10min if the http request fails. It is only visible during a request or on error. + +## Creator +[@pidajo](https://github.com/pidajo) diff --git a/apps/widagps/metadata.json b/apps/widagps/metadata.json new file mode 100644 index 000000000..8d3d37aab --- /dev/null +++ b/apps/widagps/metadata.json @@ -0,0 +1,14 @@ +{ "id": "widagps", + "name": "AGPS Widget", + "shortName":"AGPS Widget", + "icon": "widget.png", + "type": "widget", + "version":"0.01", + "description": "Load AGPS data in the background **using Gadgetbridge**", + "readme": "README.md", + "tags": "widget,agps,http", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"widagps.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widagps/widget.js b/apps/widagps/widget.js new file mode 100644 index 000000000..57ed801e1 --- /dev/null +++ b/apps/widagps/widget.js @@ -0,0 +1,165 @@ +if (!global.WIDGETS) { + WIDGETS = {}; + Bangle.loadWidgets(); + var isTest = true; +} + +(function(){ + var warnTime = 24*60*60000; //warn missing data + var nextTime = 12*60*60000; //time between requests + var retryTime = 10*60000; //time between retries + + const JSON_FILE = "agpsdata.json"; + var isRequesting = false; + var lastAGPS = 0; + var nextGet = null; + + const WIDGET_ID = "widagps"; + WIDGETS[WIDGET_ID]={ + area:"tl", + width:24, + draw:function() { + var w = 0; + var x = this.x, y = this.y; + g.reset(); + if (isRequesting) { + g.setColor("#00f"); + w = 24; + } + else { + if (Date.now() - lastAGPS > warnTime) { + g.setColor("#f00"); + w = 24; + } + } + if (w) { +g.drawImage(atob("FBQBAAAABgAAYAAPAACwABOAATgAI8ACPABD4AQ+AEPgD8EAg/AQP4AD+CD/wjD8WAHmAAY="), x + 1, y + 1); + } + if (WIDGETS[WIDGET_ID].width != w) { + WIDGETS[WIDGET_ID].width = w; + Bangle.drawWidgets(); + } + } + }; + + var _GB = global.GB; + + global.GB = function(msg) { + //console.log(msg); + if (msg.t == "http") { + applyAGPS(msg.resp); + } + if (_GB) { + _GB(msg); + } + } + + function nextAGPS(when) { + if (nextGet) { + clearTimeout(nextGet); + nextGet = null; + } + console.log("Next AGPS request:", new Date(Date.now() + when)); + nextGet = setTimeout(() => { + getAGPS(); + }, when); + } + + function applyAGPS(data) { + isRequesting = false; + var success = false; + if (data) { + success = setAGPS(data); + } + if (success) { + lastAGPS = Date.now(); + nextAGPS(nextTime); + require("Storage").writeJSON(JSON_FILE, {lastAGPS: lastAGPS}); + } + else { + console.log("Failed to apply AGPS data"); + nextAGPS(retryTime); + } + Bangle.drawWidgets(); + } + + function setAGPS(data) { + var js = jsFromBase64(data); + Bangle.setGPSPower(true, "agpsdata"); + try { + eval(js); + Bangle.setGPSPower(false, "agpsdata"); + return true; + } + catch(e) { + console.log("Error:", e); + } + Bangle.setGPSPower(false, "agpsdata"); + return false; + } + + function jsFromBase64(b64) { + var bin = atob(b64); + var chunkSize = 128; + var js = "";//"Bangle.setGPSPower(1);\n"; // turn GPS on + var gnss_select="1"; + js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode + // What about: + // NAV-TIMEUTC (0x01 0x10) + // NAV-PV (0x01 0x03) + // or AGPS.zip uses AID-INI (0x0B 0x01) + + for (var i=0;i { + GB({t:"http", resp:testData}); + }, 5000); + } + //check for request timeout + setTimeout(() => { + if (isRequesting) { + applyAGPS(); + } + }, 10000); + Bangle.drawWidgets(); + } + + var data = require("Storage").readJSON(JSON_FILE); + if (data && data.lastAGPS) { + //lastAGPS = data.lastAGPS; + } + + nextAGPS(Math.max(0, nextTime - (Date.now() - lastAGPS))); + NRF.on('connect', () => { + if (Date.now() - lastAGPS > warnTime) { + nextAGPS(0); + } + }); + + var testData = "QUdOU1MgZGF0YSBmcm9tIENBU0lDLgpEYXRhTGVuZ3RoOiAyNTk4LgpMaW1pdGF0aW9uOiAzLzEwMDAuCrrOSAAIB7YdxSr+Sg2h8NYlBux1jiUgQbrXgJk/KJvFZVv8pP//uy3i/PH6rv9EMQH6SwBfAOxepgDsXgAAlCULALv/AAtCAAAAAQMAALQ7kly6zkgACAdBzVam9HANoXGycgoqGmnG5X9h3mKrWicvBKhXAp7//+00U/9j/jP/SDHM/Vn/JADrXqYA614AAF6d6v8DAADaEAAAAAIDAADKmrVTus5IAAgHTUirJrjvDKHJDjACcmXJJ+8Lv6rw5LQnl4OChUCt//9XKG3/+u6ZE84ZQuwDAMf/7F6mAOxeAADa0Pb/mP8ABDUAAAADAwAA4pBeVLrOSAAIB5291DTIzAyhJGfzAJ7pCIcIUQMgcfEoJ2TIjrHkqv//YjDTCKj/wRPdGIz/8/9NAOxepgDsXgAAl9/6/yQAAPbhAAAABAMAAIJ7sXC6zkgACAeKKDOgmwEOocKrFwP8Jmgqq4lOQ1VtLSfEi8yDVqn//5MskP6E7WQRPxzv6vv/0//sXqYA7F4AAM4w/f/0/wDoJwAAAAUDAABcUW5Hus5IAAgHu+Va48nxDaFaw0UBkVc73Y3FB+HFmDgoUWIPW+Ck//+iLcf9X/t5/ikzgPrT/wgA7F6mAOxeAAADVgsAiQAACB0AAAAGAwAAvsu9zbrOSAAIB0OVp/7+JQ2hFANPCF+F6KOOMwe9/nC5JsX0CtthqP//WzhzADr/dgqbIRj/nwDj/+xepgDsXgAAP4oKAPv/AOg4AAAABwMAAM4qVwS6zkgACAey9J9b+zwOofLIzwObDg0HLdEmMOA3PydWaUQvr6b//0wxwf/7EJ8K/yLREhkANADsXqYA7F4AALug/f/y/wALLAAAAAgDAACs6Ue+us5IAAgHm7NJLLhaDKEumRQBAFtVTExfuke3AeEmNg9cr2Cq//92MTIJm/63FCkXPv4gABgA616mAOteAACQQfX/HgAAAzUAAAAJAwAAfmebX7rOSAAIB9fgVnnchw2hNlTrAyWnJ5rnBMWFV9GyJzPfZYV8rf//Oyh2AA3xmhLdGtXu/f/Z/+xepgDsXgAA8gXx/37/AAU/AAAACgMAAPbBtfm6zkgACAc+F7Ct97wMoVEVPQCJJG1yeakXMm80PSet+BBd+aH//5AzPf2J+uX97jG7+fj/DgDsXqYA7F4AAGqE//8WAADufQIAAAsDAADELmhius5IAAgHW3g6rwRJDqFpPmYEPf8lNRy9lKO/IX8nJPRjCLOt//90Lir7QQcEGFYUTAguAA4A7F6mAOxeAADrZfj/zP8A5SsAAAAMAwAA/vB8ZbrOSAAIB1mVSsfiXw2hUX8RA60JSyUgLkEgC910JykTq7Usqv//8S9rBw//HhIpGyT/+v8fAOxepgDsXgAAitgKAD4AAOcpAAAADQMAAPoqnZW6zkgACAcCLzz8Q1ANocBvBQFuUWqAxVSgoRjR0SaS5AkH3qr//7cx3vlcB2AYPROjCOr/vf/sXqYA7F4AAA5Y/P/7/wDvGwMAAA4DAABMXoD/us5IAAgH+RiyseCLDKGnOjkHATiXLOud6AnbGuclr2roqg2l///FNxcHi/0hFLoW4fyj/10A7F6mAOxeAAANRf7/GgAA6TQAAAAPAwAAOjJsarrOSAAIB1/+xFM3Xg2hVDKBBg6Tsx25u5hYROV9J/QyJQmLrf//zC1e+7QGxRfmFOAHhf+s/+xepgDsXgAAaE/v/+j/AOonAAAAEAMAAAb9ka66zkgACAc74z4AUZEMofLN6wbbUEjD/w54MWPC3SfOFa4yQ6f//4wtrwH5EOwKOCRTFLz/UP/sXqYA7F4AAOaAFAApAADoOQAAABEDAAC+xoUHus5IAAgH1yXSbYfZDaFOrzwBIM5keona3uYD8JYnC6ekWw+l//8JMC/9ivsaAGEwM/ssAN//7F6mAOxeAAAymwQAof8A7l8AAAASAwAA9kus4rrOSAAIB/9znedCsA2h5VDBBLdHG1Uw8P6P93bRJw5UgTR9qf//LS1BAj0TAwkqJioWBABHAOxepgDsXgAAD54FACwAAN6xAAAAEwMAAEboQta6zkgACAdVJvSzetsMoRclcgKU4/OAvUl5A73wdCYbpBB/P6X//08yQ/4N7voNgx5160gAFwDsXqYA7F4AADa+EADk/wDuLwAAABQDAADyTPBuus5IAAgHhpZXHJnuDaGfsmkMbSLS2QeTnDZBLR8ntwGPV8mj//+SM6n8rftj/EUyZfw7AaX/7F6mAOxeAAAvSQUAAAAA6j4AAAAVAwAAVC23P7rOSAAIB8FSSGuHmw2hXoTeBoU+tLS9TdsrYGopJ+h3h7O9p///mjEIB3X/GhN5GXP/c//V/+xepgDsXgAASREJADgAAO4rAAAAFgMAAMqlmN26zkgACAeGsPJwF8ENoU2cJwHhxiN62PG7uVyKfydPWVyEG6z//8spSgCQ8NkRnxsl7sr/EQDsXqYA7F4AAI0S///u/wDudQEAABcDAABUYe3ous5IAAgHtHJyvWVdDaFr1mwGwPVWIZcaxOQXwAwmeHqW1+Sh//+PPnAAQgAOCxwhof8sAGwA7F6mAOxeAAAhMQcAtf8ABjsAAAAYAwAAsOXsgbrOSAAIB4nxObhoSQ2hOjBcBf2n5ijflOaX/Iz8JpPiNAUTrP//ajEL++oEchfzE9AFSgDT/+tepgDrXgAAohMLACkAAAweAAAAGQMAAFrje3e6zkgACAexAdJ/MyYOoeXvjwPazb0PcXcXfIVaNCZ2mjMDkqn//z02W/qPBMcW7BM7BcX/6//sXqYA7F4AABv4BgAVAAAPIgAAABoDAACqA6wGus5IAAgHxYz/b8dNDaH6/WQF3AILHLKDgTJ+VponRocZMKSm//8bLycAMRANDG8i/RHR/2wA7F6mAOxeAAArEwcAHAAABEgAAQAbAwAA0hkH57rOSAAIB5HXkJcOjA2h8B4kAebzvl2gmkU1K1TzJ86wODP6qP//0CzCAM4OmQrkI1UR7f/n/+xepgDsXgAAcs/u/9//AOplAAAAHQMAAGqvKTa6zkgACAekogIGh+8NoYBTBQMDtQeTiKQ4u0KYHiYkF4HbzaT//7A80v9N/gQLmSC4/icA6v/sXqYA7F4AAB2V7v/2/wAIGQAAAB4DAACQRQ0Tus5IAAgHUsOCUJ71DaHkPFgFdJcpEDguP6ldR+UmgbrM2+6m//9vOPT/S/7vC1sh6/49AJH/7F6mAOxeAAAQCfr/8/8A4wwAAAAfAwAA7IYNqLrOSAAIByZDZIfa4AyhxlEVA9QtFKN+R+1NPIIIJ8xm1q8Qqv//djDzB9H+pBNQGGj+rv++/+xepgDsXgAA3f/6/67/AAFUAAAAIAMAAJSG0BW6zhQACAWVGZOmAAAAAPr///8SEpCmiQcDAD4zLlK6zhAACAZIDf33DwP+/jYK//gDAAAAoBoC9g=="; + +})() +if (global.isTest) { + Bangle.drawWidgets(); +} + diff --git a/apps/widagps/widget.png b/apps/widagps/widget.png new file mode 100644 index 000000000..ee16c114a Binary files /dev/null and b/apps/widagps/widget.png differ diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json new file mode 100644 index 000000000..b6d8bd62b --- /dev/null +++ b/apps/widalarmeta/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widalarmeta", + "name": "Alarm & Timer ETA", + "shortName": "Alarm ETA", + "version": "0.01", + "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots" : [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"widalarmeta.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widalarmeta/screenshot.png b/apps/widalarmeta/screenshot.png new file mode 100644 index 000000000..41a109557 Binary files /dev/null and b/apps/widalarmeta/screenshot.png differ diff --git a/apps/widalarmeta/widget.js b/apps/widalarmeta/widget.js new file mode 100644 index 000000000..0cddf953a --- /dev/null +++ b/apps/widalarmeta/widget.js @@ -0,0 +1,35 @@ +(() => { + const alarms = require("Storage").readJSON("sched.json",1) || []; + + function draw() { + const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm)).filter(a => a !== undefined); + const next = Math.min.apply(null, times); + if (next > 0 && next < 86400000) { + const hours = Math.floor((next % 86400000) / 3600000).toString(); + const minutes = Math.floor(((next % 86400000) % 3600000) / 60000).toString(); + + g.reset(); // reset the graphics context to defaults (color/font/etc) + g.setFontAlign(0,0); // center fonts + g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); + + var text = hours.padStart(2, '0') + ":" + minutes.padStart(2, '0'); + g.setFont("6x8:1x2"); + g.drawString(text, this.x+this.width/2, this.y+12); + if (this.width === 0) { + this.width = 6*5+2; + Bangle.drawWidgets(); // width changed, re-layout + } + } + } + + setInterval(function() { + WIDGETS["widalarmeta"].draw(WIDGETS["widalarmeta"]); + }, 30000); // update every half minute + + // add your widget + WIDGETS["widalarmeta"]={ + area:"tl", + width: 0, // hide by default = assume no timer + draw:draw + }; +})(); diff --git a/apps/widalarmeta/widget.png b/apps/widalarmeta/widget.png new file mode 100644 index 000000000..cfd942ea0 Binary files /dev/null and b/apps/widalarmeta/widget.png differ diff --git a/apps/widalt/ChangeLog b/apps/widalt/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/widalt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/widalt/README.md b/apps/widalt/README.md new file mode 100644 index 000000000..08ee4f83d --- /dev/null +++ b/apps/widalt/README.md @@ -0,0 +1,2 @@ +# Altimeter widget +Displays barometric altitude in the top right corner. diff --git a/apps/widalt/metadata.json b/apps/widalt/metadata.json new file mode 100644 index 000000000..309c5bf2c --- /dev/null +++ b/apps/widalt/metadata.json @@ -0,0 +1,18 @@ + +{ + "id": "widalt", + "name": "Altimeter widget", + "version": "0.01", + "description": "Displays barometric altitude", + "readme": "README.md", + "icon": "widalt.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS2"], + "allow_emulator":false, + "storage": [ + {"name":"widalt.wid.js","url":"widalt.wid.js"}, + {"name":"widalt.settings.js","url":"widalt.settings.js"} + ], + "data": [{"name":"widalt.json"}] + } diff --git a/apps/widalt/widalt.png b/apps/widalt/widalt.png new file mode 100644 index 000000000..43e5465bc Binary files /dev/null and b/apps/widalt/widalt.png differ diff --git a/apps/widalt/widalt.settings.js b/apps/widalt/widalt.settings.js new file mode 100644 index 000000000..57993474e --- /dev/null +++ b/apps/widalt/widalt.settings.js @@ -0,0 +1,27 @@ +(function(back) { + var settings = Object.assign({ + interval: 5000, + }, require('Storage').readJSON("widalt.json", true) || {}); + o=Bangle.getOptions(); + Bangle.getPressure().then((p)=>{ + E.showMenu({ + "" : { "title" : "Altimeter Widget" }, + "< Back" : () => back(), + 'QNH': { + value: Math.floor(o.seaLevelPressure), + min: 100, max: 10000, + format: v=>(v+"hPa\nAlt: "+(44330 * (1.0 - Math.pow(p.pressure/v, 0.1903))).toFixed(0)+"m"), + onchange: v => {Bangle.setOptions({seaLevelPressure:v});} + }, + 'update Interval': { + value: settings.interval/1000, + min: 1, max: 60, + format: v=>(v+"s"), + onchange: v => { + settings.interval=v*1000; + require('Storage').writeJSON("widalt.json", settings); + } + } + }); + }); +}) diff --git a/apps/widalt/widalt.wid.js b/apps/widalt/widalt.wid.js new file mode 100644 index 000000000..dbd1a763e --- /dev/null +++ b/apps/widalt/widalt.wid.js @@ -0,0 +1,38 @@ +(()=>{ + var alt=""; + var lastAlt=0; + var settings = Object.assign({ + interval: 5000, + }, require('Storage').readJSON("widalt.json", true) || {}); + Bangle.setBarometerPower(true,"widalt"); + Bangle.on("pressure", (p)=>{ + if (Math.floor(p.altitude)!=lastAlt) { + lastAlt=Math.floor(p.altitude); + alt=p.altitude.toFixed(0); + var w = WIDGETS["widalt"].width; + WIDGETS["widalt"].width = 1 + (alt.length)*12+16; + if (w!=WIDGETS["widalt"].width) Bangle.drawWidgets(); + else WIDGETS["widalt"].draw(); + } + Bangle.setBarometerPower(false,"widalt") + setTimeout(()=>{Bangle.setBarometerPower(true,"widalt");},settings.interval); + }); + + function draw() { + if (!Bangle.isLCDOn()) return; + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(this.x, this.y, this.x + this.width, this.y + 23); + g.setColor(g.theme.fg); + g.drawImage(atob("EBCBAAAAAAAIAAwgFXAX0BCYIIggTD/EYPZADkACf/4AAAAA"), this.x, this.y+4); +g.setFontCustom(atob("AAAAABwAAOAAAgAAHAADwAD4AB8AB8AA+AAeAADAAAAOAAP+AH/8B4DwMAGBgAwMAGBgAwOAOA//gD/4AD4AAAAAAAABgAAcAwDAGAwAwP/+B//wAAGAAAwAAGAAAAAAAAIAwHgOA4DwMA+BgOwMDmBg4wOeGA/gwDwGAAAAAAAAAGAHA8A4DwMAGBhAwMMGBjgwOcOA+/gDj4AAAAABgAAcAAHgADsAA5gAOMAHBgBwMAP/+B//wABgAAMAAAAAAAgD4OB/AwOYGBjAwMYGBjBwMe8Bh/AIHwAAAAAAAAAfAAP8AHxwB8GAdgwPMGBxgwMOOAB/gAH4AAAAAAABgAAMAABgAwMAeBgPgMHwBj4AN8AB+AAPAABAAAAAAAMfAH38B/xwMcGBhgwMMGBjgwP+OA+/gDj4AAAAAAAAOAAH4AA/gQMMGBgzwME8BhvAOPgA/4AD8AAEAAAAAAGAwA4OAHBwAAA="), 46, atob("BAgMDAwMDAwMDAwMBQ=="), 21+(1<<8)+(1<<16)); + g.setFontAlign(-1, 0); + g.drawString(alt, this.x+16, this.y + 12); + } + WIDGETS["widalt"] = { + area: "tr", + width: 6, + draw: draw + }; + +})(); diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index c12cc0d65..2dfe8336d 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -1,2 +1,9 @@ 0.01: Initial version -0.02: Do not warn multiple times for the same exceedance +0.02: Do not warn multiple times for the same exceed +0.03: Fix crash +0.04: Use Prompt with dismiss and pause + Improve barometer value median calculation +0.05: Fix warning calculation + Show difference of last measurement to pressure average of the the last three hours in the widget + Only use valid pressure values +0.06: Fix exception diff --git a/apps/widbaroalarm/README.md b/apps/widbaroalarm/README.md index fdc239170..478b48a71 100644 --- a/apps/widbaroalarm/README.md +++ b/apps/widbaroalarm/README.md @@ -15,10 +15,13 @@ Get a notification when the pressure reaches defined thresholds. 0 to disable this alarm. * Show widget: Enable/disable widget visibility * Buzz on alarm: Enable/disable buzzer on alarm - +* 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 (useful for meetings and such). From 30 to 240 min ## Widget -The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours. +The widget shows two rows: +1. pressure value of last measurement +2. difference of last measurement to pressure average of the the last three hours ## Creator Marco ([myxor](https://github.com/myxor)) diff --git a/apps/widbaroalarm/default.json b/apps/widbaroalarm/default.json index 3d81baa81..696c70819 100644 --- a/apps/widbaroalarm/default.json +++ b/apps/widbaroalarm/default.json @@ -7,5 +7,7 @@ "drop3halarm": 2, "raise3halarm": 0, "show": true, - "interval": 15 + "interval": 15, + "dismissDelayMin": 15, + "pauseDelayMin": 60 } diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index 9c58a41ab..ba6c47b37 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,14 +2,14 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.02", + "version": "0.06", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", "tags": "tool,barometer", "supports": ["BANGLEJS2"], - "dependencies": {"notify":"type"}, "readme": "README.md", + "screenshots": [{"url":"screenshot.png"}], "storage": [ {"name":"widbaroalarm.wid.js","url":"widget.js"}, {"name":"widbaroalarm.settings.js","url":"settings.js"}, diff --git a/apps/widbaroalarm/screenshot.png b/apps/widbaroalarm/screenshot.png new file mode 100644 index 000000000..fcb6ad67e Binary files /dev/null and b/apps/widbaroalarm/screenshot.png differ diff --git a/apps/widbaroalarm/settings.js b/apps/widbaroalarm/settings.js index bea6319d1..ee8ce82c2 100644 --- a/apps/widbaroalarm/settings.js +++ b/apps/widbaroalarm/settings.js @@ -87,6 +87,26 @@ }, onchange: x => save('buzz', x) }, + 'Dismiss delay': { + value: settings.dismissDelayMin, + min: 5, max: 60, + onchange: v => { + save('dismissDelayMin', v) + }, + format: x => { + return x + " min"; + } + }, + 'Pause delay': { + value: settings.pauseDelayMin, + min: 30, max: 240, + onchange: v => { + save('pauseDelayMin', v) + }, + format: x => { + return x + " min"; + } + }, }; E.showMenu(menu); } diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index 5d62156eb..d877c4384 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -1,239 +1,304 @@ (function() { - let medianPressure; - let threeHourAvrPressure; - let currentPressures = []; +let medianPressure; +let threeHourAvrPressure; +let currentPressures = []; +let stop = false; // semaphore - const LOG_FILE = "widbaroalarm.log.json"; - const SETTINGS_FILE = "widbaroalarm.json"; - const storage = require('Storage'); +const LOG_FILE = "widbaroalarm.log.json"; +const SETTINGS_FILE = "widbaroalarm.json"; +const storage = require('Storage'); - let settings; +let settings; - function loadSettings() { - settings = Object.assign( - storage.readJSON("widbaroalarm.default.json", true) || {}, - storage.readJSON(SETTINGS_FILE, true) || {} - ); - } +function loadSettings() { + settings = + Object.assign(storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {}); +} - loadSettings(); +loadSettings(); +function setting(key) { return settings[key]; } - function setting(key) { - return settings[key]; - } +function saveSetting(key, value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); +} - function saveSetting(key, value) { - settings[key] = value; - storage.write(SETTINGS_FILE, settings); - } +const interval = setting("interval"); - const interval = setting("interval"); +let history3 = + storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours - let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours +function showAlarm(body, key, type) { + if (body == undefined) + return; + stop = true; - function showAlarm(body, title) { - if (body == undefined) return; + E.showPrompt(body, { + title : "Pressure " + (type != undefined ? type : "alarm"), + buttons : {"Ok" : 1, "Dismiss" : 2, "Pause" : 3} + }).then(function(v) { + const tsNow = Math.round(Date.now() / 1000); // seconds - require("notify").show({ - title: title || "Pressure", - body: body, - icon: require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA")) - }); - - if (setting("buzz") && - !(storage.readJSON('setting.json', 1) || {}).quiet) { - Bangle.buzz(); + if (v == 1) { + saveSetting(key, tsNow); } - } - - - function didWeAlreadyWarn(key) { - return setting(key) == undefined || setting(key) > 0; - } - - function checkForAlarms(pressure) { - if (pressure == undefined || pressure <= 0) return; - - let alreadyWarned = false; - - const ts = Math.round(Date.now() / 1000); // seconds - const d = { - "ts": ts, - "p": pressure - }; - - // delete entries older than 3h - for (let i = 0; i < history3.length; i++) { - if (history3[i]["ts"] < ts - (3 * 60 * 60)) { - history3.shift(); - } + if (v == 2) { + // save timestamp of the future so that we do not warn again for the same + // event until then + saveSetting(key, tsNow + 60 * setting('dismissDelayMin')); } - // delete oldest entries until we have max 50 - while (history3.length > 50) { + if (v == 3) { + // save timestamp of the future so that we do not warn again for the same + // event until then + saveSetting(key, tsNow + 60 * setting('pauseDelayMin')); + } + stop = false; + load(); + }); + + if (setting("buzz") && !(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(); + } + + setTimeout(function() { + stop = false; + load(); + }, 20000); +} + +/* + * returns true if an alarm should be triggered + */ +function doWeNeedToAlarm(key) { + const tsNow = Math.round(Date.now() / 1000); // seconds + return setting(key) == undefined || setting(key) == 0 || tsNow > setting(key); +} + +function isValidPressureValue(pressure) { + if (pressure == undefined || pressure <= 0) + return false; + return pressure > 800 && pressure < 1200; // very rough values +} + +function handlePressureValue(pressure) { + if (pressure == undefined || pressure <= 0) + return; + + const ts = Math.round(Date.now() / 1000); // seconds + const d = {"ts" : ts, "p" : pressure}; + + history3.push(d); + + // delete oldest entries until we have max 50 + while (history3.length > 50) { + history3.shift(); + } + + // delete entries older than 3h + for (let i = 0; i < history3.length; i++) { + if (history3[i]["ts"] < ts - (3 * 60 * 60)) { history3.shift(); + } else { + break; } + } - if (setting("lowalarm")) { - // Is below the alarm threshold? - if (pressure <= setting("min")) { - if (!didWeAlreadyWarn("lastLowWarningTs")) { - showAlarm("Pressure low: " + Math.round(pressure) + " hPa"); - saveSetting("lastLowWarningTs", ts); - alreadyWarned = true; - } - } else { - saveSetting("lastLowWarningTs", 0); + // write data to storage + storage.writeJSON(LOG_FILE, history3); + + calculcate3hAveragePressure(); + + if (setting("lowalarm") || setting("highalarm") || setting("drop3halarm") || + setting("raise3halarm")) { + checkForAlarms(pressure, ts); + } +} + +function checkForAlarms(pressure, ts) { + let alreadyWarned = false; + + if (setting("lowalarm")) { + // Is below the alarm threshold? + if (pressure <= setting("min")) { + if (!doWeNeedToAlarm("lowWarnTs")) { + showAlarm("Pressure low: " + Math.round(pressure) + " hPa", "lowWarnTs", + "low"); + alreadyWarned = true; } } else { - saveSetting("lastLowWarningTs", 0); + saveSetting("lowWarnTs", 0); } + } - if (setting("highalarm")) { - // Is above the alarm threshold? - if (pressure >= setting("max")) { - if (!didWeAlreadyWarn("lastHighWarningTs")) { - showAlarm("Pressure high: " + Math.round(pressure) + " hPa"); - saveSetting("lastHighWarningTs", ts); - alreadyWarned = true; - } - } else { - saveSetting("lastHighWarningTs", 0); + if (setting("highalarm")) { + // Is above the alarm threshold? + if (pressure >= setting("max")) { + if (doWeNeedToAlarm("highWarnTs")) { + showAlarm("Pressure high: " + Math.round(pressure) + " hPa", + "highWarnTs", "high"); + alreadyWarned = true; } } else { - saveSetting("lastHighWarningTs", 0); + saveSetting("highWarnTs", 0); } + } - if (!alreadyWarned) { - // 3h change detection - const drop3halarm = setting("drop3halarm"); - const raise3halarm = setting("raise3halarm"); - if (drop3halarm > 0 || raise3halarm > 0) { - // we need at least 30min of data for reliable detection - if (history3[0]["ts"] > ts - (30 * 60)) { - return; - } - + if (history3.length > 0 && !alreadyWarned) { + // 3h change detection + const drop3halarm = setting("drop3halarm"); + const raise3halarm = setting("raise3halarm"); + if (drop3halarm > 0 || raise3halarm > 0) { + // we need at least 30 minutes of data for reliable detection + const diffDateAge = Math.abs(history3[0]["ts"] - ts); + if (diffDateAge > 30 * 60) { // Get oldest entry: const oldestPressure = history3[0]["p"]; if (oldestPressure != undefined && oldestPressure > 0) { - const diff = oldestPressure - pressure; + const diffPressure = Math.abs(oldestPressure - pressure); // drop alarm if (drop3halarm > 0 && oldestPressure > pressure) { - if (Math.abs(diff) > drop3halarm) { - if (!didWeAlreadyWarn("lastDropWarningTs")) { - showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + - Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure drop"); - saveSetting("lastDropWarningTs", ts); + if (diffPressure >= drop3halarm) { + if (doWeNeedToAlarm("dropWarnTs")) { + showAlarm((Math.round(diffPressure * 10) / 10) + + " hPa/3h from " + Math.round(oldestPressure) + + " to " + Math.round(pressure) + " hPa", + "dropWarnTs", "drop"); } } else { - saveSetting("lastDropWarningTs", 0); + if (ts > setting("dropWarnTs")) + saveSetting("dropWarnTs", 0); } } else { - saveSetting("lastDropWarningTs", 0); + if (ts > setting("dropWarnTs")) + saveSetting("dropWarnTs", 0); } // raise alarm if (raise3halarm > 0 && oldestPressure < pressure) { - if (Math.abs(diff) > raise3halarm) { - if (!didWeAlreadyWarn("lastRaiseWarningTs")) { - showAlarm((Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h from " + - Math.round(oldestPressure) + " to " + Math.round(pressure) + " hPa", "Pressure raise"); - saveSetting("lastRaiseWarningTs", ts); + if (diffPressure >= raise3halarm) { + if (doWeNeedToAlarm("raiseWarnTs")) { + showAlarm((Math.round(diffPressure * 10) / 10) + + " hPa/3h from " + Math.round(oldestPressure) + + " to " + Math.round(pressure) + " hPa", + "raiseWarnTs", "raise"); } } else { - saveSetting("lastRaiseWarningTs", 0); + if (ts > setting("raiseWarnTs")) + saveSetting("raiseWarnTs", 0); } } else { - saveSetting("lastRaiseWarningTs", 0); + if (ts > setting("raiseWarnTs")) + saveSetting("raiseWarnTs", 0); } } } } + } +} - history3.push(d); - // write data to storage - storage.writeJSON(LOG_FILE, history3); - - // calculate 3h average for widget +function calculcate3hAveragePressure() { + if (history3 != undefined && history3.length > 0) { let sum = 0; for (let i = 0; i < history3.length; i++) { sum += history3[i]["p"]; } threeHourAvrPressure = sum / history3.length; + } else { + threeHourAvrPressure = undefined; } +} +/* + turn on barometer power + take multiple measurements + sort the results + take the middle one (median) + turn off barometer power +*/ +function getPressureValue() { + if (stop) + return; + const MEDIANLENGTH = 20; + Bangle.setBarometerPower(true, "widbaroalarm"); + Bangle.on('pressure', function(e) { + while (currentPressures.length > MEDIANLENGTH) + currentPressures.pop(); + const pressure = e.pressure; + if (isValidPressureValue(pressure)) { + currentPressures.unshift(pressure); + median = currentPressures.slice().sort(); - function baroHandler(data) { - if (data) { - const pressure = Math.round(data.pressure); - if (pressure == undefined || pressure <= 0) return; - currentPressures.push(pressure); + if (median.length > 10) { + var mid = median.length >> 1; + medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); + if (medianPressure > 0) { + turnOff(); + draw(); + handlePressureValue(medianPressure); + } + } } + }); + + setTimeout(function() { turnOff(); }, 30000); +} + +function turnOff() { + if (Bangle.isBarometerOn()) + Bangle.setBarometerPower(false, "widbaroalarm"); +} + +function draw() { + if (global.WIDGETS != undefined && typeof global.WIDGETS === "object") { + global.WIDGETS["baroalarm"] = { + width : setting("show") ? 24 : 0, + area : "tr", + draw : draw + }; } + g.reset(); - /* - turn on barometer power - take 5 measurements - sort the results - take the middle one (median) - turn off barometer power - */ - function check() { - Bangle.setBarometerPower(true, "widbaroalarm"); - setTimeout(function() { - currentPressures = []; + if (this.x == undefined || this.y != 0) + return; // widget not yet there - Bangle.getPressure().then(baroHandler); - Bangle.getPressure().then(baroHandler); - Bangle.getPressure().then(baroHandler); - Bangle.getPressure().then(baroHandler); - Bangle.getPressure().then(baroHandler); + g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23); - setTimeout(function() { - Bangle.setBarometerPower(false, "widbaroalarm"); + if (setting("show")) { + g.setFont("6x8", 1).setFontAlign(1, 0); + if (medianPressure == undefined) { + // trigger a new check + getPressureValue(); - currentPressures.sort(); - - // take median value - medianPressure = currentPressures[3]; - checkForAlarms(medianPressure); - }, 1000); - }, 500); - } - - function reload() { - check(); - } - - function draw() { - if (global.WIDGETS != undefined && typeof WIDGETS === "object") { - WIDGETS["baroalarm"] = { - width: setting("show") ? 24 : 0, - reload: reload, - area: "tr", - draw: draw - }; - } - - g.reset(); - if (setting("show") && medianPressure != undefined) { - g.setFont("6x8", 1).setFontAlign(1, 0); + // lets load last value from log (if available) + if (history3.length > 0) { + medianPressure = history3[history3.length - 1]["p"]; + g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6); + } else { + g.drawString("...", this.x + 24, this.y + 6); + } + } else { g.drawString(Math.round(medianPressure), this.x + 24, this.y + 6); - if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) { - g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10); + } + + if (threeHourAvrPressure == undefined) { + calculcate3hAveragePressure(); + } + if (threeHourAvrPressure != undefined) { + if (medianPressure != undefined) { + const diff = Math.round(medianPressure - threeHourAvrPressure); + g.drawString((diff > 0 ? "+" : "") + diff, this.x + 24, + this.y + 6 + 10); } } } +} - // Let's delay the first check a bit - setTimeout(function() { - check(); - if (interval > 0) { - setInterval(check, interval * 60000); - } - }, 1000); - +if (interval > 0) { + setInterval(getPressureValue, interval * 60000); +} +getPressureValue(); })(); diff --git a/apps/widbatwarn/ChangeLog b/apps/widbatwarn/ChangeLog index 5420b9706..c0e2387db 100644 --- a/apps/widbatwarn/ChangeLog +++ b/apps/widbatwarn/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Battery Warning! 0.02: Respect Quiet Mode +0.03: Use default Bangle formatter for booleans diff --git a/apps/widbatwarn/metadata.json b/apps/widbatwarn/metadata.json index 959eeca08..26143ad4a 100644 --- a/apps/widbatwarn/metadata.json +++ b/apps/widbatwarn/metadata.json @@ -2,7 +2,7 @@ "id": "widbatwarn", "name": "Battery Warning", "shortName": "Battery Warning", - "version": "0.02", + "version": "0.03", "description": "Show a warning when the battery runs low.", "icon": "widget.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/widbatwarn/settings.js b/apps/widbatwarn/settings.js index 8d15c8458..c3464a82b 100644 --- a/apps/widbatwarn/settings.js +++ b/apps/widbatwarn/settings.js @@ -2,9 +2,8 @@ * @param {function} back Use back() to return to settings menu */ (function(back) { - const SETTINGS_FILE = "widbatwarn.json", - storage = require("Storage"), - translate = require("locale").translate; + const SETTINGS_FILE = "widbatwarn.json"; + const storage = require("Storage"); // initialize with default settings... let s = { @@ -39,7 +38,6 @@ }, "Buzz": { value: s.buzz, - format: b => translate(b?"Yes":"No"), onchange: save("buzz"), }, }; diff --git a/apps/widbt_notify/ChangeLog b/apps/widbt_notify/ChangeLog index b5a50210e..8f1dab908 100644 --- a/apps/widbt_notify/ChangeLog +++ b/apps/widbt_notify/ChangeLog @@ -8,3 +8,7 @@ 0.09: Vibrate on connection loss 0.10: Bug fix 0.11: Avoid too many notifications. Change disconnected colour to red. +0.12: Prevent repeated execution of `draw()` from the current app. +0.13: Added "connection restored" notification. Fixed restoring of the watchface. +0.14: Added configuration option +0.15: Added option to hide widget when connected \ No newline at end of file diff --git a/apps/widbt_notify/metadata.json b/apps/widbt_notify/metadata.json index 0b795c2c8..6def70c64 100644 --- a/apps/widbt_notify/metadata.json +++ b/apps/widbt_notify/metadata.json @@ -1,13 +1,17 @@ { "id": "widbt_notify", "name": "Bluetooth Widget with Notification", - "version": "0.11", - "description": "Show the current Bluetooth connection status in the top right of the clock and vibrate when disconnected.", + "version": "0.15", + "description": "Show the current Bluetooth connection status in the top right of the watch. Optional buzz and/or and hide if disconnected", "icon": "widget.png", "type": "widget", "tags": "widget,bluetooth", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ - {"name":"widbt_notify.wid.js","url":"widget.js"} - ] + {"name":"widbt_notify.wid.js","url":"widget.js"}, + {"name":"widbt_notify.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widbt_notify.json"} + ] } diff --git a/apps/widbt_notify/settings.js b/apps/widbt_notify/settings.js new file mode 100644 index 000000000..1e0d5036b --- /dev/null +++ b/apps/widbt_notify/settings.js @@ -0,0 +1,69 @@ +(function(back) { + var FILE = "widbt_notify.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": "Bluetooth Widget WN" + }, + "< Back": () => back(), + "Show Widget": { + value: (settings.showWidget !== undefined ? settings.showWidget : true), + onchange: v => { + settings.showWidget = v; + writeSettings(); + } + }, + "Buzz on Connect": { + value: (settings.buzzOnConnect !== undefined ? settings.buzzOnConnect : true), + onchange: v => { + settings.buzzOnConnect = v; + writeSettings(); + } + }, + "Buzz on loss": { + value: (settings.buzzOnLoss !== undefined ? settings.buzzOnLoss : true), + onchange: v => { + settings.buzzOnLoss = v; + writeSettings(); + } + }, + "Hide connected": { + value: (settings.hideConnected !== undefined ? settings.hideConnected : false), + onchange: v => { + settings.hideConnected = v; + writeSettings(); + } + } + }; + + E.showMenu(mainmenu); + +}); diff --git a/apps/widbt_notify/widget.js b/apps/widbt_notify/widget.js index 47765f3d0..de2baa3cf 100644 --- a/apps/widbt_notify/widget.js +++ b/apps/widbt_notify/widget.js @@ -2,42 +2,106 @@ WIDGETS.bluetooth_notify = { area: "tr", width: 15, warningEnabled: 1, + + // ------------ Settings -------- very lame - need to improve + readshowWidget: function() { + var showWidget; + const SETTINGSFILE = "widbt_notify.json"; + function def (value, def) {return value !== undefined ? value : def;} + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + showWidget = def(settings.showWidget, true); + return showWidget; + }, + + readBuzzOnConnect: function() { + var buzzOnConnect; + const SETTINGSFILE = "widbt_notify.json"; + function def (value, def) {return value !== undefined ? value : def;} + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + buzzOnConnect = def(settings.buzzOnConnect, true); + return buzzOnConnect; + }, + + readBuzzOnLoss: function() { + var buzzOnLoss; + const SETTINGSFILE = "widbt_notify.json"; + function def (value, def) {return value !== undefined ? value : def;} + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + buzzOnLoss = def(settings.buzzOnLoss, true); + return buzzOnLoss; + }, + + readHideConnected: function() { + var hideConnected; + const SETTINGSFILE = "widbt_notify.json"; + function def (value, def) {return value !== undefined ? value : def;} + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + hideConnected = def(settings.hideConnected, true); + return hideConnected; + }, + + + // ------------ Settings -------- + draw: function() { - g.reset(); - if (NRF.getSecurityStatus().connected) { - g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); - } else { - // g.setColor(g.theme.dark ? "#666" : "#999"); - g.setColor("#f00"); // red is easier to distinguish from blue + if (WIDGETS.bluetooth_notify.readshowWidget()){ + g.reset(); + if (NRF.getSecurityStatus().connected) { + if (!WIDGETS.bluetooth_notify.readHideConnected()) { + g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); + } + } else { + // g.setColor(g.theme.dark ? "#666" : "#999"); + g.setColor("#f00"); // red is easier to distinguish from blue + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); + } } - g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y); }, redrawCurrentApp: function(){ if(typeof(draw)=='function'){ + g.clear(); draw(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); }else{ load(); // fallback. This might reset some variables } }, connect: function() { - WIDGETS.bluetooth_notify.draw(); + + if(WIDGETS.bluetooth_notify.warningEnabled == 1){ + E.showMessage(/*LANG*/'Connection\nrestored.', 'Bluetooth'); + setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. + + WIDGETS.bluetooth_notify.warningEnabled = 0; + setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds. + + var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; + if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnConnect()){ + Bangle.buzz(700, 1); // buzz on connection resume + } + } + WIDGETS.bluetooth_notify.draw(); + }, disconnect: function() { - if(WIDGETS.bluetooth_notify.warningEnabled == 1){ - E.showMessage(/*LANG*/'Connection\nlost.', 'Bluetooth'); - setInterval(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. - - WIDGETS.bluetooth_notify.warningEnabled = 0; - setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds. - - var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; - if(!quiet){ - Bangle.buzz(700, 1); // buzz on connection loss + if(WIDGETS.bluetooth_notify.warningEnabled == 1){ + E.showMessage(/*LANG*/ 'Connection\nlost.', 'Bluetooth'); + setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'. + + WIDGETS.bluetooth_notify.warningEnabled = 0; + setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds. + + var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet; + if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnLoss()){ + Bangle.buzz(700, 1); // buzz on connection loss + } } - } + WIDGETS.bluetooth_notify.draw(); } }; diff --git a/apps/widbthide/metadata.json b/apps/widbthide/metadata.json new file mode 100644 index 000000000..59b13adb4 --- /dev/null +++ b/apps/widbthide/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widbthide", + "name": "Bluetooth Widget (hides when no connection)", + "version": "0.01", + "description": "Shows Bluetooth icon (when connected) in the top right of the clock", + "icon": "widget.png", + "type": "widget", + "tags": "widget,bluetooth", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbthide.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widbthide/widget.js b/apps/widbthide/widget.js new file mode 100644 index 000000000..620811b36 --- /dev/null +++ b/apps/widbthide/widget.js @@ -0,0 +1,13 @@ +WIDGETS["bluetooth"]={area:"tr",draw:function() { + if (WIDGETS.bluetooth.width==0) + return; + g.reset(); + g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); +},changed:function() { + WIDGETS.bluetooth.width = NRF.getSecurityStatus().connected?15:0; + Bangle.drawWidgets(); +},width:NRF.getSecurityStatus().connected?15:0 +}; +NRF.on('connect',WIDGETS.bluetooth.changed); +NRF.on('disconnect',WIDGETS.bluetooth.changed); diff --git a/apps/widbthide/widget.png b/apps/widbthide/widget.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/widbthide/widget.png differ diff --git a/apps/widcw/ChangeLog b/apps/widcw/ChangeLog index a4bc24d1a..07b8f7424 100644 --- a/apps/widcw/ChangeLog +++ b/apps/widcw/ChangeLog @@ -1 +1,2 @@ -0.01: First version \ No newline at end of file +0.01: First version +0.02: Fix memory leak \ No newline at end of file diff --git a/apps/widcw/metadata.json b/apps/widcw/metadata.json index 653b093ec..467ab1729 100644 --- a/apps/widcw/metadata.json +++ b/apps/widcw/metadata.json @@ -1,7 +1,7 @@ { "id": "widcw", "name": "Calendar Week Widget", - "version": "0.01", + "version": "0.02", "description": "Widget which shows the current calendar week", "icon": "widget.png", "type": "widget", diff --git a/apps/widcw/widget.js b/apps/widcw/widget.js index ef43a4551..e33ad0aad 100644 --- a/apps/widcw/widget.js +++ b/apps/widcw/widget.js @@ -34,8 +34,8 @@ } // redraw when date changes - setTimeout(()=>WIDGETS["widcw"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000); - + if (WIDGETS["widcw"].to) clearTimeout(WIDGETS["widcw"].to); + WIDGETS["widcw"].to = setTimeout(()=>WIDGETS["widcw"].draw(), (86401 - Math.floor(date/1000) % 86400)*1000); } // add your widget diff --git a/apps/widday/ChangeLog b/apps/widday/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/widday/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/widday/README.md b/apps/widday/README.md new file mode 100644 index 000000000..61f48aa4c --- /dev/null +++ b/apps/widday/README.md @@ -0,0 +1,9 @@ +# Day Widget + +Just shows the day of the current date, to save space in the widget area. The month and year should be known because they don't change that often. Just the number in maximum size for readability. + +![](screenshot.png) + +## Creator +[@pidajo](https://github.com/pidajo) + diff --git a/apps/widday/app-icon.js b/apps/widday/app-icon.js new file mode 100644 index 000000000..aa17aedc5 --- /dev/null +++ b/apps/widday/app-icon.js @@ -0,0 +1 @@ +atob("MDCEAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIBEREREREREREREREREREREREREREQIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////zMz////8zMzMzMzP////xIiIf///////czM////PMzMzMzMzP///xIiIf/////zzMzM////PMzMzMzMzP///xIiIf////PczMzM////PMzMzMzMzP///xIiIf////3MzMzM////8zMzMz3MzP///xIiIf////3MM8zM//////////zMw////xIiIf////0/88zM/////////zzMz////xIiIf//////88zM/////////8zM3////xIiIf//////88zM////////88zM/////xIiIf//////88zM/////////MzN/////xIiIf//////88zM////////PMzD/////xIiIf//////88zM////////3Mzf/////xIiIf//////88zM////////zMw//////xIiIf//////88zM///////9zMz//////xIiIf//////88zM///////8zMP//////xIiIf//////88zM//////88zM///////xIiIf//////88zM///////MzN///////xIiIf////8zM8zM//////PMzD///////xIiIf////3MzMzMzM3///3Mzf///////xIiIf////3MzMzMzM3//zzMw////////xIiIf////PMzMzMzM3//9zMz////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIf///////////////////////////xIiIBEREREREREREREREREREREREREREQIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==") diff --git a/apps/widday/metadata.json b/apps/widday/metadata.json new file mode 100644 index 000000000..82de23035 --- /dev/null +++ b/apps/widday/metadata.json @@ -0,0 +1,15 @@ +{ "id": "widday", + "name": "Day Widget", + "shortName":"My Timer", + "icon": "widget.png", + "type": "widget", + "version":"0.01", + "description": "Just the day of the current date as widget", + "readme": "README.md", + "tags": "widget,say,date", + "supports": ["BANGLEJS", "BANGLEJS2"], + "screenshots" : [ { "url":"screenshot.png" } ], + "storage": [ + {"name":"widday.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widday/screenshot.png b/apps/widday/screenshot.png new file mode 100644 index 000000000..9a7a9f8cf Binary files /dev/null and b/apps/widday/screenshot.png differ diff --git a/apps/widday/widget.js b/apps/widday/widget.js new file mode 100644 index 000000000..cdea76a29 --- /dev/null +++ b/apps/widday/widget.js @@ -0,0 +1,27 @@ +(() => { + var width = 32; // width of the widget + + function draw() { + var date = new Date(); + g.reset(); // reset the graphics context to defaults (color/font/etc) + g.setFontAlign(0,1); // center fonts + //g.drawRect(this.x, this.y, this.x+width-1, this.y+23); // check the bounds! + + var text = date.getDate(); + g.setFont("Vector", 24); + g.drawString(text, this.x+width/2+1, this.y + 28); + //g.setColor(0, 0, 1); + //g.drawRect(this.x, this.y, this.x+width-2, this.y+1); + } + + setInterval(function() { + WIDGETS["widday"].draw(WIDGETS["widdateday"]); + }, 10*60000); // update every 10 minutes + + // add your widget + WIDGETS["widday"]={ + area:"bl", // tl (top left), tr (top right), bl (bottom left), br (bottom right) + width: width, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout + draw:draw // called to draw the widget + }; +})() diff --git a/apps/widday/widget.png b/apps/widday/widget.png new file mode 100644 index 000000000..462097871 Binary files /dev/null and b/apps/widday/widget.png differ diff --git a/apps/widdst/ChangeLog b/apps/widdst/ChangeLog new file mode 100644 index 000000000..e350137ee --- /dev/null +++ b/apps/widdst/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Checks for correct firmware; E.setDST(...) moved to boot.js diff --git a/apps/widdst/README.md b/apps/widdst/README.md new file mode 100644 index 000000000..ca41c2bed --- /dev/null +++ b/apps/widdst/README.md @@ -0,0 +1,34 @@ +# Daylight savings time widget + +This widget will set the daylight saving rules on your watch. Note you may need a firmware update before this can work. + +## Settings + +You need to set your timezone, and your daylight savings rules, in `Settings -> Apps -> Daylight Savings`. The settings are + +**Enabled** enables or disables the widget + +**Icon** enables or disables the showing of a small "DST" icon when daylight savings is in effect + +**Base TZ** is your "base" timezone - i.e. the timezone you keep when daylight savings is not in effect. It is positive east. + +**Change** is the size of the daylight savings change, which is +1 hour almost everywhere. + +**DST Start, DST End** set the rules for the start and end of daylight savings. + +## Setting the daylight savings rules + +The page for setting **DST Start** and **DST End** ask you to describe the rules for daylight savings changes. For instance, in the UK, the rules are + +**DST Start** `The [last] [Sun] of [Mar] minus [0 days] at [01:00]` + +**DST End** `The [last] [Sun] of [Oct] minus [0 days] at [02:00]` + +In most of the US, the rules are + +**DST Start** `The [2nd] [Sun] of [Mar] minus [0 days] at [02:00]` + +**DST End** `The [1st] [Sun] of [Nov] minus [0 days] at [02:00]` + + + diff --git a/apps/widdst/boot.js b/apps/widdst/boot.js new file mode 100644 index 000000000..b0a844532 --- /dev/null +++ b/apps/widdst/boot.js @@ -0,0 +1,15 @@ +(() => { + + if (E.setDST) { + var dstSettings = require('Storage').readJSON('widdst.json',1)||{}; + if (dstSettings.has_dst) { + E.setDST(60*dstSettings.dst_size, 60*dstSettings.tz, dstSettings.dst_start.dow_number, dstSettings.dst_start.dow, + dstSettings.dst_start.month, dstSettings.dst_start.day_offset, 60*dstSettings.dst_start.at, + dstSettings.dst_end.dow_number, dstSettings.dst_end.dow, dstSettings.dst_end.month, dstSettings.dst_end.day_offset, + 60*dstSettings.dst_end.at); + } else { + E.setDST(0,0,0,0,0,0,0,0,0,0,0,0); + } + } + +})() diff --git a/apps/widdst/icon.png b/apps/widdst/icon.png new file mode 100644 index 000000000..aa7142959 Binary files /dev/null and b/apps/widdst/icon.png differ diff --git a/apps/widdst/metadata.json b/apps/widdst/metadata.json new file mode 100644 index 000000000..f280c96eb --- /dev/null +++ b/apps/widdst/metadata.json @@ -0,0 +1,16 @@ +{ "id": "widdst", + "name": "Daylight Saving", + "version":"0.02", + "description": "Widget to set daylight saving rules. Requires Espruino 2v14.49 or later - see the instructions below for more information.", + "icon": "icon.png", + "type": "widget", + "tags": "widget,tool", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widdst.wid.js","url":"widget.js"}, + {"name":"widdst.boot.js","url":"boot.js"}, + {"name":"widdst.settings.js","url":"settings.js"} + ], + "data": [{"name":"widdst.json"}] +} diff --git a/apps/widdst/settings.js b/apps/widdst/settings.js new file mode 100644 index 000000000..9a7e579b7 --- /dev/null +++ b/apps/widdst/settings.js @@ -0,0 +1,184 @@ +(function(back) { + + var dows = require("date_utils").dows(0,1); + var months = require("date_utils").months(1); + + var settings = Object.assign({ + has_dst: false, + show_icon: true, + tz: 0, + dst_size: 1, + dst_start: { + dow_number: 4, // "1st", "2nd", "3rd", "4th", "last" + dow: 0, // "Sun", "Mon", ... + month: 2, + day_offset: 0, + at: 1 + }, + dst_end: { + dow_number: 4, + dow: 0, + month: 9, + day_offset: 0, + at: 2 + } + }, require("Storage").readJSON("widdst.json", true) || {}); + + var dst_start_end = { + is_start: true, + day_offset: 0, + dow_number: 0, + dow: 0, + month: 0, + at: 0 + }; + + function writeSettings() { + require('Storage').writeJSON("widdst.json", settings); + } + + function writeSubMenuSettings() { + if (dst_start_end.is_start) { + settings.dst_start.day_offset = dst_start_end.day_offset; + settings.dst_start.dow_number = dst_start_end.dow_number; + settings.dst_start.dow = dst_start_end.dow; + settings.dst_start.month = dst_start_end.month; + settings.dst_start.at = dst_start_end.at; + } else { + settings.dst_end.day_offset = dst_start_end.day_offset; + settings.dst_end.dow_number = dst_start_end.dow_number; + settings.dst_end.dow = dst_start_end.dow; + settings.dst_end.month = dst_start_end.month; + settings.dst_end.at = dst_start_end.at; + } + writeSettings(); + } + + function hoursToString(h) { + return (h|0) + ':' + (((6*h)%6)|0) + (((60*h)%10)|0); + } + + function getDSTStartEndMenu(start) { + dst_start_end.is_start = start; + if (start) { + dst_start_end.day_offset = settings.dst_start.day_offset; + dst_start_end.dow_number = settings.dst_start.dow_number; + dst_start_end.dow = settings.dst_start.dow; + dst_start_end.month = settings.dst_start.month; + dst_start_end.at = settings.dst_start.at; + } else { + dst_start_end.day_offset = settings.dst_end.day_offset; + dst_start_end.dow_number = settings.dst_end.dow_number; + dst_start_end.dow = settings.dst_end.dow; + dst_start_end.month = settings.dst_end.month; + dst_start_end.at = settings.dst_end.at; + } + return { + "": { + "Title": start ? /*LANG*/"DST Start" : /*LANG*/"DST End" + }, + "< Back": () => E.showMenu(dstMenu), + /*LANG*/"The" : { + value: dst_start_end.dow_number, + format: v => [/*LANG*/"1st",/*LANG*/"2nd",/*LANG*/"3rd",/*LANG*/"4th",/*LANG*/"last"][v], + min: 0, + max: 4, + onchange: v => { + dst_start_end.dow_number = v; + writeSubMenuSettings(); + } + }, + " -" : { + value: dst_start_end.dow, + format: v => dows[v], + min:0, + max:6, + onchange: v => { + dst_start_end.dow = v; + writeSubMenuSettings(); + } + }, + /*LANG*/"of": { + value: dst_start_end.month, + format: v => months[v], + min: 0, + max: 11, + onchange: v => { + dst_start_end.month = v; + writeSubMenuSettings(); + } + }, + /*LANG*/"minus" : { + value: dst_start_end.day_offset, + format: v => v + ((v == 1) ? /*LANG*/" day" : /*LANG*/" days"), + min: 0, + max: 7, + onchange: v => { + dst_start_end.day_offset = v; + writeSubMenuSettings(); + } + }, + /*LANG*/"at": { + value: dst_start_end.at, + format: v => hoursToString(v), + min: 0, + max: 23, + // step: 0.05, // every 3 minutes - FOR DEVELOPMENT PURPOSES + onchange: v => { + dst_start_end.at = v; + writeSubMenuSettings(); + } + } + } + } + + var dstMenu = { + "": { + "Title": /*LANG*/"Daylight Saving" + }, + "< Back": () => back(), + /*LANG*/"Enabled": { + value: settings.has_dst, + format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + onchange: v => { + settings.has_dst = v; + writeSettings(); + } + }, + /*LANG*/"Icon": { + value: settings.show_icon, + format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", + onchange: v => { + settings.show_icon = v; + writeSettings(); + } + }, + /*LANG*/"Base TZ": { + value: settings.tz, + format: v => (v >= 0 ? '+' + hoursToString(v) : '-' + hoursToString(-v)), + onchange: v => { + settings.tz = v; + writeSettings(); + }, + min: -13, + max: 13, + step: 0.25 + }, + /*LANG*/"Change": { + value: settings.dst_size, + format: v => (v >= 0 ? '+' + hoursToString(v): '-' + hoursToString(-v)), + min: -1, + max: 1, + step: 0.5, + onchange: v=> { + settings.dst_size = v; + writeSettings(); + } + }, + /*LANG*/"DST Start": () => E.showMenu(getDSTStartEndMenu(true)), + /*LANG*/"DST End": () => E.showMenu(getDSTStartEndMenu(false)) + }; + + E.showMenu(dstMenu); + +}); diff --git a/apps/widdst/widget.js b/apps/widdst/widget.js new file mode 100644 index 000000000..f690cc68f --- /dev/null +++ b/apps/widdst/widget.js @@ -0,0 +1,55 @@ +(() => { + + // our setTimeout() return value for the function that periodically check the status of DST + var check_timeout = undefined; + + // Called by draw() when we are not in DST or when we are not displaying the icon + function clear() { + if (this.width) { + this.width = 0; + Bangle.drawWidgets(); + } + } + + // draw, or erase, our little "dst" icon in the widgets area + function draw() { + var dstSettings = require('Storage').readJSON('widdst.json',1)||{}; + if ((dstSettings.has_dst) && (dstSettings.show_icon)) { + var now = new Date(); + if (now.getIsDST()) { + if (this.width) { + g.drawImage( + { + width : 24, height : 24, bpp : 1, palette: new Uint16Array([g.theme.bg, g.theme.fg]), + buffer : atob("AAAAAAAAPAAAIgAAIQAAIQAAIQAAIQAAIngAPIQAAIAAAPAAAAwAAAQAAIX8AHggAAAgAAAgAAAgAAAgAAAgAAAgAAAAAAAA") + }, this.x, this.y + ); + } else { + this.width = 24; + Bangle.drawWidgets(); + } + } else { + clear(); + } + if (check_timeout) clearTimeout(check_timeout); + check_timeout = setTimeout( function() { + check_timeout = undefined; + draw(); + }, 3600000 - (now.getTime() % 3600000)); // Check every hour. + } else { + clear(); + } + } + + // Register ourselves + if (E.setDST) { + WIDGETS["widdst"] = { + area: "tl", + width: 0, + draw: draw + }; + } else { + E.showAlert("Firmware update needed to support Daylight Saving Time"); + } + +})() diff --git a/apps/widgps/ChangeLog b/apps/widgps/ChangeLog index f68fc701c..b530843e7 100644 --- a/apps/widgps/ChangeLog +++ b/apps/widgps/ChangeLog @@ -3,3 +3,7 @@ 0.03: Fix positioning 0.04: Show GPS fix status 0.05: Don't poll for GPS status, override setGPSPower handler (fix #1456) +0.06: Periodically update so the always on display does show current GPS fix +0.07: Alternative marker icon (configurable via settings) +0.08: Add ability to hide the icon when GPS is off, for a cleaner appearance. +0.09: Do not take widget space if icon is hidden diff --git a/apps/widgps/README.md b/apps/widgps/README.md index 37e088bcf..98f0ba6f7 100644 --- a/apps/widgps/README.md +++ b/apps/widgps/README.md @@ -6,12 +6,22 @@ The GPS can quickly run the battery down if it is on all the time so it is useful to know if it has been switched on or not. - Uses Bangle.isGPSOn() -- Shows in grey when the GPS is off -- Shows in amber when the GPS is on but has no fix -- Shows in green when the GPS is on and has a fix +There are two icons which can be used to visualize the GPS/GNSS status: +1. A cross colored depending on the GPS/GNSS status + - Shows in grey when the GPS is off + - Shows in amber when the GPS is on but has no fix + - Shows in green when the GPS is on and has a fix +2. Different place markers depending on GPS/GNSS status + +You can also choose to hide the icon when the GPS is off in the settings. + 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/) Extended by Marco ([myxor](https://github.com/myxor)) + +Extended by khromov ([khromov](https://github.com/khromov)) + +Place marker icons from [icons8.com](https://icons8.com/icon/set/maps/material-outlined). diff --git a/apps/widgps/default.json b/apps/widgps/default.json new file mode 100644 index 000000000..28482ddb0 --- /dev/null +++ b/apps/widgps/default.json @@ -0,0 +1 @@ +{"crossIcon": true, "hideWhenGpsOff": false} \ No newline at end of file diff --git a/apps/widgps/metadata.json b/apps/widgps/metadata.json index 39bff2fad..cfd35f5bb 100644 --- a/apps/widgps/metadata.json +++ b/apps/widgps/metadata.json @@ -1,14 +1,19 @@ { "id": "widgps", "name": "GPS Widget", - "version": "0.05", - "description": "Tiny widget to show the power and fix status of the GPS", + "version": "0.09", + "description": "Tiny widget to show the power and fix status of the GPS/GNSS", "icon": "widget.png", "type": "widget", "tags": "widget,gps", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ - {"name":"widgps.wid.js","url":"widget.js"} + {"name":"widgps.wid.js","url":"widget.js"}, + {"name":"widgps.default.json","url":"default.json"}, + {"name":"widgps.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"widgps.json"} ] } diff --git a/apps/widgps/settings.js b/apps/widgps/settings.js new file mode 100644 index 000000000..7a1c186c9 --- /dev/null +++ b/apps/widgps/settings.js @@ -0,0 +1,32 @@ +(function(back) { +function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); +} + +function readSettings() { + settings = Object.assign( + require('Storage').readJSON("widgps.default.json", true) || {}, + require('Storage').readJSON(FILE, true) || {}); +} + +var FILE = "widgps.json"; +var settings; +readSettings(); + +var mainmenu = { + '' : {'title' : 'GPS widget'}, + '< Back' : back, + "Cross icon" : { + value : settings.crossIcon , + onchange : v => { writeSettings("crossIcon", v); } + }, + "Hide icon when GPS off" : { + value : settings.hideWhenGpsOff , + onchange : v => { writeSettings("hideWhenGpsOff", v); } + }, +}; +E.showMenu(mainmenu); +}); \ No newline at end of file diff --git a/apps/widgps/widget.js b/apps/widgps/widget.js index bfdb89d33..73351eaa6 100644 --- a/apps/widgps/widget.js +++ b/apps/widgps/widget.js @@ -1,24 +1,85 @@ -(function(){ - // override setGPSPower so we know if GPS is on or off - var oldSetGPSPower = Bangle.setGPSPower; - Bangle.setGPSPower = function(on,id) { - var isGPSon = oldSetGPSPower(on,id); - WIDGETS.gps.draw(); - return isGPSon; - } +(function() { - WIDGETS.gps={area:"tr",width:24,draw:function() { +let settings = Object.assign( + require('Storage').readJSON("widgps.default.json", true) || {}, + require('Storage').readJSON("widgps.json", true) || {} +); + +var interval; + +// override setGPSPower so we know if GPS is on or off +var oldSetGPSPower = Bangle.setGPSPower; +Bangle.setGPSPower = function(on, id) { + var isGPSon = oldSetGPSPower(on, id); + WIDGETS.gps.width = !isGPSon && settings.hideWhenGpsOff ? 0 : 24; + Bangle.drawWidgets(); + return isGPSon; +}; + +WIDGETS.gps = { + area : "tr", + width : !Bangle.isGPSOn() && settings.hideWhenGpsOff ? 0 : 24, + draw : function() { g.reset(); - if (Bangle.isGPSOn()) { - const gpsObject = Bangle.getGPSFix(); - if (gpsObject && gpsObject.fix > 0) { - g.setColor("#0F0"); // on and has fix = green - } else { - g.setColor("#FD0"); // on but no fix = amber - } - } else { - g.setColor("#888"); // off = grey + + // check if we need to update the widget periodically + if (Bangle.isGPSOn() && interval === undefined) { + interval = setInterval( + function() { WIDGETS.gps.draw(WIDGETS.gps); }, + 10 * 1000); // update every 10 seconds to show gps fix/no fix + } else if (!Bangle.isGPSOn() && interval !== undefined) { + clearInterval(interval); + interval = undefined; } - g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), this.x, 2+this.y); - }}; + if (settings.crossIcon) { + if (Bangle.isGPSOn()) { + const gpsObject = Bangle.getGPSFix(); + if (gpsObject && gpsObject.fix > 0) { + g.setColor("#0F0"); // on and has fix = green + } else { + g.setColor("#FD0"); // on but no fix = amber + } + + g.drawImage( + atob( + "GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), + this.x, 2 + this.y); + + } else { + if(!settings.hideWhenGpsOff) { + g.setColor("#888"); // off = grey + + g.drawImage( + atob( + "GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), + this.x, 2 + this.y); + } + } + + } else { // marker icons + if (Bangle.isGPSOn()) { + const gpsObject = Bangle.getGPSFix(); + if (gpsObject && gpsObject.fix > 0) { + // on and has fix + g.drawImage( + atob("GBiBAAAAAAAAAAB+AAD/AAHDgAMAwAcAwAY8YAY8YAY8YAY8YAMAwAMAwAOBwAGBgAHDgADDAABmAAB+AAA8AAAYAAAAAAAAAAAAAA=="), + this.x, 2 + this.y); + + } else { + // GNSS on but no fix + g.drawImage( + atob("GBiBAAAAAAAAAAh+AA3/gA+B4A8AcA+AMAA8GAB+GADnDADDDADDDDDDADBmADB+ABg8ABgYAAwB8A4A8AeB8AH/sAB+EAAAAAAAAA=="), + this.x, 2 + this.y); + } + } else { + // GNSS off + if(!settings.hideWhenGpsOff) { + g.drawImage( + atob("GBiBAAAAAAAAAAB+ABj/ABxDgA4AwAcAwAeMYAfEYAbgYAZwYAM4wAMcQAOOAAGHAAHDgADDwABm4AB+cAA8OAAYGAAAAAAAAAAAAA=="), + this.x, 2 + this.y); + } + } + } + } +}; })(); diff --git a/apps/widmeda/ChangeLog b/apps/widmeda/ChangeLog new file mode 100644 index 000000000..7415150e6 --- /dev/null +++ b/apps/widmeda/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Medical Alert Widget! diff --git a/apps/widmeda/README.md b/apps/widmeda/README.md new file mode 100644 index 000000000..0bbfc4dc3 --- /dev/null +++ b/apps/widmeda/README.md @@ -0,0 +1,23 @@ +# Medical Alert Widget + +Shows a medical alert logo in the top right widget area, and a medical alert message in the bottom widget area. + +**Note:** this is not a replacement for a medical alert band but hopefully a useful addition. + +## Features + +Implemented: + +- Basic medical alert logo and message +- Only display bottom widget on clocks +- High contrast colours depending on theme + +Future: + +- Configure when to show bottom widget (always/never/clocks) +- Configure medical alert text +- Show details when touched + +## Creator + +James Taylor ([jt-nti](https://github.com/jt-nti)) diff --git a/apps/widmeda/metadata.json b/apps/widmeda/metadata.json new file mode 100644 index 000000000..346306b8e --- /dev/null +++ b/apps/widmeda/metadata.json @@ -0,0 +1,15 @@ +{ "id": "widmeda", + "name": "Medical Alert Widget", + "shortName":"Medical Alert", + "version":"0.01", + "description": "Display a medical alert in the bottom widget section.", + "icon": "widget.png", + "type": "widget", + "tags": "health,medical,tools,widget", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{"url":"screenshot_light.png"}], + "storage": [ + {"name":"widmeda.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widmeda/screenshot_light.png b/apps/widmeda/screenshot_light.png new file mode 100644 index 000000000..bf92d753d Binary files /dev/null and b/apps/widmeda/screenshot_light.png differ diff --git a/apps/widmeda/widget.js b/apps/widmeda/widget.js new file mode 100644 index 000000000..a2d109596 --- /dev/null +++ b/apps/widmeda/widget.js @@ -0,0 +1,30 @@ +(() => { + // Top right star of life logo + WIDGETS["widmedatr"]={ + area: "tr", + width: 24, + draw: function() { + g.reset(); + g.setColor("#f00"); + g.drawImage(atob("FhYBAAAAA/AAD8AAPwAc/OD/P8P8/x/z/n+/+P5/wP58A/nwP5/x/v/n/P+P8/w/z/Bz84APwAA/AAD8AAAAAA=="), this.x + 1, this.y + 1); + } + }; + + // Bottom medical alert message + WIDGETS["widmedabl"]={ + area: "bl", + width: Bangle.CLOCK?Bangle.appRect.w:0, + draw: function() { + // Only show the widget on clocks + if (!Bangle.CLOCK) return; + + g.reset(); + g.setBgColor(g.theme.dark ? "#fff" : "#f00"); + g.setColor(g.theme.dark ? "#f00" : "#fff"); + g.setFont("Vector",18); + g.setFontAlign(0,0); + g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23); + g.drawString("MEDICAL ALERT", this.width / 2, this.y + ( 23 / 2 )); + } + }; +})(); diff --git a/apps/widmeda/widget.png b/apps/widmeda/widget.png new file mode 100644 index 000000000..249bf15bf Binary files /dev/null and b/apps/widmeda/widget.png differ diff --git a/apps/widminbat/ChangeLog b/apps/widminbat/ChangeLog new file mode 100644 index 000000000..8ff6e2cb6 --- /dev/null +++ b/apps/widminbat/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Version: Display at under 30% battery diff --git a/apps/widminbat/metadata.json b/apps/widminbat/metadata.json new file mode 100644 index 000000000..1c7c10b15 --- /dev/null +++ b/apps/widminbat/metadata.json @@ -0,0 +1,13 @@ +{ "id": "widminbat", + "name": "Minimal Battery", + "shortName":"MinBat", + "version":"0.01", + "description": "A minimal version of the battery widget that only appears if the battery is running low (below 30%)", + "icon": "widget.png", + "type": "widget", + "tags": "widget,battery,minimal", + "supports" : ["BANGLEJS2", "BANGLEJS"], + "storage": [ + {"name":"widminbat.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widminbat/widget.js b/apps/widminbat/widget.js new file mode 100644 index 000000000..9c088bf06 --- /dev/null +++ b/apps/widminbat/widget.js @@ -0,0 +1,29 @@ +(()=>{ + function getWidth() { + return E.getBattery() <= 30 ? 40 : 0; + } + WIDGETS.minbat={area:"tr",width:getWidth(),draw:function() { + if(this.width < 40) return; + var s = 39; + var bat = E.getBattery(); + var x = this.x, y = this.y; + g.reset(); + g.clearRect(x,y,x+s,y+24); + g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14); + var barWidth = bat*(s-12)/100; + var color = bat < 15 ? "#f00" : "#f80"; + g.setColor(color).fillRect(x+4,y+6,x+4+barWidth,y+17); + },update: function() { + var newWidth = getWidth(); + if(newWidth != this.width) { + this.width = newWidth; + Bangle.drawWidgets();//relayout + }else{ + this.draw(); + } + }}; + setInterval(()=>{ + var widget = WIDGETS.minbat; + if(widget) {widget.update();} + }, 10*60*1000); +})(); diff --git a/apps/widminbat/widget.png b/apps/widminbat/widget.png new file mode 100644 index 000000000..b04bc4ef9 Binary files /dev/null and b/apps/widminbat/widget.png differ diff --git a/apps/widminbt/ChangeLog b/apps/widminbt/ChangeLog new file mode 100644 index 000000000..28f11c1c7 --- /dev/null +++ b/apps/widminbt/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release diff --git a/apps/widminbt/metadata.json b/apps/widminbt/metadata.json new file mode 100644 index 000000000..a78f9e0a4 --- /dev/null +++ b/apps/widminbt/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widminbt", + "name": "Minimal Bluetooth Widget", + "version": "0.01", + "description": "Appears whenever bluetooth is disconnected", + "icon": "widget.png", + "type": "widget", + "tags": "widget,bluetooth,minimal", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widminbt.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widminbt/widget.js b/apps/widminbt/widget.js new file mode 100644 index 000000000..87439f8c4 --- /dev/null +++ b/apps/widminbt/widget.js @@ -0,0 +1,15 @@ +(()=> { + WIDGETS.minbt={area:"tr",width:NRF.getSecurityStatus().connected?0:15,draw:function() { + if(this.width<15)return; + g.reset(); + g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f")); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); + g.setColor("#f00"); + g.drawImage(atob("CxSBAMA8DYG4YwxzBmD4DwHAGAeA8DcGYY4wzB2B4DA="), 2+this.x, 2+this.y); + },changed:function(){ + WIDGETS.minbt.width=NRF.getSecurityStatus().connected?0:15; + Bangle.drawWidgets(); + }}; + NRF.on('connect',WIDGETS.minbt.changed); + NRF.on('disconnect',WIDGETS.minbt.changed); +})(); diff --git a/apps/widminbt/widget.png b/apps/widminbt/widget.png new file mode 100644 index 000000000..661d1a64c Binary files /dev/null and b/apps/widminbt/widget.png differ diff --git a/apps/widmp/ChangeLog b/apps/widmp/ChangeLog index 02658296a..a51ac080a 100644 --- a/apps/widmp/ChangeLog +++ b/apps/widmp/ChangeLog @@ -2,3 +2,7 @@ 0.02: Fix position and overdraw bugs 0.03: Better memory usage, theme support 0.04: Replace the 8 phases by a more exact drawing, see forum.espruino.com/conversations/371985 +0.05: Fixed the algorithm for calculating the moon's phase +0.06: Darkmode, custom colours, and fix a bug with acting on mylocation changes +0.07: Use default Bangle formatter for booleans +0.08: Better formula for the moon's phase diff --git a/apps/widmp/metadata.json b/apps/widmp/metadata.json index 94f05a426..654b5a383 100644 --- a/apps/widmp/metadata.json +++ b/apps/widmp/metadata.json @@ -1,13 +1,15 @@ { "id": "widmp", - "name": "Moon Phase Widget", - "version": "0.04", - "description": "Display the current moon phase in blueish for both hemispheres. In the southern hemisphere the 'My Location' app is needed.", + "name": "Moon Phase", + "version": "0.08", + "description": "Display the current moon phase in blueish (in light mode) or white (in dark mode) for both hemispheres. In the southern hemisphere the 'My Location' app is needed.", "icon": "widget.png", "type": "widget", "tags": "widget,tools", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ - {"name":"widmp.wid.js","url":"widget.js"} - ] + {"name":"widmp.wid.js","url":"widget.js"}, + {"name":"widmp.settings.js","url":"settings.js"} + ], + "data": [{"name":"widmp.json"}] } diff --git a/apps/widmp/settings.js b/apps/widmp/settings.js new file mode 100644 index 000000000..a389f7918 --- /dev/null +++ b/apps/widmp/settings.js @@ -0,0 +1,72 @@ +(function(back) { + + var settings = Object.assign({ + default_colour: true, + red: 0, + green: 0, + blue: 0, + }, require('Storage').readJSON("widmp.json", true) || {}); + + function writeSettings() { + require('Storage').writeJSON("widmp.json", settings); + if (WIDGETS["widmp"]) WIDGETS["widmp"].draw(); + } + + function writeSettingsCustom() { + settings.default_colour = false; + mainmenu["Default"].value = false; + writeSettings(); + } + + var mainmenu = { + "": { + "title": "Moon colour" + }, + "< Back": () => back(), + "Default": { + value: (settings.default_colour !== undefined ? settings.default_colour : true), + onchange: v => { + settings.default_colour = v; + writeSettings(); + } + }, + "Custom...": () => E.showMenu(custommenu) + }; + + var custommenu = { + "": { + "title": "Custom colour..." + }, + "< Back": () => E.showMenu(mainmenu), + "red": { + value: 0|settings.red, + min: 0, + max: 4, + onchange: v => { + settings.red = v; + writeSettingsCustom(); + } + }, + "green": { + value: 0|settings.green, + min: 0, + max: 4, + onchange: v => { + settings.green = v; + writeSettingsCustom(); + } + }, + "blue": { + value: 0|settings.blue, + min: 0, + max: 4, + onchange: v => { + settings.blue = v; + writeSettingsCustom(); + } + } + }; + + E.showMenu(mainmenu); + +}); diff --git a/apps/widmp/widget.js b/apps/widmp/widget.js index 6da572aab..e5aa7fef2 100644 --- a/apps/widmp/widget.js +++ b/apps/widmp/widget.js @@ -1,25 +1,26 @@ -WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() { - const CenterX = this.x + 12, CenterY = this.y + 12, Radius = 11; - var southernHemisphere = false; // when in southern hemisphere, use the "My Location" App +(() => { - const simulate = false; // simulate one month in one minute - const updateR = 1000; // update every x ms in simulation + var lastCalculated = 0; // When we last calculated the phase + var phase = 0; // The last phase we calculated + var southernHemisphere = false; // when in southern hemisphere -- use the "My Location" App - function moonPhase() { - const d = Date(); - var month = d.getMonth(), year = d.getFullYear(), day = d.getDate(); - if (simulate) day = d.getSeconds() / 2 +1; - if (month < 3) {year--; month += 12;} - mproz = ((365.25 * year + 30.6 * ++month + day - 694039.09) / 29.5305882); - mproz = mproz - (mproz | 0); // strip integral digits, result is between 0 and <1 - if (simulate) console.log(mproz + " " + day); - return (mproz); + // https://github.com/deirdreobyrne/LunarPhase + function moonPhase(sec) { + d = (4.847408287988257 + sec/406074.7465115577) % (2.0*Math.PI); + m = (6.245333801867877 + sec/5022682.784840698) % (2.0*Math.PI); + l = (4.456038755040014 + sec/378902.2499653011) % (2.0*Math.PI); + t = d+1.089809730923715e-01 * Math.sin(l)-3.614132757006379e-02 * Math.sin(m)+2.228248661252023e-02 * Math.sin(d+d-l)+1.353592753655652e-02 * Math.sin(d+d)+4.238560208195022e-03 * Math.sin(l+l)+1.961408105275610e-03 * Math.sin(d); + k = (1.0 - Math.cos(t))/2.0; + if ((t >= Math.PI) && (t < 2.0*Math.PI)) { + k = -k; + } + return (k); // Goes 0 -> 1 for waxing, and from -1 -> 0 for waning } function loadLocation() { // "mylocation.json" is created by the "My Location" app location = require("Storage").readJSON("mylocation.json",1)||{"lat":50.1236,"lon":8.6553,"location":"Frankfurt"}; - if (location.lat < 0) southernHemisphere = true; + southernHemisphere = (location.lat < 0); } // code source: github.com/rozek/banglejs-2-activities/blob/main/README.md#drawmoonphase @@ -38,26 +39,60 @@ WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() { g.drawLine(CenterX-leftFactor*y,CenterY+x, CenterX+rightFactor*y,CenterY+x); } } + + function setMoonColour(g) { + var settings = Object.assign({ + default_colour: true, + red: 0, + green: 0, + blue: 0, + }, require('Storage').readJSON("widmp.json", true) || {}); + if (settings.default_colour) { + if (g.theme.dark) { + g.setColor(0xffff); // white + } else { + // rrrrrggggggbbbbb + // 0000010000011111 + g.setColor(0x41f); // blue-ish + } + } else { + g.setColor(settings.red/4, settings.green/4, settings.blue/4); + } + } - function updateWidget() { + + function draw() { + const CenterX = this.x + 12, CenterY = this.y + 12, Radius = 11; + + loadLocation(); g.reset().setColor(g.theme.bg); g.fillRect(CenterX - Radius, CenterY - Radius, CenterX + Radius, CenterY + Radius); - g.setColor(0x41f); - mproz = moonPhase(); // mproz = 0..<1 + millis = (new Date()).getTime(); + if ((millis - lastCalculated) >= 7000000) { // if it's more than 7,000 sec since last calculation, re-calculate! + phase = moonPhase(millis/1000); + lastCalculated = millis; + } - leftFactor = mproz * 4 - 1; - rightFactor = (1 - mproz) * 4 - 1; - if (mproz >= 0.5) leftFactor = 1; else rightFactor = 1; + if (phase < 0) { // waning - phase goes from -1 to 0 + leftFactor = 1; + rightFactor = -1 - 2*phase; + } else { // waxing - phase goes from 0 to 1 + rightFactor = 1; + leftFactor = -1 + 2*phase; + } if (true == southernHemisphere) { var tmp=leftFactor; leftFactor=rightFactor; rightFactor=tmp; } + setMoonColour(g); drawMoonPhase(CenterX,CenterY, Radius, leftFactor,rightFactor); - - if (simulate) setTimeout(updateWidget, updateR); } - loadLocation(); - updateWidget(); -} }; + WIDGETS["widmp"] = { + area: "tr", + width: 24, + draw: draw + }; + +})(); diff --git a/apps/widram/ChangeLog b/apps/widram/ChangeLog index e7b406081..7b00c8a48 100644 --- a/apps/widram/ChangeLog +++ b/apps/widram/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Widget! 0.02: Now also visible on Bangle.js 2 +0.03: Remove global declaration of BANGLEJS2 var (fix #2123) diff --git a/apps/widram/metadata.json b/apps/widram/metadata.json index 19ae6d311..ebf23742b 100644 --- a/apps/widram/metadata.json +++ b/apps/widram/metadata.json @@ -2,7 +2,7 @@ "id": "widram", "name": "RAM Widget", "shortName": "RAM Widget", - "version": "0.02", + "version": "0.03", "description": "Display your Bangle's RAM usage percentage in a widget", "icon": "widget.png", "type": "widget", diff --git a/apps/widram/widget.js b/apps/widram/widget.js index 210c85357..07b7c0a5f 100644 --- a/apps/widram/widget.js +++ b/apps/widram/widget.js @@ -1,6 +1,6 @@ (() => { function draw() { - BANGLEJS2 = process.env.HWVERSION==2; + const BANGLEJS2 = process.env.HWVERSION==2; g.reset(); var m = process.memory(); var percent = Math.round(m.usage*100/m.total); diff --git a/apps/widscrlock/ChangeLog b/apps/widscrlock/ChangeLog new file mode 100644 index 000000000..84e926362 --- /dev/null +++ b/apps/widscrlock/ChangeLog @@ -0,0 +1,3 @@ +0.01: 25/Jun/2022 Added Screenlock widget to depository. +0.02: 26/Jun/2022 Tidied up graphics. Fixed versioning style... +0.03: 28/Jun/2022 Tidied up code. diff --git a/apps/widscrlock/README.md b/apps/widscrlock/README.md new file mode 100644 index 000000000..043bfbf40 --- /dev/null +++ b/apps/widscrlock/README.md @@ -0,0 +1,3 @@ +A Bangle 2 widget to lock the screen. + +Tap on the widget's lock icon to lock and darken the touch-screen as happens with the LCD timeout. diff --git a/apps/widscrlock/metadata.json b/apps/widscrlock/metadata.json new file mode 100644 index 000000000..5110d76c1 --- /dev/null +++ b/apps/widscrlock/metadata.json @@ -0,0 +1,14 @@ +{ "id": "widscrlock", + "name": "Screenlock Widget", + "shortName":"Screenlock", + "version":"0.03", + "description": "Lock a Bangle 2 screen by tapping a widget.", + "icon": "widget.png", + "type": "widget", + "tags": "widget, screenlock", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widscrlock.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widscrlock/widget.js b/apps/widscrlock/widget.js new file mode 100644 index 000000000..cbb71e4cc --- /dev/null +++ b/apps/widscrlock/widget.js @@ -0,0 +1,32 @@ +// Screenlock Widget + +(() => { + function draw() { + // Draw icon. + g.reset(); + g.drawImage(atob("GBiDAkkkkiSSSUkkkkkkiSSSSSUkkkkiSSf/ySSUkkkSSf///ySSkkiST/ySf+SQUkiST+SST+SAUkSSfySSSfwACkSSfySSSewACiSSfySSSWwAASSSfySSSGwAASSSfySSQGwAASST///+222AASST///2222AASST//+A222AASST//wAG22AASST/+AAA22AASST/2wAG22AAUST+22A222ACkiT222A222ACkiSG22A22wAUkkQA22222ACkkkiAG222wAUkkkkSAAAAASkkkkkkSQACSkkkg=="),scrlock.x,scrlock.y); + } + + // add widget. + WIDGETS.widscrlock={ + area:"tr", + width: 24, + draw:draw // Draw widget. + }; + + var scrlock = WIDGETS.widscrlock; + + function restoreTimeout(){ + // Restore LCDTimeout settings. + Bangle.setLCDTimeout(options.lockTimeout / 1000); + } + + var options = []; + Bangle.on('touch', function(button, xy) { + if(xy.x>=scrlock.x && xy.x<=scrlock.x+23 && xy.y>=scrlock.y && xy.y<=scrlock.y+23) { + options = Bangle.getOptions(); // Store current Timeout settings. + Bangle.setLCDTimeout(0.1); // Lock screen. + setTimeout(restoreTimeout, 1000); + } + }); +})(); diff --git a/apps/widscrlock/widget.png b/apps/widscrlock/widget.png new file mode 100644 index 000000000..76c490785 Binary files /dev/null and b/apps/widscrlock/widget.png differ diff --git a/apps/widshipbell/metadata.json b/apps/widshipbell/metadata.json new file mode 100644 index 000000000..c130b04ee --- /dev/null +++ b/apps/widshipbell/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "widshipbell", + "name": "Ship's bell Widget", + "shortName": "Ship's bell", + "version": "0.01", + "description": "A widget that buzzes according to a nautical bell, one strike at 04:30, two strikes at 05:00, up to eight strikes at 08:00 and so on.", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widshipbell.wid.js","url":"widget.js"}, + {"name":"widshipbell.settings.js","url":"settings.js"} + ], + "data": [{"name":"widshipbell.json"}] +} diff --git a/apps/widshipbell/settings.js b/apps/widshipbell/settings.js new file mode 100644 index 000000000..bb47e9b20 --- /dev/null +++ b/apps/widshipbell/settings.js @@ -0,0 +1,27 @@ +(function(back) { + var FILE = "widshipbell.json"; + // Load settings + var settings = Object.assign({ + strength: 1, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Ship's bell" }, + "< Back" : () => back(), + 'Strength': { + value: settings.strength, + min: 0, max: 2, + format: v => ["Off", "Weak", "Strong"][v], + onchange: v => { + settings.strength = v; + writeSettings(); + } + }, + }); +}) + diff --git a/apps/widshipbell/widget.js b/apps/widshipbell/widget.js new file mode 100644 index 000000000..e37edb6fd --- /dev/null +++ b/apps/widshipbell/widget.js @@ -0,0 +1,52 @@ +(() => { + const strength = Object.assign({ + strength: 1, + }, require('Storage').readJSON("widshipbell.json", true) || {}).strength; + + function replaceAll(target, search, replacement) { + return target.split(search).join(replacement); + } + + function check() { + const now = new Date(); + const currentMinute = now.getMinutes(); + const currentSecond = now.getSeconds(); + const etaMinute = 30-(currentMinute % 30); + + if (etaMinute === 30 && currentSecond === 0) { + const strikeHour = now.getHours() % 4; + // buzz now + let pattern=''; + if (strikeHour === 0 && currentMinute == 0) { + pattern = '.. .. .. ..'; + } else if (strikeHour === 0 && currentMinute === 30) { + pattern = '.'; + } else if (strikeHour === 1 && currentMinute === 0) { + pattern = '..'; + } else if (strikeHour === 1 && currentMinute === 30) { + pattern = '.. .'; + } else if (strikeHour === 2 && currentMinute === 0) { + pattern = '.. ..'; + } else if (strikeHour === 2 && currentMinute === 30) { + pattern = '.. .. .'; + } else if (strikeHour === 3 && currentMinute === 0) { + pattern = '.. .. ..'; + } else if (strikeHour === 3 && currentMinute === 30) { + pattern = '.. .. .. .'; + } + pattern = replaceAll(pattern, ' ', ' '); // 4x pause + pattern = replaceAll(pattern, '.', '. '); // pause between bells + if (strength === 2) { // strong selected + pattern = replaceAll(pattern, '.', ':'); + } + require("buzz").pattern(pattern); + } + + const etaSecond = etaMinute*60-currentSecond; + setTimeout(check, etaSecond*1000); + } + + if (strength !== 0) { + check(); + } +})(); diff --git a/apps/widshipbell/widget.png b/apps/widshipbell/widget.png new file mode 100644 index 000000000..891679f7d Binary files /dev/null and b/apps/widshipbell/widget.png differ diff --git a/apps/widslimbat/metadata.json b/apps/widslimbat/metadata.json new file mode 100644 index 000000000..a83046e90 --- /dev/null +++ b/apps/widslimbat/metadata.json @@ -0,0 +1,13 @@ +{ "id": "widslimbat", + "name": "Slim battery widget with cells", + "shortName":"Slim battery with cells", + "version":"0.01", + "description": "A small (13px wide) battery widget with cells", + "icon": "widget.png", + "type": "widget", + "tags": "widget", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"widslimbat.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widslimbat/widget.js b/apps/widslimbat/widget.js new file mode 100644 index 000000000..4a8bb3b5d --- /dev/null +++ b/apps/widslimbat/widget.js @@ -0,0 +1,55 @@ +(() => { + const intervalLow = 60000; // update time when not charging + const intervalHigh = 2000; // update time when charging + const outline = atob("CRSBAD4AP/AYDAYDAYDAYDAYDAYDAYDAYD/w"); + + let COLORS = { + 'black': g.theme.dark ? "#fff" : "#000", + 'charging': "#0f0", + 'low': "#f00", + }; + + function draw() { + var i; + var oCol = COLORS.low; + var cCol = COLORS.low; + var nCells = 0; + + const bat = E.getBattery(); + if (bat>5) { + oCol = COLORS.black; + nCells = 1 + Math.floor((bat-6)/19); + } + if (nCells>1) + cCol = COLORS.black; + if (Bangle.isCharging()) + oCol = COLORS.charging; + g.reset(); + g.setColor(oCol).drawImage(outline,this.x+2,this.y+2); + for (i=0;iWIDGETS["widslimbat"].draw(),intervalLow); + + WIDGETS["widslimbat"]={ + area:"tr", + width:13, + draw:draw + }; +})(); diff --git a/apps/widslimbat/widget.png b/apps/widslimbat/widget.png new file mode 100644 index 000000000..a9c7d416d Binary files /dev/null and b/apps/widslimbat/widget.png differ diff --git a/apps/widtwenties/ChangeLog b/apps/widtwenties/ChangeLog new file mode 100644 index 000000000..87935d810 --- /dev/null +++ b/apps/widtwenties/ChangeLog @@ -0,0 +1,2 @@ +0.01: New Widget! +0.02: Fix calling null on draw \ No newline at end of file diff --git a/apps/widtwenties/README.md b/apps/widtwenties/README.md new file mode 100644 index 000000000..1dea18b8e --- /dev/null +++ b/apps/widtwenties/README.md @@ -0,0 +1,15 @@ +# Twenties + +Follow the [20-20-20 rule](https://www.aoa.org/AOA/Images/Patients/Eye%20Conditions/20-20-20-rule.pdf) with discrete reminders. Your BangleJS will buzz every 20 minutes for you to look away from your screen, and then buzz 20 seconds later to look back. Additionally, alternate between standing and sitting every 20 minutes to be standing for [more than 30 minutes](https://uwaterloo.ca/kinesiology-health-sciences/how-long-should-you-stand-rather-sit-your-work-station) per hour. + +## Usage + +Download this widget and, as long as your watch-face supports widgets, it will automatically run in the background. + +## Features + +Vibrate to remind you to stand up and look away for healthy living. + +## Creator + +[@splch](https://github.com/splch/) diff --git a/apps/widtwenties/metadata.json b/apps/widtwenties/metadata.json new file mode 100644 index 000000000..2e51457ac --- /dev/null +++ b/apps/widtwenties/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widtwenties", + "name": "Twenties", + "shortName": "twenties", + "version": "0.02", + "description": "Buzzes every 20m to stand / sit and look 20ft away for 20s.", + "icon": "widget.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [{ "name": "widtwenties.wid.js", "url": "widget.js" }] +} diff --git a/apps/widtwenties/widget.js b/apps/widtwenties/widget.js new file mode 100644 index 000000000..58bc622eb --- /dev/null +++ b/apps/widtwenties/widget.js @@ -0,0 +1,24 @@ +// WIDGETS = {}; // <-- for development only + +/* run widgets in their own function scope so +they don't interfere with currently-running apps */ +(() => { + const move = 20 * 60 * 1000; // 20 minutes + const look = 20 * 1000; // 20 seconds + + buzz = _ => { + Bangle.buzz().then(_ => { + setTimeout(Bangle.buzz, look); + }); + }; + + // add widget + WIDGETS.twenties = { + buzz: buzz, + draw: _ => { return null; }, + }; + + setInterval(WIDGETS.twenties.buzz, move); // buzz to stand / sit +})(); + +// Bangle.drawWidgets(); // <-- for development only \ No newline at end of file diff --git a/apps/widtwenties/widget.png b/apps/widtwenties/widget.png new file mode 100644 index 000000000..7c6b02055 Binary files /dev/null and b/apps/widtwenties/widget.png differ diff --git a/apps/widviztime/changelog b/apps/widviztime/ChangeLog similarity index 100% rename from apps/widviztime/changelog rename to apps/widviztime/ChangeLog diff --git a/apps/wpmoto/ChangeLog b/apps/wpmoto/ChangeLog new file mode 100644 index 000000000..2f35c81fe --- /dev/null +++ b/apps/wpmoto/ChangeLog @@ -0,0 +1,3 @@ +... +0.02: First update with ChangeLog Added +0.03: Move waypoints.json (and editor) to 'waypoints' app diff --git a/apps/wpmoto/app.js b/apps/wpmoto/app.js index 7deacb6ca..c8e30a583 100644 --- a/apps/wpmoto/app.js +++ b/apps/wpmoto/app.js @@ -1,6 +1,6 @@ var loc = require("locale"); -var waypoints = require("Storage").readJSON("waypoints.json") || []; +var waypoints = require("waypoints").load(); var wp = waypoints[0]; if (wp == undefined) wp = {name:"NONE"}; var wp_bearing = 0; @@ -196,7 +196,7 @@ function addCurrentWaypoint() { } function saveWaypoints() { - require("Storage").writeJSON("waypoints.json", waypoints); + require("waypoints").save(waypoints); } function deleteWaypoint(w) { diff --git a/apps/wpmoto/metadata.json b/apps/wpmoto/metadata.json index f562b23ba..01ca4edd8 100644 --- a/apps/wpmoto/metadata.json +++ b/apps/wpmoto/metadata.json @@ -2,17 +2,16 @@ "id": "wpmoto", "name": "Waypointer Moto", "shortName": "Waypointer Moto", - "version": "0.02", + "version": "0.03", "description": "Waypoint-based motorcycle navigation aid", "icon": "wpmoto.png", "tags": "tool,outdoors,gps", "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}], "readme": "README.md", - "interface": "wpmoto.html", + "dependencies" : { "waypoints":"type" }, "storage": [ {"name":"wpmoto.app.js","url":"app.js"}, {"name":"wpmoto.img","url":"icon.js","evaluate":true} - ], - "data": [{"name":"waypoints.json","url":"waypoints.json"}] + ] } diff --git a/apps/wpmoto/waypoints.json b/apps/wpmoto/waypoints.json deleted file mode 100644 index 8a4ab83b8..000000000 --- a/apps/wpmoto/waypoints.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { - "name":"NONE" - }, -] diff --git a/apps/wristlight/Icon.png b/apps/wristlight/Icon.png new file mode 100644 index 000000000..0bbcd3e36 Binary files /dev/null and b/apps/wristlight/Icon.png differ diff --git a/apps/wristlight/README.md b/apps/wristlight/README.md new file mode 100644 index 000000000..e69d61b76 --- /dev/null +++ b/apps/wristlight/README.md @@ -0,0 +1,9 @@ +# Wrist Light + +A flash light on your wrist with different colors + +![](screenshot.png) +![](screenshot_red.png) + +## Creator +[@pidajo](https://github.com/pidajo) diff --git a/apps/wristlight/app.js b/apps/wristlight/app.js new file mode 100644 index 000000000..24bc1eab2 --- /dev/null +++ b/apps/wristlight/app.js @@ -0,0 +1,52 @@ +function draw(color) { + if (color == undefined) { + color = -1; + } + g.clear(); + g.setColor(color); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); +} + +function draw2Pattern() { + colors = ["ff0000", "8080ff", "00ff00", + "ffffff"]; + drawPattern(2, colors); +} + +function draw3Pattern() { + colors = ["ff0000", "00ff00", "0000ff", + "ff00ff", "ffffff", "00ffff", + "ffff00", "ff8000", "ff0080"]; + drawPattern(3, colors); +} + +function drawPattern(size, colors) { + g.clear(); + var w = g.getWidth() / size; + var h = g.getHeight() / size; + for (var i = 0; i < size; i++) { + for (var j = 0; j < size; j++) { + var color = colors[i*size + j]; + g.setColor("#" + color); + g.fillRect(j * w, i * h, j * w + w, i * h + h); + } + } + Bangle.on("touch", function(btn, xy) { + var x = parseInt((xy.x) / w); + var y = parseInt((xy.y) / h); + draw("#" + colors[y * size + x]); + }); +} + +// Clear the screen once, at startup +// draw immediately at first +draw3Pattern(); + +/* +require("Storage").write("wristlight.info",{ + "id":"wristlight", + "name":"Wrist Light", + "src":"wristlight.app.js", + "icon":"wristlight.img" +}); +*/ diff --git a/apps/wristlight/metadata.json b/apps/wristlight/metadata.json new file mode 100644 index 000000000..af1d700df --- /dev/null +++ b/apps/wristlight/metadata.json @@ -0,0 +1,16 @@ +{ "id": "wristlight", + "name": "Wrist Light", + "shortName":"Wrist Light", + "icon": "wristlight48.png", + "version":"0.01", + "description": "A flash light with different colors on your wrist", + "tags": "flash,light", + "allow_emulator":true, + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme":"README.md", + "screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot_red.png" } ], + "storage": [ + {"name":"wristlight.app.js","url":"app.js"}, + {"name":"wristlight.img","url":"wristlight-icon.js","evaluate":true} + ] +} diff --git a/apps/wristlight/screenshot.png b/apps/wristlight/screenshot.png new file mode 100644 index 000000000..31850d1ca Binary files /dev/null and b/apps/wristlight/screenshot.png differ diff --git a/apps/wristlight/screenshot_red.png b/apps/wristlight/screenshot_red.png new file mode 100644 index 000000000..06ac7a190 Binary files /dev/null and b/apps/wristlight/screenshot_red.png differ diff --git a/apps/wristlight/wristlight-icon.js b/apps/wristlight/wristlight-icon.js new file mode 100644 index 000000000..689372499 --- /dev/null +++ b/apps/wristlight/wristlight-icon.js @@ -0,0 +1 @@ +atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzMzMzMAN3d3d3d3QDu7u7u7gAAAAAADMzMzMzMDd3d3d3d3dDu7u7u7uAAAAAAzMzMzMzMDd3d3d3d3dDu7u7u7u4AAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAMzMzMzMzMDd3d3d3d3dDu7u7u7u7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu7u7u7uwAAAA//8AAAAHd3d3d3cAAAALu7u7u7u7AAD/////AAB3d3d3d3dwAAALu7u7u7u7AA//////8AB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7D/////////B3d3d3d3dwAAALu7u7u7u7A////////zB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AP///////wB3d3d3d3dwAAALu7u7u7u7AA//////8AB3d3d3d3dwAAALu7u7u7u7AAD/////AAB3d3d3d3dwAAAMu7u7u7u8AAACMzMgAAB3d3d3d3dwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALu7u7u7u7AIiIiIiIiACIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAALu7u7u7u7CYiIiIiIiJCIiIiIiIiAAAAAu7u7u7u7CYiIiIiIiJCIiIiIiIgAAAAAq7u7u7u7CYiIiIiIiJCIiIiIiIAAAAAAALu7u7u7CoiIiIiIiKCIiIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") diff --git a/apps/wristlight/wristlight48.png b/apps/wristlight/wristlight48.png new file mode 100644 index 000000000..8f0ff59f7 Binary files /dev/null and b/apps/wristlight/wristlight48.png differ diff --git a/backup.js b/backup.js index 8a894666e..06d98a366 100644 --- a/backup.js +++ b/backup.js @@ -101,6 +101,7 @@ function bangleUpload() { return Comms.showMessage(`Restoring...`); }) .then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n")) .then(() => Comms.uploadCommandList(cmds, 0, cmds.length)) + .then(() => getInstalledApps(true)) .then(() => Comms.showMessage(Const.MESSAGE_RELOAD)) .then(() => { Progress.hide({sticky:true}); diff --git a/bin/apploader.js b/bin/apploader.js index 427a0ef99..26c4c1f09 100755 --- a/bin/apploader.js +++ b/bin/apploader.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env node /* Simple Command-line app loader for Node.js =============================================== @@ -8,35 +8,44 @@ as a normal dependency) because we want `sanitycheck.js` to be able to run *quickly* in travis for every commit, and we don't want NPM pulling in (and compiling native modules) for Noble. + */ var SETTINGS = { pretokenise : true }; var APPSDIR = __dirname+"/../apps/"; -var Utils = require("../core/js/utils.js"); -var AppInfo = require("../core/js/appinfo.js"); var noble; -try { - noble = require('@abandonware/noble'); -} catch (e) {} -if (!noble) try { - noble = require('noble'); -} catch (e) { } +["@abandonware/noble", "noble"].forEach(module => { + if (!noble) try { + noble = require(module); + } catch(e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + } +}); if (!noble) { console.log("You need to:") console.log(" npm install @abandonware/noble") console.log("or:") console.log(" npm install noble") + process.exit(1); } - -var apps = []; - function ERROR(msg) { console.error(msg); process.exit(1); } +//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); +var AppInfo = require("../core/js/appinfo.js"); +global.Const = { + /* Are we only putting a single app on a device? If so + apps should all be saved as .bootcde and we write info + about the current app into app.info */ + SINGLE_APP_ONLY : false, +}; +var deviceId = "BANGLEJS2"; var apps = []; var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); dirs.forEach(dir => { @@ -54,6 +63,10 @@ dirs.forEach(dir => { var args = process.argv; +var bangleParam = args.findIndex(arg => /-b\d/.test(arg)); +if (bangleParam!==-1) { + deviceId = "BANGLEJS"+args.splice(bangleParam, 1)[0][2]; +} if (args.length==3 && args[2]=="list") cmdListApps(); else if (args.length==3 && args[2]=="devices") cmdListDevices(); else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]); @@ -68,7 +81,10 @@ apploader.js list - list available apps apploader.js devices - list available device addresses -apploader.js install appname [de:vi:ce:ad:dr:es] +apploader.js install [-b1] appname [de:vi:ce:ad:dr:es] + +NOTE: By default this App Loader expects the device it uploads to +(deviceId) to be BANGLEJS2, pass '-b1' for it to work with Bangle.js 1 `); process.exit(0); } @@ -104,7 +120,10 @@ function cmdInstallApp(appId, deviceAddress) { fileGetter:function(url) { console.log(__dirname+"/"+url); return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, settings : SETTINGS}).then(files => { + }, + settings : SETTINGS, + device : { id : deviceId } + }).then(files => { //console.log(files); var command = files.map(f=>f.cmd).join("\n")+"\n"; bangleSend(command, deviceAddress).then(() => process.exit(0)); diff --git a/bin/create_app_supports_field.js b/bin/create_app_supports_field.js index d6aada357..24d6694f2 100644 --- a/bin/create_app_supports_field.js +++ b/bin/create_app_supports_field.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env nodejs /* Quick hack to add proper 'supports' field to apps.json */ diff --git a/bin/create_apps_json.sh b/bin/create_apps_json.sh index 665079fc6..c9f310e57 100755 --- a/bin/create_apps_json.sh +++ b/bin/create_apps_json.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # ================================================================ # apps.json used to contain the metadata for every app. Now the # metadata is stored in each apps's directory - app/yourapp/metadata.js @@ -23,7 +23,7 @@ echo "[" > "$outfile" first=1 for app in apps/*/; do echo "Processing $app..."; - if [[ "$app" =~ ^apps/_example.* ]]; then + if [[ "$app" =~ ^apps/_example.* ]] || [ ! -e "$app/"metadata.json ]; then echo "Ignoring $app" else if [ $first -eq 1 ]; then diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 1c2d9cb77..1dc5ec073 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env nodejs /* Mashes together a bunch of different apps to make a single firmware JS file which can be uploaded. diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 7940e551d..87065dab4 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -1,4 +1,4 @@ -#!/usr/bin/nodejs +#!/usr/bin/env node /* Mashes together a bunch of different apps into a big binary blob. We then store this *inside* the Bangle.js firmware and can use it @@ -23,13 +23,13 @@ if (DEVICE=="BANGLEJS") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs1_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", - "about","alarm","widbat","widbt","welcome" + "about","alarm","sched","widbat","widbt","welcome" ]; } else if (DEVICE=="BANGLEJS2") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs2_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","antonclk","setting", - "about","alarm","health","widlock","widbat","widbt","widid","welcome" + "about","alarm","sched","health","widlock","widbat","widbt","widid","welcome" ]; } else { console.log("USAGE:"); diff --git a/bin/pre-publish.sh b/bin/pre-publish.sh index ee73968d7..710160ded 100755 --- a/bin/pre-publish.sh +++ b/bin/pre-publish.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd `dirname $0`/.. nodejs bin/sanitycheck.js || exit 1 diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 8fdb5a4d2..8bd23dd77 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -1,4 +1,4 @@ -#!/usr/bin/node +#!/usr/bin/env node /* Checks for any obvious problems in apps.json */ @@ -17,13 +17,25 @@ try { } var BASEDIR = __dirname+"/../"; -var APPSDIR = BASEDIR+"apps/"; -function ERROR(s) { - console.error("ERROR: "+s); - process.exit(1); +var APPSDIR_RELATIVE = "apps/"; +var APPSDIR = BASEDIR + APPSDIR_RELATIVE; +var warningCount = 0; +var errorCount = 0; +function ERROR(msg, opt) { + // file=app.js,line=1,col=5,endColumn=7 + opt = opt||{}; + console.log(`::error${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + errorCount++; } -function WARN(s) { - console.log("Warning: "+s); +function WARN(msg, opt) { + // file=app.js,line=1,col=5,endColumn=7 + opt = opt||{}; + if (KNOWN_WARNINGS.includes(msg)) { + console.log(`Known warning : ${msg}`); + } else { + console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + } + warningCount++; } var apps = []; @@ -43,16 +55,20 @@ dirs.forEach(dir => { } catch (e) { console.log(e); var m = e.toString().match(/in JSON at position (\d+)/); + var messageInfo = { + file : "apps/"+dir.name+"/metadata.json", + }; if (m) { var char = parseInt(m[1]); + messageInfo.line = appsFile.substr(0,char).split("\n").length; console.log("==============================================="); - console.log("LINE "+appsFile.substr(0,char).split("\n").length); + console.log("LINE "+messageInfo.line); console.log("==============================================="); console.log(appsFile.substr(char-10, 20)); console.log("==============================================="); } console.log(m); - ERROR(dir.name+"/metadata.json not valid JSON"); + ERROR(messageInfo.file+" not valid JSON", messageInfo); } }); @@ -65,9 +81,19 @@ const APP_KEYS = [ const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports' +const METADATA_TYPES = ["app","clock","widget","bootloader","RAM","launch","textinput","scheduler","notify","locale","settings","waypoints"]; // values allowed for "type" field const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"]; +const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD provide... + 'textinput' : ['textinput'], + 'waypoints' : ['waypoints'], + // notify? +}; +/* These are warnings we know about but don't want in our output */ +var KNOWN_WARNINGS = [ +"App gpsrec data file wildcard .gpsrc? does not include app ID" +]; function globToRegex(pattern) { const ESCAPE = '.*+-?^${}()|[]\\'; @@ -86,82 +112,99 @@ let allFiles = []; let existingApps = []; apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); - if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`); + var appDirRelative = APPSDIR_RELATIVE+app.id+"/"; + var appDir = APPSDIR+app.id+"/"; + var metadataFile = appDirRelative+"metadata.json"; + if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`, {file:metadataFile}); existingApps.push(app.id); //console.log(`Checking ${app.id}...`); - var appDir = APPSDIR+app.id+"/"; + if (!fs.existsSync(APPSDIR+app.id)) ERROR(`App ${app.id} has no directory`); - if (!app.name) ERROR(`App ${app.id} has no name`); + if (!app.name) ERROR(`App ${app.id} has no name`, {file:metadataFile}); var isApp = !app.type || app.type=="app"; - if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`); - if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`); + if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`, {file:metadataFile}); + if (app.type && !METADATA_TYPES.includes(app.type)) + ERROR(`App ${app.id} 'type' is one one of `+METADATA_TYPES, {file:metadataFile}); + if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`, {file:metadataFile}); else { app.supports.forEach(dev => { if (!SUPPORTS_DEVICES.includes(dev)) - ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`); + ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`, {file:metadataFile}); }); } - if (!app.version) WARN(`App ${app.id} has no version`); + if (!app.version) ERROR(`App ${app.id} has no version`, {file:metadataFile}); else { if (!fs.existsSync(appDir+"ChangeLog")) { - if (app.version != "0.01") - WARN(`App ${app.id} has no ChangeLog`); + var invalidChangeLog = fs.readdirSync(appDir).find(f => f.toLowerCase().startsWith("changelog") && f!="ChangeLog"); + if (invalidChangeLog) + ERROR(`App ${app.id} has wrongly named ChangeLog (${invalidChangeLog})`, {file:appDirRelative+invalidChangeLog}); + else if (app.version != "0.01") + WARN(`App ${app.id} has no ChangeLog`, {file:metadataFile}); } else { var changeLog = fs.readFileSync(appDir+"ChangeLog").toString(); var versions = changeLog.match(/\d+\.\d+:/g); - if (!versions) ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`); + if (!versions) ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`, {file:metadataFile}); var lastChangeLog = versions.pop().slice(0,-1); if (lastChangeLog != app.version) - WARN(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`); + ERROR(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`, {file:appDirRelative+"ChangeLog", line:changeLog.split("\n").length-1}); } } - if (!app.description) ERROR(`App ${app.id} has no description`); - if (!app.icon) ERROR(`App ${app.id} has no icon`); - if (!fs.existsSync(appDir+app.icon)) ERROR(`App ${app.id} icon doesn't exist`); + if (!app.description) ERROR(`App ${app.id} has no description`, {file:metadataFile}); + if (!app.icon) ERROR(`App ${app.id} has no icon`, {file:metadataFile}); + if (!fs.existsSync(appDir+app.icon)) ERROR(`App ${app.id} icon doesn't exist`, {file:metadataFile}); if (app.screenshots) { - if (!Array.isArray(app.screenshots)) ERROR(`App ${app.id} screenshots is not an array`); + if (!Array.isArray(app.screenshots)) ERROR(`App ${app.id} screenshots is not an array`, {file:metadataFile}); app.screenshots.forEach(screenshot => { if (!fs.existsSync(appDir+screenshot.url)) - ERROR(`App ${app.id} screenshot file ${screenshot.url} not found`); + ERROR(`App ${app.id} screenshot file ${screenshot.url} not found`, {file:metadataFile}); }); } - if (app.readme && !fs.existsSync(appDir+app.readme)) ERROR(`App ${app.id} README file doesn't exist`); - if (app.custom && !fs.existsSync(appDir+app.custom)) ERROR(`App ${app.id} custom HTML doesn't exist`); - if (app.customConnect && !app.custom) ERROR(`App ${app.id} has customConnect but no customn HTML`); - if (app.interface && !fs.existsSync(appDir+app.interface)) ERROR(`App ${app.id} interface HTML doesn't exist`); + if (app.readme && !fs.existsSync(appDir+app.readme)) ERROR(`App ${app.id} README file doesn't exist`, {file:metadataFile}); + if (app.custom && !fs.existsSync(appDir+app.custom)) ERROR(`App ${app.id} custom HTML doesn't exist`, {file:metadataFile}); + if (app.customConnect && !app.custom) ERROR(`App ${app.id} has customConnect but no customn HTML`, {file:metadataFile}); + if (app.interface && !fs.existsSync(appDir+app.interface)) ERROR(`App ${app.id} interface HTML doesn't exist`, {file:metadataFile}); if (app.dependencies) { if (("object"==typeof app.dependencies) && !Array.isArray(app.dependencies)) { Object.keys(app.dependencies).forEach(dependency => { if (!["type","app"].includes(app.dependencies[dependency])) - ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`); + ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`, {file:metadataFile}); + if (app.dependencies[dependency]=="type" && !METADATA_TYPES.includes(dependency)) + ERROR(`App ${app.id} 'type' dependency must be one of `+METADATA_TYPES, {file:metadataFile}); + }); } else - ERROR(`App ${app.id} 'dependencies' must be an object`); + ERROR(`App ${app.id} 'dependencies' must be an object`, {file:metadataFile}); } + var fileNames = []; app.storage.forEach((file) => { - if (!file.name) ERROR(`App ${app.id} has a file with no name`); - if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); + if (!file.name) ERROR(`App ${app.id} has a file with no name`, {file:metadataFile}); + if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`, {file:metadataFile}); let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS) - if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) + if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`, {file:metadataFile}) if (fileNames.includes(file.name) && !file.supports) // assume that there aren't duplicates if 'supports' is set - ERROR(`App ${app.id} file ${file.name} is a duplicate`); + ERROR(`App ${app.id} file ${file.name} is a duplicate`, {file:metadataFile}); if (file.supports && !Array.isArray(file.supports)) - ERROR(`App ${app.id} file ${file.name} supports field must be an array`); + ERROR(`App ${app.id} file ${file.name} supports field must be an array`, {file:metadataFile}); if (file.supports) file.supports.forEach(dev => { if (!SUPPORTS_DEVICES.includes(dev)) - ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`); + ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`, {file:metadataFile}); }); fileNames.push(file.name); - allFiles.push({app: app.id, file: file.name}); - if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); - if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); + var fileInternal = false; + if (app.type && INTERNAL_FILES_IN_APP_TYPE[app.type]) { + if (INTERNAL_FILES_IN_APP_TYPE[app.type].includes(file.name)) + fileInternal = true; + } + allFiles.push({app: app.id, file: file.name, internal:fileInternal}); + if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`, {file:metadataFile}); + if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`, {file:metadataFile}); var fileContents = ""; if (file.content) fileContents = file.content; if (file.url) fileContents = fs.readFileSync(appDir+file.url).toString(); - if (file.supports && !Array.isArray(file.supports)) ERROR(`App ${app.id} file ${file.name} supports field is not an array`); + if (file.supports && !Array.isArray(file.supports)) ERROR(`App ${app.id} file ${file.name} supports field is not an array`, {file:metadataFile}); if (file.evaluate) { try { acorn.parse("("+fileContents+")"); @@ -173,7 +216,7 @@ apps.forEach((app,appIdx) => { console.log("====================================================="); console.log(fileContents); console.log("====================================================="); - ERROR(`App ${app.id}'s ${file.name} has evaluate:true but is not valid JS expression`); + ERROR(`App ${app.id}'s ${file.name} has evaluate:true but is not valid JS expression`, {file:appDirRelative+file.url}); } } if (file.name.endsWith(".js")) { @@ -188,11 +231,18 @@ apps.forEach((app,appIdx) => { console.log("====================================================="); console.log(fileContents); console.log("====================================================="); - ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); + ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`, {file:appDirRelative+file.url}); + } + // clock app checks + if (app.type=="clock") { + var a = fileContents.indexOf("Bangle.loadWidgets()"); + var b = fileContents.indexOf("Bangle.setUI("); + if (a>=0 && b>=0 && a { else { match = fileContents.match(/^\s*require\(\"heatshrink\"\)\.decompress\(\s*atob\(\s*\"([^"]*)\"\s*\)\s*\)\s*$/); if (match) icon = heatshrink.decompress(Buffer.from(match[1], 'base64')); - else ERROR(`JS icon ${file.name} does not match the pattern 'require("heatshrink").decompress(atob("..."))'`); + else ERROR(`JS icon ${file.name} does not match the pattern 'require("heatshrink").decompress(atob("..."))'`, {file:appDirRelative+file.url}); } if (match) { if (icon[0] > 48 || icon[0] < 24 || icon[1] > 48 || icon[1] < 24) { - if (GRANDFATHERED_ICONS.includes(app.id)) WARN(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); - else ERROR(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); + if (GRANDFATHERED_ICONS.includes(app.id)) WARN(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`, {file:appDirRelative+file.url}); + else ERROR(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`, {file:appDirRelative+file.url}); } } } }); let dataNames = []; (app.data||[]).forEach((data)=>{ - if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`); + if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`, {file:metadataFile}); if (dataNames.includes(data.name||data.wildcard)) - ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`, {file:metadataFile}); dataNames.push(data.name||data.wildcard) allFiles.push({app: app.id, data: (data.name||data.wildcard)}); if ('name' in data && 'wildcard' in data) - ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`); + ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`, {file:metadataFile}); if (isGlob(data.name)) - ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); + ERROR(`App ${app.id} data file name ${data.name} contains wildcards`, {file:metadataFile}); if (data.wildcard) { if (!isGlob(data.wildcard)) - ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`); + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`, {file:metadataFile}); if (data.wildcard.replace(/\?|\*/g,'') === '') - ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`); + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`, {file:metadataFile}); else if (data.wildcard.replace(/\?|\*/g,'').length < 3) - WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`); + WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`, {file:metadataFile}); else if (!data.wildcard.includes(app.id)) - WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`); + WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`, {file:metadataFile}); } let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS) - if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`) + if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`, {file:metadataFile}) if ('storageFile' in data && typeof data.storageFile !== 'boolean') - ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`, {file:metadataFile}); for (const key in data) { if (!DATA_KEYS.includes(key)) - ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`, {file:metadataFile}); } }); // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) @@ -250,32 +300,41 @@ apps.forEach((app,appIdx) => { WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`)*/ // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) if (fileNames.includes(app.id+".settings.json")) - WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) + WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`, {file:metadataFile}) if (fileNames.includes(app.id+".json")) - WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`) + WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`, {file:metadataFile}) // warn if storage file matches data file of same app dataNames.forEach(dataName=>{ const glob = globToRegex(dataName) fileNames.forEach(fileName=>{ if (glob.test(fileName)) { - if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`) - else WARN(`App ${app.id} storage file ${fileName} is also listed in data`) + if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`, {file:metadataFile}) + else WARN(`App ${app.id} storage file ${fileName} is also listed in data`, {file:metadataFile}) } }) }) //console.log(fileNames); - if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); - if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); - if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`, {file:metadataFile}); + if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`, {file:metadataFile}); + if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`, {file:metadataFile}); for (const key in app) { - if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`, {file:metadataFile}); + } + if (app.type && INTERNAL_FILES_IN_APP_TYPE[app.type]) { + INTERNAL_FILES_IN_APP_TYPE[app.type].forEach(fileName => { + if (!fileNames.includes(fileName)) + ERROR(`App ${app.id} should include file named ${fileName} but it doesn't`, {file:metadataFile}); + }); } }); + + // Do not allow files from different apps to collide let fileA + while(fileA=allFiles.pop()) { if (VALID_DUPLICATES.includes(fileA.file)) - return; + break; const nameA = (fileA.file||fileA.data), globA = globToRegex(nameA), typeA = fileA.file?'storage':'data' @@ -285,9 +344,16 @@ while(fileA=allFiles.pop()) { typeB = fileB.file?'storage':'data' if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) - ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) - else if (fileA.app != fileB.app) - WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`); + else if (fileA.app != fileB.app && (!fileA.internal) && (!fileB.internal)) + WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`); } }) } + +console.log("=================================="); +console.log(`${errorCount} errors, ${warningCount} warnings`); +console.log("=================================="); +if (errorCount) { + process.exit(1); +} diff --git a/bin/thumbnailer.js b/bin/thumbnailer.js index b6862741a..0895098e9 100755 --- a/bin/thumbnailer.js +++ b/bin/thumbnailer.js @@ -1,4 +1,4 @@ -#!/usr/bin/node +#!/usr/bin/env node /* var EMULATOR = "banglejs2"; @@ -37,7 +37,8 @@ var SETTINGS = { var Const = { }; module = undefined; -eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); +var Espruino = require(__dirname + "/../core/lib/espruinotools.js"); +//eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); eval(require("fs").readFileSync(__dirname + "/../core/js/utils.js").toString()); eval(require("fs").readFileSync(__dirname + "/../core/js/appinfo.js").toString()); var apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); @@ -78,7 +79,7 @@ function getThumbnail(appId, imageFn) { settings : SETTINGS, device : { id : DEVICEID } }).then(files => { - console.log("AppInfo returned");//, files); + console.log(`AppInfo returned for ${appId}`);//, files); flashMemory.set(factoryFlashMemory); jsTransmitString("reset()\n"); console.log("Uploading..."); @@ -88,6 +89,7 @@ function getThumbnail(appId, imageFn) { appLog = ""; jsTransmitString(command); console.log("Done."); + jsTransmitString("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); jsStopIdle(); var rgba = new Uint8Array(GFX_WIDTH*GFX_HEIGHT*4); @@ -96,7 +98,7 @@ function getThumbnail(appId, imageFn) { var firstPixel = rgba32[0]; var blankImage = rgba32.every(col=>col==firstPixel) - if (appLog.indexOf("Uncaught")>=0) + if (appLog.replace("Uncaught Storage Updated!", "").indexOf("Uncaught")>=0) erroredApps.push( { id : app.id, log : appLog } ); if (!blankImage) { diff --git a/core b/core index 44d49cdbd..6857957f5 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 44d49cdbd59dc6a7e09cc0facee89d338ccc7d04 +Subproject commit 6857957f5aedfd9d175ecbf8e49d08bb167b8128 diff --git a/css/main.css b/css/main.css index f4850babe..96a102119 100644 --- a/css/main.css +++ b/css/main.css @@ -19,7 +19,7 @@ } .tile.column.col-6.col-sm-12.col-xs-12.app-tile { - border: solid 1px #fafafa; + border: solid 1px #dadee4; margin: 0; min-height: 150px; padding-top: 0.5rem; diff --git a/index.html b/index.html index de7facd5a..b141cffc9 100644 --- a/index.html +++ b/index.html @@ -147,6 +147,10 @@ Always update time when we connect +