diff --git a/README.md b/README.md index 49f616964..ac80b8270 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ Bangle.js App Loader (and Apps) ================================ -[![Build Status](https://travis-ci.org/espruino/BangleApps.svg?branch=master)](https://travis-ci.org/espruino/BangleApps) +[![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) -* Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) +* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/) **All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, @@ -49,25 +49,25 @@ easily distinguish between file types, we use the following: ## Adding your app to the menu -* Come up with a unique (all lowercase, no spaces) name, we'll assume `7chname`. Bangle.js +* Come up with a unique (all lowercase, no spaces) name, we'll assume `myappid`. Bangle.js is limited to 28 char filenames and appends a file extension (eg `.js`) so please try and keep filenames short to avoid overflowing the buffer. -* Create a folder called `apps/`, lets assume `apps/7chname` +* Create a folder called `apps/`, lets assume `apps/myappid` * We'd recommend that you copy files from 'Example Applications' (below) as a base, or... -* `apps/7chname/app.png` should be a 48px icon -* Use http://www.espruino.com/Image+Converter to create `apps/7chname/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" +* `apps/myappid/app.png` should be a 48px icon +* Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String" * Create an entry in `apps.json` as follows: ``` -{ "id": "7chname", +{ "id": "myappid", "name": "My app's human readable name", "shortName" : "Short Name", "icon": "app.png", "description": "A detailed description of my great app", "tags": "", "storage": [ - {"name":"7chname.app.js","url":"app.js"}, - {"name":"7chname.img","url":"app-icon.js","evaluate":true} + {"name":"myappid.app.js","url":"app.js"}, + {"name":"myappid.img","url":"app-icon.js","evaluate":true} ], }, ``` @@ -95,12 +95,12 @@ Be aware of the delay between commits and updates on github.io - it can take a f Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/) (4 discs), upload your files into the places described in your JSON: -* `app-icon.js` -> `7chname.img` +* `app-icon.js` -> `myappid.img` Now load `app.js` up in the editor, and click the down-arrow to the bottom right of the `Send to Espruino` icon. Click `Storage` and then either choose -`7chname.app.js` (if you'd uploaded your app previously), or `New File` -and then enter `7chname.app.js` as the name. +`myappid.app.js` (if you'd uploaded your app previously), or `New File` +and then enter `myappid.app.js` as the name. Now, clicking the `Send to Espruino` icon will load the app directly into Espruino **and** will automatically run it. @@ -115,10 +115,13 @@ and set it to `Load default application`. ## Example Applications To make the process easier we've come up with some example applications that you can use as a base -when creating your own. Just come up with a unique 7 character name, copy `apps/_example_app` -or `apps/_example_widget` to `apps/7chname`, and add `apps/_example_X/add_to_apps.json` to +when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app` +or `apps/_example_widget` to `apps/myappid`, and add `apps/_example_X/add_to_apps.json` to `apps.json`. +**Note:** the max filename length is 28 chars, so we suggest an app ID of under +20 so that when `.app.js`/etc gets added to the end the filename isn't cropped. + **If you're making a widget** please start the name with `wid` to make it easy to find! @@ -192,8 +195,8 @@ and which gives information about the app for the Launcher. ``` { "name":"Short Name", // for Bangle.js menu - "icon":"*7chname", // for Bangle.js menu - "src":"-7chname", // source file + "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 @@ -217,8 +220,10 @@ and which gives information about the app for the Launcher. { "id": "appid", // 7 character app id "name": "Readable name", // readable name "shortName": "Short name", // short name for launcher - "icon": "icon.png", // icon in apps/ + "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 "type":"...", // optional(if app) - // 'app' - an application // 'widget' - a widget @@ -226,7 +231,9 @@ and which gives information about the app for the Launcher. // 'bootloader' - code that runs at startup only // 'RAM' - code that runs and doesn't upload anything to storage "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" : { "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 // that contains more information about this app (usage, etc) @@ -259,6 +266,9 @@ and which gives information about the app for the Launcher. // (eg it's evaluated as JS) "noOverwrite":true // if supplied, this file will not be overwritten if it // already exists + "supports": ["BANGLEJS2"]// if supplied, this file will ONLY be uploaded to the device + // types named in the array. This allows different versions of + // the app to be uploaded for different platforms }, ] "data": [ // list of files the app writes to @@ -306,10 +316,10 @@ version of what's in `apps.json`: + + + + diff --git a/apps/gallifr/screenshot_time.png b/apps/gallifr/screenshot_time.png new file mode 100644 index 000000000..2754138c4 Binary files /dev/null and b/apps/gallifr/screenshot_time.png differ diff --git a/apps/gpstime/ChangeLog b/apps/gpstime/ChangeLog index a3bd6351e..4d9bbc8a2 100644 --- a/apps/gpstime/ChangeLog +++ b/apps/gpstime/ChangeLog @@ -1,2 +1,3 @@ 0.03: Fix time output on new firmwares when no GPS time set (fix #104) -0.04: Fix shown UTC time zone sign \ No newline at end of file +0.04: Fix shown UTC time zone sign +0.05: Use new 'layout library for Bangle2, fix #764 by adding a back button diff --git a/apps/gpstime/gpstime-icon.js b/apps/gpstime/gpstime-icon.js index 665c8d5f6..99998c6c4 100644 --- a/apps/gpstime/gpstime-icon.js +++ b/apps/gpstime/gpstime-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA=")) +require("heatshrink").decompress(atob("mEw4UA////G161hyd8Jf4ALlQLK1WABREC1WgBZEK32oFxPW1QuJ7QwIFwOqvQLHhW31NaBY8qy2rtUFoAuG3W61EVqALF1+qr2gqtUHQu11dawNVqo6F22q9XFBYIwEhWqz2r6oLBGAheBqwuBBYx2CFwQLGlWqgoLCMAsKLoILChR6EgQuDqkqYYsBFweqYYoLDoWnYYoLD/WVYYv8FwXqPoIwEn52BqGrPoILEh/1FwOl9SsBBYcD/pdB2uq/QvEh/8LoOu1xHFh8/gGp9WWL4oMBgWltXeO4owBgWt1ReFYYh2GYYmXEQzDD3wiHegYKIGAJRGAAguJAH4AC")) diff --git a/apps/gpstime/gpstime.js b/apps/gpstime/gpstime.js index a061d2e23..8c80953fa 100644 --- a/apps/gpstime/gpstime.js +++ b/apps/gpstime/gpstime.js @@ -1,68 +1,75 @@ -var img = require("heatshrink").decompress(atob("mEwghC/AH8A1QWVhWq0AuVAAIuVAAIwT1WinQwTFwMzmQwTCYMjlUqGCIuBlWi0UzC6JdBIoMjC4UDmAuOkYXBPAWgmczLp2ilUiVAUDC4IwLFwIUBLoJ2BFwQwM1WjCgJ1DFwQwLFwJ1B0SQCkQWDGBQXBCgK9BDgKQBAAgwJOwUzRgIDBC54wCkZdGPBwACRgguDBIIwLFxEJBQIwLFxGaBYQwKFxQwLgAWGmQuBcAQwJC48ifYYwJgUidgsyC4L7DGBIXBdohnBCgL7BcYIXIGAqMCIoL7DL5IwERgIUBLoL7BO5QXBGAK7DkWiOxQXGFwOjFoUyFxZhDgBdCCgJ1CCxYxCgBABkcqOwIuNGAQXC0S9BLpgAFXoIwBmYuPAAYwCLp4wHFyYwDFyYwDFygwCCyoA/AFQA=")); +function satelliteImage() { + return require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AGnE4F1wvsF34wgFldcLdyMYsoACF1WJF4YxPFzOtF4wxNFzAvKSiIvU1ovIGAkJAAQucF5QxCFwYwbF4QwLrwvjYIVfrwABrtdq9Wqwvkq4oCAAtXmYvi1teE4NXrphCrxoCGAbvdSIoAHNQNeFzQvGeRQvCsowrYYNfF8YwHZQQFCF8QwGF4owjeYovBroHEMERhEF8IwNrtWryYFF8YwCq4vhGBeJF5AwaxIwKwVXFwwvandfMJeJF8M6nZiLGQIvdstfGAVlGBZkCxJeZJQIwCGIRjMFzYACGIc6r/+FsIvGGIYABEzYvPGQYvusovkAH4A/AH4A/ACo=")); +} + +var fix; Bangle.setLCDPower(1); Bangle.setLCDTimeout(0); +var Layout = require("Layout"); +Bangle.setGPSPower(1, "app"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +E.showMessage("Loading..."); // avoid showing rubbish on screen -g.clear(); - -var fix; -Bangle.setGPSPower(1); -Bangle.on('GPS',function(f) { - fix = f; - g.reset(1); - g.setFont("6x8",2); - g.setFontAlign(0,0); - g.clearRect(90,30,239,90); - if (fix.fix) { - g.drawString("GPS",170,40); - g.drawString("Acquired",170,60); +function setGPSTime() { + if (fix.time!==undefined) { + setTime(fix.time.getTime()/1000); + E.showMessage("System time set", {img:require("heatshrink").decompress(atob("lEo4UBvvv///vEFBYNVAAWq1QFDBAgKGrQJD0oJDtQJD1IICqwGBFoIDByocDwAJBgQeDtWoJwcqDwWq0EAgfAgEKHoQcCBIQeBGAQaBBIQzBytaEwQJDlWlrQmBBIkK0tqBI+ptRNCBIcCBKhECBIh6CAgUL8AJHl/4BI8+3gJRl/8GJH/BI8Ah6MDLIZQB+BjGAAIoBBI84BIaVCAAaVBVIYJEWYLkEXobRDAAbRBcoYACcoT5DEwYJCtQoElWpBINaDwYcB0oJBGQIzCAYIwBDwQGBAAIcCDwYACDgQACBIYIEBQYFDA="))}); } else { - g.drawString("Waiting for",170,40); - g.drawString("GPS Fix",170,60); + E.showMessage("No GPS time to set"); } - g.setFont("6x8"); - g.drawString(fix.satellites+" satellites",170,80); - g.clearRect(0,100,239,239); - var t = ["","","","---",""]; - if (fix.time!==undefined) + Bangle.removeListener('GPS',onGPS); + setTimeout(function() { + fix = undefined; + layout.forgetLazyState(); // redraw all next time + Bangle.on('GPS',onGPS); + }, 2000); +} + +var layout = new Layout( { + type:"v", c: [ + {type:"h", c:[ + {type:"img", src:satelliteImage }, + { type:"v", fillx:1, c: [ + {type:"txt", font:"6x8:2", label:"Waiting\nfor GPS", id:"status" }, + {type:"txt", font:"6x8", label:"---", id:"sat" }, + ]}, + ]}, + {type:"txt", fillx:1, filly:1, font:"6x8:2", label:"---", id:"gpstime" } + ]},{lazy:true, btns: [ + { label : "Set", cb : setGPSTime}, + { label : "Back", cb : ()=>load() } + ]}); + + +function onGPS(f) { + if (fix===undefined) { + g.clear(); + Bangle.drawWidgets(); + } + fix = f; + if (fix.fix) { + layout.status.label = "GPS\nAcquired"; + } else { + layout.status.label = "Waiting\nfor GPS"; + } + layout.sat.label = fix.satellites+" satellites"; + + var t = ["","---",""]; + if (fix.time!==undefined) { t = fix.time.toString().split(" "); - /* - [ - "Sun", - "Nov", - "10", - "2019", - "15:55:35", - "GMT+0100" - ] - */ - //g.setFont("6x8",2); - //g.drawString(t[0],120,110); // day - g.setFont("6x8",3); - g.drawString(t[1]+" "+t[2],120,135); // date - g.setFont("6x8",2); - g.drawString(t[3],120,160); // year - g.setFont("6x8",3); - g.drawString(t[4],120,185); // time - if (fix.time) { - // timezone var tz = (new Date()).getTimezoneOffset()/-60; if (tz==0) tz="UTC"; else if (tz>0) tz="UTC+"+tz; else tz="UTC"+tz; - g.setFont("6x8",2); - g.drawString(tz,120,210); // gmt - g.setFontAlign(0,0,3); - g.drawString("Set",230,120); - g.setFontAlign(0,0); - } -}); -setInterval(function() { - g.drawImage(img,48,48,{scale:1.5,rotate:Math.sin(getTime()*2)/2}); -},100); -setWatch(function() { - if (fix.time!==undefined) - setTime(fix.time.getTime()/1000); -}, BTN2, {repeat:true}); + t = [t[1]+" "+t[2],t[3],t[4],t[5],tz]; + } + + layout.gpstime.label = t.join("\n"); + layout.render(); +} + +Bangle.on('GPS',onGPS); diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/Changelog new file mode 100644 index 000000000..7f837e50e --- /dev/null +++ b/apps/gpstouch/Changelog @@ -0,0 +1 @@ +0.01: First version diff --git a/apps/gpstouch/README.md b/apps/gpstouch/README.md new file mode 100644 index 000000000..7329f9833 --- /dev/null +++ b/apps/gpstouch/README.md @@ -0,0 +1,16 @@ +# GPS Touch + +- A touch controlled GPS watch for Bangle JS 2 +- Key feature is the conversion of Lat/Lon into Ordinance Servey Grid Reference +- Swipe left and right to change the display +- Select GPS and switch the GPS On or Off by touching twice in the top half of the display +- Select LOGGER and switch the GPS Recorder On or Off by touching twice in the top half of the display +- Displays the GPS time in the bottom half of the screen when the GPS is powered on, otherwise 00:00:00 +- Select display of Course, Speed, Altitude, Longitude, Latitude, Ordinance Servey Grid Reference + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) +![](screenshot4.png) diff --git a/apps/gpstouch/geotools.js b/apps/gpstouch/geotools.js new file mode 100644 index 000000000..5adc57872 --- /dev/null +++ b/apps/gpstouch/geotools.js @@ -0,0 +1,128 @@ +/** + * + * A module of Geo functions for use with gps fixes + * + * let geo = require("geotools"); + * let os = geo.gpsToOSGrid(fix); + * let ref = geo.gpsToOSMapRef(fix); + * + */ + +Number.prototype.toRad = function() { return this*Math.PI/180; }; +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* Ordnance Survey Grid Reference functions (c) Chris Veness 2005-2014 */ +/* - www.movable-type.co.uk/scripts/gridref.js */ +/* - www.movable-type.co.uk/scripts/latlon-gridref.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +function OsGridRef(easting, northing) { + this.easting = 0|easting; + this.northing = 0|northing; +} +OsGridRef.latLongToOsGrid = function(point) { + var lat = point.lat.toRad(); + var lon = point.lon.toRad(); + + var a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes + var F0 = 0.9996012717; // NatGrid scale factor on central meridian + var lat0 = (49).toRad(), lon0 = (-2).toRad(); // NatGrid true origin is 49�N,2�W + var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres + var e2 = 1 - (b*b)/(a*a); // eccentricity squared + var n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n; + + var cosLat = Math.cos(lat), sinLat = Math.sin(lat); + var nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat); // transverse radius of curvature + var rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5); // meridional radius of curvature + var eta2 = nu/rho-1; + + var Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0); + var Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0); + var Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0)); + var Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0)); + var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc + + var cos3lat = cosLat*cosLat*cosLat; + var cos5lat = cos3lat*cosLat*cosLat; + var tan2lat = Math.tan(lat)*Math.tan(lat); + var tan4lat = tan2lat*tan2lat; + + var I = M + N0; + var II = (nu/2)*sinLat*cosLat; + var III = (nu/24)*sinLat*cos3lat*(5-tan2lat+9*eta2); + var IIIA = (nu/720)*sinLat*cos5lat*(61-58*tan2lat+tan4lat); + var IV = nu*cosLat; + var V = (nu/6)*cos3lat*(nu/rho-tan2lat); + var VI = (nu/120) * cos5lat * (5 - 18*tan2lat + tan4lat + 14*eta2 - 58*tan2lat*eta2); + + var dLon = lon-lon0; + var dLon2 = dLon*dLon, dLon3 = dLon2*dLon, dLon4 = dLon3*dLon, dLon5 = dLon4*dLon, dLon6 = dLon5*dLon; + + var N = I + II*dLon2 + III*dLon4 + IIIA*dLon6; + var E = E0 + IV*dLon + V*dLon3 + VI*dLon5; + + return new OsGridRef(E, N); +}; + +/* + * converts northing, easting to standard OS grid reference. + * + * [digits=10] - precision (10 digits = metres) + * to_map_ref(8, 651409, 313177); => 'TG 5140 1317' + * to_map_ref(0, 651409, 313177); => '651409,313177' + * + */ +function to_map_ref(digits, easting, northing) { + if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision '${digits}'`); // eslint-disable-line comma-spacing + + let e = easting; + let n = northing; + + // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7 + if (digits == 0) { + const format = { useGrouping: false, minimumIntegerDigits: 6, maximumFractionDigits: 3 }; + const ePad = e.toLocaleString('en', format); + const nPad = n.toLocaleString('en', format); + return `${ePad},${nPad}`; + } + + // get the 100km-grid indices + const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000); + + // translate those into numeric equivalents of the grid letters + let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5); + let l2 = (19 - n100km) * 5 % 25 + e100km % 5; + + // compensate for skipped 'I' and build grid letter-pairs + if (l1 > 7) l1++; + if (l2 > 7) l2++; + const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0)); + + // strip 100km-grid indices from easting & northing, and reduce precision + e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2)); + n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2)); + + // pad eastings & northings with leading zeros + e = e.toString().padStart(digits/2, '0'); + n = n.toString().padStart(digits/2, '0'); + + return `${letterPair} ${e} ${n}`; +} + +/** + * + * Module exports section, example code below + * + * let geo = require("geotools"); + * let os = geo.gpsToOSGrid(fix); + * let ref = geo.gpsToOSMapRef(fix); + */ + +// get easting and northings +exports.gpsToOSGrid = function(gps_fix) { + return OsGridRef.latLongToOsGrid(gps_fix); +} + +// string with an OS Map grid reference +exports.gpsToOSMapRef = function(gps_fix) { + let os = OsGridRef.latLongToOsGrid(last_fix); + return to_map_ref(6, os.easting, os.northing); +} diff --git a/apps/gpstouch/gpstouch.app.js b/apps/gpstouch/gpstouch.app.js new file mode 100644 index 000000000..4e49dd1e5 --- /dev/null +++ b/apps/gpstouch/gpstouch.app.js @@ -0,0 +1,246 @@ +const h = g.getHeight(); +const w = g.getWidth(); +let geo = require("geotools"); +let last_fix; +let listennerCount = 0; + +function log_debug(o) { + //console.log(o); +} + +function resetLastFix() { + last_fix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + speed: 0, + time: 0, + course: 0, + satellites: 0 + }; +} + +function processFix(fix) { + last_fix.time = fix.time; + log_debug(fix); + + if (fix.fix) { + if (!last_fix.fix) { + // we dont need to suppress this in quiet mode as it is user initiated + Bangle.buzz(1500); // buzz on first position + } + last_fix = fix; + } +} + +function draw() { + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4].substr(0,5); + var hh = da[4].substr(0,2); + var mm = da[4].substr(3,2); + + g.reset(); + drawTop(d,hh,mm); + drawInfo(); +} + +function drawTop(d,hh,mm) { + g.setFont("Vector", w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.bg); + g.fillRect(0, 24, w, ((h-24)/2) + 24); + g.setColor(g.theme.fg); + + g.setFontAlign(1,0); // right aligned + g.drawString(hh, (w/2) - 6, ((h-24)/4) + 24); + g.setFontAlign(-1,0); // left aligned + g.drawString(mm, (w/2) + 6, ((h-24)/4) + 24); + + // for the colon + g.setFontAlign(0,0); // centre aligned + if (d.getSeconds()&1) g.drawString(":", w/2, ((h-24)/4) + 24); +} + +function drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + g.setFont("Vector", w/7); + g.setFontAlign(0, 0); + + if (infoData[infoMode].get_color) + g.setColor(infoData[infoMode].get_color()); + else + g.setColor("#0ff"); + g.fillRect(0, ((h-24)/2) + 24 + 1, w, h); + + if (infoData[infoMode].is_control) + g.setColor("#fff"); + else + g.setColor("#000"); + + g.drawString((infoData[infoMode].calc()), w/2, (3*(h-24)/4) + 24); + } +} + +const infoData = { + ID_LAT: { + calc: () => 'Lat: ' + last_fix.lat.toFixed(4), + }, + ID_LON: { + calc: () => 'Lon: ' + last_fix.lon.toFixed(4), + }, + ID_SPEED: { + calc: () => 'Speed: ' + last_fix.speed.toFixed(1), + }, + ID_ALT: { + calc: () => 'Alt: ' + last_fix.alt.toFixed(0), + }, + ID_COURSE: { + calc: () => 'Course: '+ last_fix.course.toFixed(0), + }, + ID_SATS: { + calc: () => 'Satelites: ' + last_fix.satellites, + }, + ID_TIME: { + calc: () => formatTime(last_fix.time), + }, + OS_REF: { + calc: () => !last_fix.fix ? "OO 000 000" : geo.gpsToOSMapRef(last_fix), + }, + GPS_POWER: { + calc: () => (Bangle.isGPSOn()) ? 'GPS On' : 'GPS Off', + action: () => toggleGPS(), + get_color: () => Bangle.isGPSOn() ? '#f00' : '#00f', + is_control: true, + }, + GPS_LOGGER: { + calc: () => 'Logger ' + loggerStatus(), + action: () => toggleLogger(), + get_color: () => loggerStatus() == "ON" ? '#f00' : '#00f', + is_control: true, + }, +}; + +function toggleGPS() { + if (loggerStatus() == "ON") + return; + + Bangle.setGPSPower(Bangle.isGPSOn() ? 0 : 1, 'gpstouch'); + // add or remove listenner + if (Bangle.isGPSOn()) { + if (listennerCount == 0) { + Bangle.on('GPS', processFix); + listennerCount++; + log_debug("listennerCount=" + listennerCount); + } + } else { + if (listennerCount > 0) { + Bangle.removeListener("GPS", processFix); + listennerCount--; + log_debug("listennerCount=" + listennerCount); + } + } + resetLastFix(); +} + +function loggerStatus() { + var settings = require("Storage").readJSON("gpsrec.json",1)||{}; + if (settings == {}) return "Install"; + return settings.recording ? "ON" : "OFF"; +} + +function toggleLogger() { + var settings = require("Storage").readJSON("gpsrec.json",1)||{}; + if (settings == {}) return; + + settings.recording = !settings.recording; + require("Storage").write("gpsrec.json", settings); + + if (WIDGETS["gpsrec"]) + WIDGETS["gpsrec"].reload(); + + if (settings.recording && listennerCount == 0) { + Bangle.on('GPS', processFix); + listennerCount++; + log_debug("listennerCount=" + listennerCount); + } +} + +function formatTime(now) { + try { + var fd = now.toUTCString().split(" "); + return fd[4]; + } catch (e) { + return "00:00:00"; + } +} + +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + +Bangle.on('swipe', dir => { + if (dir == 1) prevInfo(); else nextInfo(); + draw(); +}); + +let prevTouch = 0; + +Bangle.on('touch', function(button, xy) { + let dur = 1000*(getTime() - prevTouch); + prevTouch = getTime(); + + if (dur <= 1000 && xy.y < h/2 && infoData[infoMode].is_control) { + Bangle.buzz(); + if (infoData[infoMode] && infoData[infoMode].action) { + infoData[infoMode].action(); + draw(); + } + } +}); + +// 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(); +}); + +resetLastFix(); + +// add listenner if already powered on, plus tag app +if (Bangle.isGPSOn() || loggerStatus() == "ON") { + Bangle.setGPSPower(1, 'gpstouch'); + if (listennerCount == 0) { + Bangle.on('GPS', processFix); + listennerCount++; + log_debug("listennerCount=" + listennerCount); + } +} + +g.clear(); +var secondInterval = setInterval(draw, 1000); +draw(); +// Show launcher when button pressed +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/gpstouch/gpstouch.icon.js b/apps/gpstouch/gpstouch.icon.js new file mode 100644 index 000000000..c4cf85676 --- /dev/null +++ b/apps/gpstouch/gpstouch.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA=")) diff --git a/apps/gpstouch/gpstouch.png b/apps/gpstouch/gpstouch.png new file mode 100644 index 000000000..c411356ae Binary files /dev/null and b/apps/gpstouch/gpstouch.png differ diff --git a/apps/gpstouch/screenshot1.png b/apps/gpstouch/screenshot1.png new file mode 100644 index 000000000..03cb1e2a9 Binary files /dev/null and b/apps/gpstouch/screenshot1.png differ diff --git a/apps/gpstouch/screenshot2.png b/apps/gpstouch/screenshot2.png new file mode 100644 index 000000000..a05794b34 Binary files /dev/null and b/apps/gpstouch/screenshot2.png differ diff --git a/apps/gpstouch/screenshot3.png b/apps/gpstouch/screenshot3.png new file mode 100644 index 000000000..9e3115d72 Binary files /dev/null and b/apps/gpstouch/screenshot3.png differ diff --git a/apps/gpstouch/screenshot4.png b/apps/gpstouch/screenshot4.png new file mode 100644 index 000000000..924371f5f Binary files /dev/null and b/apps/gpstouch/screenshot4.png differ diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog index 0ca30d066..aaa55d01a 100644 --- a/apps/hcclock/ChangeLog +++ b/apps/hcclock/ChangeLog @@ -1,2 +1,2 @@ 0.01: base code - +0.02: saved settings when switching color scheme \ No newline at end of file diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js index 98abbc6f3..4664dd763 100644 --- a/apps/hcclock/hcclock.app.js +++ b/apps/hcclock/hcclock.app.js @@ -174,19 +174,52 @@ function fmtDate(day,month,year,hour) let ap = "(AM)"; if(hour == 0 || hour > 12) ap = "(PM)"; - return months[month] + " " + day + " " + year + " "+ ap; + return months[month] + " " + day + " " + year + " "+ ap; } else return months[month] + ". " + day + " " + year; } -// Handles Flipping colors, then refreshes the UI + +////////////////////////////////////////// +// +// HANDLE COLORS + SETTINGS +// + +function getColorScheme() +{ + let settings = require('Storage').readJSON("hcclock.json", true) || {}; + if (!("scheme" in settings)) { + settings.scheme = 0; + } + return settings.scheme; +} + +function setColorScheme(value) +{ + let settings = require('Storage').readJSON("hcclock.json", true) || {}; + settings.scheme = value; + require('Storage').writeJSON('hcclock.json', settings); + + if(value == 0) // White + { + bg = 255; + fg = 0; + } + else // Black + { + bg = 0; + fg = 255; + } + redraw(); +} + function flipColors() { - let t = bg; - bg = fg; - fg = t; - redraw(); + if(getColorScheme() == 0) + setColorScheme(1); + else + setColorScheme(0); } ////////////////////////////////////////// @@ -197,7 +230,7 @@ function flipColors() // Initialize g.clear(); Bangle.loadWidgets(); -redraw(); +setColorScheme(getColorScheme()); // Define Refresh Interval setInterval(updateTime, interval); diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 5560f00bc..5eb96a0ea 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -1 +1,8 @@ 0.01: New App! +0.02: Modified data format to include daily summaries +0.03: Settings to turn HRM on +0.04: Add HRM graph view + Don't restart HRM when changing apps if we've already got a good BPM value +0.05: Fix daily summary calculation +0.06: Fix daily health summary for movement (a line got deleted!) +0.07: Added coloured bar charts diff --git a/apps/health/README.md b/apps/health/README.md index 0ba0d8228..c69e2e45b 100644 --- a/apps/health/README.md +++ b/apps/health/README.md @@ -14,10 +14,18 @@ To view data, run the `Health` app from your watch. Stores: -* Heart rate (TODO) +* Heart rate * Step count * Movement +## Settings + +* **Heart Rt** - Whether to monitor heart rate or not + * **Off** - Don't turn HRM on, but record heart rate if the HRM was turned on by another app/widget + * **10 Min** - Turn HRM on every 10 minutes (for each heath entry) and turn it off after 2 minutes, or when a good reading is found + * **Always** - Keep HRM on all the time (more accurate recording, but reduces battery life to ~36 hours) + + ## Technical Info Once installed, the `health.boot.js` hooks onto the `Bangle.health` event and @@ -28,7 +36,6 @@ to grab historical health info. ## TODO -* **Extend file format to include combined data for each day (to make graphs faster)** * `interface` page for desktop to allow data to be viewed and exported in common formats * More features in app: * Step counting goal (ensure pedometers use this) diff --git a/apps/health/app.js b/apps/health/app.js index f2df52972..eae45c190 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -1,28 +1,74 @@ +function getSettings() { + return require("Storage").readJSON("health.json",1)||{}; +} + +function setSettings(s) { + require("Storage").writeJSON("health.json",s); +} + function menuMain() { + swipe_enabled = false; + clearButton(); E.showMenu({ "":{title:"Health Tracking"}, "< Back":()=>load(), "Step Counting":()=>menuStepCount(), - "Movement":()=>menuMovement() + "Movement":()=>menuMovement(), + "Heart Rate":()=>menuHRM(), + "Settings":()=>menuSettings() + }); +} + +function menuSettings() { + swipe_enabled = false; + clearButton(); + var s=getSettings(); + E.showMenu({ + "":{title:"Health Tracking"}, + "< Back":()=>menuMain(), + "Heart Rt":{ + value : 0|s.hrm, + min : 0, max : 2, + format : v=>["Off","10 mins","Always"][v], + onchange : v => { s.hrm=v;setSettings(s); } + } }); } function menuStepCount() { + swipe_enabled = false; + clearButton(); E.showMenu({ "":{title:"Step Counting"}, "< Back":()=>menuMain(), - "per hour":()=>stepsPerHour() + "per hour":()=>stepsPerHour(), + "per day":()=>stepsPerDay() }); } function menuMovement() { + swipe_enabled = false; + clearButton(); E.showMenu({ "":{title:"Movement"}, "< Back":()=>menuMain(), - "per hour":()=>movementPerHour() + "per hour":()=>movementPerHour(), + "per day":()=>movementPerDay(), }); } +function menuHRM() { + swipe_enabled = false; + clearButton(); + E.showMenu({ + "":{title:"Heart Rate"}, + "< Back":()=>menuMain(), + "per hour":()=>hrmPerHour(), + "per day":()=>hrmPerDay(), + }); +} + + function stepsPerHour() { E.showMessage("Loading..."); var data = new Uint16Array(24); @@ -30,14 +76,51 @@ function stepsPerHour() { g.clear(1); Bangle.drawWidgets(); g.reset(); - require("graph").drawBar(g, data, { - y:24, - miny: 0, - axes : true, - gridx : 6, - gridy : 500 + setButton(menuStepCount); + barChart("HOUR", data); +} + +function stepsPerDay() { + E.showMessage("Loading..."); + var data = new Uint16Array(31); + require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); + g.clear(1); + Bangle.drawWidgets(); + g.reset(); + setButton(menuStepCount); + barChart("DAY", data); +} + +function hrmPerHour() { + E.showMessage("Loading..."); + 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]++; }); - Bangle.setUI("updown", ()=>menuStepCount()); + data.forEach((d,i)=>data[i] = d/cnt[i]); + g.clear(1); + Bangle.drawWidgets(); + g.reset(); + setButton(menuHRM); + barChart("HOUR", data); +} + +function hrmPerDay() { + E.showMessage("Loading..."); + 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]++; + }); + data.forEach((d,i)=>data[i] = d/cnt[i]); + g.clear(1); + Bangle.drawWidgets(); + g.reset(); + setButton(menuHRM); + barChart("DAY", data); } function movementPerHour() { @@ -47,14 +130,123 @@ function movementPerHour() { g.clear(1); Bangle.drawWidgets(); g.reset(); - require("graph").drawLine(g, data, { - y:24, - miny: 0, - axes : true, - gridx : 6, - ylabel : null - }); - Bangle.setUI("updown", ()=>menuStepCount()); + setButton(menuMovement); + barChart("HOUR", data); +} + +function movementPerDay() { + E.showMessage("Loading..."); + var data = new Uint16Array(31); + require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); + g.clear(1); + Bangle.drawWidgets(); + g.reset(); + setButton(menuMovement); + barChart("DAY", data); +} + +// Bar Chart Code + +const w = g.getWidth(); +const h = g.getHeight(); + +var data_len; +var chart_index; +var chart_max_datum; +var chart_label; +var chart_data; +var swipe_enabled = false; +var btn; + +// find the max value in the array, using a loop due to array size +function max(arr) { + var m = -Infinity; + + for(var i=0; i< arr.length; i++) + if(arr[i] > m) m = arr[i]; + return m; +} + +// find the end of the data, the array might be for 31 days but only have 2 days of data in it +function get_data_length(arr) { + var nlen = arr.length; + + for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--) + nlen--; + + return nlen; +} + +function barChart(label, dt) { + data_len = get_data_length(dt); + chart_index = Math.max(data_len - 5, -5); // choose initial index that puts the last day on the end + chart_max_datum = max(dt); // find highest bar, for scaling + chart_label = label; + chart_data = dt; + drawBarChart(); + swipe_enabled = true; +} + +function drawBarChart() { + const bar_bot = 140; + const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre + var bar_top; + var bar; + + g.setColor(g.theme.bg); + g.fillRect(0,24,w,h); + + for (bar = 1; bar < 10; bar++) { + if (bar == 5) { + g.setFont('6x8', 2); + g.setFontAlign(0,-1) + g.setColor(g.theme.fg); + g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150); + g.setColor("#00f"); + } else { + g.setColor("#0ff"); + } + + // 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) + bar_top = bar_bot - 100 * (chart_data[chart_index + bar - 1]) / chart_max_datum; + else + bar_top = bar_bot; + + g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top); + g.setColor(g.theme.fg); + g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top); + } +} + +function next_bar() { + chart_index = Math.min(data_len - 5, chart_index + 1); +} + +function prev_bar() { + // HOUR data starts at index 0, DAY data starts at index 1 + chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1); +} + +Bangle.on('swipe', dir => { + if (!swipe_enabled) return; + if (dir == 1) prev_bar(); else next_bar(); + drawBarChart(); +}); + +// use setWatch() as Bangle.setUI("updown",..) interacts with swipes +function setButton(fn) { + if (process.env.HWVERSION == 1) + btn = setWatch(fn, BTN2); + else + btn = setWatch(fn, BTN1); +} + +function clearButton() { + if (btn !== undefined) { + clearWatch(btn); + btn = undefined; + } } Bangle.loadWidgets(); diff --git a/apps/health/boot.js b/apps/health/boot.js index d6b84ce98..386d75833 100644 --- a/apps/health/boot.js +++ b/apps/health/boot.js @@ -1,10 +1,27 @@ +(function(){ + var settings = require("Storage").readJSON("health.json",1)||{}; + var hrm = 0|settings.hrm; + if (hrm==1) { + function onHealth() { + Bangle.setHRMPower(1, "health"); + setTimeout(()=>Bangle.setHRMPower(0, "health"),2*60000); // give it 2 minutes + } + Bangle.on("health", onHealth); + Bangle.on('HRM', h => { + if (h.confidence>80) Bangle.setHRMPower(0, "health"); + }); + if (Bangle.getHealthStatus().bpmConfidence) return; + onHealth(); + } else Bangle.setHRMPower(hrm!=0, "health"); +})(); + Bangle.on("health", health => { // ensure we write health info for *last* block var d = new Date(Date.now() - 590000); const DB_RECORD_LEN = 4; const DB_RECORDS_PER_HR = 6; - const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24; + const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/; const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31; const DB_HEADER_LEN = 8; const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; @@ -17,6 +34,12 @@ Bangle.on("health", health => { (DB_RECORDS_PER_HR*d.getHours()) + (0|(d.getMinutes()*DB_RECORDS_PER_HR/60)); } + function getRecordData(health) { + return String.fromCharCode( + health.steps>>8,health.steps&255, // 16 bit steps + health.bpm, // 8 bit bpm + Math.min(health.movement / 8, 255)); // movement + } var rec = getRecordIdx(d); var fn = getRecordFN(d); @@ -30,9 +53,32 @@ Bangle.on("health", health => { } else { require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header } - var recordData = String.fromCharCode( - health.steps>>8,health.steps&255, // 16 bit steps - health.bpm, // 8 bit bpm - Math.min(health.movement / 8, 255)); // movement - require("Storage").write(fn, recordData, DB_HEADER_LEN+(rec*DB_RECORD_LEN), DB_FILE_LEN); + var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN); + require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN); + if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return; + // we're at the end of the day. Read in all of the data for the day and sum it up + var sumPos = recordPos + DB_RECORD_LEN; // record after the current one is the sum + if (f.substr(sumPos, DB_RECORD_LEN)!="\xFF\xFF\xFF\xFF") { + print("HEALTH ERR: Daily summary already written!"); + return; + } + health = { steps:0, bpm:0, movement:0, movCnt:0, bpmCnt:0}; + var records = DB_RECORDS_PER_HR*24; + for (var i=0;i + + + + +
+ + + + + diff --git a/apps/health/lib.js b/apps/health/lib.js index 791c4ce22..70305bff8 100644 --- a/apps/health/lib.js +++ b/apps/health/lib.js @@ -1,6 +1,6 @@ const DB_RECORD_LEN = 4; const DB_RECORDS_PER_HR = 6; -const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24; +const DB_RECORDS_PER_DAY = DB_RECORDS_PER_HR*24 + 1/*summary*/; const DB_RECORDS_PER_MONTH = DB_RECORDS_PER_DAY*31; const DB_HEADER_LEN = 8; const DB_FILE_LEN = DB_HEADER_LEN + DB_RECORDS_PER_MONTH*DB_RECORD_LEN; @@ -16,12 +16,12 @@ function getRecordIdx(d) { // Read all records from the given month exports.readAllRecords = function(d, cb) { - var rec = getRecordIdx(d); var fn = getRecordFN(d); var f = require("Storage").read(fn); + if (f===undefined) return; var idx = DB_HEADER_LEN; for (var day=0;day<31;day++) { - for (var hr=0;hr<24;hr++) { + for (var hr=0;hr<24;hr++) { // actually 25, see below for (var m=0;mg.getWidth()) { hrmOffset=0; - g.clearRect(0,80,239,239); - g.moveTo(-100,0); + g.clearRect(0,80,g.getWidth(),g.getHeight()); + lastHrmPt = [-100,0]; } y = E.clip(btm-v.filt/4,btm-10,btm); g.setColor(1,0,0).fillRect(hrmOffset,btm, hrmOffset, y); y = E.clip(170 - (v.raw/2),80,btm); - g.setColor(g.theme.fg).lineTo(hrmOffset, y); + g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y); + lastHrmPt = [hrmOffset, y]; if (counter !==undefined) { counter = undefined; - g.clear(); + g.clearRect(0,24,g.getWidth(),g.getHeight()); } }); @@ -65,7 +67,10 @@ function countDown() { setTimeout(countDown, 1000); } } -g.clear().setFont("6x8",2).setFontAlign(0,0); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +g.reset().setFont("6x8",2).setFontAlign(0,0); g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); countDown(); @@ -79,13 +84,14 @@ function readHRM() { if (!hrmInfo) return; if (hrmOffset==0) { - g.clearRect(0,100,239,239); - g.moveTo(-100,0); + g.clearRect(0,100,g.getWidth(),g.getHeight()); + lastHrmPt = [-100,0]; } for (var i=0;i<2;i++) { var a = hrmInfo.raw[hrmOffset]; hrmOffset++; y = E.clip(170 - (a*2),100,230); - g.setColor(g.theme.fg).lineTo(hrmOffset, y); + g.setColor(g.theme.fg).drawLine(lastHrmPt[0],lastHrmPt[1],hrmOffset, y); + lastHrmPt = [hrmOffset, y]; } } diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/ios/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/ios/app-icon.js b/apps/ios/app-icon.js new file mode 100644 index 000000000..b74048750 --- /dev/null +++ b/apps/ios/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AGEB/4AGwARHv4RH/wQGj4QHAAP4CIoQJAAIRWg4RL8ARVn4RL/gR/CJv9BIP934DFEZH+v/0AgMv+wRK+YCBz/7C4PfCJOfAQO//JHMCIX3/d/CJ//t4RJF4JlCCIP/koRKEYh+DCIxlBCIQADCJQgCn4DCCJSbBHIIDBXYQRI/+Sp4DB7ZsCfdQRzg4RL8ARVgARLCAgRSj4QJ/ARFgF/CA/+CA0AgIRHwARHAH4AnA")) diff --git a/apps/ios/app.js b/apps/ios/app.js new file mode 100644 index 000000000..b210886fd --- /dev/null +++ b/apps/ios/app.js @@ -0,0 +1,2 @@ +// Config app not implemented yet +setTimeout(()=>load("messages.app.js"),10); diff --git a/apps/ios/app.png b/apps/ios/app.png new file mode 100644 index 000000000..79aa78f3a Binary files /dev/null and b/apps/ios/app.png differ diff --git a/apps/ios/boot.js b/apps/ios/boot.js new file mode 100644 index 000000000..c3ccb9275 --- /dev/null +++ b/apps/ios/boot.js @@ -0,0 +1,131 @@ +bleServiceOptions.ancs = true; +Bangle.ancsMessageQueue = []; + +/* Handle ANCS events coming in, and fire off 'notify' events +when we actually have all the information we need */ +E.on('ANCS',msg=>{ + /* eg: + { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true + } */ + + //console.log("ANCS",msg.event,msg.id); + // don't need info for remove events - pass these on + if (msg.event=="remove") + return E.emit("notify", msg); + + // not a remove - we need to get the message info first + function ancsHandler() { + var msg = Bangle.ancsMessageQueue[0]; + NRF.ancsGetNotificationInfo( msg.uid ).then( info => { + E.emit("notify", Object.assign(msg, info)); + Bangle.ancsMessageQueue.shift(); + if (Bangle.ancsMessageQueue.length) + ancsHandler(); + }); + } + Bangle.ancsMessageQueue.push(msg); + // if this is the first item in the queue, kick off ancsHandler, + // otherwise ancsHandler will handle the rest + if (Bangle.ancsMessageQueue.length==1) + ancsHandler(); +}); + +// Handle ANCS events with all the data +E.on('notify',msg=>{ +/* Info from ANCS event plus + "uid" : int, + "appId" : string, + "title" : string, + "subtitle" : string, + "message" : string, + "messageSize" : string, + "date" : string, + "posAction" : string, + "negAction" : string, + "name" : string, +*/ + var appNames = { + "com.netflix.Netflix" : "Netflix", + "com.google.ios.youtube" : "YouTube", + "com.google.hangouts" : "Hangouts", + "com.skype.SkypeForiPad": "Skype", + "com.atebits.Tweetie2": "Twitter" + // could also use NRF.ancsGetAppInfo(msg.appId) here + }; + var unicodeRemap = { + '2019':"'" + }; + var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); + if (appNames[msg.appId]) msg.a + require("messages").pushMessage({ + t : msg.event, + id : msg.uid, + src : appNames[msg.appId] || msg.appId, + title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer), + subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer), + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) + }); + // TODO: posaction/negaction? +}); + +// Apple media service +E.on('AMS',a=>{ + function push(m) { + var msg = { t : "modify", id : "music", title:"Music" }; + if (a.id=="artist") msg.artist = m; + else if (a.id=="album") msg.artist = m; + else if (a.id=="title") msg.tracl = m; + else return; // duration? need to reformat + require("messages").pushMessage(msg); + } + if (a.truncated) NRF.amsGetMusicInfo(a.id).then(push) + else push(a.value); +}); + +// Music control +Bangle.musicControl = cmd => { + // play, pause, playpause, next, prev, volup, voldown, repeat, shuffle, skipforward, skipback, like, dislike, bookmark + NRF.amsCommand(cmd); +} + +/* +// For testing... + +NRF.ancsGetNotificationInfo = function(uid) { + print("ancsGetNotificationInfo",uid); + return Promise.resolve({ + "uid" : uid, + "appId" : "Hangouts", + "title" : "Hello", + "subtitle" : "There", + "message" : "Lots and lots of text", + "messageSize" : 100, + "date" : "...", + "posAction" : "ok", + "negAction" : "cancel", + "name" : "Fred", + }); +}; + +E.emit("ANCS", { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true +}); + +*/ diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 09569d8da..bd8a9bd03 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -4,4 +4,5 @@ 0.04: Now displays widgets 0.05: Use g.theme for colours 0.06: Use Bangle.setUI for buttons -0.07: Theme colours fix \ No newline at end of file +0.07: Theme colours fix +0.08: Merge Bangle.js 1 and 2 launchers diff --git a/apps/launch/app.js b/apps/launch/app-bangle1.js similarity index 98% rename from apps/launch/app.js rename to apps/launch/app-bangle1.js index 449e16e62..3d4682e55 100644 --- a/apps/launch/app.js +++ b/apps/launch/app-bangle1.js @@ -16,7 +16,7 @@ function drawMenu() { var w = g.getWidth(); var h = g.getHeight(); var m = w/2; - var n = (h-48)/64; + var n = Math.floor((h-48)/64); if (selected>=n+menuScroll) menuScroll = 1+selected-n; if (selected{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" || !app.type)); +apps.sort((a,b)=>{ + var n=(0|a.sortorder)-(0|b.sortorder); + if (n) return n; // do sortorder first + if (a.nameb.name) return 1; + return 0; +}); +apps.forEach(app=>{ + if (app.icon) + app.icon = s.read(app.icon); // should just be a link to a memory area +}); +// FIXME: not needed after 2v11 +var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; +// FIXME: check not needed after 2v11 +if (g.wrapString) { + g.setFont(font); + apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); +} + +function drawApp(i, r) { + var app = apps[i]; + if (!app) return; + g.clearRect(r.x,r.y,r.x+r.w-1, r.y+r.h-1); + g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,r.y+32); + if (app.icon) try {g.drawImage(app.icon,8,r.y+8);} catch(e){} +} + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +E.showScroller({ + h : 64, c : apps.length, + draw : drawApp, + select : i => { + var app = apps[i]; + if (!app) return; + if (!app.src || require("Storage").read(app.src)===undefined) { + E.showMessage("App Source\nNot found"); + setTimeout(drawMenu, 2000); + } else { + E.showMessage("Loading..."); + load(app.src); + } + } +}); diff --git a/apps/launchb2/ChangeLog b/apps/launchb2/ChangeLog deleted file mode 100644 index a96ee84e1..000000000 --- a/apps/launchb2/ChangeLog +++ /dev/null @@ -1,3 +0,0 @@ -0.01: New App! -0.02: Fix occasional missed image when scrolling up -0.03: Text wrapping, better font diff --git a/apps/launchb2/app.js b/apps/launchb2/app.js deleted file mode 100644 index 371326498..000000000 --- a/apps/launchb2/app.js +++ /dev/null @@ -1,79 +0,0 @@ -var s = require("Storage"); -var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type)); -apps.sort((a,b)=>{ - var n=(0|a.sortorder)-(0|b.sortorder); - if (n) return n; // do sortorder first - if (a.nameb.name) return 1; - return 0; -}); -var APPH = 64; -var menuScroll = 0; -var menuShowing = false; -var w = g.getWidth(); -var h = g.getHeight(); -var n = Math.ceil((h-24)/APPH); -var menuScrollMax = APPH*apps.length - (h-24); -// FIXME: not needed after 2v11 -var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; - -apps.forEach(app=>{ - if (app.icon) - app.icon = s.read(app.icon); // should just be a link to a memory area -}); -if (g.wrapString) { // FIXME: check not needed after 2v11 - g.setFont(font); - apps.forEach(app=>app.name = g.wrapString(app.name, g.getWidth()-64).join("\n")); -} - -function drawApp(i) { - var y = 24+i*APPH-menuScroll; - var app = apps[i]; - if (!app || y<-APPH || y>=g.getHeight()) return; - g.setFont(font).setFontAlign(-1,0).drawString(app.name,64,y+32); - if (app.icon) try {g.drawImage(app.icon,8,y+8);} catch(e){} -} - -function drawMenu() { - g.reset().clearRect(0,24,w-1,h-1); - g.setClipRect(0,24,g.getWidth()-1,g.getHeight()-1); - for (var i=0;i{ - var dy = e.dy; - if (menuScroll - dy < 0) - dy = menuScroll; - if (menuScroll - dy > menuScrollMax) - dy = menuScroll - menuScrollMax; - if (!dy) return; - g.reset().setClipRect(0,24,g.getWidth()-1,g.getHeight()-1); - g.scroll(0,dy); - menuScroll -= dy; - if (e.dy < 0) { - drawApp(Math.floor((menuScroll+24+g.getHeight())/APPH)-1); - if (e.dy <= -APPH) drawApp(Math.floor((menuScroll+24+g.getHeight())/APPH)-2); - } else { - drawApp(Math.floor((menuScroll+24)/APPH)); - if (e.dy >= APPH) drawApp(Math.floor((menuScroll+24)/APPH)+1); - } - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); -}); -Bangle.on("touch",(_,e)=>{ - if (e.y<20) return; - var i = Math.floor((e.y+menuScroll-24) / APPH); - var app = apps[i]; - if (!app) return; - if (!app.src || require("Storage").read(app.src)===undefined) { - E.showMessage("App Source\nNot found"); - setTimeout(drawMenu, 2000); - } else { - E.showMessage("Loading..."); - load(app.src); - } -}); -Bangle.loadWidgets(); -Bangle.drawWidgets(); diff --git a/apps/launchb2/app.png b/apps/launchb2/app.png deleted file mode 100644 index 8b4e6caa2..000000000 Binary files a/apps/launchb2/app.png and /dev/null differ diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog new file mode 100644 index 000000000..c7ec09d30 --- /dev/null +++ b/apps/lcars/ChangeLog @@ -0,0 +1 @@ +0.01: Launch app diff --git a/apps/lcars/README.md b/apps/lcars/README.md new file mode 100644 index 000000000..fdce30c1b --- /dev/null +++ b/apps/lcars/README.md @@ -0,0 +1,8 @@ +# LCARS clock + +A simple LCARS inspired clock that shows: + * Current time + * Current date + * Battery level + * Steps + diff --git a/apps/lcars/background.png b/apps/lcars/background.png new file mode 100644 index 000000000..1ee4297c6 Binary files /dev/null and b/apps/lcars/background.png differ diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js new file mode 100644 index 000000000..cf884a6b7 --- /dev/null +++ b/apps/lcars/lcars.app.js @@ -0,0 +1,99 @@ +const locale = require('locale'); + + +/* + * Assets: Images, fonts etc. + */ +var img = { + width : 176, height : 151, bpp : 3, + transparent : 0, + buffer : require("heatshrink").decompress(atob("gF58+eAR14IN1fvv374CN7yD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/AH4A/AH4A/AB1z588+YCN+RBuj158+eARyD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4AUhyD/gEDQaHz4BCuQaNAIN0PQaHIIN0BQaF5IN0AQaHPkBBug6DQ8iEvQaE8yBBuhyDPAQNAINsBQaACBkhCuQaACpVo0cQaACo4CFGjyD/AAMPQf4ACQf4ADgiD+AH4A/AH8J02atICIwEAgPnz15AR3gEgM27dt2wCTF4IABgYROgN9+/fAR14ILsaQBKDakwjKF5oABKZ6DwgxTPQeEmQf5cPQeMBLhyDxgJTRQd0JKaKDuhKD/gENQf6D/F4VNQf8AKaKDvKBYnBAGZQKzBB1QZOwIGqDJsBA2QZJA3QZGYIPCDH4CD/0xA4QY+wIPKDGwCD/tpB6Qf6DHthA5QY1oIPSD/QY9gQf/bIPaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/AF8JQYgCdsEHnnz54CJgIdLwEAhqDEATtggPnz15ARHkgIdLIIKAgQcCAgQcAA/gAA==")) +} + +Graphics.prototype.setFontMinaSmall = function(scale) { + // Actual height 18 (17 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAA/8w/8wAAQAAAAAA4AA8AAAAA8AAwAAAAAAEABEABEQB/w/8AxEABEwB/w/8AxEABEABEAAAAH4MP8MMEM8GPcGOMGMMGMMH4ABwAAA/gAwgAggAggQwhw/nAAOAA4ADgAOfw8YwwQwAQQAYwAfwAPgAGAAfg85w/wwzgww4wwdgwHggHgAPwAIQAAA8AAwAAAAAAfwH/+fAP4ABgAAgAA4ABfAPH/+A/wAAAAAAEAAHgAfAAfAAHgAMAAAAAAAABgABgABgAf4AP4ABgABgABAAAAAADAADwADAAAAAgAAwAAwAAwAAwAAwAAAAADAADAADAAAAAAEAA8AH4A+AHwA+AAwAAAAAf/g//wwAwwAwwAwwAwwAwwAwf/gH+AAAAAAAYAAwAAwAA//wAAAAAAAAAwAwwBwwDwwDwwGwwcww4wfwwPAwAAAAAAwAwwQwwQwwQwwQwwYww4wf/gHHAAAAAEAAeAA+ADmAPGAcGAwGAh/wD/wAEAAEAAAAf4w/4wwwwwwwwwwwwwwwww/wgfgAAAAAAP/Af/gwwwwwwwwwwwwwwwwwww/gAAAgAAwAAwAAwAwwHww/Az4A/AA8AAAAAAAAfPg//wxwwwwwwwwwwwwww//wffgAAAAAAfwQ/wwwQwwYwwQwwQwwQw//wP/AAAAAAAMDAMDAMDAAAAAAAMDAMDwMDAAAAAAADgADgAHwAGwAMYAMYAIIAAAAEQAGYAGYAGYAGYAGYAGYAGYAAAAMYAMYAGwAGwAHgADgADAAAAAwAAwAAwAAwcwwcwwQAwQA/wAfgAAAAAAAB/8D/+TAGbHjbPzbMTbMzbMzb/zZ/zYAGf/+H/8AAAAAAABwAPwA+AH+A+GA8GAfmAD+AAfgADwAAQAAA//w//wwwwwwwwwwwwwxww//wffgAAAAAAP/Af/gwBwwAwwAwwAwwAwwAwwAwAAA//w//wwAwwAwwAwwAwwAw4Bwf/gH+AAAAAAAf/g//wwQwgQQgQQgQQgQQgQQgAQAAAf/w//wwQAgQAgQAgQAgQAgQAgAAAAAP/Af/gwAwwAwwAwwYwwYwwfwwfwAAAAAA//w//wAYAAYAAYAAYAAYAAYA//w//wAAA//w//wAAAAAAAAwAAwAAw//w//AAAA//w//wAYAA4AD8AHHAeDg4AwgAQAAAAAA//g//wAAwAAwAAwAAwAAwAAwAAQAAAP/w//w+AAPwAB+AAHwADwA/gH4A/AA/4A//wAAwAAA//w//wcAAPAADgAA4AAeAAHAADw//wAAAAAAH/Af/g4AwwAwwAwwAwwAwwAwcDwP/gB4AAAA//w//wwQAwQAwQAwYAwwA/wAPgAAAAH/Af/gwAwwAwwAwwA8wA8wA2cDkP/gB4AAAA//w//wwYAwYAwYAwcAwfA/zwPgwAAAAAAfgA/wwwQwwYwwYwwYwwYwwfwAPgAAAAAAwAAwAAwAA//w//wwAAwAAwAAwAAAAA/+A//gABwAAwAAwAAwAAwAAwAPg//AAAAAAA4AA/AAH4AA/AAHwADwAfgD8AfgA8AAgAAAAA4AA/AAH4AA/AAHwAHwA/AP4A/4Aw/AAHwAHwA/AP4A+AAwAAAAAwAw8DwOHAD8AB4AD8AOHA8DwwAwAAAAAAwAA8AAPAADwAA/wB/wHgAeAA4AAgAAgAQwBwwHwwOww8wxww3gw+Aw4AwwAQAAAH//f//YAAYAAQAAwAA+AAPwAB+AAPwAB8AAMQAAYAAYAAf//AAAAAA"), 32, atob("BgUHDAoRCwMGBggJBQYFBwwHCwsLCwsKCwsFBQkICQoPDAsKDAoKCwsEBgsKDgwMCgwLCwoMDBELCwoGBwY="), 18+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontMinaLarge = function(scale) { + // Actual height 35 (34 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAB4AAAAAPgAAAAA+AAAAAD4AAAAAHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAH4AAAAD/gAAAB/8AAAA/+AAAAf/AAAAP/gAAAH/wAAAD/4AAAD/8AAAB/+AAAAP/AAAAA/AAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///4AA////wAH////gA+AAAfADwAAA8AOAAABwA4AAAHADgAAAcAOAAABwA4AAAHADgAAAcAOAAABwA4AAAHADgAAAcAOAAABwA8AAAPAD4AAB8AH////gAP///8AAf///gAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAcAAAAADgAAAAAOAAAAAB4AAAAAHAAAAAA8AAAAAD////8AP////wA/////AD////8AAAAAAAAAAAAAAAAAAAAAGAAAAAA4AAAHADgAAA8AOAAAHwA4AAA/ADgAAD8AOAAAfwA4AAD/ADgAAfcAOAAD5wA4AAfHADgAD4cAOAAfBwA4AD8HADwAfgcAPAD8BwAeA/AHAB+f4AcAD//ABwAH/wAHAAH8AAcAAAAAAAAAAAAAAAAAAAAAGAAABgAYAAAGADgAAAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADwB4A8APAPgDwAfD//+AB////4AD/8//AAD/B/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAA8AAAAAPwAAAAD/AAAAAf8AAAAH9wAAAB/HAAAAPwcAAAD+BwAAAfgHAAAH8AcAAB/ABwAAPwAHAAA+AAcAADgABwAAIAAHgAAAH///AAB///8AAH///wAAAAcAAAAABwAAAAAHAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAH/8AYAP//wBgA///AHAD//wAcAOAPABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgA8AOAOADwA4A8APADgD8H4AOAH//gA4AP/8AAAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAD//+AAB///+AAP///8AB/BgH4APgOAHwA8A4APADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDgAcAOAOABwA4A4AHADgDwA8AOAP//wA4Af/+ABgA//wAAAA/8AAAAAAAAAAAAAAAAAAAAAA4AAAAADgAAAAAOAAAAAA4AAAAADgAAAAAOAAAAQA4AAAHADgAAD8AOAAA/wA4AAf+ADgAP/gAOAD/wAA4B/8AADg/+AAAOP/AAAA//wAAAD/4AAAAP8AAAAA/AAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/h/4AA//P/wAH////gA+B/AfADwD4A8AOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAOAHABwA4AcAHADgBwAcAPAPgDwA8A+APAB////4AH////gAP/j/8AAD4B8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAA//gAYAH//ABwA//+AHADwB4AcAOADgBwA4AOAHADgA4AcAOADgBwA4AOAHADgA4AcAOADgBwA4AOAHADgA4AcAPADgDwA+AOAfAB+A4f4AD////AAH///4AAD//8AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAeAAB8AD4AAHwAPgAAfAA+AAB4AB4AAAAAAAAAAAAAAAAAAAAAA="), 46, atob("CxAaDhgYGBgZFhkZCw=="), 40+(scale<<8)+(1<<16)); +} + + +/* + * Queue drawing every minute + */ +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +/* + * Draw watch face + */ +function draw(){ + g.reset(); + g.clearRect(0, 24, g.getWidth(), g.getHeight()); + + // Draw background image + g.drawImage(img, 0, 24); + + // Write time + var currentDate = new Date(); + var timeStr = locale.time(currentDate,1); + g.setFontAlign(0,0,0); + g.setFontMinaLarge(); + g.drawString(timeStr, 115, 53); + + // Write date + g.setFontAlign(-1,-1,0); + g.setFontMinaSmall(); + + var dayName = locale.dow(currentDate, true).toUpperCase(); + var day = currentDate.getDate(); + g.drawString("DATE:", 40, 107); + g.drawString(dayName + " " + day, 100, 105); + + // Draw battery + var bat = E.getBattery(); + g.drawString("BAT:", 40, 127); + g.drawString(bat+"%", 100, 127); + + // Draw steps + var steps = Bangle.getStepCount(); + g.drawString("STEP:", 40, 147); + g.drawString(steps, 100, 147); + + // Queue draw in one minute + queueDraw(); +} + +// Clear the screen once, at startup +g.setTheme({bg:"#000",fg:"#fff",dark:true}).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/lcars/lcars.icon.js b/apps/lcars/lcars.icon.js new file mode 100644 index 000000000..c404728e0 --- /dev/null +++ b/apps/lcars/lcars.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgeevPnAQsc+fPngCE+/fvoCEvAbIA4/AgFzEZwRBjwjNvBUBEZ3eCIMOEZtwCIMBEZuARYU5EZecTocHEZf0CIcBEbvgaggjKTwIAEbQpoHAAiSEeoYQHJQr1CCBJKEIgcBI4xKFaIdt3AOFgfuAYMeEYLRBj1pLQ4ICuYjBAgPbtoRHhu3AYN5VoMGzVpI49502AgPPVoM27dsK48N23cgE5CgOmzVoCI4LBzCSB8EP2wjJgILBAYMAhIjBsAjJzVwg47C7YRJEYhfBEZXmEZ53CI4q2BEAiVCkwjCNYaMGboQjDkBfDCAbdB04EBgyPDC4YAD/dt2wRCHIM5njXCCAcHboOmCIQ0B5/nfYT6DFIIjBeAcOvM8+EAjitFEYJEBAANzEYOeeowjCFgUDzwjB+YrDgAgBEYWcA4Mc+YjCvAQCgftEANuDIYOBEYXPNwIAIg4OCCgXkCBEOEZDvBEAhEB4AjF/inB8+OJQOOvILBoAjGU4IFDAQYjGbQIdCAQt4EY0DEZACDEYceEZACDC4bLBEZwCO")) diff --git a/apps/lcars/lcars.png b/apps/lcars/lcars.png new file mode 100644 index 000000000..167352ef4 Binary files /dev/null and b/apps/lcars/lcars.png differ diff --git a/apps/matrixclock/ChangeLog b/apps/matrixclock/ChangeLog index d53df991b..7cc9144b1 100644 --- a/apps/matrixclock/ChangeLog +++ b/apps/matrixclock/ChangeLog @@ -1 +1,2 @@ -0.01: Initial Release +0.01: Initial Release +0.02: Support for Bangle 2 diff --git a/apps/matrixclock/matrixclock.js b/apps/matrixclock/matrixclock.js index 0bf33fd68..ab18c13b8 100644 --- a/apps/matrixclock/matrixclock.js +++ b/apps/matrixclock/matrixclock.js @@ -12,6 +12,8 @@ const Locale = require('locale'); const SHARD_COLOR =[0,1.0,0]; 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 @@ -111,7 +113,7 @@ var dateStr = ""; var last_draw_time = null; const TIME_X_COORD = 20; -const TIME_Y_COORD = 100; +const TIME_Y_COORD = g.getHeight() / 2; const DATE_X_COORD = 170; const DATE_Y_COORD = 30; const RESET_PROBABILITY = 0.5; @@ -141,29 +143,26 @@ function draw_clock(){ } var now = new Date(); // draw time. Have to draw time on every loop - g.setFont("Vector",45); - g.setFontAlign(-1,-1,0); + + g.setFont("Vector", g.getWidth() / 5); + g.setFontAlign(0,-1); if(last_draw_time == null || now.getMinutes() != last_draw_time.getMinutes()){ - g.setColor(0,0,0); - g.drawString(timeStr, TIME_X_COORD, TIME_Y_COORD); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, TIME_Y_COORD); timeStr = format_time(now); } - g.setColor(SHARD_COLOR[0], - SHARD_COLOR[1], - SHARD_COLOR[2]); - g.drawString(timeStr, TIME_X_COORD, TIME_Y_COORD); + g.setColor(SHARD_COLOR[0], SHARD_COLOR[1], SHARD_COLOR[2]); + g.drawString(timeStr, w/2, TIME_Y_COORD); // // draw date when it changes g.setFont("Vector",15); - g.setFontAlign(-1,-1,0); + g.setFontAlign(0,-1,0); if(last_draw_time == null || now.getDate() != last_draw_time.getDate()){ - g.setColor(0,0,0); - g.drawString(dateStr, DATE_X_COORD, DATE_Y_COORD); + g.setColor(g.theme.fg); + g.drawString(dateStr, w/2, DATE_Y_COORD); dateStr = format_date(now); - g.setColor(SHARD_COLOR[0], - SHARD_COLOR[1], - SHARD_COLOR[2]); - g.drawString(dateStr, DATE_X_COORD, DATE_Y_COORD); + g.setColor(SHARD_COLOR[0], SHARD_COLOR[1], SHARD_COLOR[2]); + g.drawString(dateStr, w/2, DATE_Y_COORD); } last_draw_time = now; } @@ -232,10 +231,10 @@ function startTimers(){ Bangle.on('lcdPower', (on) => { if (on) { - console.log("lcdPower: on"); + //console.log("lcdPower: on"); startTimers(); } else { - console.log("lcdPower: off"); + //console.log("lcdPower: off"); clearTimers(); } }); diff --git a/apps/matrixclock/screenshot_matrix.png b/apps/matrixclock/screenshot_matrix.png new file mode 100644 index 000000000..3d843848c Binary files /dev/null and b/apps/matrixclock/screenshot_matrix.png differ diff --git a/apps/menusmall/ChangeLog b/apps/menusmall/ChangeLog index 5560f00bc..6de3d41f4 100644 --- a/apps/menusmall/ChangeLog +++ b/apps/menusmall/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: add `wrap` option, use Bangle.appRect \ No newline at end of file diff --git a/apps/menusmall/boot.js b/apps/menusmall/boot.js index 59e47b178..43c66089f 100644 --- a/apps/menusmall/boot.js +++ b/apps/menusmall/boot.js @@ -1,28 +1,23 @@ "";//not entirely sure why we need this - related to how bootupdate adds these to .boot0 E.showMenu = function(items) { - g.clear(1).flip(); // clear screen if no menu supplied - Bangle.drawWidgets(); + g.clearRect(Bangle.appRect); // clear screen if no menu supplied if (!items) { Bangle.setUI(); return; } - var w = g.getWidth(); - var h = g.getHeight(); + var menuItems = Object.keys(items); var options = items[""]; if (options) menuItems.splice(menuItems.indexOf(""),1); if (!(options instanceof Object)) options = {}; - options.fontHeight=14; - options.x=0; - options.x2=w-1; - options.y=24; - options.y2=h-12; + options.fontHeight = options.fontHeight|14; if (options.selected === undefined) options.selected = 0; - var x = 0|options.x; - var x2 = options.x2||(g.getWidth()-1); - var y = 0|options.y; - var y2 = options.y2||(g.getHeight()-1); + var ar = Bangle.appRect; + var x = ar.x; + var x2 = ar.x2; + var y = ar.y; + var y2 = ar.y2 - 11; // padding at end for arrow if (options.title) y += 15; var loc = require("locale"); @@ -51,7 +46,6 @@ E.showMenu = function(items) { rows = 1+rowmax-rowmin; } } - var less = idx>0; while (rows--) { var name = menuItems[idx]; var item = items[name]; @@ -82,7 +76,7 @@ E.showMenu = function(items) { g.setColor((idxitem.max) item.value = item.max; + if (item.min!==undefined && item.valueitem.max) item.value = item.wrap ? item.min : item.max; if (item.onchange) item.onchange(item.value); l.draw(options.selected,options.selected); } else { var a=options.selected; - options.selected = (dir+options.selected)%menuItems.length; - if (options.selected<0) options.selected += menuItems.length; + options.selected = (dir+options.selected+menuItems.length)%menuItems.length; l.draw(Math.min(a,options.selected), Math.max(a,options.selected)); } } diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog new file mode 100644 index 000000000..4f7df3859 --- /dev/null +++ b/apps/messages/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Add 'messages' library +0.03: Fixes for Bangle.js 1 diff --git a/apps/messages/README.md b/apps/messages/README.md new file mode 100644 index 000000000..c243ec06a --- /dev/null +++ b/apps/messages/README.md @@ -0,0 +1,21 @@ +# Messages app + +**THIS APP IS CURRENTLY BETA** + +This app handles the display of messages and message notifications. It stores +a list of currently received messages and allows them to be listed, viewed, +and responded to. + +It is a replacement for the old `notify`/`gadgetbridge` apps. + +## Usage + +... + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app + +## Creator + +Gordon Williams diff --git a/apps/messages/app-icon.js b/apps/messages/app-icon.js new file mode 100644 index 000000000..6ed3c1141 --- /dev/null +++ b/apps/messages/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA=")) diff --git a/apps/messages/app.js b/apps/messages/app.js new file mode 100644 index 000000000..6c7cf5fc9 --- /dev/null +++ b/apps/messages/app.js @@ -0,0 +1,237 @@ +/* MESSAGES is a list of: + {id:int, + src, + title, + subject, + body, + sender, + tel:string, + new:true // not read yet + } +*/ + +/* For example for maps: + +// a message +{"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="} + +*/ + +var Layout = require("Layout"); +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; +var colBg = g.theme.dark ? "#141":"#4f4"; +var colSBg1 = g.theme.dark ? "#121":"#cFc"; +var colSBg2 = g.theme.dark ? "#242":"#9F9"; +// hack for 2v10 firmware's lack of ':size' font handling +try { + g.setFont("6x8:2"); +} catch (e) { + g._setFont = g.setFont; + g.setFont = function(f,s) { + if (f.includes(":")) { + f = f.split(":"); + return g._setFont(f[0],f[1]); + } + return g._setFont(f,s); + }; +} + + +var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; +if (!Array.isArray(MESSAGES)) MESSAGES=[]; +var onMessagesModified = function(msg) { + // TODO: if new, show this new one + if (msg.new) Bangle.buzz(); + showMessage(msg.id); +}; +function saveMessages() { + require("Storage").writeJSON("messages.json",MESSAGES) +} + +function getBackImage() { + return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); +} +function getMessageImage(msg) { + if (msg.img) return atob(msg.img); + var s = (msg.src||"").toLowerCase(); + 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=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + 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=="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 (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 atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); +} + + +function showMapMessage(msg) { + var m; + var distance, street, target, eta; + m=msg.title.match(/(.*) - (.*)/); + if (m) { + distance = m[1]; + street = m[2]; + } else street=msg.title; + m=msg.body.match(/(.*) - (.*)/); + if (m) { + target = m[1]; + eta = m[2]; + } else target=msg.body; + layout = new Layout({ type:"v", c: [ + {type:"txt", font:fontMedium, label:target, bgCol:colBg, fillx:1, pad:2 }, + {type:"h", bgCol:colBg, fillx:1, c: [ + {type:"txt", font:"6x8", label:"Towards" }, + {type:"txt", font:fontLarge, label:street } + ]}, + {type:"h",fillx:1, filly:1, c: [ + msg.img?{type:"img",src:atob(msg.img), scale:2}:{}, + {type:"v", fillx:1, c: [ + {type:"txt", font:fontLarge, label:distance||"" } + ]}, + ]}, + {type:"txt", font:"6x8:2", label:eta } + ]}); + g.clearRect(Bangle.appRect); + layout.render(); + Bangle.setUI("updown",function() { + // any input to mark as not new and return to menu + msg.new = false; + saveMessages(); + layout = undefined; + checkMessages(); + }); +} + +function showMusicMessage(msg) { + function fmtTime(s) { + var m = Math.floor(s/60); + s = (s%60).toString().padStart(2,0); + return m+":"+s; + } + + function back() { + msg.new = false; + saveMessages(); + layout = undefined; + checkMessages(); + } + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:colBg, c: [ + { type:"btn", src:getBackImage, cb:back }, + { type:"v", fillx:1, c: [ + { type:"txt", font:fontLarge, label:msg.artist, pad:2 }, + { type:"txt", font:fontMedium, label:msg.album, pad:2 } + ]} + ]}, + {type:"txt", font:fontLarge, label:msg.track, fillx:1, filly:1, pad:2 }, + Bangle.musicControl?{type:"h",fillx:1, c: [ + {type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play + {type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause + {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):"--:--" } + ]}); + g.clearRect(Bangle.appRect); + layout.render(); +} + +function showMessage(msgid) { + var msg = MESSAGES.find(m=>m.id==msgid); + if (!msg) return checkMessages(); // go home if no message found + if (msg.src=="Maps") return showMapMessage(msg); + if (msg.id=="music") return showMusicMessage(msg); + // Normal text message display + var title=msg.title, titleFont = fontLarge; + if (title) { + var w = g.getWidth()-40; + if (g.setFont(titleFont).stringWidth(title) > w) + titleFont = fontMedium; + if (g.setFont(titleFont).stringWidth(title) > w) + title = g.wrapString(title, w).join("\n"); + } + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:colBg, c: [ + { type:"img", src:getMessageImage(msg), pad:2 }, + { type:"v", fillx:1, c: [ + {type:"txt", font:fontMedium, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2 }, + title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{}, + ]}, + ]}, + {type:"txt", font:fontMedium, label:msg.body||"", wrap:true, fillx:1, filly:1, pad:2 }, + {type:"h",fillx:1, c: [ + {type:"btn", src:getBackImage(), cb:()=>checkMessages(true)}, // back + msg.new?{type:"btn", src:atob("HRiBAD///8D///wj///Fj//8bj//x3z//Hvx/8/fx/j+/x+Ad/B4AL8Rh+HxwH+PHwf+cf5/+x/n/PH/P8cf+cx5/84HwAB4fgAD5/AAD/8AAD/wAAD/AAAD8A=="), cb:()=>{ + msg.new = false; // read mail + saveMessages(); + checkMessages(); + }}:{} + ]} + ]}); + g.clearRect(Bangle.appRect); + layout.render(); +} + +function checkMessages(forceShowMenu) { + // If no messages, just show 'no messages' and return + if (!MESSAGES.length) + return E.showPrompt("No Messages",{ + title:"Messages", + img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), + buttons : {"Ok":1} + }).then(() => { load() }); + // we have >0 messages + // If we have a new message, show it + if (!forceShowMenu) { + var newMessages = MESSAGES.filter(m=>m.new); + if (newMessages.length) + return showMessage(newMessages[0].id); + } + // Otherwise show a menu + E.showScroller({ + h : 48, + c : MESSAGES.length+1, + draw : function(idx, r) {"ram" + var msg = MESSAGES[idx-1]; + if (msg && msg.new) g.setBgColor(colBg); + else g.setBgColor((idx&1) ? colSBg1 : colSBg2); + g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setColor(g.theme.fg); + 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); + if (msg.id=="music") { + title = msg.artist || "Music"; + body = msg.track; + } + if (img) { + g.drawImage(img, x+24, r.y+24, {rotate:0}); // force centering + x += 50; + } + var m = msg.title+"\n"+msg.body; + if (msg.src) g.setFontAlign(1,-1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+2); + if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont("6x8"); + var l = g.wrapString(body, r.w-14); + if (l.length>3) { + l = l.slice(0,3); + l[l.length-1]+="..."; + } + g.drawString(l.join("\n"), x+10,r.y+20); + } + }, + select : idx => { + if (idx==0) load(); + else showMessage(MESSAGES[idx-1].id); + } + }); +} + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +checkMessages(); diff --git a/apps/messages/app.png b/apps/messages/app.png new file mode 100644 index 000000000..c9177692e Binary files /dev/null and b/apps/messages/app.png differ diff --git a/apps/messages/lib.js b/apps/messages/lib.js new file mode 100644 index 000000000..f3ea242e5 --- /dev/null +++ b/apps/messages/lib.js @@ -0,0 +1,37 @@ +exports.pushMessage = function(event) { + /* event is: + {t:"add",id:int, src,title,subject,body,sender,tel, important:bool} // add new + {t:"add",id:int, id:"music", state, artist, track, etc} // add new + {t:"remove-",id:int} // remove + {t:"modify",id:int, title:string} // modified + */ + var messages, inApp = "undefined"!=typeof MESSAGES; + if (inApp) + messages = MESSAGES; // we're in an app that has already loaded messages + else // no app - load messages + messages = require("Storage").readJSON("messages.json",1)||[]; + // now modify/delete as appropriate + var mIdx = messages.findIndex(m=>m.id==event.id); + if (event.t=="remove") { + if (mIdx>=0) messages.splice(mIdx, 1); // remove item + mIdx=-1; + } else { // add/modify + if (event.t=="add") event.new=true; // new message + if (mIdx<0) mIdx=messages.push(event)-1; + else Object.assign(messages[mIdx], event); + } + require("Storage").writeJSON("messages.json",messages); + // if in app, process immediately + if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); + // ok, saved now - we only care if it's new + if (event.t!="add") return; + // otherwise load after a delay, 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 (Bangle.CLOCK || event.important) return load("messages.app.js"); + if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know + WIDGETS.messages.newMessage(); + }, 500); +} diff --git a/apps/messages/widget.js b/apps/messages/widget.js new file mode 100644 index 000000000..eda4a85a5 --- /dev/null +++ b/apps/messages/widget.js @@ -0,0 +1,20 @@ +WIDGETS["messages"]={area:"tl",width:0,draw:function() { + if (!this.width) return; + var c = (Date.now()-this.t)/1000; + g.reset().setBgColor((c&1) ? "#0f0" : "#030").setColor((c&1) ? "#000" : "#fff"); + g.clearRect(this.x,this.y,this.x+this.width,this.y+23); + g.setFont("6x8:1x2").setFontAlign(0,0).drawString("MESSAGES", this.x+this.width/2, this.y+12); + //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute + if (c<120 && (Date.now()-this.l)>4000) { + this.l = Date.now(); + Bangle.buzz(); // buzz every 4 seconds + } + setTimeout(()=>WIDGETS["messages"].draw(), 1000); +},newMessage:function() { + WIDGETS["messages"].t=Date.now(); // first time + WIDGETS["messages"].l=Date.now()-10000; // last buzz + if (WIDGETS["messages"].c!==undefined) return; // already called + WIDGETS["messages"].width=64; + Bangle.drawWidgets(); + Bangle.setLCDPower(1);// turns screen on +}}; diff --git a/apps/multiclock/ChangeLog b/apps/multiclock/ChangeLog index 2f27f7f28..9d02ae85e 100644 --- a/apps/multiclock/ChangeLog +++ b/apps/multiclock/ChangeLog @@ -1,13 +1,11 @@ -0.01: New App! -0.02: Separate *.face.js files for faces -0.03: Renaming -0.04: Bug Fixes -0.05: Add README -0.06: Add txt clock -0.07: Add Time Date clock and fix font sizes -0.08: Add pinned clock face -0.09: Added Pedometer clock -0.10: Added GPS and Grid Ref clock faces -0.11: Updated Pedometer clock to retrieve steps from either wpedom or activepedom -0.12: Removed GPS and Grid Ref clock faces, superceded by GPS setup and Walkers Clock -0.13: Localised digi.js and timdat.js \ No newline at end of file +0.01: Initial version +0.02: Add pinned clock facility +0.03: Lnng touch switch to night clock - ANCS off, dimmed +0.04: use theme, font heights etc +0.05: make Bangle compatible +0.06: add minute tick for efficiency and nifty A clock +0.07: compatible with Bang;e.js 2 +0.08: fix minute tick bug + + + diff --git a/apps/multiclock/README.md b/apps/multiclock/README.md index b1773b8df..e8b8335ea 100644 --- a/apps/multiclock/README.md +++ b/apps/multiclock/README.md @@ -1,30 +1,11 @@ # Multiclock -This is a clock app that supports multiple clock faces. The user can switch between faces while retaining widget state which makes the switch fast and preserves state such as bluetooth connections. Currently there are four clock faces as shown below. To my eye, these faces look better when widgets are hidden using **widviz**. - +This is a clock app that supports multiple clock faces. The user can switch between faces while retaining widget state which makes the switch fast. Currently there are four clock faces as shown below. There are currently an anlog, digital, text, big digit, time and date, and a clone of the Nifty-A-Clock faces. ### Analog Clock Face -![](anaface.jpg) - -### Digital Clock Face -![](digiface.jpg) - -### Big Digit Clock Face -![](bigface.jpg) - -### Text Clock Face -![](txtface.jpg) - -### Time and Date Clock Face ## Controls -Clock faces are kept in a circular list. - -*BTN1* - switches to the next clock face. - -*BTN2* - switches to the app launcher. - -*BTN3* - switches to the previous clock face. +Swipe left and right on both the Bangle and Bangle 2 switch between faces. BTN1 & BTH3 also switch faces on the Bangle. ## Adding a new face Clock faces are described in javascript storage files named `name.face.js`. For example, the Analog Clock Face is described in `ana.face.js`. These files have the following structure: @@ -38,7 +19,7 @@ Clock faces are described in javascript storage files named `name.face.js`. For function drawAll(){ //draw background + initial state of digits, hands etc } - return {init:drawAll, tick:onSecond}; + return {init:drawAll, tick:onSecond, tickpersec:true}; } return getFace; })(); @@ -47,6 +28,5 @@ For those familiar with the structure of widgets, this is similar, however, ther The app at start up loads all files `*.face.js`. The simplest way of adding a face is thus to load it into `Storage` using the WebIDE. Similarly, to remove an unwanted face, simply delete it from `Storage` using the WebIDE. -## Support +If `tickpersec` is false then `tick` is only called each minute as this is more power effcient - especially on the BAngle 2. -Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev). \ No newline at end of file diff --git a/apps/multiclock/ana.js b/apps/multiclock/ana.face.js similarity index 59% rename from apps/multiclock/ana.js rename to apps/multiclock/ana.face.js index 4fd5a7251..af1c84c9f 100644 --- a/apps/multiclock/ana.js +++ b/apps/multiclock/ana.face.js @@ -5,54 +5,60 @@ const p = Math.PI/2; const PRad = Math.PI/180; + var cx = g.getWidth()/2; + var cy = 12+g.getHeight()/2; + var scale = (g.getHeight()-24)/(240-24); + scale = scale>=1 ? 1 : scale; + function seconds(angle, r) { const a = angle*PRad; - const x = 120+Math.sin(a)*r; - const y = 134-Math.cos(a)*r; + const x = cx+Math.sin(a)*r; + const y = cy-Math.cos(a)*r; if (angle % 90 == 0) { - g.setColor(0,1,1); + g.setColor(g.theme.fg2); g.fillRect(x-6,y-6,x+6,y+6); } else if (angle % 30 == 0){ - g.setColor(0,1,1); + g.setColor(g.theme.fg); g.fillRect(x-4,y-4,x+4,y+4); } else { - g.setColor(1,1,1); + g.setColor(g.theme.fg); g.fillRect(x-1,y-1,x+1,y+1); } } function hand(angle, r1,r2, r3) { + r1 = scale*r1; r2=scale*r2; r3 = scale*r3; const a = angle*PRad; g.fillPoly([ - 120+Math.sin(a)*r1, - 134-Math.cos(a)*r1, - 120+Math.sin(a+p)*r3, - 134-Math.cos(a+p)*r3, - 120+Math.sin(a)*r2, - 134-Math.cos(a)*r2, - 120+Math.sin(a-p)*r3, - 134-Math.cos(a-p)*r3]); + cx+Math.sin(a)*r1, + cy-Math.cos(a)*r1, + cx+Math.sin(a+p)*r3, + cy-Math.cos(a+p)*r3, + cx+Math.sin(a)*r2, + cy-Math.cos(a)*r2, + cx+Math.sin(a-p)*r3, + cy-Math.cos(a-p)*r3]); } var minuteDate; var secondDate; function onSecond() { - g.setColor(0,0,0); + g.setColor(g.theme.bg); hand(360*secondDate.getSeconds()/60, -5, 90, 3); if (secondDate.getSeconds() === 0) { hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -16, 60, 7); hand(360*minuteDate.getMinutes()/60, -16, 86, 7); minuteDate = new Date(); } - g.setColor(1,1,1); + g.setColor(g.theme.fg); hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -16, 60, 7); hand(360*minuteDate.getMinutes()/60, -16, 86, 7); - g.setColor(0,1,1); + g.setColor(g.theme.fg2); secondDate = new Date(); hand(360*secondDate.getSeconds()/60, -5, 90, 3); - g.setColor(0,0,0); - g.fillCircle(120,134,2); + g.setColor(g.theme.bg); + g.fillCircle(cx,cy,2); } function drawAll() { @@ -60,11 +66,11 @@ // draw seconds g.setColor(1,1,1); for (let i=0;i<60;i++) - seconds(360*i/60, 100); + seconds(360*i/60, 100*scale); onSecond(); } - return {init:drawAll, tick:onSecond}; + return {init:drawAll, tick:onSecond, tickpersec:true}; } return getFace; diff --git a/apps/multiclock/anaface.jpg b/apps/multiclock/anaface.jpg deleted file mode 100644 index 86aaccd54..000000000 Binary files a/apps/multiclock/anaface.jpg and /dev/null differ diff --git a/apps/multiclock/apps_entry.json b/apps/multiclock/apps_entry.json deleted file mode 100644 index 6383609c1..000000000 --- a/apps/multiclock/apps_entry.json +++ /dev/null @@ -1,19 +0,0 @@ -{ "id": "multiclock", - "name": "Multi Clock", - "icon": "multiclock.png", - "version":"0.06", - "description": "Clock with multiple faces - Big, Analogue, Digital, Text.\n Switch between faces with BT1 & BTN3", - "readme": "README.md", - "tags": "clock", - "type":"clock", - "allow_emulator":false, - "storage": [ - {"name":"multiclock.app.js","url":"clock.min.js"}, - {"name":"big.face.js","url":"big.min.js"}, - {"name":"ana.face.js","url":"ana.min.js"}, - {"name":"digi.face.js","url":"digi.min.js"}, - {"name":"txt.face.js","url":"txt.min.js"}, - {"name":"ped.face.js","url":"ped.js"}, - {"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true} - ] - }, diff --git a/apps/multiclock/big.face.js b/apps/multiclock/big.face.js new file mode 100644 index 000000000..2db4ee4d4 --- /dev/null +++ b/apps/multiclock/big.face.js @@ -0,0 +1,31 @@ +(() => { + + function getFace(){ + + const W = g.getWidth(); + const H = g.getHeight(); + const F = 132*H/240; // reasonable approximation + + function drawTime() { + d = new Date() + g.reset(); + var da = d.toString().split(" "); + var time = da[4].substr(0, 5).split(":"); + var hours = time[0], + minutes = time[1]; + g.clearRect(0,24,W-1,H-1); + g.setColor(g.theme.fg); + g.setFont("Vector",F); + g.setFontAlign(0,-1); + g.drawString(hours,W/2,24,true); + g.setColor(g.theme.fg2); + g.drawString(minutes,W/2,12+H/2,true); + } + + + return {init:drawTime, tick:drawTime, tickpersecond:false}; + } + + return getFace; + +})(); \ No newline at end of file diff --git a/apps/multiclock/big.js b/apps/multiclock/big.js deleted file mode 100644 index 2e83d8fb5..000000000 --- a/apps/multiclock/big.js +++ /dev/null @@ -1,32 +0,0 @@ -(() => { - - function getFace(){ - - function drawTime(d) { - g.reset(); - var da = d.toString().split(" "); - var time = da[4].substr(0, 5).split(":"); - var hours = time[0], - minutes = time[1]; - g.clearRect(0,24,239,239); - g.setColor(1,1,1); - g.setFont("Vector",132); - g.drawString(hours,50,24,true); - g.drawString(minutes,50,132,true); - } - - function onSecond(){ - var t = new Date(); - if (t.getSeconds() === 0) drawTime(t); - } - - function drawAll(){ - drawTime(new Date()); - } - - return {init:drawAll, tick:onSecond}; - } - - return getFace; - -})(); \ No newline at end of file diff --git a/apps/multiclock/bigface.jpg b/apps/multiclock/bigface.jpg deleted file mode 100644 index 685726864..000000000 Binary files a/apps/multiclock/bigface.jpg and /dev/null differ diff --git a/apps/multiclock/clock.info b/apps/multiclock/clock.info new file mode 100644 index 000000000..441de1463 --- /dev/null +++ b/apps/multiclock/clock.info @@ -0,0 +1 @@ +{"id":"clock","name":"Clock","type":"clock","src":"clock.app.js","icon":"clock.img","version":"0.06","files":"clock.info,clock.app.js,big.face.js,ana.face.js,digi.face.js,txt.face.js"} \ No newline at end of file diff --git a/apps/multiclock/clock.js b/apps/multiclock/clock.js deleted file mode 100644 index 50410f096..000000000 --- a/apps/multiclock/clock.js +++ /dev/null @@ -1,69 +0,0 @@ -var FACES = []; -var STOR = require("Storage"); -STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); -var lastface = STOR.readJSON("multiclock.json")||{pinned:0}; -var iface = lastface.pinned; -var face = FACES[iface](); -var intervalRefSec; - -function stopdraw() { - if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} -} - -function startdraw() { - g.clear(); - g.reset(); - Bangle.drawWidgets(); - face.init(); - intervalRefSec = setInterval(face.tick,1000); -} - -function setButtons(){ - function newFace(inc){ - var n = FACES.length-1; - iface+=inc; - iface = iface>n?0:iface<0?n:iface; - stopdraw(); - face = FACES[iface](); - startdraw(); - } - function finish(){ - if (lastface.pinned!=iface){ - lastface.pinned=iface; - STOR.write("multiclock.json",lastface); - } - Bangle.showLauncher(); - } - setWatch(finish, BTN2, {repeat:false,edge:"falling"}); - setWatch(newFace.bind(null,1), BTN1, {repeat:true,edge:"rising"}); - setWatch(newFace.bind(null,-1), BTN3, {repeat:true,edge:"rising"}); -} - -var SCREENACCESS = { - withApp:true, - request:function(){ - this.withApp=false; - stopdraw(); - clearWatch(); - }, - release:function(){ - this.withApp=true; - startdraw(); - setButtons(); - } -}; - -Bangle.on('lcdPower',function(on) { - if (!SCREENACCESS.withApp) return; - if (on) { - startdraw(); - } else { - stopdraw(); - } -}); - -g.clear(); -Bangle.loadWidgets(); -startdraw(); -setButtons(); - diff --git a/apps/multiclock/digi.face.js b/apps/multiclock/digi.face.js new file mode 100644 index 000000000..21f339afc --- /dev/null +++ b/apps/multiclock/digi.face.js @@ -0,0 +1,38 @@ +(() => { + +function getFace(){ + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + + var buf = Graphics.createArrayBuffer(W,92,1,{msb:true}); + function flip() { + g.setColor(g.theme.fg); + g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,H/2-34); + } + + var W = g.getWidth(); + var H = g.getHeight(); + + function drawTime() { + buf.clear(); + buf.setColor(1); + var d = new Date(); + var da = d.toString().split(" "); + var time = da[4]; + buf.setFont("Vector",54*scale); + buf.setFontAlign(0,-1); + buf.drawString(time,W/2,0); + buf.setFont("6x8",scale<1?1:2); + buf.setFontAlign(0,-1); + var date = d.toString().substr(0,15); + buf.drawString(date, W/2, 70*scale); + flip(); + } + return {init:drawTime, tick:drawTime, tickpersec:true}; +} + +return getFace; + +})(); \ No newline at end of file diff --git a/apps/multiclock/digi.js b/apps/multiclock/digi.js deleted file mode 100644 index 0b2ca4aaa..000000000 --- a/apps/multiclock/digi.js +++ /dev/null @@ -1,33 +0,0 @@ -(() => { - -var locale = require("locale"); - -function getFace(){ - - var buf = Graphics.createArrayBuffer(240,92,1,{msb:true}); - function flip() { - g.setColor(1,1,1); - g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,85); - } - - function drawTime() { - buf.clear(); - buf.setColor(1); - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4]; - buf.setFont("Vector",54); - buf.setFontAlign(0,-1); - buf.drawString(time,buf.getWidth()/2,0); - buf.setFont("6x8",2); - buf.setFontAlign(0,-1); - var date = locale.dow(d, 1) + " " + locale.date(d, 1); - buf.drawString(date, buf.getWidth()/2, 70); - flip(); - } - return {init:drawTime, tick:drawTime}; -} - -return getFace; - -})(); \ No newline at end of file diff --git a/apps/multiclock/digiface.jpg b/apps/multiclock/digiface.jpg deleted file mode 100644 index b0323bd55..000000000 Binary files a/apps/multiclock/digiface.jpg and /dev/null differ diff --git a/apps/multiclock/dk.face.js b/apps/multiclock/dk.face.js new file mode 100644 index 000000000..a89397a75 --- /dev/null +++ b/apps/multiclock/dk.face.js @@ -0,0 +1,40 @@ +(() => { + function getFace(){ + + const locale = require("locale"); + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + + function drawClock(){ + var now=Date(); + d=now.toString().split(' '); + var min=d[4].substr(3,2); + var sec=d[4].substr(-2); + var tm=d[4].substring(0,5); + var hr=d[4].substr(0,2); + lastmin=min; + g.reset(); + g.clearRect(0,24,W-1,H-1); + g.setColor(g.theme.fg); + g.setFontAlign(0,-1); + g.setFontVector(80*scale); + g.drawString(tm,4+W/2,H/2+24-80*scale); + g.setFontVector(36*scale); + g.setColor(g.theme.fg2); + d[1] = locale.month(now,3); + d[0] = locale.dow(now,3); + var dt=d[0]+" "+d[1]+" "+d[2];//+" "+d[3]; + g.drawString(dt,W/2,H/2+24); + g.flip(); + } + + + return {init:drawClock, tick:drawClock, tickpersec:false}; + } + + return getFace; + +})(); + diff --git a/apps/multiclock/multiclock-icon.img b/apps/multiclock/multiclock-icon.img new file mode 100644 index 000000000..57e0a935f Binary files /dev/null and b/apps/multiclock/multiclock-icon.img differ diff --git a/apps/multiclock/multiclock-icon.js b/apps/multiclock/multiclock-icon.js index 41a59f503..bad6313ba 100644 --- a/apps/multiclock/multiclock-icon.js +++ b/apps/multiclock/multiclock-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwkEogA/AFGIAAQVVDKQWHDB1IC5OECx8z///mYYOBoWDCoIADnBJLFwQWGDAgwIEYU/CQXwh4EC+YwKBIOPFQYXE//4C5BGCIQgXF/5IILo4XGMIQXHLoYXIMIRGMC45IHC4KkGC45IBC4yNEC5KRBC7h2HC5B4GC5EggQXOBwvygEAl6QHC4sikRGEhGAJAgNBC75HIgZHNO48AgIJER54xCiYXKa5AxCGAjvPGA4XIwYXHbQs4C46QGGAbZDB4IXEPBQAEOwwXDJBJGEC4xILIxQwDSJCNDFwwXDMIh0ELoQXIJARhDC4hdCIw4wEDAQXDCwQuIGAgABmYXBmYHDFxIYGAAoWLJIgAGCxgYJCxwZGCqIA/AC4A=")) +require("heatshrink").decompress(atob("mEwwkEogA/AFGIAAQVVDKQWHDB1IC5OECx8z///mYYOBoWDCoIADnBJLFwQWGDAgwIEYU/CQXwh4EC+YwKBIOPFQYXE//4C5BGCIQgXF/5IILo4XGMIQXHLoYXIMIRGMC45IHC4KkGC45IBC4yNEC5KRBC7h2HC5B4GC5EggQXOBwvygEAl6QHC4sikRGEhGAJAgNBC75HIgZHNO48AgIJER54xCiYXKa5AxCGAjvPGA4XIwYXHbQs4C46QGGAbZDB4IXEPBQAEOwwXDJBJGEC4xILIxQwDSJCNDFwwXDMIh0ELoQXIJARhDC4hdCIw4wEDAQXDCwQuIGAgABmYXBmYHDFxIYGAAoWLJIgAGCxgYJCxwZGCqIA/AC4A=")) \ No newline at end of file diff --git a/apps/multiclock/multiclock.app.js b/apps/multiclock/multiclock.app.js new file mode 100644 index 000000000..c24e5c94b --- /dev/null +++ b/apps/multiclock/multiclock.app.js @@ -0,0 +1,86 @@ +var FACES = []; +var STOR = require("Storage"); +STOR.list(/\.face\.js$/).forEach(face=>FACES.push(eval(require("Storage").read(face)))); +var lastface = STOR.readJSON("clock.json") || {pinned:0} +var iface = lastface.pinned; +var face = FACES[iface](); +var intervalRefSec; +var intervalRefSec; +var tickTimeout; + +function stopdraw() { + if(intervalRefSec) {intervalRefSec=clearInterval(intervalRefSec);} + if(tickTimeout) {tickTimeout=clearTimeout(tickTimeout);} + g.clear(); +} + +function queueMinuteTick() { + if (tickTimeout) clearTimeout(tickTimeout); + tickTimeout = setTimeout(function() { + tickTimeout = undefined; + face.tick(); + queueMinuteTick(); + }, 60000 - (Date.now() % 60000)); +} + +function startdraw() { + g.reset(); + face.init(); + if (face.tickpersec) + intervalRefSec = setInterval(face.tick,1000); + else + queueMinuteTick(); + Bangle.drawWidgets(); +} + +var SCREENACCESS = { + withApp:true, + request:function(){ + this.withApp=false; + stopdraw(); + }, + release:function(){ + this.withapp=true; + startdraw(); + setButtons(); + } +}; + +Bangle.on('lcdPower',function(b) { + if (!SCREENACCESS.withApp) return; + if (b) { + startdraw(); + } else { + stopdraw(); + } +}); + +function setButtons(){ + function newFace(inc){ + if (!inc) Bangle.showLauncher(); + else { + var n = FACES.length-1; + iface+=inc; + iface = iface>n?0:iface<0?n:iface; + stopdraw(); + face = FACES[iface](); + startdraw(); + } + } + Bangle.setUI("leftright", newFace); +} + +E.on('kill',()=>{ + if (iface!=lastface.pinned){ + lastface.pinned=iface; + STOR.write("clock.json",lastface); + } +}); + +Bangle.loadWidgets(); +g.clear(); +startdraw(); +setButtons(); + + + diff --git a/apps/multiclock/nifty.face.js b/apps/multiclock/nifty.face.js new file mode 100644 index 000000000..2c2af6063 --- /dev/null +++ b/apps/multiclock/nifty.face.js @@ -0,0 +1,55 @@ +(() => { + function getFace(){ + + const locale = require("locale"); + const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + + 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 d02(value) { + return ('0' + value).substr(-2); + } + + function drawClock() { + g.reset(); + g.clearRect(0, widget, viewport.width, viewport.height); + var now = new Date(); + const hour = d02(now.getHours() - (is12Hour && now.getHours() > 12 ? 12 : 0)); + const minutes = d02(now.getMinutes()); + const day = d02(now.getDay()); + const month = d02(now.getMonth() + 1); + const year = now.getFullYear(); + const month2 = locale.month(now, 3); + const day2 = locale.dow(now, 3); + 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.fillRect(center.x + 30 * scale, center.y - 72 * scale, center.x + 32 * scale, center.y + 74 * scale); + g.setFontAlign(-1, 0).setFont("Vector", 16 * scale); + g.drawString(year, center.x + 40 * scale, center.y - 62 * scale); + g.drawString(month, center.x + 40 * scale, center.y - 44 * scale); + g.drawString(day, center.x + 40 * scale, center.y - 26 * scale); + g.drawString(month2, center.x + 40 * scale, center.y + 48 * scale); + g.drawString(day2, center.x + 40 * scale, center.y + 66 * scale); + } + + + return {init:drawClock, tick:drawClock, tickpersec:false}; + } + + return getFace; + +})(); + diff --git a/apps/multiclock/ped.js b/apps/multiclock/ped.js deleted file mode 100644 index a0f81e2e5..000000000 --- a/apps/multiclock/ped.js +++ /dev/null @@ -1,41 +0,0 @@ -(() => { - - function getFace(){ - - function draw() { - let steps = "-"; - let show_steps = false; - - // only attempt to get steps if activepedom is loaded - if (WIDGETS.activepedom !== undefined) { - steps = WIDGETS.activepedom.getSteps(); - } else if (WIDGETS.wpedom !== undefined) { - steps = WIDGETS.wpedom.getSteps(); - } - - var d = new Date(); - var da = d.toString().split(" "); - var time = da[4].substr(0,5); - - g.reset(); - g.clearRect(0,24,239,239); - g.setFont("Vector", 80); - g.setColor(1,1,1); // white - g.setFontAlign(0, -1); - g.drawString(time, g.getWidth()/2, 60); - g.setColor(0,255,0); // green - g.setFont("Vector", 60); - g.drawString(steps, g.getWidth()/2, 160); - } - - function onSecond(){ - var t = new Date(); - if ((t.getSeconds() % 5) === 0) draw(); - } - - return {init:draw, tick:onSecond}; - } - - return getFace; - -})(); diff --git a/apps/multiclock/timdat.js b/apps/multiclock/timdat.js deleted file mode 100644 index a4a93a691..000000000 --- a/apps/multiclock/timdat.js +++ /dev/null @@ -1,47 +0,0 @@ -(() => { - var locale = require("locale"); - var dayFirst = ["en_GB", "en_IN", "en_NAV", "de_DE", "nl_NL", "fr_FR", "en_NZ", "en_AU", "de_AT", "en_IL", "es_ES", "fr_BE", "de_CH", "fr_CH", "it_CH", "it_IT", "tr_TR", "pt_BR", "cs_CZ", "pt_PT"]; - var withDot = ["de_DE", "nl_NL", "de_AT", "de_CH", "hu_HU", "cs_CZ", "sl_SI"]; - - function getFace(){ - - var lastmin=-1; - function drawClock(){ - var d=Date(); - if (d.getMinutes()==lastmin) return; - var tm=d.toString().split(' ')[4].substring(0,5); - lastmin=d.getMinutes(); - g.reset(); - g.clearRect(0,24,239,239); - var w=g.getWidth(); - g.setColor(0xffff); - g.setFontVector(80); - g.drawString(tm,4+(w-g.stringWidth(tm))/2,64); - g.setFontVector(36); - g.setColor(0x07ff); - var dt=locale.dow(d, 1) + " "; - if (dayFirst.includes(locale.name)) { - dt+=d.getDate(); - if (withDot.includes(locale.name)) { - dt+="."; - } - dt+=" " + locale.month(d, 1); - } else { - dt+=locale.month(d, 1) + " " + d.getDate(); - } - g.drawString(dt,(w-g.stringWidth(dt))/2,160); - g.flip(); - } - - function drawFirst(){ - lastmin=-1; - drawClock(); - } - - return {init:drawFirst, tick:drawClock}; - } - - return getFace; - -})(); - diff --git a/apps/multiclock/txt.js b/apps/multiclock/txt.face.js similarity index 53% rename from apps/multiclock/txt.js rename to apps/multiclock/txt.face.js index 130455176..fddc07214 100644 --- a/apps/multiclock/txt.js +++ b/apps/multiclock/txt.face.js @@ -1,8 +1,14 @@ (() => { function getFace(){ - - function drawTime(d) { + + + var W = g.getWidth(); + var H = g.getHeight(); + var scale = W/240; + var F = 44 * scale; + + function drawTime() { function convert(n){ var t0 = [" ","one","two","three","four","five","six","seven","eight","nine"]; var t1 = ["ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen","seventeen","eighteen","nineteen"]; @@ -13,28 +19,25 @@ return "error"; } g.reset(); - g.clearRect(0,40,239,210); - g.setColor(1,1,1); + g.clearRect(0,24,W-1,H-1); + var d = new Date(); + g.setColor(g.theme.fg); g.setFontAlign(0,0); - g.setFont("Vector",44); + g.setFont("Vector",F); var txt = convert(d.getHours()); - g.drawString(txt.top,120,60); - g.drawString(txt.bot,120,100); + g.setColor(g.theme.fg); + g.drawString(txt.top,W/2,H/2+24-2*F); + g.setColor(g.theme.fg2); + g.drawString(txt.bot,W/2,H/2+24-F); txt = convert(d.getMinutes()); - g.drawString(txt.top,120,140); - g.drawString(txt.bot,120,180); + g.setColor(g.theme.fg); + g.drawString(txt.top,W/2,H/2+24); + g.setColor(g.theme.fg2); + g.drawString(txt.bot,W/2,H/2+24+F); } - function onSecond(){ - var t = new Date(); - if (t.getSeconds() === 0) drawTime(t); - } - function drawAll(){ - drawTime(new Date()); - } - - return {init:drawAll, tick:onSecond}; + return {init:drawTime, tick:drawTime, tickpersec:false}; } return getFace; diff --git a/apps/multiclock/txtface.jpg b/apps/multiclock/txtface.jpg deleted file mode 100644 index e38341257..000000000 Binary files a/apps/multiclock/txtface.jpg and /dev/null differ diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index e0e967166..1277f0d9d 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -2,3 +2,4 @@ 0.02: Display 12 hour clock as 12:xx not 00:xx when just into PM 0.03: Make it work with Gadgetbridge, Notifications fullscreen on a Bangle 2 0.04: Leave space at the bottom for Chrono widget, set back option at first option +0.05: Added 2 new fonts diff --git a/apps/pastel/README.md b/apps/pastel/README.md index 9e8c133ec..324c3915a 100644 --- a/apps/pastel/README.md +++ b/apps/pastel/README.md @@ -1,7 +1,7 @@ # Pastel Clock - a configurable clock with custom fonts and background * Designed specifically for Bangle 1 and Bangle 2 -* A choice of 5 different custom fonts +* A choice of 7 different custom fonts * Supports the Light and Dark themes * Has a settings menu, change font, enable/disable the grid and the date display @@ -15,3 +15,6 @@ I came up with the name Pastel due to the shade of the grid background. ![](screenshot_b1_light.jpg) ![](screenshot_b2_dark.jpg) +![](screenshot_monoton.jpg) +![](screenshot_elite.jpg) + diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index 98f8af7f9..1fe3e4a58 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -47,6 +47,16 @@ var scale = 1; // size multiplier for this font g.setFontCustom(font, 46, widths, 58+(scale<<8)+(1<<16)); }; +Graphics.prototype.setFontMonoton = function(scale) { + // Actual height 44 (43 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAD+AAAAAAf8AAAAAD/ggAAAAf8HwAAAD/g/4AAAf8H/AAAD/g/4OAAf8H/B/AD/g/4P+Af8H/B/wAfg/4P+AAMH/B/wAAA/4H+AAAD/A/4AAAB4H/AAAAAA/4AAAAAH/AAAAAAP4AAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH//gAAAAf//8AAAA/AAPgAAA8f/x8AAB4//+PAAB5+APxwABzwfwecAAzj//jnAA7n+P85gA7ngAPO4AbnH/xzsAdnP/+c3AN3PAHndgGzOAA5m4DbuAAO7MD9mAADN2Bs3AAB2bA2bAAAbNgbNgAANmwNmwAAGzYGzYAADZsDdmAADN2B+7AABuzAbMwABmbgNneAD3NgHZ3+/3MwBuc//nO4A7nB8HGYAM58AfOcAHeP/+OcABzx/8ecAAc+AA+cAAHH//8cAAB4//48AAAPg+B8AAAD+AP4AAAAP//wAAAAA/+AAAAAAAAAAAAAAAAAAABsAAAAAAA2AAAAAAAbAAAAAAANgAAAAAAGwAAAAAADf////8ABv////+AA3/////AAbAAAAAAAN/////wAG/////4ADYAAAAAABv////+AA3/////AAb/////gANgAAAAAAG/////4ADf////8AAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAADcAAAA2wBs2AADbYA2bAADtsAbZgADm2AftwAHjbAN24AHNtgGzYAPO2wDZsAPebYBs2AOeNsA2bAec22AbNge87bANmwc55tgG7c8542wD9355zbYA2Z5zztsAbODzjm2ANz/nnjbADc/nnhtgBnCPHA2wA74fPAbYAOf+OANsADj8eAG2AA8A+ADbAAP/8AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAABgG6AAAAuwDdsAAG3YBs2AADZuA27AABu3A/ZgAA7dgbtwAAduwN2w2zG3YGzYbZjZsDZsNsxs2Bs2G2Y2bA2bDbMbNgbNhtmNmwNmw2zGzYG7MbdnbsD939m/d2A2Z/7PM3AbuBtwO7AOz73eeZgDc/9n+dwB3H2Y8cwAZ4HnA84AGf/5/84ADz/OP44AAeALwB4AAH/+//4AAA/+H/wAAADwAfAAAAAAAAAAAAAAAAAAAAAAAZsAAAAAB82AAAAAD+bAAAAAHzNgAAAAPjmwAAAAfHzYAAAB+P5sAAAD8fM2AAAHw+ObAAAPj8fNgAAfH4/mwAA+Ph8zYAAcfH4ZsAAA+Px82AAB8fD+bAAD4+HzNgABh8PhmwAAH4/AzYAAPx+AZsAAPD4AM2AAGHwP+bfgAfgH/NvwA/AABmwAA8AAAzYAAYAA/5t+AAAAf82/AAAAAGbAAAAAADNgAAAAAAAAAAAAAAAAAAAAAAAGAAE///ADAAGf//gBwADP//wCcABmAAADmAAz//8C7gAZ//+DMwAMwAAA3YAGf//hZsADP//xu3ABn//4zdgAzDNsNuwAZhu2GzYAMw2bDZsAGYbNhs2ADMNmw2bABmGzYbNgAzDZsdmwAZhs2M3YAMw3d+zcAGYZm+ZsADMOzgd2ABmDM883AAzB3P87AAZgZx47gAMwOcB5gAGYDn/5gADMA4/zwAAAAPADwAAAAD8fgAAAAAf/gAAAAAB+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAD////AAAHwAADwAAHH//8eAAHP///ngAHfgAB8wAHeH/8PcADcf//x3ADsf//+ZgBu8AADu4B2c//8zMA3d///M2AbNwAB2bgduxs2bswP2Z2/O3YGzYzbDZsDZsbths2Bs2Nmw2bA2bGzYbNgbNjZsNmwNmxs2GzYH7c2bHbsDtmbtzdmA2bM3fs3AbMHZnO7AM3BuYOZgHZAzP+dgBmAMx+cwA7gHeAcwAMgB3584AHAAc/84ABgAHHx4AAAAB4D4AAAAAf/wAAAAAD/gAAAAAAAAAAAAAAAAAAZsAAAAAAM2AAAAAAGbAAAAAADNgAAAAABmwAAAAAAzYAAAAAAZsAAAAAAM2AAAAPAGbAAAB/gDNgAAP+ABmwAD/wYAzYAf+D8AZsD/wf8AM2f8D/gAGT/gf8HgAf8D/g/wB/g/8H/AA8H/g/4MAA/8H/B+AH/g/4P+AH4H/B/wADA/4P+AAAH/B/wAAA/4P+AAAAfB/wAAAAAP+AAAAAB/wAAAAAD+AAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAGAAwAAAA/8H/gAAB//v/8AAB4B/APgADz+PP54ABn/x//OABng8eDzAB3HHOcdwA3P9z/nYA7P/d/5mAbOBmYO7ANmebvzNwP3fs392YGzc3ZmbsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNjNmNmwNmxmzGzYGzYzZjZsDZsZsxs2Bs2M2Y2bA2bGbMbNgbNzNmNmwP2Zm7s3YDbv7M+7MBszt3OZuA3MGZwd2ANn/uf8zAGY+zn47gDvAc4A7gA78/Pj5gAOf/z/zwADj8cPjwAA+A/gHgAAH/9//gAAA/4P/AAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/4AAAAAHw/AAAAAHADwADAAHP8cABgAHf/nAA4AHeB5gDuAHcfOYAzADc/7uBdwBs8ezBmYBu4DdgbsA2Z924s3AbN+bM3bgfsxt2duwNm4zbG3YGzYZtjZsDZsM2xs2Bs2GbY2bA2bDNsbNgbNhv2NmwP242zO3YHbszbm7sBs3AAHZuA2Z///M2Abuf//O7AGzh/8ObgDc8AA+dgB3P//+dwAdx//8cwAGeAAA8wADn///44AA8///54AAPgAAB4AAB+AAPwAAAP///gAAAA//+AAAAAAAAAAAAAAAAAAAAAAAAAAAADbBmwAAABtgzYAAAA2wZsAAAAbYM2AAAANsGbAAAAG2DNgAAADbBmwAAABtgzYAAAA2wZsAAAAAAAAAAAAAAAAAAA="), 46, atob("DRYpFR0eHiImHygmDQ=="), 49+(scale<<8)+(1<<16)); +} + +Graphics.prototype.setFontSpecialElite = function(scale) { + // Actual height 40 (39 - 0) + g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAYAAAAAAAfwAAAAAAP/AAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAB/+AAAAAAf/gAAAAAH/4AAAAAAv8AAAAAAN6AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAfAAAAAAAPwAAAAAAP8AAAAAAH+AAAAAAH+AAAAAAD+AAAAAAD/AAAAAAD/AAAAAAB/AAAAAAB/AAAAAAB/AAAAAAB/gAAAAAB/gAAAAAB/gAAAAAA/gAAAAAB/wAAAAAA/4AAAAAA/wAAAAAA/4AAAAAA/4AAAAAAf8AAAAAAP8AAAAAAD8AAAAAAA8AAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//wAAAAP///gAAAH/9/+AAAD/gAf4AAB/AAB+AAA/AAAHwAAPAAAA+AADgAAAPgAAwAAAD4AAcAAAAfAAHAAAAHwABwAAAB8AA4AAAAfAAOAAAAHwABwAAAB8AAcAAAA/AAHgAAAPgAB+AAAH4AAPgAAD8AAD+AAD+AAA/4Af/AAAB////AAAAP///wAAAAP//gAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAOAAAAHAADgAAAD4AB4AAAA+AAeAAAAPgAHgAAAD4AB4AAAAeAAeAAAAHgAHgAAAB4AB4AAAAcAAeAAAAPAAH4AAP/wAB/////+AAf/////gAH/////4AB///+/+AAAAQAAPgAAAAAAB4AAAAAAAeAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPgAAAAAADwAAAAAAA+AAAAAAAPgAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAcAAAB+AAfwAAA/wAf/AAA/8Af/wAAf/AP/8AAGPwP//AADh8D48AAA4OB8OAAAOAAfDgAAHAAPg4AABwADwPAAAcAB8DwAAHAAeAeAABwAHAHgAAcADwB8AAHAB4APgAB4A+AB4AAPAfAAeAAD4fgADgAAf/4AA4AAD/8AAeAAAf+AAfAAAB8AAPwAAAAAAD4AAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAf8AAB/wAH/wAAf8AB/8AAH+AAffgAB4AcDx4AAcAPAAfAAHAPwAHwABwHwAA8AAcB+AAPAAHA/gADwABw/4AA8AAcf+AAPAAHP/gAHwABz74AB8AAf8fAA/AAH8DwAPgAD/A8AHwAA/gHwP8AAPwA//+AADwAH//AAAAAA//gAAAAAD/wAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAP8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/PAAAAAA/jwAAAAAfg8AAAAAPwPAAAAAH4DwAAAAD4A8HAAAD8APBwAAB+ADw8AAA+AA8PAAA/AAPDgAAPgADw8AAHwAB8/AAD+B///wAA/////8AAP/////AAB+f///wAAAAAHx8AAAAAB8PAAAAAAPDwAAAAADw8AAAAAA4PAAAAAAODwAAAAADgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAB/gAAH//wf8AAB//+H/gAAf//h/4AAHJ/wf/AABwD4D/4AAeA+AAeAAHgPAAHgAB4DwAB4AAeA4AAeAAHgOAAHgAA4DgAB4AAOA8AAeAADgPAAHgAB4D4ADwAAeAeAB4AAHAHwAeAABgAfAPgAAYAD8fgAAAAA//wAAAAAH/4AAAAAAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8AAAAAA//wAAAAB///AAAAB////AAAB/7//wAAA/AfB+AAAfAPAPwAAPgHgB+AAHwBwAPgAB4A8AB8AAeAPAAfAAPADwAHwADgA8AB8AA8APAAfAAPADwAHwADwA8AB8AA8AHgA+AAP8B4APgAD/wfAH4AA/8D4D8AAH/A///AAB/wH//gAAH8A//wAAA8AH/4AAAAAA/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAB/4AAAAAA/8AAAAAAP+AAAAAAD+AAAAAAAeAAAAAAAHAAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHwAAA/8AD+AAB//AA/gAB//wAP4AB//gAB+AB//AAAfwB//AAAH8A/wAAAA/A/wAAAAHw/wAAAAB8/wAAAAAffwAAAAAP/wAAAAAD/4AAAAAA/4AAAAAAP8AAAAAAD4AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAP/wAAAH8H/+AAAH/z//wAAD////+AAB///h/gAA/B/gH4AAPAP4A/AAHwB8AHwAB4APAB8AAeADgAfAAHgA4ADwABwAOAA8AAcADgAPAAHAA4ADwAB4AeAA8AAfAHwAPAADwB8AHgAA+A/gD4AAPgP4B+AAB+P/h/gAAP////wAAB/8f/4AAAP8D/8AAAAAAf8AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAA/4APAAAA//gH4AAAP/8D/gAAP4Pg/8AADwB8P/AAB4AfD/4AAeAD4d+AAHAA+AfwADwAHgH8AA4AA8A/AAOAAPAPgADgADwD4AA8AA4B+AAPAAeAfAAB4AHgHgAAeADwD4AAHwA8A8AAA+AfA/AAAHp/h/gAAA3//+gAAAB//+gAAAAd//gAAAACf/wAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAABwB/AAAAB/A/8AAAA/wf/gAAAP+H/4AAAH/h/+AAAB/8f/gAAAP+H/4AAAD/h/+AAAAfwf/gAAAH8C/wAAAAAA3oAAAAAABwAAAAAAAAAAAAAAAAAAAA=="), 46, atob("ERwfHB0cHxsdHB4dEQ=="), 50+(scale<<8)+(1<<16)); +} + const SETTINGS_FILE = "pastel.json"; let settings = undefined; @@ -113,6 +123,10 @@ function draw() { g.setFontCabinSketch(); else if (settings.font == "Orbitron") g.setFontOrbitron(); + else if (settings.font == "Monoton") + g.setFontMonoton(); + else if (settings.font == "Elite") + g.setFontSpecialElite(); else g.setFontLato(); diff --git a/apps/pastel/pastel.settings.js b/apps/pastel/pastel.settings.js index 2e4afadc8..a8aadd58f 100644 --- a/apps/pastel/pastel.settings.js +++ b/apps/pastel/pastel.settings.js @@ -22,14 +22,14 @@ storage.write(SETTINGS_FILE, settings) } - var font_options = ["Lato","Architect","GochiHand","CabinSketch","Orbitron"]; + var font_options = ["Lato","Architect","GochiHand","CabinSketch","Orbitron","Monoton","Elite"]; E.showMenu({ '': { 'title': 'Pastel Clock' }, '< Back': back, 'Font': { value: 0 | font_options.indexOf(s.font), - min: 0, max: 4, + min: 0, max: 6, format: v => font_options[v], onchange: v => { s.font = font_options[v]; diff --git a/apps/pastel/screenshot_elite.jpg b/apps/pastel/screenshot_elite.jpg new file mode 100644 index 000000000..b881830ed Binary files /dev/null and b/apps/pastel/screenshot_elite.jpg differ diff --git a/apps/pastel/screenshot_monoton.jpg b/apps/pastel/screenshot_monoton.jpg new file mode 100644 index 000000000..8abfe3bc9 Binary files /dev/null and b/apps/pastel/screenshot_monoton.jpg differ diff --git a/apps/pastel/screenshot_pastel.png b/apps/pastel/screenshot_pastel.png new file mode 100644 index 000000000..d489f1914 Binary files /dev/null and b/apps/pastel/screenshot_pastel.png differ diff --git a/apps/pomodo/CHANGELOG.md b/apps/pomodo/CHANGELOG.md index b8c5dd621..b4667aff8 100644 --- a/apps/pomodo/CHANGELOG.md +++ b/apps/pomodo/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2021-11-18 + +- [Feature] Ported to Banglejs2 + ## 2019-11-27 - [Feature] App now saves the last interval value diff --git a/apps/pomodo/ChangeLog b/apps/pomodo/ChangeLog index 5560f00bc..3630ae7b6 100644 --- a/apps/pomodo/ChangeLog +++ b/apps/pomodo/ChangeLog @@ -1 +1,2 @@ +0.02: Ported to Banglejs2. 0.01: New App! diff --git a/apps/pomodo/pomodoro.js b/apps/pomodo/pomodoro.js index 3e11739da..96d2e8d6a 100644 --- a/apps/pomodo/pomodoro.js +++ b/apps/pomodo/pomodoro.js @@ -11,8 +11,9 @@ const STATES = { var counterInterval; class State { - constructor (state) { + constructor (state, device) { this.state = state; + this.device = device; this.next = null; } @@ -47,8 +48,8 @@ class State { } class InitState extends State { - constructor (time) { - super(STATES.INIT); + constructor (device) { + super(STATES.INIT, device); this.timeCounter = parseInt(storage.read(".pomodo") || DEFAULT_TIME, 10); } @@ -58,7 +59,7 @@ class InitState extends State { } setButtons () { - setWatch(() => { + this.device.setBTN1(() => { if (this.timeCounter + 300 > 3599) { this.timeCounter = 3599; } else { @@ -67,23 +68,23 @@ class InitState extends State { this.draw(); - }, BTN1, { repeat: true }); + }); - setWatch(() => { + this.device.setBTN3(() => { if (this.timeCounter - 300 > 0) { this.timeCounter -= 300; this.draw(); } - }, BTN3, { repeat: true }); + }); - setWatch(() => { + this.device.setBTN4(() => { if (this.timeCounter - 60 > 0) { this.timeCounter -= 60; this.draw(); } - }, BTN4, { repeat: true }); + }); - setWatch(() => { + this.device.setBTN5(() => { if (this.timeCounter + 60 > 3599) { this.timeCounter = 3599; } else { @@ -92,15 +93,15 @@ class InitState extends State { this.draw(); - }, BTN5, { repeat: true }); + }); - setWatch(() => { + this.device.setBTN2(() => { this.saveTime(); - const startedState = new StartedState(this.timeCounter); + const startedState = new StartedState(this.timeCounter, this.device); this.setNext(startedState); this.next.go(); - }, BTN2, { repeat: true }); + }); } draw () { @@ -112,14 +113,14 @@ class InitState extends State { } class StartedState extends State { - constructor (timeCounter) { - super(STATES.STARTED); + constructor (timeCounter, buttons) { + super(STATES.STARTED, buttons); this.timeCounter = timeCounter; } draw () { - drawCounter(this.timeCounter, 120, 120); + drawCounter(this.timeCounter, g.getWidth() / 2, g.getHeight() / 2); } init () { @@ -137,15 +138,15 @@ class StartedState extends State { this.draw(); } - const doneState = new DoneState(); + const doneState = new DoneState(this.device); this.setNext(doneState); counterInterval = setInterval(countDown.bind(this), 1000); } } class BreakState extends State { - constructor () { - super(STATES.BREAK); + constructor (buttons) { + super(STATES.BREAK, buttons); } draw () { @@ -153,44 +154,40 @@ class BreakState extends State { } init () { - const startedState = new StartedState(TIME_BREAK); + const startedState = new StartedState(TIME_BREAK, this.device); this.setNext(startedState); this.next.go(); } } + class DoneState extends State { - constructor () { - super(STATES.DONE); + constructor (device) { + super(STATES.DONE, device); } setButtons () { - setWatch(() => { - const initState = new InitState(); - clearTimeout(this.timeout); - initState.go(); - }, BTN1, { repeat: true }); + this.device.setBTN1(() => { + }); - setWatch(() => { - const breakState = new BreakState(); - clearTimeout(this.timeout); - breakState.go(); - }, BTN3, { repeat: true }); + this.device.setBTN3(() => { + }); - setWatch(() => { - }, BTN2, { repeat: true }); + this.device.setBTN2(() => { + }); } draw () { g.clear(); - g.setFont("6x8", 2); - g.setFontAlign(0, 0, 3); - g.drawString("AGAIN", 230, 50); - g.drawString("BREAK", 230, 190); - g.setFont("Vector", 45); - g.setFontAlign(-1, -1); - - g.drawString('You\nare\na\nhero!', 50, 40); + E.showPrompt("You are a hero!", { + buttons : {"AGAIN":1,"BREAK":2} + }).then((v) => { + var nextSate = (v == 1 + ? new InitState(this.device) + : new BreakState(this.device)); + clearTimeout(this.timeout); + nextSate.go(); + }); } init () { @@ -215,13 +212,61 @@ class DoneState extends State { } } +class Bangle1 { + setBTN1(callback) { + setWatch(callback, BTN1, { repeat: true }); + } + + setBTN2(callback) { + setWatch(callback, BTN2, { repeat: true }); + } + + setBTN3(callback) { + setWatch(callback, BTN3, { repeat: true }); + } + + setBTN4(callback) { + setWatch(callback, BTN4, { repeat: true }); + } + + setBTN5(callback) { + setWatch(callback, BTN5, { repeat: true }); + } +} + +class Bangle2 { + setBTN1(callback) { + Bangle.on('touch', function(zone, e) { + if (e.y < g.getHeight() / 2) { + callback(); + } + }); + } + + setBTN2(callback) { + setWatch(callback, BTN1, { repeat: true }); + } + + setBTN3(callback) { + Bangle.on('touch', function(zone, e) { + if (e.y > g.getHeight() / 2) { + callback(); + } + }); + } + + setBTN4(callback) { } + + setBTN5(callback) { } +} + function drawCounter (currentValue, x, y) { if (currentValue < 0) { return; } - x = x || 120; - y = y || 120; + x = x || g.getWidth() / 2; + y = y || g.getHeight() / 2; let minutes = 0; let seconds = 0; @@ -249,7 +294,10 @@ function drawCounter (currentValue, x, y) { } function init () { - const initState = new InitState(); + device = (process.env.HWVERSION==1 + ? new Bangle1() + : new Bangle2()); + const initState = new InitState(device); initState.go(); } diff --git a/apps/qalarm/ChangeLog b/apps/qalarm/ChangeLog new file mode 100644 index 000000000..135e69d23 --- /dev/null +++ b/apps/qalarm/ChangeLog @@ -0,0 +1,2 @@ +0.01: First version! +0.02: Fixed alarms not working and localised days of week. \ No newline at end of file diff --git a/apps/qalarm/app-icon.js b/apps/qalarm/app-icon.js new file mode 100644 index 000000000..1a014b796 --- /dev/null +++ b/apps/qalarm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("/wA/AH4A/AH4AF0WiF1wwtF73GB53MAAgkY4wABFqIxPEhQuXGB4vUFxYwMEpBpGBwouNGAwfFF5I1KF6ZQHGAwNLFx4wHF/4v/F/4v/AoYGDF6gaFF5AwHL7QuMBJQvWEpwvxBQ4uRGBAkJT4wuWGBIuIRjKRNF8wwXFy4wWFzIwU53NFzPN5wuR5/PGK4tBDYSNQ5wVCCwIzBAAQoIAAQWGSJ5HFDYYAQIYTCRKRIeBAAYmDAAZsJMCQAbeCAybFiQ0XFTQAIzgAGFcYvz0QAGF84wGF1AwFF1QA/AH4A/ADQ=")) diff --git a/apps/qalarm/app.js b/apps/qalarm/app.js new file mode 100644 index 000000000..64f601bf6 --- /dev/null +++ b/apps/qalarm/app.js @@ -0,0 +1,271 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +let alarms = require("Storage").readJSON("qalarm.json", 1) || []; +/* +Alarm format: +{ + on : true, + t : 23400000, // Time of day since midnight in ms + msg : "Eat chocolate", // (optional) Must be set manually from the IDE + last : 0, // Last day of the month we alarmed on - so we don't alarm twice in one day! + rp : true, // Repeat + as : false, // Auto snooze + hard: true, // Whether the alarm will be like HardAlarm or not + timer : 300, // (optional) If set, this is a timer and it's the time in seconds + daysOfWeek: [true,true,true,true,true,true,true] // What days of the week the alarm is on. First item is Sunday, 2nd is Monday, etc. +} +*/ + +function formatTime(t) { + mins = 0 | (t / 60000) % 60; + hrs = 0 | (t / 3600000); + return hrs + ":" + ("0" + mins).substr(-2); +} + +function formatTimer(t) { + mins = 0 | (t / 60) % 60; + hrs = 0 | (t / 3600); + return hrs + ":" + ("0" + mins).substr(-2); +} + +function getCurrentTime() { + let time = new Date(); + return ( + time.getHours() * 3600000 + + time.getMinutes() * 60000 + + time.getSeconds() * 1000 + ); +} + +function showMainMenu() { + const menu = { + "": { title: "Alarms" }, + "New Alarm": () => showEditAlarmMenu(-1), + "New Timer": () => showEditTimerMenu(-1), + }; + alarms.forEach((alarm, idx) => { + let txt = + (alarm.timer ? "TIMER " : "ALARM ") + + (alarm.on ? "on " : "off ") + + (alarm.timer ? formatTimer(alarm.timer) : formatTime(alarm.t)); + menu[txt] = function () { + if (alarm.timer) showEditTimerMenu(idx); + else showEditAlarmMenu(idx); + }; + }); + menu["< Back"] = () => { + load(); + }; + + if (WIDGETS["qalarm"]) WIDGETS["qalarm"].reload(); + return E.showMenu(menu); +} + +function showEditAlarmMenu(alarmIndex, alarm) { + const newAlarm = alarmIndex < 0; + + if (!alarm) { + if (newAlarm) { + alarm = { + t: 43200000, + on: true, + rp: true, + as: false, + hard: false, + daysOfWeek: new Array(7).fill(true), + }; + } else { + alarm = Object.assign({}, alarms[alarmIndex]); // Copy object in case we don't save it + } + } + + let hrs = 0 | (alarm.t / 3600000); + let mins = 0 | (alarm.t / 60000) % 60; + let secs = 0 | (alarm.t / 1000) % 60; + + const menu = { + "": { title: alarm.msg ? alarm.msg : "Alarms" }, + Hours: { + value: hrs, + onchange: function (v) { + if (v < 0) v = 23; + if (v > 23) v = 0; + hrs = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + Minutes: { + value: mins, + onchange: function (v) { + if (v < 0) v = 59; + if (v > 59) v = 0; + mins = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + Seconds: { + value: secs, + onchange: function (v) { + if (v < 0) v = 59; + if (v > 59) v = 0; + secs = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + 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()), + }; + + function getAlarm() { + alarm.t = hrs * 3600000 + mins * 60000 + secs * 1000; + + alarm.last = 0; + // If alarm is for tomorrow not today (eg, in the past), set day + if (alarm.t < getCurrentTime()) alarm.last = new Date().getDate(); + + return alarm; + } + + menu["> Save"] = function () { + if (newAlarm) alarms.push(getAlarm()); + else alarms[alarmIndex] = getAlarm(); + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + eval(require("Storage").read("qalarmcheck.js")); + showMainMenu(); + }; + + if (!newAlarm) { + menu["> Delete"] = function () { + alarms.splice(alarmIndex, 1); + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + eval(require("Storage").read("qalarmcheck.js")); + showMainMenu(); + }; + } + menu["< Back"] = showMainMenu; + return E.showMenu(menu); +} + +function showDaysMenu(alarmIndex, alarm) { + const menu = { + "": { title: alarm.msg ? alarm.msg : "Alarms" }, + "< Back": () => showEditAlarmMenu(alarmIndex, alarm), + }; + + for (let i = 0; i < 7; i++) { + let dayOfWeek = require("locale").dow({ getDay: () => i }); + menu[dayOfWeek] = { + value: alarm.daysOfWeek[i], + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => (alarm.daysOfWeek[i] = v), + }; + } + + return E.showMenu(menu); +} + +function showEditTimerMenu(timerIndex) { + var newAlarm = timerIndex < 0; + + let alarm; + if (newAlarm) { + alarm = { + timer: 300, + on: true, + rp: false, + as: false, + hard: false, + }; + } else { + alarm = alarms[timerIndex]; + } + + let hrs = 0 | (alarm.timer / 3600); + let mins = 0 | (alarm.timer / 60) % 60; + let secs = (0 | alarm.timer) % 60; + + const menu = { + "": { title: "Timer" }, + Hours: { + value: hrs, + onchange: function (v) { + if (v < 0) v = 23; + if (v > 23) v = 0; + hrs = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + Minutes: { + value: mins, + onchange: function (v) { + if (v < 0) v = 59; + if (v > 59) v = 0; + mins = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + Seconds: { + value: secs, + onchange: function (v) { + if (v < 0) v = 59; + if (v > 59) v = 0; + secs = v; + this.value = v; + }, // no arrow fn -> preserve 'this' + }, + 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), + }, + }; + function getTimer() { + alarm.timer = hrs * 3600 + mins * 60 + secs; + alarm.t = (getCurrentTime() + alarm.timer * 1000) % 86400000; + return alarm; + } + menu["> Save"] = function () { + if (newAlarm) alarms.push(getTimer()); + else alarms[timerIndex] = getTimer(); + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + eval(require("Storage").read("qalarmcheck.js")); + showMainMenu(); + }; + if (!newAlarm) { + menu["> Delete"] = function () { + alarms.splice(timerIndex, 1); + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + eval(require("Storage").read("qalarmcheck.js")); + showMainMenu(); + }; + } + menu["< Back"] = showMainMenu; + return E.showMenu(menu); +} + +showMainMenu(); diff --git a/apps/qalarm/app.png b/apps/qalarm/app.png new file mode 100644 index 000000000..14edf4150 Binary files /dev/null and b/apps/qalarm/app.png differ diff --git a/apps/qalarm/boot.js b/apps/qalarm/boot.js new file mode 100644 index 000000000..6713ad9e1 --- /dev/null +++ b/apps/qalarm/boot.js @@ -0,0 +1 @@ +eval(require("Storage").read("qalarmcheck.js")); diff --git a/apps/qalarm/qalarm.js b/apps/qalarm/qalarm.js new file mode 100644 index 000000000..6b31ba645 --- /dev/null +++ b/apps/qalarm/qalarm.js @@ -0,0 +1,153 @@ +// This file shows the alarm + +function formatTime(t) { + let hrs = Math.floor(t / 3600000); + let mins = Math.round((t / 60000) % 60); + return hrs + ":" + ("0" + mins).substr(-2); +} + +function getCurrentTime() { + let time = new Date(); + return ( + time.getHours() * 3600000 + + time.getMinutes() * 60000 + + time.getSeconds() * 1000 + ); +} + +function getRandomInt(max) { + return Math.floor(Math.random() * Math.floor(max)); +} + +function getRandomFromRange( + lowerRangeMin, + lowerRangeMax, + higherRangeMin, + higherRangeMax +) { + let lowerRange = lowerRangeMax - lowerRangeMin; + let higherRange = higherRangeMax - higherRangeMin; + let fullRange = lowerRange + higherRange; + let randomNum = getRandomInt(fullRange); + if (randomNum <= lowerRangeMax - lowerRangeMin) { + return randomNum + lowerRangeMin; + } else { + return randomNum + (higherRangeMin - lowerRangeMax); + } +} + +function showNumberPicker(currentGuess, randomNum) { + if (currentGuess == randomNum) { + E.showMessage("" + currentGuess + "\n PRESS ENTER", "Get to " + randomNum); + } else { + E.showMessage("" + currentGuess, "Get to " + randomNum); + } +} + +function showPrompt(msg, buzzCount, alarm) { + E.showPrompt(msg, { + title: alarm.timer ? "TIMER!" : "ALARM!", + buttons: { Sleep: true, Ok: false }, // default is sleep so it'll come back in 10 mins + }).then(function (sleep) { + buzzCount = 0; + if (sleep) { + if (alarm.ohr === undefined) alarm.ohr = alarm.t; + alarm.t += 10 / 60; // 10 minutes + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + load(); + } else { + alarm.last = new Date().getDate(); + if (alarm.ohr !== undefined) { + alarm.t = alarm.ohr; + delete alarm.ohr; + } + if (!alarm.rp) alarm.on = false; + require("Storage").write("qalarm.json", JSON.stringify(alarms)); + load(); + } + }); +} + +function showAlarm(alarm) { + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) return; // total silence + let msg = formatTime(alarm.t); + let buzzCount = 20; + if (alarm.msg) msg += "\n" + alarm.msg + "!"; + + if (alarm.hard) { + let okClicked = false; + let currentGuess = 10; + let randomNum = getRandomFromRange(0, 7, 13, 20); + showNumberPicker(currentGuess, randomNum); + setWatch( + (o) => { + if (!okClicked && currentGuess < 20) { + currentGuess = currentGuess + 1; + showNumberPicker(currentGuess, randomNum); + } + }, + BTN1, + { repeat: true, edge: "rising" } + ); + + setWatch( + (o) => { + if (currentGuess == randomNum) { + okClicked = true; + showPrompt(msg, buzzCount, alarm); + } + }, + BTN2, + { repeat: true, edge: "rising" } + ); + + setWatch( + (o) => { + if (!okClicked && currentGuess > 0) { + currentGuess = currentGuess - 1; + showNumberPicker(currentGuess, randomNum); + } + }, + BTN3, + { repeat: true, edge: "rising" } + ); + } else { + showPrompt(msg, buzzCount, alarm); + } + + function buzz() { + Bangle.buzz(500).then(() => { + setTimeout(() => { + Bangle.buzz(500).then(function () { + setTimeout(() => { + Bangle.buzz(2000).then(function () { + if (buzzCount--) setTimeout(buzz, 2000); + else if (alarm.as) { + // auto-snooze + buzzCount = 20; + setTimeout(buzz, 600000); // 10 minutes + } + }); + }, 100); + }); + }, 100); + }); + } + buzz(); +} + +let time = new Date(); +let t = getCurrentTime(); +let alarms = require("Storage").readJSON("qalarm.json", 1) || []; + +let active = alarms.filter( + (alarm) => + alarm.on && + alarm.t < t && + alarm.last != time.getDate() && + (alarm.timer || alarm.daysOfWeek[time.getDay()]) +); + +if (active.length) { + showAlarm(active.sort((a, b) => a.t - b.t)[0]); +} diff --git a/apps/qalarm/qalarmcheck.js b/apps/qalarm/qalarmcheck.js new file mode 100644 index 000000000..9a3f10d5e --- /dev/null +++ b/apps/qalarm/qalarmcheck.js @@ -0,0 +1,41 @@ +/** + * This file checks for upcoming alarms and schedules qalarm.js to deal with them and itself to continue doing these checks. + */ + +print("Checking for alarms..."); + +clearInterval(); + +function getCurrentTime() { + let time = new Date(); + return ( + time.getHours() * 3600000 + + time.getMinutes() * 60000 + + time.getSeconds() * 1000 + ); +} + +let time = new Date(); +let t = getCurrentTime(); + +let nextAlarms = (require("Storage").readJSON("qalarm.json", 1) || []) + .filter( + (alarm) => + alarm.on && + alarm.t > t && + alarm.last != time.getDate() && + (alarm.timer || alarm.daysOfWeek[time.getDay()]) + ) + .sort((a, b) => a.t - b.t); + +if (nextAlarms[0]) { + setTimeout(() => { + eval(require("Storage").read("qalarmcheck.js")); + load("qalarm.js"); + }, nextAlarms[0].t - t); +} else { + // No alarms found: will re-check at midnight + setTimeout(() => { + eval(require("Storage").read("qalarmcheck.js")); + }, 86400000 - t); +} diff --git a/apps/qalarm/widget.js b/apps/qalarm/widget.js new file mode 100644 index 000000000..f80aff653 --- /dev/null +++ b/apps/qalarm/widget.js @@ -0,0 +1,22 @@ +WIDGETS["qalarm"] = { + 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 () { + WIDGETS["qalarm"].width = ( + require("Storage").readJSON("qalarm.json", 1) || [] + ).some((alarm) => alarm.on) + ? 24 + : 0; + }, +}; +WIDGETS["qalarm"].reload(); diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog new file mode 100644 index 000000000..2ea6e9fa8 --- /dev/null +++ b/apps/recorder/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Use 'recorder.log..' rather than 'record.log..' + Fix interface.html +0.03: Fix theme and maps/graphing if no GPS diff --git a/apps/recorder/README.md b/apps/recorder/README.md new file mode 100644 index 000000000..ba53a99f2 --- /dev/null +++ b/apps/recorder/README.md @@ -0,0 +1,27 @@ +# Recorder + +![icon](app.png) + +This app allows you to record data every few seconds - it can run in background. + +Usually you'd record GPS (but this is not required). The data can later be exported as CSV, KML or GPX files via the Download button in the Bangle.js App Store entry for Recorder. + +## Usage + +First run the `Recorder` app, here you can configure what you want to record, how often, +and you can start and stop recordings. + +You can record + +* **Time** The current time +* **GPS** GPS Latitude, Longitude and Altitude +* **Steps** Steps counted by the step counter +* **HR** Heart rate + +**Note:** It is possible for other apps to record information using this app +as well. They need to define a `foobar.recorder.js` file - see the `getRecorders` +function in `widget.js` for more information. + +## Tips + +When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix. diff --git a/apps/recorder/app-icon.js b/apps/recorder/app-icon.js new file mode 100644 index 000000000..4181d2b12 --- /dev/null +++ b/apps/recorder/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///vPWvN8kvkuu14/s3OMjN0Kf4AQ2vaCB0Ftu3oARfgdt23AGsO2NaHQT+MB2XJCJ1MyXJsAQMgUky3JkARMjIRBpIRNvMl2VJlAQLldvtmSpN+CJcbt2ECJsBregggRBv4RRdJfbgEkyVLq3ACJ1Jq3dCBMKBYIRBpFW7d0CJUDsgRBhdbtvQCJHYgUTm1IgEttuwCI8GCIMSpMwA4MVEZPoCIUkaxj7BoQRPiQRCwARNpARByARNpMJCJyNBgKjBCJy1CCJ79BfZYNDCJoxBBoQnCABBVFN4IRJPIoRLV4sCpMgCJTbECJYKFCJUJBQsJfpoA/A")) diff --git a/apps/recorder/app-settings.json b/apps/recorder/app-settings.json new file mode 100644 index 000000000..4a3117a17 --- /dev/null +++ b/apps/recorder/app-settings.json @@ -0,0 +1,6 @@ +{ + "recording":false, + "file":"record.log0.csv", + "period":10, + "record" : ["gps"] +} diff --git a/apps/recorder/app.js b/apps/recorder/app.js new file mode 100644 index 000000000..d29959e25 --- /dev/null +++ b/apps/recorder/app.js @@ -0,0 +1,412 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var settings; + +var osm; +try { // if it's installed, use the OpenStreetMap module + osm = require("openstmap"); +} catch (e) {} + +function loadSettings() { + settings = require("Storage").readJSON("recorder.json",1)||{}; + var changed = false; + if (!settings.file) { + changed = true; + settings.file = "recorder.log0.csv"; + } + if (!Array.isArray(settings.record)) { + settings.record = ["gps"]; + changed = true; + } + if (changed) + require("Storage").writeJSON("recorder.json", settings); +} +loadSettings(); + +function updateSettings() { + require("Storage").writeJSON("recorder.json", settings); + if (WIDGETS["recorder"]) + WIDGETS["recorder"].reload(); +} + +function getTrackNumber(filename) { + return parseInt(filename.match(/^recorder\.log(.*)\.csv$/)[1]||0); +} + +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); + if (v) settings.record.push(id); + updateSettings(); + } + }; + } + const mainmenu = { + '': { 'title': 'Recorder' }, + '< Back': ()=>{load();}, + 'RECORD': { + value: !!settings.recording, + format: v=>v?"On":"Off", + onchange: v => { + setTimeout(function() { + E.showMenu(); + WIDGETS["recorder"].setRecording(v).then(function() { + print("Complete"); + loadSettings(); + print(settings.recording); + showMainMenu(); + }); + }, 1); + } + }, + 'File #': { + value: getTrackNumber(settings.file), + min: 0, + max: 99, + step: 1, + onchange: v => { + settings.recording = false; // stop recording if we change anything + settings.file = "recorder.log"+v+".csv"; + updateSettings(); + } + }, + 'View Tracks': ()=>{viewTracks();}, + 'Time Period': { + value: settings.period||10, + min: 1, + max: 120, + step: 1, + format: v=>v+"s", + onchange: v => { + settings.recording = false; // stop recording if we change anything + settings.period = v; + updateSettings(); + } + } + }; + var recorders = WIDGETS["recorder"].getRecorders(); + Object.keys(recorders).forEach(id=>{ + mainmenu["Log "+recorders[id]().name] = menuRecord(id); + }); + return E.showMenu(mainmenu); +} + + + +function viewTracks() { + const menu = { + '': { 'title': 'Tracks' } + }; + var found = false; + require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{ + found = true; + menu["Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); + }); + if (!found) + menu["No Tracks found"] = function(){}; + menu['< Back'] = () => { showMainMenu(); }; + return E.showMenu(menu); +} + +function getTrackInfo(filename) { + "ram" + var minLat = 90; + var maxLat = -90; + var minLong = 180; + var maxLong = -180; + var starttime, duration=0; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + var fields, timeIdx, latIdx, lonIdx; + var nl = 0, c, n; + if (l!==undefined) { + fields = l.trim().split(","); + timeIdx = fields.indexOf("Time"); + latIdx = fields.indexOf("Latitude"); + lonIdx = fields.indexOf("Longitude"); + l = f.readLine(f); + } + if (l!==undefined) { + c = l.split(","); + starttime = parseInt(c[timeIdx]); + } + // pushed this loop together to try and bump loading speed a little + while(l!==undefined) { + ++nl;c=l.split(",");l = f.readLine(f); + if (c[latIdx]=="")continue; + n = +c[latIdx];if(n>maxLat)maxLat=n;if(nmaxLong)maxLong=n;if(nylen ? screenSize/xlen : screenSize/ylen; + return { + fn : getTrackNumber(filename), + fields : fields, + filename : filename, + time : new Date(starttime*1000), + records : nl, + minLat : minLat, maxLat : maxLat, + minLong : minLong, maxLong : maxLong, + lat : (minLat+maxLat)/2, lon : (minLong+maxLong)/2, + lfactor : lfactor, + scale : scale, + duration : Math.round(duration) + }; +} + +function asTime(v){ + var mins = Math.floor(v/60); + var secs = v-mins*60; + return ""+mins.toString()+"m "+secs.toString()+"s"; +} + +function viewTrack(filename, info) { + if (!info) { + E.showMessage("Loading...","Track "+getTrackNumber(filename)); + info = getTrackInfo(filename); + } + //console.log(info); + const menu = { + '': { 'title': 'Track '+info.fn } + }; + if (info.time) + menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){}; + menu["Duration"] = { value : asTime(info.duration)}; + menu["Records"] = { value : ""+info.records }; + if (info.fields.includes("Latitude")) + menu['Plot Map'] = function() { + info.qOSTM = false; + plotTrack(info); + }; + if (osm && info.fields.includes("Latitude")) + menu['Plot OpenStMap'] = function() { + info.qOSTM = true; + plotTrack(info); + } + if (info.fields.includes("Altitude")) + menu['Plot Alt.'] = function() { + plotGraph(info, "Altitude"); + }; + menu['Plot Speed'] = function() { + plotGraph(info, "Speed"); + }; + // TODO: steps, heart rate? + menu['Erase'] = function() { + E.showPrompt("Delete Track?").then(function(v) { + if (v) { + settings.recording = false; + updateSettings(); + var f = require("Storage").open(filename,"r"); + f.erase(); + viewTracks(); + } else + viewTrack(n, info); + }); + }; + menu['< Back'] = () => { viewTracks(); }; + return E.showMenu(menu); +} + +function plotTrack(info) { + "ram" + + function distance(lat1,long1,lat2,long2) { "ram" + var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360); + var y = lat2 - lat1; + return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180; + } + + // Function to convert lat/lon to XY + var getMapXY; + if (info.qOSTM) { + getMapXY = osm.latLonToXY.bind(osm); + } else { + getMapXY = function(lat, lon) { "ram" + return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale), + y:cy + Math.round((info.lat - lat)*info.scale)}; + }; + } + + E.showMenu(); // remove menu + E.showMessage("Drawing...","Track "+info.fn); + g.flip(); // on buffered screens, draw a not saying we're busy + g.clear(1); + var s = require("Storage"); + var W = g.getWidth(); + var H = g.getHeight(); + var cx = W/2; + var cy = 24 + (H-24)/2; + if (!info.qOSTM) { + g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]); + g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50); + } else { + osm.lat = info.lat; + osm.lon = info.lon; + osm.draw(); + g.setColor("#000"); + } + var latIdx = info.fields.indexOf("Latitude"); + var lonIdx = info.fields.indexOf("Longitude"); + g.drawString(asTime(info.duration),10,220); + var f = require("Storage").open(info.filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + l = f.readLine(f); // skip headers + var ox=0; + var oy=0; + var olat,olong,dist=0; + var i=0, c = l.split(","); + // skip until we find our first data + while(l!==undefined && c[latIdx]=="") { + c = l.split(","); + l = f.readLine(f); + } + // now start plotting + var lat = +c[latIdx]; + var long = +c[lonIdx]; + var mp = getMapXY(lat, long); + g.moveTo(mp.x,mp.y); + g.setColor("#0f0"); + g.fillCircle(mp.x,mp.y,5); + if (info.qOSTM) g.setColor("#f09"); + else g.setColor(g.theme.fg); + l = f.readLine(f); + g.flip(); // force update + while(l!==undefined) { + c = l.split(",");l = f.readLine(f); + if (c[latIdx]=="")continue; + lat = +c[latIdx]; + long = +c[lonIdx]; + mp = getMapXY(lat, long); + g.lineTo(mp.x,mp.y); + if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible + var d = distance(olat,olong,lat,long); + if (!isNaN(d)) dist+=d; + olat = lat; + olong = long; + ox = mp.x; + oy = mp.y; + if (++i > 100) { g.flip();i=0; } + } + g.setColor("#f00"); + g.fillCircle(ox,oy,5); + if (info.qOSTM) g.setColor("#000"); + else g.setColor(g.theme.fg); + g.drawString(require("locale").distance(dist),120,220); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString("Back",230,200); + setWatch(function() { + viewTrack(info.fn, info); + }, global.BTN3||BTN1); + Bangle.drawWidgets(); + g.flip(); +} + +function plotGraph(info, style) { + "ram" + E.showMenu(); // remove menu + E.showMessage("Calculating...","Track "+info.fn); + var filename = info.filename; + var infn = new Float32Array(80); + var infc = new Uint16Array(80); + var title; + var lt = 0; // last time + var tn = 0; // count for each time period + var strt, dur = info.duration; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + l = f.readLine(f); // skip headers + var nl = 0, c, i; + var timeIdx = info.fields.indexOf("Time"); + if (l!==undefined) { + c = l.split(","); + strt = c[timeIdx]; + } + if (style=="Altitude") { + title = "Altitude (m)"; + var altIdx = info.fields.indexOf("Altitude"); + while(l!==undefined) { + ++nl;c=l.split(",");l = f.readLine(f); + if (c[altIdx]=="") continue; + i = Math.round(80*(c[timeIdx] - strt)/dur); + infn[i]+=+c[altIdx]; + infc[i]++; + } + } else if (style=="Speed") { + title = "Speed (m/s)"; + var latIdx = info.fields.indexOf("Latitude"); + var lonIdx = info.fields.indexOf("Longitude"); + // skip until we find our first data + while(l!==undefined && c[latIdx]=="") { + c = l.split(","); + l = f.readLine(f); + } + // now iterate + var p,lp = Bangle.project({lat:c[1],lon:c[2]}); + var t,dx,dy,d,lt = c[timeIdx]; + while(l!==undefined) { + ++nl;c=l.split(","); + t = c[timeIdx]; + i = Math.round(80*(t - strt)/dur); + p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]}); + dx = p.x-lp.x; + dy = p.y-lp.y; + d = Math.sqrt(dx*dx+dy*dy); + if (t!=lt) { + infn[i]+=d / (t-lt); // speed + infc[i]++; + } + lp = p; + lt = t; + l = f.readLine(f); + } + } else throw new Error("Unknown type "+style); + var min=100000,max=-100000; + for (var i=0;i0) infn[i]/=infc[i]; + var n = infn[i]; + if (n>max) max=n; + if (n 8) { + grid*=2; + } + // draw + g.clear(1).setFont("6x8",1); + var r = require("graph").drawLine(g, infn, { + x:4,y:24, + width: g.getWidth()-24, + height: g.getHeight()-(24+8), + axes : true, + gridy : grid, + gridx : 50, + title: title, + xlabel : x=>Math.round(x*dur/(60*infn.length))+" min" // minutes + }); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + g.drawString("Back",230,200); + setWatch(function() { + viewTrack(info.filename, info); + }, global.BTN3||BTN1); + g.flip(); +} + +showMainMenu(); diff --git a/apps/recorder/app.png b/apps/recorder/app.png new file mode 100644 index 000000000..036f5d132 Binary files /dev/null and b/apps/recorder/app.png differ diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html new file mode 100644 index 000000000..ad0de4887 --- /dev/null +++ b/apps/recorder/interface.html @@ -0,0 +1,255 @@ + + + + + +
+ + + + + diff --git a/apps/recorder/settings.js b/apps/recorder/settings.js new file mode 100644 index 000000000..2a9a7a0d8 --- /dev/null +++ b/apps/recorder/settings.js @@ -0,0 +1,4 @@ +(function(back) { + // just go right to our app - we need all the memory + load("record.app.js"); +})(); diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js new file mode 100644 index 000000000..09893bbb7 --- /dev/null +++ b/apps/recorder/widget.js @@ -0,0 +1,222 @@ +(() => { + var storageFile; // file for GPS track + var entriesWritten = 0; + var activeRecorders = []; + var writeInterval; + + function loadSettings() { + var settings = require("Storage").readJSON("recorder.json",1)||{}; + settings.period = settings.period||10; + if (!settings.file || !settings.file.startsWith("recorder.log")) + settings.recording = false; + return settings; + } + + function getRecorders() { + var recorders = { + gps:function() { + var lat = 0; + var lon = 0; + var alt = 0; + var samples = 0; + var hasFix = 0; + function onGPS(f) { + hasFix = f.fix; + if (!hasFix) return; + lat += f.lat; + lon += f.lon; + alt += f.alt; + samples++; + } + return { + name : "GPS", + fields : ["Latitude","Longitude","Altitude"], + getValues : () => { + var r = ["","",""]; + if (samples) + r = [(lat/samples).toFixed(6),(lon/samples).toFixed(6),Math.round(alt/samples)]; + samples = 0; lat = 0; lon = 0; alt = 0; + return r; + }, + start : () => { + hasFix = false; + Bangle.on('GPS', onGPS); + Bangle.setGPSPower(1,"recorder"); + }, + stop : () => { + hasFix = false; + Bangle.removeListener('GPS', onGPS); + Bangle.setGPSPower(0,"recorder"); + }, + draw : (x,y) => g.setColor(hasFix?"#f00":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y) + }; + }, + hrm:function() { + var bpm = 0, bpmConfidence = 0; + var hasBPM = false; + function onHRM(h) { + if (h.confidence >= bpmConfidence) { + bpmConfidence = h.confidence; + bpm = h.bpm; + if (bpmConfidence) hasBPM = true; + } + } + return { + name : "HR", + fields : ["Heartrate"], + getValues : () => { + var r = [bpmConfidence?bpm:""]; + bpm = 0; bpmConfidence = 0; + return r; + }, + start : () => { + hasBPM = false; + Bangle.on('HRM', onHRM); + Bangle.setHRMPower(1,"recorder"); + }, + stop : () => { + hasBPM = false; + Bangle.removeListener('HRM', onHRM); + Bangle.setHRMPower(0,"recorder"); + }, + draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y) + }; + }, + steps:function() { + var lastSteps = 0; + return { + name : "Steps", + fields : ["Steps"], + getValues : () => { + var c = Bangle.getStepCount(), r=[c-lastSteps]; + lastSteps = c; + return r; + }, + start : () => { lastSteps = Bangle.getStepCount(); }, + stop : () => {}, + draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y) + }; + } + // TODO: recAltitude from pressure sensor + }; + /* eg. foobar.recorder.js + (function(recorders) { + recorders.foobar = { + name : "Foobar", + fields : ["foobar"], + getValues : () => [123], + start : () => {}, + stop : () => {}, + draw (x,y) => {} // draw 12x12px status image + } + }) + */ + require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders)); + return recorders; + } + + function writeLog() { + entriesWritten++; + WIDGETS["recorder"].draw(); + try { + var fields = [Math.round(getTime())]; + activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.getValues())); + if (storageFile) storageFile.write(fields.join(",")+"\n"); + } catch(e) { + // If storage.write caused an error, disable + // GPS recording so we don't keep getting errors! + console.log("recorder: error", e); + var settings = loadSettings(); + settings.recording = false; + require("Storage").write("recorder.json", settings); + reload(); + } + } + + // Called by the GPS app to reload settings and decide what to do + function reload() { + var settings = loadSettings(); + if (writeInterval) clearInterval(writeInterval); + writeInterval = undefined; + + activeRecorders.forEach(rec => rec.stop()); + activeRecorders = []; + entriesWritten = 0; + + if (settings.recording) { + // set up recorders + var recorders = getRecorders(); // TODO: order?? + settings.record.forEach(r => { + var recorder = recorders[r]; + if (!recorder) { + console.log("Recorder for "+E.toJS(r)+"+not found"); + return; + } + var activeRecorder = recorder(); + activeRecorder.start(); + activeRecorders.push(activeRecorder); + // TODO: write field names? + }); + WIDGETS["recorder"].width = 15 + ((activeRecorders.length+1)>>1)*12; // 12px per recorder + // open/create file + if (require("Storage").list(settings.file).length) { // Append + storageFile = require("Storage").open(settings.file,"a"); + // TODO: what if loaded modules are different?? + } else { + storageFile = require("Storage").open(settings.file,"w"); + // New file - write headers + var fields = ["Time"]; + activeRecorders.forEach(recorder => fields.push.apply(fields,recorder.fields)); + storageFile.write(fields.join(",")+"\n"); + } + // start recording... + WIDGETS["recorder"].draw(); + writeInterval = setInterval(writeLog, settings.period*1000); + } else { + WIDGETS["recorder"].width = 0; + storageFile = undefined; + } + } + // add the widget + WIDGETS["recorder"]={area:"tl",width:0,draw:function() { + if (!writeInterval) return; + g.reset(); g.drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2); + activeRecorders.forEach((recorder,i)=>{ + recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12); + }); + },getRecorders:getRecorders,reload:function() { + reload(); + Bangle.drawWidgets(); // relayout all widgets + },setRecording:function(isOn) { + var settings = loadSettings(); + if (isOn && !settings.recording && require("Storage").list(settings.file).length) + return E.showPrompt("Overwrite\nLog 0?",{title:"Recorder",buttons:{Yes:"yes",No:"no"}}).then(selection=>{ + if (selection=="no") return false; // just cancel + if (selection=="yes") require("Storage").open(settings.file,"r").erase(); + // TODO: Add 'new file' option + return WIDGETS["recorder"].setRecording(1); + }); + settings.recording = isOn; + require("Storage").write("recorder.json", settings); + WIDGETS["recorder"].reload(); + return Promise.resolve(settings.recording); + }/*,plotTrack:function(m) { // m=instance of openstmap module + // if we're here, settings was already loaded + var f = require("Storage").open(settings.file,"r"); + var l = f.readLine(f); + if (l===undefined) return; + var c = l.split(","); + var mp = m.latLonToXY(+c[1], +c[2]); + g.moveTo(mp.x,mp.y); + l = f.readLine(f); + while(l!==undefined) { + c = l.split(","); + mp = m.latLonToXY(+c[1], +c[2]); + g.lineTo(mp.x,mp.y); + g.fillCircle(mp.x,mp.y,2); // make the track more visible + l = f.readLine(f); + } + }*/}; + // load settings, set correct widget width + reload(); +})() diff --git a/apps/s7clk/README.md b/apps/s7clk/README.md new file mode 100644 index 000000000..6b91abfe3 --- /dev/null +++ b/apps/s7clk/README.md @@ -0,0 +1,4 @@ +# Simple 7 Segment Clock + +![](screenshot_s7segment.png) + diff --git a/apps/s7clk/screenshot_s7segment.png b/apps/s7clk/screenshot_s7segment.png new file mode 100644 index 000000000..a0386e540 Binary files /dev/null and b/apps/s7clk/screenshot_s7segment.png differ diff --git a/apps/sclock/ChangeLog b/apps/sclock/ChangeLog index 44a0ec504..dc76b8299 100644 --- a/apps/sclock/ChangeLog +++ b/apps/sclock/ChangeLog @@ -3,3 +3,4 @@ 0.04: Make this clock do 12h and 24h 0.05: setUI, screen size changes 0.06: Use Bangle.setUI for button/launcher handling +0.07: Update *on* the minute rather than every 15 secs diff --git a/apps/sclock/clock-simple.js b/apps/sclock/clock-simple.js index 8fb204d22..a399b05a7 100644 --- a/apps/sclock/clock-simple.js +++ b/apps/sclock/clock-simple.js @@ -1,4 +1,3 @@ -/* jshint esversion: 6 */ const big = g.getWidth()>200; const timeFontSize = big?6:5; const dateFontSize = big?3:2; @@ -14,7 +13,19 @@ const yposGMT = xyCenter*1.9; // Check settings for what type our clock should be var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; -function drawSimpleClock() { +// 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() { // get date var d = new Date(); var da = d.toString().split(" "); @@ -60,11 +71,18 @@ function drawSimpleClock() { var gmt = da[5]; g.setFont(font, gmtFontSize); g.drawString(gmt, xyCenter, yposGMT, true); + + queueDraw(); } -// handle switch display on by pressing BTN1 -Bangle.on('lcdPower', function(on) { - if (on) drawSimpleClock(); +// 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; + } }); // clean app screen @@ -74,8 +92,5 @@ Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); -// refesh every 15 sec -setInterval(drawSimpleClock, 15E3); - // draw now -drawSimpleClock(); +draw(); diff --git a/apps/sclock/screenshot_simplec.png b/apps/sclock/screenshot_simplec.png new file mode 100644 index 000000000..a12db3ec8 Binary files /dev/null and b/apps/sclock/screenshot_simplec.png differ diff --git a/apps/score/README.md b/apps/score/README.md index 1de1ccdb5..6a5bf8172 100644 --- a/apps/score/README.md +++ b/apps/score/README.md @@ -4,6 +4,9 @@ This app will allow you to keep scores for most kinds of sports. To correct a falsely awarded point simply open and close the menu within .5 seconds. This will put the app into correction mode (indicated by the `R`). In this mode any score increments will be decrements. To move back a set, reduce both players scores to 0, then decrement one of the scores once again. +## Screenshot +![](screenshot_score.png) + ## Bangle.js 1 | Keybinding | Description | |---------------------|------------------------------| diff --git a/apps/score/screenshot_score.png b/apps/score/screenshot_score.png new file mode 100644 index 000000000..662c59e9e Binary files /dev/null and b/apps/score/screenshot_score.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 49915ee21..faa50405f 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -32,4 +32,7 @@ 0.27: Add Theme menu 0.28: Update Quiet Mode widget (if present) 0.29: Add Customize to Theme menu -0.30: Move '< Back' to the top of menus \ No newline at end of file +0.30: Move '< Back' to the top of menus +0.31: Remove Bangle 1 settings when running on Bangle 2 +0.32: Fix 'beep' menu on Bangle.js 2 +0.33: Really fix 'beep' menu on Bangle.js 2 this time diff --git a/apps/setting/settings-icon.js b/apps/setting/settings-icon.js index 7b68f80c0..abc7a3060 100644 --- a/apps/setting/settings-icon.js +++ b/apps/setting/settings-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AFEiAAgX/C/4SFkADBgQXFBIgECAAYSCkAWGBIoXGyQTHABBZLkUhiMRiQXLIQwVBAAZlIC44tCAAYxGIxIWFGA4XIFwwwHXBAWHGAwXHFxAwGPAYXTX44XDiAJBgIXGyDAHFAYKDMAq+EGAgXNCwwX/C453XU6IWHa6ZFCC6JJCC4hgEAAoOEC5AwIFwhgEBAgwIBoqmGGBIuFVAgXFGAwLFYAoLFGIYtFeA4MGABMpC4pICkBMGBIpGFC4SuIBIoWFAAxZLC/4X/AFQ")) +require("heatshrink").decompress(atob("mEw4UA///+Nj5lCt9TH+cBqtVoALWqALTgoLUiALFgoLDqoBBAAQGCHAdRBYdFKwZECqv614ECGQQsCr2q1W1BYkVAoPqBYOrAoNUBYdXBQIAB6oLDEQgkEBYdaBYelBYt6BYetBYYvBtWq0EK1WpF4ZfBIwIFBJATCDBY6PDiuq1AEBlWqBdA7KKZZrLQZabNWZLLLcZb7LBYVV/WvAgRfCNYNRBAVVoq/FJQRECR4gnBEwQEBggLDGQg4CBag4DBaBWBBaoATA")) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index a0e535df7..fcf651b6f 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -1,6 +1,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); +const BANGLEJS2 = process.env.HWVERSION==2; const storage = require('Storage'); let settings; @@ -37,7 +38,7 @@ function resetSettings() { quiet: 0, // quiet mode: 0: off, 1: priority only, 2: total silence timeout: 10, // Default LCD timeout in seconds vibrate: true, // Vibration enabled by default. App must support - beep: "vib", // Beep enabled by default. App must support + beep: BANGLEJS2?true:"vib", // Beep enabled by default. App must support timezone: 0, // Set the timezone for the device HID: false, // BLE HID mode, off by default clock: null, // a string for the default clock's name @@ -71,8 +72,37 @@ if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this alway const boolFormat = v => v ? "On" : "Off"; function showMainMenu() { - var beepV = [false, true, "vib"]; - var beepN = ["Off", "Piezo", "Vibrate"]; + var beepMenuItem; + if (BANGLEJS2) { + beepMenuItem = { + value: settings.beep!=false, + format: boolFormat, + onchange: v => { + settings.beep = v; + updateSettings(); + if (settings.beep) { + analogWrite(VIBRATE,0.1,{freq:2000}); + setTimeout(()=>VIBRATE.reset(),200); + } // beep with vibration moter + } + }; + } else { // Bangle.js 1 + var beepV = [false, true, "vib"]; + var beepN = ["Off", "Piezo", "Vibrate"]; + beepMenuItem = { + value: Math.max(0 | beepV.indexOf(settings.beep),0), + min: 0, max: beepV.length-1, + format: v => beepN[v], + onchange: v => { + settings.beep = beepV[v]; + if (v==1) { analogWrite(D18,0.5,{freq:2000});setTimeout(()=>D18.reset(),200); } // piezo on Bangle.js 1 + else if (v==2) { analogWrite(VIBRATE,0.1,{freq:2000});setTimeout(()=>VIBRATE.reset(),200); } // vibrate + updateSettings(); + } + }; + } + + const mainmenu = { '': { 'title': 'Settings' }, '< Back': ()=>load(), @@ -87,17 +117,7 @@ function showMainMenu() { updateSettings(); } }, - 'Beep': { - value: 0 | beepV.indexOf(settings.beep), - min: 0, max: 2, - format: v => beepN[v], - onchange: v => { - settings.beep = beepV[v]; - if (v==1) { analogWrite(D18,0.5,{freq:2000});setTimeout(()=>D18.reset(),200); } // piezo - else if (v==2) { analogWrite(D13,0.1,{freq:2000});setTimeout(()=>D13.reset(),200); } // vibrate - updateSettings(); - } - }, + 'Beep': beepMenuItem, 'Vibration': { value: settings.vibrate, format: boolFormat, @@ -119,6 +139,7 @@ function showMainMenu() { 'Reset Settings': ()=>showResetMenu(), 'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }, }; + return E.showMenu(mainmenu); } @@ -144,7 +165,7 @@ function showBLEMenu() { } }, 'HID': { - value: 0 | hidV.indexOf(settings.HID), + value: Math.max(0,0 | hidV.indexOf(settings.HID)), min: 0, max: 3, format: v => hidN[v], onchange: v => { @@ -191,7 +212,7 @@ function showThemeMenu() { 'Light BW': ()=>{ upd({ fg:cl("#000"), bg:cl("#fff"), - fg2:cl("#00f"), bg2:cl("#0ff"), + fg2:cl("#000"), bg2:cl("#cff"), fgH:cl("#000"), bgH:cl("#0ff"), dark:false }); @@ -356,7 +377,10 @@ function showLCDMenu() { settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1; updateOptions(); } - }, + } + }; + if (!BANGLEJS2) + Object.assign(lcdMenu, { 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, @@ -372,7 +396,8 @@ function showLCDMenu() { settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3; updateOptions(); } - }, + }}); + Object.assign(lcdMenu, { 'Wake on FaceUp': { value: settings.options.wakeOnFaceUp, format: boolFormat, @@ -427,7 +452,7 @@ function showLCDMenu() { updateOptions(); } } - } + }); return E.showMenu(lcdMenu) } function showQuietModeMenu() { diff --git a/apps/simplest/ChangeLog b/apps/simplest/ChangeLog index d69da4ddc..f37015d6a 100644 --- a/apps/simplest/ChangeLog +++ b/apps/simplest/ChangeLog @@ -1,2 +1,3 @@ 0.01: Modified for use with new bootloader and firmware 0.02: Use Bangle.setUI for button/launcher handling +0.03: Fix display for Bangle 2 diff --git a/apps/simplest/app.js b/apps/simplest/app.js index 2ed4e5580..68564ff33 100644 --- a/apps/simplest/app.js +++ b/apps/simplest/app.js @@ -1,14 +1,17 @@ +const h = g.getHeight(); +const w = g.getWidth(); + function draw() { var d = new Date(); var da = d.toString().split(" "); var time = da[4].substr(0,5); g.reset(); - g.clearRect(0, 30, 239, 99); + g.clearRect(0, 30, w, 99); g.setFontAlign(0, -1); - g.setFont("Vector", 80); - g.drawString(time, 120, 40); + g.setFont("Vector", w/3); + g.drawString(time, w/2, 40); } // handle switch display on by pressing BTN1 diff --git a/apps/simplest/screenshot_simplest.png b/apps/simplest/screenshot_simplest.png new file mode 100644 index 000000000..9affc3d1c Binary files /dev/null and b/apps/simplest/screenshot_simplest.png differ diff --git a/apps/slomoclock/ChangeLog b/apps/slomoclock/ChangeLog new file mode 100644 index 000000000..cfab5da55 --- /dev/null +++ b/apps/slomoclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Created app +0.10: Different colour schemes selectable in SloMo Clock settings. diff --git a/apps/slomoclock/README.md b/apps/slomoclock/README.md new file mode 100644 index 000000000..9a6bbbdd2 --- /dev/null +++ b/apps/slomoclock/README.md @@ -0,0 +1,6 @@ +# SloMo Clock + +Simple 24h clock with large digits. + +![](Screenshot.JPG) + diff --git a/apps/slomoclock/app-icon.js b/apps/slomoclock/app-icon.js new file mode 100644 index 000000000..22e264124 --- /dev/null +++ b/apps/slomoclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("oFAwhC/ABOIABgfymYAKD+Z/9hGDL5c4wAf/XzjASTxqgQhAfPMB2IPxiACIBo+BDxqACIBg+CLxpANHwQPBABgvCIBT8CJ5owDD5iPOOAQfLBojiDCYQGFGIQfICIQfdBYJNMOI6SHD8jeNOIYzID8hfRD9LfEAoTdFBIifLAAIffBoQRBAJpxMD84JCD+S/GL56fID8ALBb6ZhID8qtJCZ4fgT4YDBABq/PD7RNEL6IRKD8WID5pfCD5kzNhKSFmYfMBwSeOGBoPDABgvCJ5wAON5pADABivPIAIAOd5xABABweOD4J+OD58IQBj8LD/6gUDyAfhXzgfiP/wA2")) diff --git a/apps/slomoclock/app.js b/apps/slomoclock/app.js new file mode 100644 index 000000000..e3933af1b --- /dev/null +++ b/apps/slomoclock/app.js @@ -0,0 +1,118 @@ +/* +Simple watch [slomoclock] +Mike Bennett mike[at]kereru.com +0.01 : Initial +0.03 : Use Layout library +*/ + +var v='0.10'; + +// Colours +const col = []; +col[2] = 0xF800; +col[3] = 0xFAE0; +col[4] = 0xF7E0; +col[5] = 0x4FE0; +col[6] = 0x019F; +col[7] = 0x681F; +col[8] = 0xFFFF; + +const colH = []; +colH[0]= 0x001F; +colH[1]= 0x023F; +colH[2]= 0x039F; +colH[3]= 0x051F; +colH[4]= 0x067F; +colH[5]= 0x07FD; +colH[6]= 0x07F6; +colH[7]= 0x07EF; +colH[8]= 0x07E8; +colH[9]= 0x07E3; +colH[10]= 0x07E0; +colH[11]= 0x5FE0; +colH[12]= 0x97E0; +colH[13]= 0xCFE0; +colH[14]= 0xFFE0; +colH[15]= 0xFE60; +colH[16]= 0xFC60; +colH[17]= 0xFAA0; +colH[18]= 0xF920; +colH[19]= 0xF803; +colH[20]= 0xF80E; +colH[21]= 0x981F; +colH[22]= 0x681F; +colH[23]= 0x301F; + +// Colour incremented with every 10 sec timer event +var colNum = 0; +var lastMin = -1; + +var Layout = require("Layout"); +var layout = new Layout( { + type:"h", c: [ + {type:"v", c: [ + {type:"txt", font:"40%", label:"", id:"hour", valign:1}, + {type:"txt", font:"40%", label:"", id:"min", valign:-1}, + ]}, + {type:"v", c: [ + {type:"txt", font:"10%", label:"", id:"day", col:0xEFE0, halign:1}, + {type:"txt", font:"10%", label:"", id:"mon", col:0xEFE0, halign:1}, + ]} + ] +}, {lazy:true}); + +// update the screen +function draw() { + var date = new Date(); + + // Update time + var timeStr = require("locale").time(date,1); + var hh = parseFloat(timeStr.substring(0,2)); + var mm = parseFloat(timeStr.substring(3,5)); + + // Surprise colours + if ( lastMin != mm ) colNum = Math.floor(Math.random() * 24); + lastMin = mm; + + layout.hour.label = timeStr.substring(0,2); + layout.min.label = timeStr.substring(3,5); + + // Mysterion (0) different colour each hour. Surprise (1) different colour every 10 secs. + layout.hour.col = cfg.colour==0 ? colH[hh] : cfg.colour==1 ? colH[colNum] : col[cfg.colour]; + layout.min.col = cfg.colour==0 ? colH[hh] : cfg.colour==1 ? colH[colNum] :col[cfg.colour]; + + // Update date + layout.day.label = date.getDate(); + layout.mon.label = require("locale").month(date,1); + + layout.render(); +} + +// Events + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(draw, 10000); + draw(); // draw immediately + } +}); + +var secondInterval = setInterval(draw, 10000); + +// Configuration +let cfg = require('Storage').readJSON('slomoclock.json',1)||{}; +cfg.colour = cfg.colour||0; // Colours + +// update time and draw +g.clear(); +draw(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); + +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/slomoclock/settings.js b/apps/slomoclock/settings.js new file mode 100644 index 000000000..af67069dc --- /dev/null +++ b/apps/slomoclock/settings.js @@ -0,0 +1,38 @@ +(function(back) { + + let settings = require('Storage').readJSON('slomoclock.json',1)||{}; + + function writeSettings() { + require('Storage').write('slomoclock.json',settings); + } + + function setColour(c) { + settings.colour = c; + writeSettings(); + } + + const appMenu = { + '': {'title': 'SloMo Clock'}, + '< Back': back, + 'Colours' : function() { E.showMenu(colMenu); } + //,'Widget Space Top' : {value : settings.widTop, format : v => v?"On":"Off",onchange : () => { settings.widTop = !settings.widTop; writeSettings(); } + //,'Widget Space Bottom' : {value : settings.widBot, format : v => v?"On":"Off",onchange : () => { settings.widBot = !settings.widBot; writeSettings(); } + }; + + const colMenu = { + '': {'title': 'Colours'}, + '< Back': function() { E.showMenu(appMenu); }, + 'Mysterion' : function() { setColour(0); }, + 'Surprise' : function() { setColour(1); }, + 'Red' : function() { setColour(2); }, + 'Orange' : function() { setColour(3); }, + 'Yellow' : function() { setColour(4); }, + 'Green' : function() { setColour(5); }, + 'Blue' : function() { setColour(6); }, + 'Violet' : function() { setColour(7); }, + 'White' : function() { setColour(8); } + }; + + E.showMenu(appMenu); + +}); diff --git a/apps/slomoclock/watch.png b/apps/slomoclock/watch.png new file mode 100644 index 000000000..b77f302d5 Binary files /dev/null and b/apps/slomoclock/watch.png differ diff --git a/apps/speedalt/settings.js b/apps/speedalt/settings.js index 488ba3b81..63d77971e 100644 --- a/apps/speedalt/settings.js +++ b/apps/speedalt/settings.js @@ -32,7 +32,7 @@ const appMenu = { - '': {'title': 'GPS Speed Alt'}, + '': {'title': 'GPS Adv Sprt'}, '< Back': back, '< Load GPS Adv Sport': ()=>{load('speedalt.app.js');}, 'Units' : function() { E.showMenu(unitsMenu); }, diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog new file mode 100644 index 000000000..91f01988e --- /dev/null +++ b/apps/speedalt2/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial import. +0.07: Add swipe to change screens. diff --git a/apps/speedalt2/README.md b/apps/speedalt2/README.md new file mode 100644 index 000000000..30a706b7b --- /dev/null +++ b/apps/speedalt2/README.md @@ -0,0 +1,134 @@ +# GPS Speed, Altimeter and Distance to Waypoint + +What is the difference between **GPS Adventure Sports** and **GPS Adventure Sports II** ? + +**GPS Adventure Sports** has 3 screens, each of which display different sets of information. + +**GPS Adventure Sports II** has 5 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, Position or Time. + +In all other respect they perform the same functions. + +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. + +## Buttons and Controls + +**BTN1** ( Speed and Altitude ) Short press < 2 secs toggles the display between last reading and maximum recorded. Long press > 2 secs resets the recorded maximum values. + +**BTN1** ( Distance ) Select next waypoint. Last fix distance from selected waypoint is displayed. + +**BTN2** : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts. + +**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, Position and Time + +**BTN3** : Long press exit and return to watch. + +**Touch Screen** If the 'Touch' setting is ON then : + +Swipe Left/Right cycles between the five screens. + +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 ). + +## Kalman Filter + +This filter smooths the altitude and the speed values and reduces these values 'jumping around' from one GPS fix to the next. The down side of this is that if these values change rapidly ( eg. a quick change in altitude ) then it can take a few GPS fixes for the values to move to the new values. Disabling the Kalman filter in the settings will cause the raw values to be displayed from each GPS fix as they are found. + +## Loss of fix + +When the GPS obtains a fix the number of satellites is displayed as 'Sats:nn'. When unable to obtain a fix then the last known fix is used and the age of that fix in seconds is displayed as 'Age:nn'. Seeing 'Sats' or 'Age' indicates whether the GPS has a current fix or not. + +## 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. + +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. + +## 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.) + +Sample waypoints.json (My sailing waypoints) + +
+[
+  {
+  "name":"NONE"
+  },
+  {
+  "name":"Omori",
+  "lat":-38.9058670,
+  "lon":175.7613350
+  },
+  {
+  "name":"DeltaW",
+  "lat":-38.9438550,
+  "lon":175.7676930
+  },
+  {
+  "name":"DeltaE",
+  "lat":-38.9395240,
+  "lon":175.7814420
+  },
+  {
+  "name":"BtClub",
+  "lat":-38.9446020,
+  "lon":175.8475720
+  },
+  {
+  "name":"Hapua",
+  "lat":-38.8177750,
+  "lon":175.8088720
+  },
+  {
+  "name":"Nook",
+  "lat":-38.7848090,
+  "lon":175.7839440
+  },
+  {
+  "name":"ChryBy",
+  "lat":-38.7975050,
+  "lon":175.7551960
+  },
+  {
+  "name":"Waiha",
+  "lat":-38.7219630,
+  "lon":175.7481520
+  },
+  {
+  "name":"KwaKwa",
+  "lat":-38.6632310,
+  "lon":175.8670320
+  },
+  {
+  "name":"Hatepe",
+  "lat":-38.8547420,
+  "lon":176.0089124
+  },
+  {
+  "name":"Kinloc",
+  "lat":-38.6614442,
+  "lon":175.9161607
+  }
+]
+
+ +## Comments and Feedback + +Developed for my use in sailing, cycling and motorcycling. If you find this software useful or have feedback drop me a line mike[at]kereru.com. Enjoy! + +## Thanks + +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-icon.js b/apps/speedalt2/app-icon.js new file mode 100644 index 000000000..f4f24a18b --- /dev/null +++ b/apps/speedalt2/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AE+sFtoABF12swItsF9QuFR4IwmFwwvnFw4vCGEYuIF4JgjFxIvkFxQvCGBfOAAQvqFwYwRFxYvDGBIvUFxgv/F6IuNF4n+0nB4TvXFxwvF4XBAALlPF7ZfBGC4uPF4rABGAYAGTQwvad4YwKFzYvIGBQvfFwgAE3Qvt4IvEFzgvCLxO7Lx7vULzIzTFwIvgGZheFRAiNRGSQvpGYouesYAGmQAKq3CE4PIC4wviq2eFwPCroveCRSGEC6Qv0DAwRLcoouWC4VdVYQXkr1eAgVdAoIABroNEB4gHHC5QvHwQSDAAOCA74vH1uICQIABxGtA74vIAEwv/F/4vXAH4A/AHY")) diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js new file mode 100644 index 000000000..0db9629c7 --- /dev/null +++ b/apps/speedalt2/app.js @@ -0,0 +1,725 @@ +/* +Speed and Altitude [speedalt2] +Mike Bennett mike[at]kereru.com +0.01 : Initial +0.06 : Add Posn screen +0.07 : Add swipe to change screens same as BTN3 +*/ +var v = '1.05'; + +/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ +var KalmanFilter = (function () { + 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + /** + * KalmanFilter + * @class + * @author Wouter Bulten + * @see {@link http://github.com/wouterbulten/kalmanjs} + * @version Version: 1.0.0-beta + * @copyright Copyright 2015-2018 Wouter Bulten + * @license MIT License + * @preserve + */ + var KalmanFilter = + /*#__PURE__*/ + function () { + /** + * Create 1-dimensional kalman filter + * @param {Number} options.R Process noise + * @param {Number} options.Q Measurement noise + * @param {Number} options.A State vector + * @param {Number} options.B Control vector + * @param {Number} options.C Measurement vector + * @return {KalmanFilter} + */ + function KalmanFilter() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + _ref$R = _ref.R, + R = _ref$R === void 0 ? 1 : _ref$R, + _ref$Q = _ref.Q, + Q = _ref$Q === void 0 ? 1 : _ref$Q, + _ref$A = _ref.A, + A = _ref$A === void 0 ? 1 : _ref$A, + _ref$B = _ref.B, + B = _ref$B === void 0 ? 0 : _ref$B, + _ref$C = _ref.C, + C = _ref$C === void 0 ? 1 : _ref$C; + + _classCallCheck(this, KalmanFilter); + + this.R = R; // noise power desirable + + this.Q = Q; // noise power estimated + + this.A = A; + this.C = C; + this.B = B; + this.cov = NaN; + this.x = NaN; // estimated signal without noise + } + /** + * Filter a new value + * @param {Number} z Measurement + * @param {Number} u Control + * @return {Number} + */ + + + _createClass(KalmanFilter, [{ + key: "filter", + value: function filter(z) { + var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + + if (isNaN(this.x)) { + this.x = 1 / this.C * z; + this.cov = 1 / this.C * this.Q * (1 / this.C); + } else { + // Compute prediction + var predX = this.predict(u); + var predCov = this.uncertainty(); // Kalman gain + + var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction + + this.x = predX + K * (z - this.C * predX); + this.cov = predCov - K * this.C * predCov; + } + + return this.x; + } + /** + * Predict next value + * @param {Number} [u] Control + * @return {Number} + */ + + }, { + key: "predict", + value: function predict() { + var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + return this.A * this.x + this.B * u; + } + /** + * Return uncertainty of filter + * @return {Number} + */ + + }, { + key: "uncertainty", + value: function uncertainty() { + return this.A * this.cov * this.A + this.R; + } + /** + * Return the last filtered measurement + * @return {Number} + */ + + }, { + key: "lastMeasurement", + value: function lastMeasurement() { + return this.x; + } + /** + * Set measurement noise Q + * @param {Number} noise + */ + + }, { + key: "setMeasurementNoise", + value: function setMeasurementNoise(noise) { + this.Q = noise; + } + /** + * Set the process noise R + * @param {Number} noise + */ + + }, { + key: "setProcessNoise", + value: function setProcessNoise(noise) { + this.R = noise; + } + }]); + + return KalmanFilter; + }(); + + return KalmanFilter; + +}()); + + +var buf = Graphics.createArrayBuffer(240,160,2,{msb:true}); + +// Load fonts +//require("Font7x11Numeric7Seg").add(Graphics); + +var lf = {fix:0,satellites:0}; +var showMax = 0; // 1 = display the max values. 0 = display the cur fix +var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on. +var canDraw = 1; +var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. +var tmrLP; // Timer for delay in switching to low power after screen turns off + +var max = {}; +max.spd = 0; +max.alt = 0; +max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data. + +var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values; + +var wp = {}; // Waypoint to use for distance from cur position. + +function nxtWp(inc){ + cfg.wp+=inc; + loadWp(); +} + +function loadWp() { + var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}]; + if (cfg.wp>=w.length) cfg.wp=0; + if (cfg.wp<0) cfg.wp = w.length-1; + savSettings(); + wp = w[cfg.wp]; +} + +function radians(a) { + return a*Math.PI/180; +} + +function distance(a,b){ + var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); + var y = radians(b.lat-a.lat); + + // Distance in selected units + var d = Math.sqrt(x*x + y*y) * 6371000; + d = (d/parseFloat(cfg.dist)).toFixed(2); + if ( d >= 100 ) d = parseFloat(d).toFixed(1); + if ( d >= 1000 ) d = parseFloat(d).toFixed(0); + + return d; +} + +function drawScrn(dat) { + + if (!canDraw) return; + + buf.clear(); + + 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.setFontVector(s); + buf.drawString(n,126,52); + + + // Primary Units + buf.setFontAlign(-1,1); //left, bottom + buf.setColor(2); + buf.setFontVector(35); + buf.drawString(dat.unit,5,164); + + if ( dat.max ) drawMax(); // MAX display indicator + if ( dat.wp ) drawWP(); // Waypoint name + + //Sats + if ( dat.sat ) { + if ( dat.age > 10 ) { + if ( dat.age > 90 ) dat.age = '>90'; + drawSats('Age:'+dat.age); + } + else drawSats('Sats:'+dat.sats); + } + + g.reset(); + g.drawImage(img,0,40); + + if ( pwrSav ) LED1.reset(); + else LED1.set(); + +} + +function drawPosn(dat) { + if (!canDraw) return; + buf.clear(); + + var x, y; + x=210; + y=0; + buf.setFontAlign(1,-1); + buf.setFontVector(60); + buf.setColor(1); + + buf.drawString(dat.lat,x,y); + buf.drawString(dat.lon,x,y+70); + + x = 240; + buf.setColor(2); + buf.setFontVector(40); + buf.drawString(dat.ns,x,y); + buf.drawString(dat.ew,x,y+70); + + + //Sats + if ( dat.sat ) { + if ( dat.age > 10 ) { + if ( dat.age > 90 ) dat.age = '>90'; + drawSats('Age:'+dat.age); + } + else drawSats('Sats:'+dat.sats); + } + + g.reset(); + g.drawImage(img,0,40); + + if ( pwrSav ) LED1.reset(); + else LED1.set(); + +} + +function drawClock() { + if (!canDraw) return; + + buf.clear(); + var x, y; + x=185; + y=0; + 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); + + if ( pwrSav ) LED1.reset(); + else LED1.set(); +} + +function drawWP() { + var nm = wp.name; + if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = ''; + buf.setColor(2); + + buf.setFontAlign(0,1); //left, bottom + buf.setFontVector(48); + buf.drawString(nm.substring(0,8),120,140); + +} + +function drawSats(sats) { + buf.setColor(3); + buf.setFont("6x8", 2); + buf.setFontAlign(1,1); //right, bottom + buf.drawString(sats,240,160); +} + +function drawMax() { + buf.setFontVector(30); + buf.setColor(2); + buf.setFontAlign(0,1); //centre, bottom + buf.drawString('MAX',120,164); +} + +function onGPS(fix) { + + if ( emulator ) { + fix.fix = 1; + fix.speed = 10 + (Math.random()*5); + fix.alt = 354 + (Math.random()*50); + fix.lat = -38.92; + fix.lon = 175.7613350; + fix.course = 245; + fix.satellites = 12; + fix.time = new Date(); + fix.smoothed = 0; + } + + var m; + + var sp = '---'; + var al = '---'; + var di = '---'; + var age = '---'; + var lat = '---.--'; + var ns = ''; + var ew = ''; + var lon = '---.--'; + + + if (fix.fix) lf = fix; + + if (lf.fix) { + + // Smooth data + if ( lf.smoothed !== 1 ) { + if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed); + if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt); + lf.smoothed = 1; + if ( max.n <= 15 ) max.n++; + } + + + // Speed + if ( cfg.spd == 0 ) { + m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units + sp = parseFloat(m[1]); + cfg.spd_unit = m[2]; + } + else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units + + if ( sp < 10 ) sp = sp.toFixed(1); + else sp = Math.round(sp); + + if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = sp; + + // Altitude + al = lf.alt; + al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); + + if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = al; + + // Distance to waypoint + di = distance(lf,wp); + if (isNaN(di)) di = 0; + + // Age of last fix (secs) + age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); + + // Lat / Lon + ns = 'N'; + if ( lf.lat < 0 ) ns = 'S'; + lat = Math.abs(lf.lat.toFixed(2)); + + ew = 'E'; + if ( lf.lon < 0 ) ew = 'W'; + lon = Math.abs(lf.lon.toFixed(2)); + + } + + if ( cfg.modeA == 0 ) { + // Speed + if ( showMax ) + drawScrn({ + val:max.spd, + unit:cfg.spd_unit, + sats:lf.satellites, + age:age, + max:true, + wp:false, + sat:true + }); // Speed maximums + else + drawScrn({ + val:sp, + unit:cfg.spd_unit, + sats:lf.satellites, + age:age, + max:false, + wp:false, + sat:true + }); + } + + if ( cfg.modeA == 1 ) { + // Alt + if ( showMax ) + drawScrn({ + val:max.alt, + unit:cfg.alt_unit, + sats:lf.satellites, + age:age, + max:true, + wp:false, + sat:true + }); // Alt maximums + else + drawScrn({ + val:al, + unit:cfg.alt_unit, + sats:lf.satellites, + age:age, + max:false, + wp:false, + sat:true + }); + } + + if ( cfg.modeA == 2 ) { + // Dist + drawScrn({ + val:di, + unit:cfg.dist_unit, + sats:lf.satellites, + age:age, + max:false, + wp:true, + sat:true + }); + } + + if ( cfg.modeA == 3 ) { + // Position + drawPosn({ + sats:lf.satellites, + age:age, + lat:lat, + lon:lon, + ns:ns, + ew:ew, + sat:true + }); + } + + if ( cfg.modeA == 4 ) { + // Large clock + drawClock(); + } + +} + +function prevScrn() { + cfg.modeA = cfg.modeA-1; + if ( cfg.modeA < 0 ) cfg.modeA = 4; + savSettings(); + onGPS(lf); +} + +function nextScrn() { + cfg.modeA = cfg.modeA+1; + if ( cfg.modeA > 4 ) cfg.modeA = 0; + savSettings(); + onGPS(lf); +} + +// Next function on a screen +function nextFunc(dur) { + if ( cfg.modeA == 0 || cfg.modeA == 1 ) { + // Spd+Alt mode - Switch between fix and MAX + if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display + else { max.spd = 0; max.alt = 0; } // Long press resets max values. + } + else if ( cfg.modeA == 2) nxtWp(1); // Dist mode - Select next waypoint + onGPS(lf); +} + + +function updateClock() { + if (!canDraw) return; + if ( cfg.modeA != 4 ) return; + drawClock(); + if ( emulator ) {max.spd++;max.alt++;} +} + +function startDraw(){ + canDraw=true; + g.clear(); + Bangle.drawWidgets(); + setLpMode('SuperE'); // off + onGPS(lf); // draw app screen +} + +function stopDraw() { + canDraw=false; + if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix. +} + +function savSettings() { + require("Storage").write('speedalt2.json',cfg); +} + +function setLpMode(m) { + if (tmrLP) {clearInterval(tmrLP);tmrLP = false;} // Stop any scheduled drop to low power + if ( !gpssetup ) return; + gpssetup.setPowerMode({power_mode:m}); +} + +// == Events + +function setButtons(){ + + // BTN1 - Max speed/alt or next waypoint + setWatch(function(e) { + var dur = e.time - e.lastTime; + nextFunc(dur); + }, BTN1, { edge:"falling",repeat:true}); + + // Power saving on/off + setWatch(function(e){ + pwrSav=!pwrSav; + if ( pwrSav ) { + LED1.reset(); + var s = require('Storage').readJSON('setting.json',1)||{}; + var t = s.timeout||10; + Bangle.setLCDTimeout(t); + } + else { + Bangle.setLCDTimeout(0); +// Bangle.setLCDPower(1); + LED1.set(); + } + }, BTN2, {repeat:true,edge:"falling"}); + + // BTN3 - next screen + setWatch(function(e){ + nextScrn(); + }, BTN3, {repeat:true,edge:"falling"}); + +/* + // Touch screen same as BTN1 short + setWatch(function(e){ + nextFunc(1); // Same as BTN1 short + }, BTN4, {repeat:true,edge:"falling"}); + setWatch(function(e){ + nextFunc(1); // Same as BTN1 short + }, BTN5, {repeat:true,edge:"falling"}); +*/ + +} + +Bangle.on('lcdPower',function(on) { + if (!SCREENACCESS.withApp) return; + if (on) startDraw(); + else stopDraw(); +}); + +Bangle.on('swipe',function(dir) { + if ( ! cfg.touch ) return; + if(dir == 1) prevScrn(); + else nextScrn(); +}); + +Bangle.on('touch', function(button){ + if ( ! cfg.touch ) return; + nextFunc(0); // Same function as short BTN1 +/* + switch(button){ + case 1: // BTN4 +console.log('BTN4'); + prevScrn(); + break; + case 2: // BTN5 +console.log('BTN5'); + nextScrn(); + break; + case 3: +console.log('MDL'); + nextFunc(0); // Centre - same function as short BTN1 + break; + } +*/ + }); + + + +// == Main Prog + +// Read settings. +let cfg = require('Storage').readJSON('speedalt2.json',1)||{}; + +cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed +cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit +cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions. +cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units +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=Position 4=Clock +cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary + +cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; +cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt; +cfg.touch = cfg.touch==undefined?true:cfg.touch; + +if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 }); +if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 }); + +loadWp(); + +/* +Colour Pallet Idx +0 : Background (black) +1 : Speed/Alt +2 : Units +3 : Sats +*/ +var img = { + width:buf.getWidth(), + height:buf.getHeight(), + bpp:2, + buffer:buf.buffer, + palette:new Uint16Array([0,0x4FE0,0xEFE0,0x07DB]) +}; + +if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]); +if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xFAE0,0xF813]); + +var SCREENACCESS = { + withApp:true, + request:function(){this.withApp=false;stopDraw();}, + release:function(){this.withApp=true;startDraw();} +}; + +var gpssetup; +try { + gpssetup = require("gpssetup"); +} catch(e) { + gpssetup = false; +} + +// All set up. Lets go. +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +onGPS(lf); +Bangle.setGPSPower(1); + +if ( gpssetup ) { + gpssetup.setPowerMode({power_mode:"SuperE"}).then(function() { Bangle.setGPSPower(1); }); +} +else { + Bangle.setGPSPower(1); +} + +Bangle.on('GPS', onGPS); + +setButtons(); +setInterval(updateClock, 10000); diff --git a/apps/speedalt2/app.png b/apps/speedalt2/app.png new file mode 100644 index 000000000..93d8e57dc Binary files /dev/null and b/apps/speedalt2/app.png differ diff --git a/apps/speedalt2/settings.js b/apps/speedalt2/settings.js new file mode 100644 index 000000000..96174a89b --- /dev/null +++ b/apps/speedalt2/settings.js @@ -0,0 +1,89 @@ +(function(back) { + + let settings = require('Storage').readJSON('speedalt2.json',1)||{}; + //settings.buzz = settings.buzz||1; + + function writeSettings() { + require('Storage').write('speedalt2.json',settings); + } + + function setUnits(m,u) { + settings.spd = m; + settings.spd_unit = u; + writeSettings(); + } + + function setUnitsAlt(m,u) { + settings.alt = m; + settings.alt_unit = u; + writeSettings(); + } + + function setUnitsDist(d,u) { + settings.dist = d; + settings.dist_unit = u; + writeSettings(); + } + + function setColour(c) { + settings.colour = c; + writeSettings(); + } + + + const appMenu = { + '': {'title': 'GPS Adv Sprt II'}, + '< Back': back, + '< Load GPS Adv Sport': ()=>{load('speedalt2.app.js');}, + 'Units' : function() { E.showMenu(unitsMenu); }, + 'Colours' : function() { E.showMenu(colMenu); }, + 'Kalman Filter' : function() { E.showMenu(kalMenu); }, + 'Touch' : { + value : settings.touch, + format : v => v?"On":"Off", + onchange : () => { settings.touch = !settings.touch; writeSettings(); } + } + }; + + const unitsMenu = { + '': {'title': 'Units'}, + '< Back': function() { E.showMenu(appMenu); }, + 'default (spd)' : function() { setUnits(0,''); }, + 'Kph (spd)' : function() { setUnits(1,'kph'); }, + 'Knots (spd)' : function() { setUnits(1.852,'kts'); }, + 'Mph (spd)' : function() { setUnits(1.60934,'mph'); }, + 'm/s (spd)' : function() { setUnits(3.6,'m/s'); }, + 'Km (dist)' : function() { setUnitsDist(1000,'km'); }, + 'Miles (dist)' : function() { setUnitsDist(1609.344,'mi'); }, + 'Nm (dist)' : function() { setUnitsDist(1852.001,'nm'); }, + 'Meters (alt)' : function() { setUnitsAlt(1,'m'); }, + 'Feet (alt)' : function() { setUnitsAlt(0.3048,'ft'); } + }; + + const colMenu = { + '': {'title': 'Colours'}, + '< Back': function() { E.showMenu(appMenu); }, + 'Default' : function() { setColour(0); }, + 'Hi Contrast' : function() { setColour(1); }, + 'Night' : function() { setColour(2); } + }; + + 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(); } + } + }; + + + E.showMenu(appMenu); + +}); diff --git a/apps/stopwatch/A.jpg b/apps/stopwatch/A.jpg new file mode 100644 index 000000000..9155b9986 Binary files /dev/null and b/apps/stopwatch/A.jpg differ diff --git a/apps/stopwatch/B.jpg b/apps/stopwatch/B.jpg new file mode 100644 index 000000000..639ff5d42 Binary files /dev/null and b/apps/stopwatch/B.jpg differ diff --git a/apps/stopwatch/ChangeLog b/apps/stopwatch/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/stopwatch/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/stopwatch/README.md b/apps/stopwatch/README.md new file mode 100644 index 000000000..30a9306d1 --- /dev/null +++ b/apps/stopwatch/README.md @@ -0,0 +1,33 @@ +# Stopwatch Touch + +A touch screen based stop watch for Bangle 2 + +## Screenshots + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) + +## Features + +* Attractive UI design +* Will run up to 99 hours +* Shows 10th of seconds up to 1 hour +* Start / Pause button +* Reset button + +## Future features + +I'm keen to complete this project with + +* Ability to dismiss the app and leave it running in the background +* A small widget to show the elapsed time on the current active clock +* Laptimes, with a way to view all the laptimes on a scrollable screen + + +## One of these is a genuine Bangle Js 2 Open Source Smartwatch, the other isn't + +Which one is which ? + +![](A.jpg) +![](B.jpg) diff --git a/apps/stopwatch/pause-24.png b/apps/stopwatch/pause-24.png new file mode 100644 index 000000000..eb3d8feaa Binary files /dev/null and b/apps/stopwatch/pause-24.png differ diff --git a/apps/stopwatch/pause-24a.png b/apps/stopwatch/pause-24a.png new file mode 100644 index 000000000..7838ef640 Binary files /dev/null and b/apps/stopwatch/pause-24a.png differ diff --git a/apps/stopwatch/play-24.png b/apps/stopwatch/play-24.png new file mode 100644 index 000000000..268b5dc31 Binary files /dev/null and b/apps/stopwatch/play-24.png differ diff --git a/apps/stopwatch/screenshot1.png b/apps/stopwatch/screenshot1.png new file mode 100644 index 000000000..6d94ce05c Binary files /dev/null and b/apps/stopwatch/screenshot1.png differ diff --git a/apps/stopwatch/screenshot2.png b/apps/stopwatch/screenshot2.png new file mode 100644 index 000000000..0baa73331 Binary files /dev/null and b/apps/stopwatch/screenshot2.png differ diff --git a/apps/stopwatch/screenshot3.png b/apps/stopwatch/screenshot3.png new file mode 100644 index 000000000..1e7cfca58 Binary files /dev/null and b/apps/stopwatch/screenshot3.png differ diff --git a/apps/stopwatch/stop-24.png b/apps/stopwatch/stop-24.png new file mode 100644 index 000000000..658d614ca Binary files /dev/null and b/apps/stopwatch/stop-24.png differ diff --git a/apps/stopwatch/stop-24a.png b/apps/stopwatch/stop-24a.png new file mode 100644 index 000000000..e89ddae05 Binary files /dev/null and b/apps/stopwatch/stop-24a.png differ diff --git a/apps/stopwatch/stopwatch.app.js b/apps/stopwatch/stopwatch.app.js new file mode 100644 index 000000000..48d4f26ea --- /dev/null +++ b/apps/stopwatch/stopwatch.app.js @@ -0,0 +1,220 @@ +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=="); + +function log_debug(o) { + //console.log(o); +} + +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; +} + +function drawTime() { + log_debug("drawTime()"); + let Tt = tCurrent-tTotal; + let Ttxt = timeToText(Tt); + + // total time + g.setFont("Vector",38); // check + g.setFontAlign(0,0); + g.clearRect(0, timeY - 21, w, timeY + 21); + 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) { + // not running, and reset + if (!running && tCurrent == tTotal && bigPlayPauseBtn.check(xy.x, xy.y)) return; + + // paused and hit play + if (!running && tCurrent != tTotal && smallPlayPauseBtn.check(xy.x, xy.y)) return; + + // paused and press reset + if (!running && tCurrent != tTotal && resetBtn.check(xy.x, xy.y)) return; + + // must be running + if (running && bigPlayPauseBtn.check(xy.x, xy.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(); +draw(); +Bangle.setUI("clock"); // Show launcher when button pressed diff --git a/apps/stopwatch/stopwatch.icon.js b/apps/stopwatch/stopwatch.icon.js new file mode 100644 index 000000000..32281b7ab --- /dev/null +++ b/apps/stopwatch/stopwatch.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///vvvvEF/muH+cDHgPABf4AElWoKhILClALH1WqAQIWHBYIABwAKEgQKD1WgBYkK1X1r4XHlWtqtVvQLG1XVBYNXHYsC1YJBBoPqC4kKEQILCvQ7EhW1BYdeBYkqytVqwCCQwkqCgILCq4LFIoILCqoLEIwIsBGQJIBBZ+pA4Na0oDBtQLGvSFCBaYjIHYR3CI5AADBYhrCAAaDHAASDGQASGCBYizCAASzFZYQACZYrjCIwb7QHgIkCvQ6EGAWq+tf1QuEGAWqAAQuFEgQKBEQw9DHIwAuA=")) diff --git a/apps/stopwatch/stopwatch.png b/apps/stopwatch/stopwatch.png new file mode 100644 index 000000000..92ffe73b7 Binary files /dev/null and b/apps/stopwatch/stopwatch.png differ diff --git a/apps/svclock/ChangeLog b/apps/svclock/ChangeLog index 671de492c..fb71fbeb8 100644 --- a/apps/svclock/ChangeLog +++ b/apps/svclock/ChangeLog @@ -1,2 +1,4 @@ 0.01: Modification of SimpleClock 0.04 to use Vectorfont 0.02: Use Bangle.setUI for button/launcher handling +0.03: Scale to BangleJS 2 and add locale +0.04: Fix rendering issue on real hardware, now update *on* the minute rather than every 15 secs diff --git a/apps/svclock/vclock-simple.js b/apps/svclock/vclock-simple.js index f3ab911bc..e08c6fa2c 100644 --- a/apps/svclock/vclock-simple.js +++ b/apps/svclock/vclock-simple.js @@ -1,84 +1,109 @@ -/* jshint esversion: 6 */ -const timeFontSize = 65; -const dateFontSize = 20; -const gmtFontSize = 10; -const font = "Vector"; +const locale = require("locale"); -const xyCenter = g.getWidth() / 2; -const yposTime = 75; -const yposDate = 130; -const yposYear = 175; -const yposGMT = 220; +var timeFontSize; +var dateFontSize; +var gmtFontSize; +var font = "Vector"; +var xyCenter = g.getWidth() / 2; +var yposTime; +var yposDate; +var yposYear; +var yposGMT; + +if (g.getWidth() > 200) { + timeFontSize = 65; + dateFontSize = 20; + gmtFontSize = 10; + + yposTime = 75; + yposDate = 130; + yposYear = 175; + yposGMT = 220; +} else { + timeFontSize = 48; + dateFontSize = 15; + gmtFontSize = 10; + + yposTime = 55; + yposDate = 95; + yposYear = 128; + yposGMT = 161; +} // Check settings for what type our clock should be var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; -function drawSimpleClock() { +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function draw() { g.clear(); Bangle.drawWidgets(); // get date var d = new Date(); - var da = d.toString().split(" "); g.reset(); // default draw styles // drawSting centered g.setFontAlign(0, 0); - // draw time - var time = da[4].substr(0, 5).split(":"); - var hours = time[0], - minutes = time[1]; - var meridian = ""; + // drawTime + var hours; if (is12Hour) { - hours = parseInt(hours,10); - meridian = "AM"; - if (hours == 0) { - hours = 12; - meridian = "AM"; - } else if (hours >= 12) { - meridian = "PM"; - if (hours>12) hours -= 12; - } - hours = (" "+hours).substr(-2); + hours = ("0" + d.getHours()%12).slice(-2); + } else { + hours = ("0" + d.getHours()).slice(-2); } + var minutes = ("0" + d.getMinutes()).slice(-2); g.setFont(font, timeFontSize); g.drawString(`${hours}:${minutes}`, xyCenter, yposTime, true); - g.setFont(font, gmtFontSize); - g.drawString(meridian, xyCenter + 102, yposTime + 10, true); + + if (is12Hour) { + g.setFont(font, gmtFontSize); + g.drawString(locale.meridian(d), xyCenter + 102, yposTime + 10, true); + } // draw Day, name of month, Date - var date = [da[0], da[1], da[2]].join(" "); g.setFont(font, dateFontSize); - - g.drawString(date, xyCenter, yposDate, true); + g.drawString([locale.dow(d,1), locale.month(d,1), d.getDate()].join(" "), xyCenter, yposDate, true); // draw year g.setFont(font, dateFontSize); g.drawString(d.getFullYear(), xyCenter, yposYear, true); // draw gmt - var gmt = da[5]; g.setFont(font, gmtFontSize); - g.drawString(gmt, xyCenter, yposGMT, true); + g.drawString(d.toString().match(/GMT[+-]\d+/), xyCenter, yposGMT, true); + + queueDraw(); } -// handle switch display on by pressing BTN1 -Bangle.on('lcdPower', function(on) { - if (on) drawSimpleClock(); +// 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 button pressed +Bangle.setUI("clock"); // clean app screen g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -// refesh every 15 sec -setInterval(drawSimpleClock, 15E3); - // draw now -drawSimpleClock(); - -// Show launcher when button pressed -Bangle.setUI("clock"); +draw(); diff --git a/apps/swiperclocklaunch/ChangeLog b/apps/swiperclocklaunch/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/swiperclocklaunch/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/swiperclocklaunch/boot.js b/apps/swiperclocklaunch/boot.js new file mode 100644 index 000000000..0bb8d588a --- /dev/null +++ b/apps/swiperclocklaunch/boot.js @@ -0,0 +1,17 @@ +// clock -> launcher +(function() { + var sui = Bangle.setUI; + Bangle.setUI = function(mode, cb) { + sui(mode,cb); + if (!mode.startsWith("clock")) return; + Bangle.swipeHandler = dir => { if (dir<0) Bangle.showLauncher(); }; + Bangle.on("swipe", Bangle.swipeHandler); + }; +})(); +// launcher -> clock +setTimeout(function() { + if (global.__FILE__ && __FILE__.endsWith(".app.js") && (require("Storage").readJSON(__FILE__.slice(0,-6)+"info",1)||{}).type=="launch") { + Bangle.swipeHandler = dir => { if (dir>0) load(); }; + Bangle.on("swipe", Bangle.swipeHandler); + } +}, 10); \ No newline at end of file diff --git a/apps/swiperclocklaunch/icon.js b/apps/swiperclocklaunch/icon.js new file mode 100644 index 000000000..c9089ce5c --- /dev/null +++ b/apps/swiperclocklaunch/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("lEoxH+AB8WAAwYQEaQrdEp4pWEyYoRC49kxGs2fX6+z1mIsgpUCQtAxAjCAA+zxFAFCAQFxAkJAAuIFBxMF1oeHgEABI+sFBomEORInJPgJ7EEyonLFAJQJBIh0IE5x6GE47CME5nXsgnGOojmME5p5HJyAnO6+IE5LEKE6JQEE4lkC5gnPUIh2SE6B4EAAesC5oAP1gnHTxpPDAQIAFeJQACH5wnP64nWAA3CBJB3WAA203fQBAp3IY4plENQ4HC2gABkjHNxAnX2nJBYeIEYf+AYVkE5oDGE4e0UgdkEwYnDUAITEACikBTwgnFxAnZFAJ2FE4lAJ7dAE4pQFY6yfCToYmDE4kW1jvX1geEE4YoF2YfFABRzD67EEEwqiGFCAmETg5QJPQYAMTQJ0GE5AoGshSPYQgmKFA72BFJWzxBzEExgoIKYOI1grC2esxBLGExwpKABolPFCwmSFKQlVFZoXP")) \ No newline at end of file diff --git a/apps/swiperclocklaunch/swiperclocklaunch.png b/apps/swiperclocklaunch/swiperclocklaunch.png new file mode 100644 index 000000000..c7f16c2b1 Binary files /dev/null and b/apps/swiperclocklaunch/swiperclocklaunch.png differ diff --git a/apps/toucher/ChangeLog b/apps/toucher/ChangeLog index 494110d55..7b5c53de7 100644 --- a/apps/toucher/ChangeLog +++ b/apps/toucher/ChangeLog @@ -3,4 +3,5 @@ 0.03: Close launcher when lcd turn off 0.04: Complete rewrite to add animation and loop ( issue #210 ) 0.05: Improve perf -0.06: Complete rewrite in 80x80, better perf, add settings \ No newline at end of file +0.06: Complete rewrite in 80x80, better perf, add settings +0.07: Added suppport for Bangle 2, added README file diff --git a/apps/toucher/README.md b/apps/toucher/README.md new file mode 100644 index 000000000..27cb32eeb --- /dev/null +++ b/apps/toucher/README.md @@ -0,0 +1,22 @@ +# Toucher - A touch based launcher, swipe left, swipe right, tap to launch + +* Designed specifically for Bangle 1 and Bangle 2 + +## Installation +- Use the App loader to install toucher +- Then delete the existing launcher +- When you restart the new launcher will be loaded +- To return to the default launcher, delete toucher and install the default launcher. + +## Bangle 1 +In the settings menu 'Low Res' refers to setting the Bangle 1 screen into 80x80 mode. +This significantly improves the animation performance. + +## Bangle 2 +The Hires/Lowres settings is ignored. +Touch the top third of the screen to launch the selected app. +Press button 1 to launch the selected app. + +## Screenshots + +![](screenshot1.jpg) diff --git a/apps/toucher/app.js b/apps/toucher/app.js index 455a29c5d..8ac198f52 100644 --- a/apps/toucher/app.js +++ b/apps/toucher/app.js @@ -7,8 +7,11 @@ let settings = Storage.readJSON(filename,1) || { debug: false }; -if(!settings.highres) Bangle.setLCDMode("80x80"); -else Bangle.setLCDMode(); +// this means that setFont('6x8',1) is actually setFont('6x8',3) +if (process.env.HWVERSION == 1) { + if(!settings.highres) Bangle.setLCDMode("80x80"); + else Bangle.setLCDMode(); +} g.clear(); g.flip(); @@ -23,7 +26,7 @@ const ORIGINAL_ICON_SIZE = 48; const STATE = { settings_open: false, index: 0, - target: 240, + target: g.getWidth(), offset: 0 }; @@ -63,7 +66,7 @@ const APPS = getApps(); function noIcon(x, y, scale){ if(scale < 0.2) return; - g.setColor(scale, scale, scale); + g.setColor(g.theme.fg); g.setFontAlign(0,0); g.setFont('6x8',settings.highres ? 6:3); g.drawString('x_x', x+1.5, y); @@ -81,23 +84,24 @@ function render(){ g.clear(); const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF ); + let cycle = 0; + let lastCycle = visibleApps.length; + visibleApps.forEach(app => { - - const x = app.x+HALF-STATE.offset; - const y = HALF - (HALF*0.3); + cycle++; + const x = app.x + HALF - STATE.offset; + const y = HALF; let dist = HALF - x; if(dist < 0) dist *= -1; - const scale = 1 - (dist / HALF); if(!scale) return; if(app.special){ - const font = settings.highres ? '6x8' : '4x6'; - const fontSize = settings.highres ? 2 : 1; - g.setFont(font, fontSize); - g.setColor(scale,scale,scale); + const fontSize = (process.env.HWVERSION == 2) ? 4 : (settings.highres ? 6 : 2); + g.setFont('6x8', fontSize); + g.setColor(g.theme.fg); g.setFontAlign(0,0); g.drawString(app.name, HALF, HALF); return; @@ -111,26 +115,69 @@ function render(){ if(icon){ icons[app.name] = icon; try { - const rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2)); - const imageScale = settings.highres ? scale*2 : scale; + let rescale; + let imageScale; + + if (process.env.HWVERSION == 1) { + // on a bangle 1 !highres means 80x80 + rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2)); + imageScale = settings.highres ? scale*2 : scale; + } else { + // !highres mode is meaningless on a bangle 2 at present + rescale = 1.25*scale*ORIGINAL_ICON_SIZE; + imageScale = 2.5*scale; + } + g.drawImage(icon, x-rescale, y-rescale, { scale: imageScale }); - } catch(e){ + } catch(e) { noIcon(x, y, scale); } - }else{ + } else { noIcon(x, y, scale); } //draw text - g.setColor(scale,scale,scale); - if(scale > 0.1){ - const font = settings.highres ? '6x8': '4x6'; - const fontSize = settings.highres ? 2 : 1; - g.setFont(font, fontSize); - g.setFontAlign(0,0); - g.drawString(app.name, HALF, HEIGHT/4*3); - } + g.setColor(g.theme.fg); + if (cycle == 2 && scale > 0.1) { + let fontSize = (process.env.HWVERSION == 2) ? 2 : 1; + if (process.env.HWVERSION == 1) { + fontSize = (settings.highres) ? 3 : 1; + } + + if (app.name.length <= 12) { + g.setFont("6x8", fontSize); + g.setFontAlign(0,1); + g.drawString(app.name, HALF, HEIGHT); + } else { + // some app names are too long for one line + var name = app.name; + var first = name.substring(0, name.indexOf(" ")); + var last = name.substring(name.indexOf(" ") + 1, name.length); + + // all this to handle long names like + // Simple 7 Segment Clock + if (last.length > 12 && process.env.HWVERSION == 1) { + g.setFont((settings.highres ? "6x8" : "4x6"),(settings.highres ? 2 : 1) ); + } else { + g.setFont("6x8", fontSize); + } + + g.setFontAlign(0,-1); + g.drawString(first, HALF, 0); + + if (last.length > 12 && process.env.HWVERSION == 1) { + g.setFont((settings.highres ? "6x8" : "4x6"),(settings.highres ? 2 : 1) ); + } else { + g.setFont("6x8", fontSize); + } + + g.setFontAlign(0,1); + g.drawString(last, HALF, HEIGHT); + } + } + + /* if(settings.highres){ const type = app.type ? app.type : 'App'; const version = app.version ? app.version : '0.00'; @@ -138,18 +185,21 @@ function render(){ g.setFontAlign(0,1); g.setFont('6x8', 1.5); g.setColor(scale,scale,scale); - g.drawString(info, HALF, 215, { scale: scale }); + g.drawString(info, HALF, HEIGHT/8*7, { scale: scale }); } + */ }); const duration = Math.floor(Date.now()-start); if(settings.debug){ g.setFontAlign(0,1); - g.setColor(0, 1, 0); - const fontSize = settings.highres ? 2 : 1; - g.setFont('4x6',fontSize); - g.drawString('Render: '+duration+'ms', HALF, HEIGHT); + g.setColor(g.theme.fgH); + const fontSize = (process.env.HWVERSION == 2) ? 2 : (settings.highres ? 2 : 1); + g.setFont(((process.env.HWVERSION == 2) ? '6x8' : (settings.highres ? '6x8' :'4x6')), fontSize); + // steal the bottom line, and print the duration + g.clearRect(0, HEIGHT - (process.env.HWVERSION == 1 && !settings.highres ? 8 : 24), WIDTH, HEIGHT); + g.drawString('Render: '+duration+' ms', HALF, HEIGHT); } g.flip(); if(STATE.offset == STATE.target) return; @@ -202,7 +252,7 @@ function run(){ E.showMessage("App Source\nNot found"); setTimeout(render, 2000); } else { - Bangle.setLCDMode(); + if (process.env.HWVERSION == 1) Bangle.setLCDMode(); g.clear(); g.flip(); E.showMessage("Loading..."); @@ -211,10 +261,11 @@ function run(){ } -// Screen event -Bangle.on('touch', function(button){ - if(STATE.settings_open) return; - switch(button){ +if (process.env.HWVERSION == 1) { + // Screen event + Bangle.on('touch', function(button){ + if(STATE.settings_open) return; + switch(button){ case 1: prev(); break; @@ -224,8 +275,17 @@ Bangle.on('touch', function(button){ case 3: run(); break; - } -}); + } + }); +} + +if (process.env.HWVERSION == 2) { + // tap at top 1/3 of screen to launch app + Bangle.on('touch', function(button, xy) { + if (xy.y < HEIGHT / 3) + run(); + }); +} Bangle.on('swipe', dir => { if(STATE.settings_open) return; @@ -238,9 +298,12 @@ Bangle.on('lcdPower', on => { if(!on) return load(); }); +if (process.env.HWVERSION == 1) { + setWatch(prev, BTN1, { repeat: true }); + setWatch(next, BTN3, { repeat: true }); + setWatch(run, BTN2, { repeat:true }); +} else { + setWatch(run, BTN1, { repeat:true }); +} -setWatch(prev, BTN1, { repeat: true }); -setWatch(next, BTN3, { repeat: true }); -setWatch(run, BTN2, { repeat:true }); - -jumpTo(1); \ No newline at end of file +jumpTo(1); diff --git a/apps/toucher/screenshot1.jpg b/apps/toucher/screenshot1.jpg new file mode 100644 index 000000000..698121cbe Binary files /dev/null and b/apps/toucher/screenshot1.jpg differ diff --git a/apps/trex/README.md b/apps/trex/README.md new file mode 100644 index 000000000..03e3f5883 --- /dev/null +++ b/apps/trex/README.md @@ -0,0 +1,4 @@ +# T-Rex + +![](screenshot_trex.png) + diff --git a/apps/trex/screenshot_trex.png b/apps/trex/screenshot_trex.png new file mode 100644 index 000000000..a66cc013f Binary files /dev/null and b/apps/trex/screenshot_trex.png differ diff --git a/apps/vernierrespirate/ChangeLog b/apps/vernierrespirate/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/vernierrespirate/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/vernierrespirate/README.md b/apps/vernierrespirate/README.md new file mode 100644 index 000000000..54dfc274b --- /dev/null +++ b/apps/vernierrespirate/README.md @@ -0,0 +1,26 @@ +# Vernier Go Direct Respiration Belt + +Connects to a [Go Direct Respiration Belt](https://www.vernier.com/product/go-direct-respiration-belt/) via Bluetooth and shows respiration rate + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALAAAACwCAYAAACvt+ReAAAAAXNSR0IArs4c6QAADldJREFUeF7tnet2GysMhZP3f+h02TEuJoD2FuKu/jhntSOELh8awYzt75+fn58v/+MR2DQC3w7wpplzs58RcIAvBuH7ScDXV+ke/P39lFjmT65ZcICXSc94Q558PhrIAsQO8Pic+IxEBCJ+sxAHgGdvk2p2eAUmEn6a6KMCh/Yh1044wKdl/DB/QgUObqUQpwB/P3oNoz8/z94F++MVGIvT0VL5Cvu5gQs9cUDLAT4aif2c+1NhX3u452HEqxzHddEB3i/Hx1scQxx64By8v1D/tgxhE+ctxPF47OFggDgcoT3+n+tIVwM4je5jYfkpxB7MmVsZQ1zaTjnA5mF3hVYRkB5irNhCeAW2yv7memJ4kSdxq/TADvDm4FmYH2/Y2CdxszdxDrAFARvr+HvOyz2Jmw2wv8yzMXytpmfPeaNz4GfP+/rP+/GyH6O1ht3H94xA/C5E7mHGaqcQXoF70rCh7vhdCORJ3CwX/V2IWZFffF72SdwsdxzgWZFffF7pYYa/0L54Am83r/QORIiLA3w7Ie5/9wj4uxDdQ+wT9IzA8gCv8rGWnklw3foIOMD62PnIBSJwLcBe2Regz8CELQB+vrj8/f3+ZICB32991notbHMdeASuBDiG1gHGYVlR0gE2ruwrJvlkm5YGuEelzFXcE6pw+tBh9rfpjFo0DvDr07c7JTz3hCy2/4QFiS6AbQB+OGSRmBMqsBQH6ToKxw5yDrDRwhiVbARORGaUvb3ncYAHAGwJFKoLlUMAs9SFzMfIOMCvaPVKUtBrpR/Vg8pJsKz+wMcUYEtnSwloTUwvvSUQrE9SUP9ROQTgHg+SpHnR62YAj6o0LYmpjW3Ri8AbZFrmYcYysoj9FvpQKBk5c4AtTgt6gdZLLwJALKOFgRnHyObsT8e36mOgZGRNALZ2thdoUhKk62hgpVZKOw8zjpF1gDOPY7UBlMZJ12uQSWOl6wzA0oMRzVzMGEY29as0tkUnGjtWrrkCWzuLBAmRQarKh8z37/fOSOBJAWZs6yXb0sb1uvtJcdNebwJYSoB0HV35WjloXPgttPDKZhik/AHTrM/pN4hEhj3lH38X5mNjqYXYAU6TQ4CAJgmVEwHOgAXrjr9UIfLxPT7+kUAGzgLsH3ZVFsTT59d1dHGEOEm+S9fRKmql5+nq710T/8UY1Fm2AjBOMbJxUBEIVLojWN/fdkPE9M+cCaCI3dWWSQKeeBqpik9inIWON4c9AWYghpyyAkW6pRPwFRcIWo5K8OR8BVqN6oKtjIfiT4Bech+dBw2fqgIzRqCyolypKkU9bNXpsEkDIBBtKUykHSctdK1etB9n9DOy1bsCSqggRwPMOoDIizItvaqiJxft2Rng0CMHH5DPG0bxf/fVQCEQ9yAGEHcHWKou4nVkUwMEgoWytzyaXNYOuqVRHB++bSI2qmKegRxmKzrTA2uDWRun1ckGhJ2nt/wqALN+FuMutHKaeRCmqQpMGVHahES3LhFCYffM2MPIBhOZMYws0xv20qvxERpTAPnDD+BUBIH3yc/HEXpl9w0HEnHgZR2s06Dn1M6FjENkkITk9LTqlsZL15nFVvJRMwcSr/8A//9RhP/jcofzklbyeMrCMVQHKofe2uleU4pd5ohKazNjm3YOZhwjC4TpQ6TcQjSeuUor18opSc/j+v+uhX9gI7U50vxMQlJdFrprOlr0o2NROSZOHwtU2sRZGxD0WeqNdcXAPhzVPGVkqnAvP6SFgya8F8CofZbxyRbFGQBbgRVvKlqrrAREKRGWCepRgWugtdoujZeuSzFHrj/fRhuRHKYvQwyfIdMLsHghvn/W1fgrr3I5tgCsZ4VHcjwFYMSwFWV6AxxXSwu4akXDUn+vxYEw8H4feKYRiKGryKT9tkWPnQPNErARrdaIxZ3tgeN3KXtsgFYBz9KOHhvRFDLrhWHpf0lXzM8o+5s+kTEiKCvOERI1KkkrxmAVmxxgZSZ63OKVplw9zAG+Ov37O+8A75/Dqz1wgK9O//7OO8D75/BqDxzgq9O/v/MO8P45vNoDB/jq9O/vvAO8fw6v9sABvjr9+zvvAO+fw6s9cICvTv/+zjvA++fwag8c4KvTv7/zDvD+ObzaAwf46vTv7/xSAFu9KM7qaXm3t2Xs/vjkPRgZk64Aaz5iYuU8o4eRTVPWMtYBbo9AN4BrH/KrgV36cGlwtfSxc+RDhbl5a58DLNnJfHYwvhvkbEz9evwdmVeKQ/y5vYfO+ONPtTjkflaWiRsTm3Z8v76GAVwyVgKvVOG044Id8fjSokmTXkpu7bNxMUjxeMn+1E42DmkbldpR0l+yV7JHiqcFrDkdUwCWql4OHOnWjQBRmleq+nGVROaJba0BnPoUV9X4Wu7f0Qqci1tuXtbOWhxKi60HxMMAlipArjKG22muwkkgMdeRCszah1Y4dmGilRCt2D3sPAJgtJcr9Welni2Wlyp5rfdj+sJc/52rkNlb3OsronILuHZHSPUjvtbuFOldIae/1OLk5kYKRBq3rSpwD2Ndp0fgzx1L9SuHHkePwCIR6NYDL+Kfm3F4BBzgwxN8unvLA5xuINLNF5qgnB50bE6u1/ei3eZvSw6eBwWr9sAIcAxEiD4mmMzcqN6SjZq5dvAXjUtNbjmANYFHEqzRWwscMieaIMk2zVySTtS2EUdhrC0fR4IrVeDWoEuPdFsClY7VQJWbH/FZMxeil4mHxgZGv1Z2qQrcGvSdAGZ81cDD6Efg0diA6G2VWQZgpv+rJacUaKvNUWvAnxuP+EeyAYUaeFbyF3BRLbI0wJqKujPA4VGuRbviAKvXBD9QG+zWqq2pbLx3f0cw7zU8Rmvs1MbUwr+ROpapwFqn0UShclo7mHGxLSmcVnZa6WH8miHrAE+I+gOu3q2OAzwhsZop0UTV5FAdGvvYMVa27OIvG59UfusKzCSb3fmHQGn6z5akMD7V5tnF35ZYPU90VnqQwTjDbOA0R1cWJwGMP0F2NsCzFq4mVtsCzMJrAfCoxK4C8Ch/teC+F/xOFVi6LWrOjdkA9m4pHGAuI9u0EC3wShW49KHRUih7QjwC4JX85XD9K70FwK3w1gDWVO2dAV7N36MBlsB9OI/CpK1smn67JSlaO/8cL2Xet0BiNdrfllgtvYmT4EWS0Roc65MBxB4rgJG5SjIr2IDav2QLsRK8pfaj1wJaAZ4VbNgSYAlcpmVAA4DIjUzoyLm8AiPZJ2RqAPeqeIh5I6EaOZcDjGQflFkVXm8hfhM4s4DUEFqmB+69+9VWtt52WZ0eWOkZ7S9Y34piSwA8qvqyEM9IJmtjtTqRR2kz/D0eYI2DzLu2vfWzT71622OpX6PLeszyFVjjsOZpEzoPq3smwKX+HfV15d43+OAAM9kUNjPo7R85LmTMYheVlW5GTy/Z6wDWViVkF74iwD397QUlo/dKgN+3H+D7GRBwa/pmtxAxDEjlZ/xlQOsluwTAvZxzvedHwAE+P8dHe+gAH53e851zgM/P8dEeOsBHp/d85xzg83N8tIcO8NHpPd85B/j8HB/toQN8dHrPd257gB9Pl3o9Peqp+wS0VojPFIDjR5oSfIysNRQrJMjaJ0t9K8RnOMA1p3OwxvLp2CAfL4IgU7oWEpiOSf89fW8AXWgPuZKdjzmkeR8yUhyCTDwX6m8sh9hZigMbH8uF8/F+x+jvRisBnAtmCYYcsMGpNJEx0CXQay/AS+DG86ZAIfaXxqcJr8UnXhiov6mcJv5XV+C0ymgCGFeiFIQcAOm/5QD7WN1Ef10CIq1UKWzxfGGxaCow62/OXiQ+tTtirypb0zu9hUArRg5WFuDSq40jK3BpoUl3JnSBIwsZuVMgdl5ZgUs9Xq33y/WtaK+YVlSp4qF9aukWn1uQTB9Z6y1z12oQ1eRRO0sLJ23Z0riNqsbDK/Aox3yeOyLgAN+R52O9dICPTe0djjnAd+T5WC+XB7h2DGWVlRFz1DaTrX6gZ9W5jVdpg9xq06jxSwOcA8tyt1vSnzupsEwIMi8zHwowMi+qi7Gvp+ySAEuBbg2ypD8X8NY5Z1bg2f5eBTAS7BaYEP21gLfMXbuFtyTZv5mnJXpGYxmwWiBi5ulViVttSO1ygI0g1KphE6oFmOmpazZp59fGR2o/ao/C0UW4qr9SzJbogWtgWZ4QsLoY4KVAW11nfGBkH/at6K8UtyUBLr0z23LkwyZT6ldnVGHGB0ZWqvCWJz8SkOz15QBOwdAmIg2ElR42wJbyjA+MrKWNo3UtAzDTx2mq3+4JZe1n5UeDZzXfEgDXnLFKRE2P1RxWScnpYW3c3V80llcDjARJU+0RvayMBcDInKv4i9j63HiO/kwcalhtE6UJMntUl9qpmZP1tSTPwls7UUBtmukvaqMDzETqJTsjsTMADqGZ4S+TFq/ATLQm/GKlBl6LCuwAk2BY3j7RTVAtSas8meoBcOnDraUcrFyFr6/AmvcIRibUGuDV/WXr3tUAIyDOfLyqhbfUQqzuLwvv9Zs4JKEtMGgSEo+ZAfBMfzXx8goMRK0FJEB9VqR1zpbxLWO1/mrHOcBA5GYktHXOlvEtY4Fwmoo4wEA4ZyS0dc6W8S1jgXCaijjAQDhHJ9RivhYdLWOBcJqKOMBCOGecQlgApNUxw98Woq8BWLO7npFMLXjowxs/B25ZLoWxrS/apGqZd4s17jD60aO6YEdvgC391eiyHrNEBR4FcKkKM0FlK9hMgHv7y8Stl6wDTEa2J8CW1bdW0RmX2QXI6LaQvQ5gbVVCEtkKYOv4EhCaOxzirwWArTquBJipTkwiWwBsGYtCgIDM+IvO21NuCYB7Oui6z46AA3x2fo/3zgE+PsVnO+gAn53f471zgI9P8dkOOsBn5/d47xzg41N8toMO8Nn5Pd67f9vZ68My1rETAAAAAElFTkSuQmCC) + +## Usage + +In the main menu: + +* `Connect` - connect and start displaying respiration +* `Vib` - Should we vibrate if the breaths per minute (BPM) is above a certain value? + * `No` - don't vibrate + * `Calculated` - vibrate if the app's reading is high. This is based on raw + sensor data and it responds quickly but may not be accurate. + * `Vernier` - vibrate if the Vernier sensor's own reading is high. This is + more accurate but responds very slowly. +* `Connect` - connect and start displaying respiration + +## TODO + +* Logging to a file? + +## Creator + +Gordon Williams diff --git a/apps/vernierrespirate/app-icon.js b/apps/vernierrespirate/app-icon.js new file mode 100644 index 000000000..f687d2a9d --- /dev/null +++ b/apps/vernierrespirate/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///9nou30h/qJf8Ah/wBasK0ALhHcMBqALBgtABYsVqgLBAYILFqtUF4MVqoKEgoLFqALGEYQLFAwILEGAlV6tUlWoitXGAgLC1WqBYsBq+VqwLBAYPVMIUFBYN61Uq1oLBHgQLC1WohWqrwLDiteEIOggQDB2pICivqA4IFBlWq1YLCq/V9WkAoMa1YHBKQVf9XUAoMX1f1KgVVEYIpDEYILBLwIuBC4YFBMAMBLAILFLQILBrdV12UBYMW3VV8tAgt9q+2BYee6t9qEFuoLHroLBqte6wvDy+1ZoILBvdWSoeV9oLD+tfBYYFBBYTEBrq5CgN1aQNQgNVAALdEAAISBAYL1EfgISCAgIKDDAQSEAH4AQ")) diff --git a/apps/vernierrespirate/app.js b/apps/vernierrespirate/app.js new file mode 100644 index 000000000..945b72b77 --- /dev/null +++ b/apps/vernierrespirate/app.js @@ -0,0 +1,256 @@ + +// get settings +var settings = require("Storage").readJSON("vernierrespirate.json",1)||{}; +settings.vibrateBPM = settings.vibrateBPM||27; +// settings.vibrate; // undefined / "calculated" / "vernier" + +function saveSettings() { + require("Storage").writeJSON("vernierrespirate.json", settings); +} + + +g.clear(); +var graphHeight = g.getHeight()-100; +var last = { + time : Date.now(), + x : 0, + y : 24, +}; +var avrValue; +var aboveAvr = false; +var lastBreath; +var lastBreaths = []; +var vibrateInterval; + +function onMsg(txt) { + print(txt); + E.showMessage(txt); +} + +function setVibrate(isOn) { + var wasOn = vibrateInterval!==undefined; + if (isOn == wasOn) return; + + if (isOn) { + vibrateInterval = setInterval(function() { + Bangle.buzz(); + }, 1000); + } else { + clearInterval(vibrateInterval); + vibrateInterval = undefined; + } +} + +function onBreath() { + var t = Date.now(); + if (lastBreath!==undefined) { + // time between breaths + var value = 60000 / (t-lastBreath); + // average of last 3 + while (lastBreaths.length>=3) lastBreaths.shift(); // keep length small + lastBreaths.push(value); + value = E.sum(lastBreaths) / lastBreaths.length; + // draw value + g.reset(); + g.clearRect(0,g.getHeight()-100,g.getWidth(),g.getHeight()-50); + g.setFont("6x8").setFontAlign(0,0); + g.drawString("Calculated measurement", g.getWidth()/2, g.getHeight()-95); + g.setFont("Vector",40).setFontAlign(0,0); + g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-70); + // set vibration IF we're doing it from our calculations + if (settings.vibrate == "calculated") + setVibrate(value > settings.vibrateBPM); + } + lastBreath = t; +} + +function onData(n, value) { + g.reset(); + if (n==2) { + function scale(v) { + return Math.max(graphHeight - (1+v*4),24); + } + if (avrValue==undefined) avrValue=value; + avrValue = avrValue*0.95 + value*0.05; + if (avrValue < 1) avrValue = 1; + if (value > avrValue) { + if (!aboveAvr) onBreath(); + aboveAvr = true; + } else aboveAvr = false; + + var t = Date.now(); + var x = Math.round((t - last.time) / 100) // 10 per second + if (last.x>=g.getWidth()) { + x = 0; + last.x = 0; + last.time = t; + g.clearRect(0,24,g.getWidth(),graphHeight); + } + var y = scale(value); + g.setPixel(x, scale(avrValue), "#f00"); + g.drawLine(last.x, last.y, x, y); + last.x = x; + last.y = y; + } + if (n==4) { + g.clearRect(0,g.getHeight()-50,g.getWidth(),g.getHeight()); + g.setFont("6x8").setFontAlign(0,0); + g.drawString("GoDirect measurement", g.getWidth()/2, g.getHeight()-45); + g.setFont("Vector",40).setFontAlign(0,0); + g.drawString(value.toFixed(2), g.getWidth()/2, g.getHeight()-20); + // set vibration IF we're doing it from our calculations + if (settings.vibrate == "vernier") + setVibrate(value > settings.vibrateBPM); + } + Bangle.setLCDPower(1); // ensure LCD is on +} + +function connect() { + var gatt, service, rx, tx; + var rollingCounter = 0xFF; + + // any button to exit + Bangle.setUI("updown", function() { + setVibrate(false); + Bangle.buzz(); + try { + if (gatt) gatt.disconnect(); + } catch (e) { + } + setTimeout(mainMenu, 1000); + }); + + function sendCommand(subCommand) { + const command = new Uint8Array(4 + subCommand.length); + command.set(new Uint8Array(subCommand), 4); + // Populate the packet header bytes + command[0] = 0x58; // header + command[1] = command.length; + command[2] = --rollingCounter; + command[3] = E.sum(command) & 0xFF; // checksum + return tx.writeValue(command); + } + function firstSetBit(v) { + return v & -v; + } + function handleResponse(dv) { + //print(dv.buffer); + var resType = dv.getUint8(0); + if (resType==0x20) { + // [32, 25, 207, 216, 6, 6, 0, 2, 252, 128, 138, 7, 191, 0, 0, 192, 127, 128, 49, 8, 191, 0, 0, 192, 127]) + // 6 = data type = real + // 6,0 = bit mask for sensors + // 2 = value count + if (dv.getUint8(4)!=6) return; //throw "Not float32 data"; + var sensorIds = dv.getUint16(5, true); + // var count = dv.getUint8(7); doesn't seem right + var offs = 9; + while (sensorIds) { + var value = dv.getFloat32(offs, true); + var s = firstSetBit(sensorIds); + if (isFinite(value)) onData(s,value); + //else print(s,value); + sensorIds &= ~s; + offs += 4; + } + } else { + var cmd = dv.getUint8(4); // cmd + //print("CMD",dv.buffer); + } + } + + onMsg("Searching..."); + NRF.requestDevice({ filters: [{ namePrefix: 'GDX-RB' }] }).then(function(device) { + device.on("gattserverdisconnected", function() { + onMsg("Device disconnected"); + }); + onMsg("Found. Connecting..."); + return device.gatt.connect({minInterval:20, maxInterval:20}); + }).then(function(g) { + gatt = g; + return gatt.getPrimaryService("d91714ef-28b9-4f91-ba16-f0d9a604f112"); + }).then(function(s) { + service = s; + return service.getCharacteristic("f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb"); + }).then(function(c) { + tx = c; + return service.getCharacteristic("b41e6675-a329-40e0-aa01-44d2f444babe"); + }).then(function(c) { + rx = c; + rx.on('characteristicvaluechanged', function(event) { + //print("EVT",event.target.value.buffer); + handleResponse(event.target.value); + }); + return rx.startNotifications(); + }).then(function() { + onMsg("Init"); + sendCommand([ // init + 0x1a, 0xa5, 0x4a, 0x06, + 0x49, 0x07, 0x48, 0x08, + 0x47, 0x09, 0x46, 0x0a, + 0x45, 0x0b, 0x44, 0x0c, + 0x43, 0x0d, 0x42, 0x0e, + 0x41, + ]); + /*setTimeout(function() { + print("Set measurement period"); + var us = 100000; // period in us + sendCommand([0x1b, 0xff, 0x00, + us & 255, + (us >> 8) & 255, + (us >> 16) & 255, + (us >> 24) & 255, + 0x00, + 0x00, + 0x00, + 0x00]); + }, 100);*/ + + /* setTimeout(function() { + print("Get sensor info"); + sendCommand([0x51, 0]); // get sensor IDs + // returns [152, 10, 1, 39, 81, 253, 54, 0, 0, 0] + // 54 is the bit mask of available channels + //sendCommand([106, 16]); // get sensor info + }, 2000);*/ + + setTimeout(function() { + onMsg("Start measurements"); + //https://github.com/VernierST/godirect-js/blob/main/src/Device.js#L588 + var channels = 6; // data channels 4 and 2 + sendCommand([ // start measurements + 0x18, 0xff, 0x01, channels, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00 + ]); + }, 500); + }).catch(function() { + onMsg("Connect Fail"); + }); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function mainMenu() { + var vibText = ["No","Calculated","Vernier"]; + var vibValue = ["","calculated","vernier"]; + E.showMenu({"":{title:"Respiration Belt"}, + "< Back" : () => { saveSettings(); load(); }, + "Connect" : () => { saveSettings(); E.showMenu(); connect(); }, + "Vib" : { + value : Math.max(vibValue.indexOf(settings.vibrate),0), + format : v => vibText[v], + min:0,max:2, + onchange : v => { settings.vibrate=vibValue[v]; } + }, + "BPM" : { + value : settings.vibrateBPM, + min:10,max:50, + onchange : v => { settings.vibrateBPM=v; } + } + }); +} + +mainMenu(); diff --git a/apps/vernierrespirate/app.png b/apps/vernierrespirate/app.png new file mode 100644 index 000000000..0f6b22af1 Binary files /dev/null and b/apps/vernierrespirate/app.png differ diff --git a/apps/waveclk/README.md b/apps/waveclk/README.md new file mode 100644 index 000000000..470a27c6c --- /dev/null +++ b/apps/waveclk/README.md @@ -0,0 +1,4 @@ +# Wave Clock + +![](screenshot.png) + diff --git a/apps/wclock/screenshot_word.png b/apps/wclock/screenshot_word.png new file mode 100644 index 000000000..56e0ce98f Binary files /dev/null and b/apps/wclock/screenshot_word.png differ diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index 519222c52..f72f77a4b 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -14,3 +14,4 @@ 0.10: Tweaks to reduce memory usage 0.11: Fix initial screen fill colour 0.12: Fix swipe direction (#800) +0.13: Mods for Bangle.js 2 diff --git a/apps/welcome/app.js b/apps/welcome/app-bangle1.js similarity index 97% rename from apps/welcome/app.js rename to apps/welcome/app-bangle1.js index 047b0cdb2..949750b38 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app-bangle1.js @@ -290,14 +290,6 @@ setWatch(()=>{ }, BTN2, {repeat:true,edge:"falling"}); setWatch(()=>move(-1), BTN1, {repeat:true}); -(function migrateSettings(){ - let global_settings = require('Storage').readJSON('setting.json', 1) - if (global_settings) { - delete global_settings.welcomed - require('Storage').write('setting.json', global_settings) - } -})() - Bangle.setLCDTimeout(0); Bangle.setLCDPower(1); move(0); diff --git a/apps/welcome/app-bangle2.js b/apps/welcome/app-bangle2.js new file mode 100644 index 000000000..93d1c5657 --- /dev/null +++ b/apps/welcome/app-bangle2.js @@ -0,0 +1,248 @@ +// exec each function from seq one after the other +function animate(seq,period) { + var c = g.getColor(); + var i = setInterval(function() { + if (seq.length) { + var f = seq.shift(); + g.setColor(c); + if (f) f(); + } else clearInterval(i); + },period); +} + +// Fade in to FG color with angled lines +function fade(col, callback) { + var n = 0; + function f() {"ram" + g.setColor(col); + for (var i=n;i<240;i+=10) g.drawLine(i,0,0,i).drawLine(i,240,240,i); + g.flip(); + n++; + if (n<10) setTimeout(f,0); + else callback(); + } + f(); +} + + +var SCENE_COUNT=10; +function getScene(n) { + if (n==0) return function() { + g.reset().setBgColor(0).clearRect(0,0,176,176); + g.setFont("6x15"); + var n=0; + var l = Bangle.getLogo(); + var im = g.imageMetrics(l); + var i = setInterval(function() { + n+=0.1; + g.setColor(n,n,n); + g.drawImage(l,(176-im.width)/2,(176-im.height)/2); + if (n>=1) { + clearInterval(i); + setTimeout(()=>g.drawString("Open",44,104), 500); + setTimeout(()=>g.drawString("Hackable",44,116), 1000); + setTimeout(()=>g.drawString("Smart Watch",44,128), 1500); + } + },50); + }; + if (n==1) return function() { + var img = require("heatshrink").decompress(atob("ptR4n/j/4gH+8H5wl+jOukVVoHZ8dt/n//n37OtgH9sHhwHp4H5xmkGiH72MRje/LL/7iIAEE7sPEgoAC+AlagIlIiMQErPxDwUYxAABwIHCj8N7nOl3uEqa6BEggnFjfM5nCkUil3gEq5KDAAQmC6QmBE4JxSEhIABiQmB8QmSXoQlCYRMdEwIlCAAIlNhYlOiO85nNEyMPEoZwIAAcsYIYmPXoYlMiKaFExX/u9VEqLBBOYrCH+czmtVqJyDEpiaCOYsgSYszmc3qtTEqMR7hzG8AlGmd1OQglOOY6aEgYlCmmZoJMCTBrnD6SaIEoU/zOUuolSjbnBJgqaCEoU5zOXX4RyQYBBzCS4X5zNDqqZCJiERJg5zBEoVJEoM1JgYlQjhMHc4JLEmZMEEp6ZIJgPzS4WTmZMVTILmFYAK+BmglCmd1JgUYJiPNEorABEIOZygDBm5MCiJMQlhMH8ByBXwIlBJgUxJiMd5nOTIzlBTAK+BAANVq4jPAAS/HJgJyCTATAEACC/B4S/IJgIlCYAgAPiS/Kn5yEYANTEyPc5niOQxMB/LlCOapyJJgbpBYAZzROQK/Gl0ATIWfEoZzBc6IlB6SYGgBJBJgpzSlhyH8EAh5MBTIjnCuIlOjjlHTAJzC/LmDTSSYIEoTABOYIlETSKYHXwIABOYM0yYmETSCYHEobnDOYqaBExu8TAwlEc4U5EoiaCmK+NTAolFEwX0TQzBMXwXiEpTBCAAomNEoS+EEo4mIYIImKEoS+EEpDoBEyUbEo3gEo4mJdAImIJY4lJEycdEoPOOBYmPuIlE+HcJYhKKTZ1fhYkB2EAhnNcYMuEhomMr8A3YABEoJyB5gjOAAYmHm9VgELEoJMBEoXAEyXzE45YBJgXwEqx1I+ByDOYJyVJw5yCgEB3cQGgJMWJwQnCu6/CgFBigDB13S/glVAAf1qomCglEoADB1QDBADEPEoNVqEAolEgEKolKErJMDYAJMD0lE0AmaEoNaAgJMCFIYAahV/IgIiDOTgABNYJMEOToiCIoJMCOTzfCN4RMBOTxsDJIRyfIwZMBKQZzfJgRyfOYZMBOUBzCJgNKOT5zDJgLoCADxKBOAIABOT6aCAARyfOYRyjOYRyjOYlKEsBzEEsBzEOUJzDOUIABOUiaDOURzCOUZzCEscKCiY")); + var im = g.imageMetrics(img); + g.reset(); + g.setBgColor("#ff00ff"); + var y = 176, speed = 5; + function balloon(callback) { + y-=speed; + var x = (176-im.width)/2; + g.drawImage(img,x,y); + g.clearRect(x,y+81,x+77,y+81+speed); + if (y>30) setTimeout(balloon,0,callback); + else callback(); + } + fade("#ff00ff", function() { + balloon(function() { + g.setColor(-1).setFont("6x15:2").setFontAlign(0,0); + g.drawString("Welcome.",88,130); + }); + }); + setTimeout(function() { + var n=0; + var i = setInterval(function() { + n+=4; + g.scroll(0,-4); + if (n>150) + clearInterval(i); + },20); + },3500); + + }; + if (n==2) return function() { + g.reset(); + g.setBgColor("#ffff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 70, y = 25, h=25; + animate([ + ()=>g.drawString("Your",x,y+=h), + ()=>g.drawString("Bangle.js",x,y+=h), + ()=>g.drawString("has one",x,y+=h), + ()=>g.drawString("button",x,y+=h), + ()=>{g.setFont("12x20:2").setFontAlign(0,0,1).drawString("HERE!",150,88);} + ],200); + }; + if (n==3) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To wake the\nscreen up, or to\nselect", 88,60); + }; + if (n==4) return function() { + g.reset(); + g.setBgColor("#00ffff").setColor(0).clear(); + g.setFontAlign(0,0).setFont("6x15:2"); + g.drawString("Long Press",88,40).setFontAlign(0,-1); + g.setFont("12x20"); + g.drawString("To go back to\nthe clock", 88,60); + }; + if (n==5) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFontAlign(0,0).setFont("12x20"); + g.drawString("If Bangle.js ever\nstops, hold the\nbutton for\nten seconds.\n\nBangle.js will\nthen reboot.", 88,78); + }; + if (n==6) return function() { + g.reset(); + g.setBgColor("#0000ff").setColor(-1).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -20, h=60; + animate([ + ()=>{g.drawString("Bangle.js has a\nfull touchscreen",x,y+=h);}, + 0,0, + ()=>{g.drawString("Drag up and down\nto scroll and\ntap to select",x,y+=h);}, + ],300); + }; + if (n==7) return function() { + g.reset(); + g.setBgColor("#00ff00").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88, y = -35, h=80; + animate([ + ()=>{g.drawString("Bangle.js comes\nwith a few\napps installed",x,y+=h);}, + 0,0, + ()=>{g.drawString("To add more, visit\nbanglejs.com/apps",x,y+=h);}, + ],400); + }; + if (n==8) return function() { + g.reset(); + g.setBgColor("#ff0000").setColor(0).clear(); + g.setFont("12x20").setFontAlign(0,0); + var x = 88; + g.drawString("You can also make\nyour own apps!",x,30); + g.drawString("Check out\nbanglejs.com",x,130); + + var rx = 0, ry = 0; + // draw a cube + function draw() { + // rotate + rx += 0.1; + ry += 0.11; + var rcx=Math.cos(rx), + rsx=Math.sin(rx), + rcy=Math.cos(ry), + rsy=Math.sin(ry); + // Project 3D coordinates into 2D + function p(x,y,z) { + var t; + t = x*rcy + z*rsy; + z = z*rcy - x*rsy; + x=t; + t = y*rcx + z*rsx; + z = z*rcx - y*rsx; + y=t; + z += 4; + return [88 + 60*x/z, 78+ 60*y/z]; + } + + var a; + // draw a series of lines to make up our cube + var s = 30; + g.clearRect(88-s,78-s,88+s,78+s); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,-1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(-1,-1,-1); g.moveTo(a[0],a[1]); + a = p(-1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,-1,-1); g.moveTo(a[0],a[1]); + a = p(1,-1,1); g.lineTo(a[0],a[1]); + a = p(1,1,-1); g.moveTo(a[0],a[1]); + a = p(1,1,1); g.lineTo(a[0],a[1]); + a = p(-1,1,-1); g.moveTo(a[0],a[1]); + a = p(-1,1,1); g.lineTo(a[0],a[1]); + } + + setInterval(draw,50); + }; + if (n==9) return function() { + g.reset(); + g.setBgColor("#ffffff");g.clear(); + g.setFontAlign(0,0); + g.setFont("12x20"); + + var x = 88, y = 10, h=21; + animate([ + ()=>g.drawString("That's it!",x,y+=h), + ()=>{g.drawString("Press",x,y+=h*2); + g.drawString("the button",x,y+=h); + g.drawString("to start",x,y+=h); + g.drawString("Bangle.js",x,y+=h);} + ],400); + } +} + +var sceneNumber = 0; + +function move(dir) { + if (dir>0 && sceneNumber+1 == SCENE_COUNT) return; // at the end + sceneNumber = (sceneNumber+dir)%SCENE_COUNT; + if (sceneNumber<0) sceneNumber=0; + clearInterval(); + getScene(sceneNumber)(); + if (sceneNumber>1) { + var l = SCENE_COUNT; + for (var i=0;i move(dir)); +setWatch(()=>{ + if (sceneNumber == SCENE_COUNT-1) + load(); + else + move(1); +}, BTN1, {repeat:true}); + +Bangle.setLCDTimeout(0); +Bangle.setLCDPower(1); +move(0); diff --git a/apps/welcome/screenshot_welcome.png b/apps/welcome/screenshot_welcome.png new file mode 100644 index 000000000..4c574839d Binary files /dev/null and b/apps/welcome/screenshot_welcome.png differ diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index a5fdc31cc..5986ecf3f 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -5,3 +5,4 @@ 0.06: Use 'g.theme' (requires bootloader 0.23) 0.07: Move CHARGING variable to more readable string 0.08: Ensure battery updates every 60s even if LCD was on at boot and stays on +0.09: Misc speed/memory tweaks diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index 739326df0..a8a0c5382 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -1,26 +1,11 @@ (function(){ - function setWidth() { WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0); } - function draw() { - var s = 39; - var x = this.x, y = this.y; - g.reset(); - if (Bangle.isCharging()) { - g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); - x+=16; - } - g.setColor(g.theme.fg); - g.fillRect(x,y+2,x+s-4,y+21); - g.clearRect(x+2,y+4,x+s-6,y+19); - g.fillRect(x+s-3,y+10,x+s,y+14); - g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17); - } Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); setWidth(); - Bangle.drawWidgets(); // relayout widgets + Bangle.drawWidgets(); // re-layout widgets g.flip(); }); var batteryInterval = Bangle.isLCDOn() ? setInterval(()=>WIDGETS["bat"].draw(), 60000) : undefined; @@ -37,6 +22,16 @@ } } }); - WIDGETS["bat"]={area:"tr",width:40,draw:draw}; + WIDGETS["bat"]={area:"tr",width:40,draw:function() { + var s = 39; + var x = this.x, y = this.y; + g.reset(); + if (Bangle.isCharging()) { + g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); + x+=16; + } + 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); + g.setColor("#0f0").fillRect(x+4,y+6,x+4+E.getBattery()*(s-12)/100,y+17); + }}; setWidth(); })() diff --git a/apps/widbat/widget.png b/apps/widbat/widget.png index 630692e38..4f7491ee9 100644 Binary files a/apps/widbat/widget.png and b/apps/widbat/widget.png differ diff --git a/apps/widbatv/ChangeLog b/apps/widbatv/ChangeLog new file mode 100644 index 000000000..55cda0f21 --- /dev/null +++ b/apps/widbatv/ChangeLog @@ -0,0 +1 @@ +0.01: New widget diff --git a/apps/widbatv/widget.js b/apps/widbatv/widget.js new file mode 100644 index 000000000..cc52a0f8e --- /dev/null +++ b/apps/widbatv/widget.js @@ -0,0 +1,19 @@ +Bangle.on('charging',function(charging) { + if(charging) Bangle.buzz(); + WIDGETS["batv"].draw(); +}); +setInterval(()=>WIDGETS["batv"].draw(), 60000); +Bangle.on('lcdPower', function(on) { + if (on) WIDGETS["batv"].draw(); // refresh at power on +}); +WIDGETS["batv"]={area:"tr",width:14,draw:function() { + var x = this.x, y = this.y; + g.reset(); + if (Bangle.isCharging()) { + g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); + } else { + g.clearRect(x,y,x+14,y+24); + g.setColor(g.theme.fg).fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2); + g.setColor("#0f0").fillRect(x+4,y+20-(E.getBattery()*16/100),x+10,y+20); + } +}}; diff --git a/apps/widbatv/widget.png b/apps/widbatv/widget.png new file mode 100644 index 000000000..e31704d7b Binary files /dev/null and b/apps/widbatv/widget.png differ diff --git a/apps/widbt/ChangeLog b/apps/widbt/ChangeLog index 4509487ac..7aa96ce5c 100644 --- a/apps/widbt/ChangeLog +++ b/apps/widbt/ChangeLog @@ -3,3 +3,4 @@ 0.04: Fix automatic update of Bluetooth connection status 0.05: Make Bluetooth widget thinner, and when on a bright theme use light grey for disabled color 0.06: Tweaking colors for dark/light themes and low bpp screens +0.07: Memory usage improvements diff --git a/apps/widbt/widget.js b/apps/widbt/widget.js index a25d2c21c..88be3d5c9 100644 --- a/apps/widbt/widget.js +++ b/apps/widbt/widget.js @@ -1,17 +1,13 @@ -(function(){ - function draw() { - 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.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); - } - function changed() { - WIDGETS["bluetooth"].draw(); - g.flip();// turns screen on - } - NRF.on('connect',changed); - NRF.on('disconnect',changed); - WIDGETS["bluetooth"]={area:"tr",width:15,draw:draw}; -})() +WIDGETS["bluetooth"]={area:"tr",width:15,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.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),2+this.x,2+this.y); +},changed:function() { + WIDGETS["bluetooth"].draw(); + Bangle.setLCDPower(1); // turn screen on +}}; +NRF.on('connect',WIDGETS["bluetooth"].changed); +NRF.on('disconnect',WIDGETS["bluetooth"].changed); diff --git a/apps/widcom/ChangeLog b/apps/widcom/ChangeLog new file mode 100644 index 000000000..5d08e91e6 --- /dev/null +++ b/apps/widcom/ChangeLog @@ -0,0 +1,3 @@ +0.02: Works with light theme + Doesn't drain battery by updating every 2 secs + Fix alignment diff --git a/apps/widcom/widget.js b/apps/widcom/widget.js index b9c911dbf..bce9453c5 100644 --- a/apps/widcom/widget.js +++ b/apps/widcom/widget.js @@ -1,30 +1,17 @@ (function(){ - //var img = E.toArrayBuffer(atob("FBSBAAAAAAAAA/wAf+AP/wH/2D/zw/w8PwfD9nw+b8Pg/Dw/w8/8G/+A//AH/gA/wAAAAAAA")); - //var img = E.toArrayBuffer(atob("GBiBAAB+AAP/wAeB4A4AcBgAGDAADHAADmABhmAHhsAfA8A/A8BmA8BmA8D8A8D4A2HgBmGABnAADjAADBgAGA4AcAeB4AP/wAB+AA==")); - var img = E.toArrayBuffer(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A")); - - function draw() { + var cp = Bangle.setCompassPower; + Bangle.setCompassPower = () => { + cp.apply(Bangle, arguments); + WIDGETS.compass.draw(); + }; + + WIDGETS.compass={area:"tr",width:24,draw:function() { g.reset(); if (Bangle.isCompassOn()) { - g.setColor(1,0.8,0); // on = amber + g.setColor(g.theme.dark ? "#FC0" : "#F00"); } else { - g.setColor(0.3,0.3,0.3); // off = grey + g.setColor(g.theme.dark ? "#333" : "#CCC"); } - g.drawImage(img, 10+this.x, 2+this.var); - } - - var timerInterval; - Bangle.on('lcdPower', function(on) { - if (on) { - WIDGETS.compass.draw(); - if (!timerInterval) timerInterval = setInterval(()=>WIDGETS.compass.draw(), 2000); - } else { - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = undefined; - } - } - }); - - WIDGETS.compass={area:"tr",width:24,draw:draw}; + g.drawImage(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A"), 2+this.x, 2+this.var); + }}; })(); diff --git a/apps/widgps/ChangeLog b/apps/widgps/ChangeLog index d80e09912..57bb53bb7 100644 --- a/apps/widgps/ChangeLog +++ b/apps/widgps/ChangeLog @@ -1,2 +1,3 @@ 0.01: First version 0.02: Don't break if running on 2v08 firmware (just don't display anything) +0.03: Fix positioning diff --git a/apps/widgps/widget.js b/apps/widgps/widget.js index 19be2abaf..6ef55e27b 100644 --- a/apps/widgps/widget.js +++ b/apps/widgps/widget.js @@ -4,11 +4,11 @@ function draw() { g.reset(); if (Bangle.isGPSOn()) { - g.setColor(1,0.8,0); // on = amber + g.setColor("#FD0"); // on = amber } else { - g.setColor(0.3,0.3,0.3); // off = grey + g.setColor("#888"); // off = grey } - g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), 10+this.x, 2+this.y); + g.drawImage(atob("GBiBAAAAAAAAAAAAAA//8B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+B//+BgYGBgYGBgYGBgYGBgYGBgYGB//+A//8AAAAAAAAAAAAA=="), this.x, 2+this.y); } var timerInterval; diff --git a/apps/widhrm/ChangeLog b/apps/widhrm/ChangeLog index 45dcfa87e..93e2eaf66 100644 --- a/apps/widhrm/ChangeLog +++ b/apps/widhrm/ChangeLog @@ -2,3 +2,4 @@ 0.02: Tweaks for variable size widget system 0.03: Ensure redrawing works with variable size widget system 0.04: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799) +0.05: Use new 'lock' event, not LCD (so it works on Bangle.js 2) diff --git a/apps/widhrm/widget.js b/apps/widhrm/widget.js index 54b105d1e..7ffe1aa6d 100644 --- a/apps/widhrm/widget.js +++ b/apps/widhrm/widget.js @@ -1,13 +1,38 @@ (() => { - var currentBPM = undefined; - var lastBPM = undefined; - var firstBPM = true; // first reading since sensor turned on + if (!Bangle.isLocked) return; // old firmware + var currentBPM; + var lastBPM; + var isHRMOn = false; - function draw() { + // turn on sensor when the LCD is unlocked + Bangle.on('lock', function(isLocked) { + if (!isLocked) { + Bangle.setHRMPower(1,"widhrm"); + currentBPM = undefined; + WIDGETS["hrm"].draw(); + } else { + Bangle.setHRMPower(0,"widhrm"); + } + }); + + var hp = Bangle.setHRMPower; + Bangle.setHRMPower = () => { + hp.apply(Bangle, arguments); + isHRMOn = Bangle.isHRMOn(); + WIDGETS["hrm"].draw(); + }; + + Bangle.on('HRM',function(d) { + currentBPM = d.bpm; + lastBPM = currentBPM; + WIDGETS["hrm"].draw(); + }); + + // add your widget + WIDGETS["hrm"]={area:"tl",width:24,draw:function() { var width = 24; g.reset(); - g.setFont("6x8", 1); - g.setFontAlign(0, 0); + g.setFont("6x8", 1).setFontAlign(0, 0); g.clearRect(this.x,this.y+15,this.x+width,this.y+23); // erase background var bpm = currentBPM, isCurrent = true; if (bpm===undefined) { @@ -16,36 +41,12 @@ } if (bpm===undefined) bpm = "--"; - g.setColor(isCurrent ? "#ffffff" : "#808080"); + g.setColor(isCurrent ? g.theme.fg : "#808080"); g.drawString(bpm, this.x+width/2, this.y+19); - g.setColor(isCurrent ? "#ff0033" : "#808080"); + g.setColor(isHRMOn ? "#ff0033" : "#808080"); g.drawImage(atob("CgoCAAABpaQ//9v//r//5//9L//A/+AC+AAFAA=="),this.x+(width-10)/2,this.y+1); g.setColor(-1); - } + }}; - // redraw when the LCD turns on - Bangle.on('lcdPower', function(on) { - if (on) { - Bangle.setHRMPower(1,"widhrm"); - firstBPM = true; - currentBPM = undefined; - WIDGETS["hrm"].draw(); - } else { - Bangle.setHRMPower(0,"widhrm"); - } - }); - - Bangle.on('HRM',function(d) { - if (firstBPM) - firstBPM=false; // ignore the first one as it's usually rubbish - else { - currentBPM = d.bpm; - lastBPM = currentBPM; - } - WIDGETS["hrm"].draw(); - }); - Bangle.setHRMPower(Bangle.isLCDOn(),"widhrm"); - - // add your widget - WIDGETS["hrm"]={area:"tl",width:24,draw:draw}; + Bangle.setHRMPower(!Bangle.isLocked(),"widhrm"); })(); diff --git a/apps/widhrt/ChangeLog b/apps/widhrt/ChangeLog index fdb495797..39520ad6a 100644 --- a/apps/widhrt/ChangeLog +++ b/apps/widhrt/ChangeLog @@ -1,3 +1,5 @@ 0.01: First version 0.02: Don't break if running on 2v08 firmware (just don't display anything) - +0.03: Works with light theme + Doesn't drain battery by updating every 2 secs + fix alignment diff --git a/apps/widhrt/widget.js b/apps/widhrt/widget.js index 8ac76def8..d9716fa24 100644 --- a/apps/widhrt/widget.js +++ b/apps/widhrt/widget.js @@ -1,28 +1,18 @@ (function(){ if (!Bangle.isHRMOn) return; // old firmware + var hp = Bangle.setHRMPower; + Bangle.setHRMPower = () => { + hp.apply(Bangle, arguments); + WIDGETS.widhrt.draw(); + }; - function draw() { + WIDGETS.widhrt={area:"tr",width:24,draw:function() { g.reset(); if (Bangle.isHRMOn()) { - g.setColor(1,0,0); // on = red + g.setColor("#f00"); // on = red } else { - g.setColor(0.3,0.3,0.3); // off = grey + g.setColor(g.theme.dark ? "#333" : "#CCC"); // off = grey } - g.drawImage(atob("FhaBAAAAAAAAAAAAAcDgD8/AYeGDAwMMDAwwADDAAMOABwYAGAwAwBgGADAwAGGAAMwAAeAAAwAAAAAAAAAAAAA="), 10+this.x, 2+this.y); - } - - var timerInterval; - Bangle.on('lcdPower', function(on) { - if (on) { - WIDGETS.widhrt.draw(); - if (!timerInterval) timerInterval = setInterval(()=>WIDGETS["widhrt"].draw(), 2000); - } else { - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = undefined; - } - } - }); - - WIDGETS.widhrt={area:"tr",width:24,draw:draw}; + g.drawImage(atob("FhaBAAAAAAAAAAAAAcDgD8/AYeGDAwMMDAwwADDAAMOABwYAGAwAwBgGADAwAGGAAMwAAeAAAwAAAAAAAAAAAAA="), 1+this.x, 1+this.y); + }}; })(); diff --git a/apps/widmp/ChangeLog b/apps/widmp/ChangeLog index 5560f00bc..3996d9e74 100644 --- a/apps/widmp/ChangeLog +++ b/apps/widmp/ChangeLog @@ -1 +1,3 @@ 0.01: New App! +0.02: Fix position and overdraw bugs + Better memory usage, theme support diff --git a/apps/widmp/widget.js b/apps/widmp/widget.js index cebdb60f5..d8abc3a9c 100644 --- a/apps/widmp/widget.js +++ b/apps/widmp/widget.js @@ -1,20 +1,6 @@ -/* jshint esversion: 6 */ -(() => { - - const BLACK = 0, MOON = 0x41f, MC = 29.5305882, NM = 694039.09; - var r = 12, mx = 0, my = 0; - - var moon = { - 0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);}, - 1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);}, - 2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, - 3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);}, - 4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, - 5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);}, - 6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, - 7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r + r, my + r);}, - 8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);} - }; +WIDGETS["widmoon"] = { area: "tr", width: 24, draw: function() { + const MC = 29.5305882, NM = 694039.09; + var r = 11, mx = this.x + 12; my = this.y + 12; function moonPhase(d) { var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate(); @@ -23,11 +9,18 @@ return Math.round(((tmp - (tmp | 0)) * 7)+1); } - function draw() { - mx = this.x; my = this.y + 12; - moon[moonPhase(Date())](); - } + const BLACK = g.theme.bg, MOON = 0x41f; + var moon = { + 0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);}, + 1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);}, + 2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);}, + 4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);}, + 6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}, + 7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r, my + r);}, + 8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);} + }; + moon[moonPhase(Date())](); +} }; - WIDGETS["widmoon"] = { area: "tr", width: 24, draw: draw }; - -})(); diff --git a/apps/widtbat/ChangeLog b/apps/widtbat/ChangeLog index 4c21f3ace..37513808a 100644 --- a/apps/widtbat/ChangeLog +++ b/apps/widtbat/ChangeLog @@ -1 +1,2 @@ 0.01: New Widget! +0.02: Theme support, memory savings diff --git a/apps/widtbat/widget.js b/apps/widtbat/widget.js index 6d5aded8b..c78653358 100644 --- a/apps/widtbat/widget.js +++ b/apps/widtbat/widget.js @@ -1,17 +1,11 @@ -/* jshint esversion: 6 */ -(() => { - const CBS = 0x41f, CBC = 0x07E0; - var xo = 6, xl = 22, yo = 9, h = 17; - - function draw() { - g.reset().setColor(CBS).drawImage(require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")), this.x + 1, this.y + 4); - g.setColor(0).fillRect(this.x + xo, this.y + yo, this.x + xl, this.y + h); - var cbc = (Bangle.isCharging()) ? CBC : CBS; - g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h); - } - Bangle.on('charging', function(charging) { - if (charging) Bangle.buzz(); - Bangle.drawWidgets(); - }); - WIDGETS["widtbat"] = { area:"tr", width:32, draw: draw }; -})(); +Bangle.on('charging', function(charging) { + if (charging) Bangle.buzz(); + WIDGETS["widtbat"].draw(); +}); +WIDGETS["widtbat"] = { area:"tr", width:32, draw: function() { + const xo = 6, xl = 22, yo = 9, h = 17; + g.reset().setColor("#08f").drawImage(require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE")), this.x + 1, this.y + 4); + g.clearRect(this.x + xo, this.y + yo, this.x + xl, this.y + h); + var cbc = (Bangle.isCharging()) ? "#0f0" : "#08f"; + g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h); +} }; diff --git a/apps/widtbat/widget.png b/apps/widtbat/widget.png index 4294f0ca3..f2943bc52 100644 Binary files a/apps/widtbat/widget.png and b/apps/widtbat/widget.png differ diff --git a/apps/widver/ChangeLog b/apps/widver/ChangeLog index adb5b038a..06bf45fbd 100644 --- a/apps/widver/ChangeLog +++ b/apps/widver/ChangeLog @@ -1 +1,3 @@ 0.01: New Widget +0.02: Display "Rel" (Release) instead of 'undefined' when there is no Build number. +0.03: Add theme support, lower memory usage diff --git a/apps/widver/widget.js b/apps/widver/widget.js index 5da66444f..b6a8b7432 100644 --- a/apps/widver/widget.js +++ b/apps/widver/widget.js @@ -1,11 +1,9 @@ -/* jshint esversion: 6 */ -(() => { - var width = 28, - ver = process.env.VERSION.split('.'); - function draw() { - g.reset().setColor(0, 0.5, 1).setFont("6x8", 1); - g.drawString(ver[0], this.x + 2, this.y + 4, true); - g.setFontAlign(0, -1, 0).drawString(ver[1], this.x + width / 2, this.y + 14, true); - } - WIDGETS["version"] = { area: "tr", width: width, draw: draw }; -})(); +WIDGETS["version"] = { area: "tr", width: 28, draw: function() { + var ver = process.env.VERSION.split('.'); + // Example: if ver is 2v11 instead of 2v10.142 write "Rel" (Release) instead of Build number + if(typeof ver[1] === 'undefined') ver[1] = "Rel"; + + g.reset().setColor((g.getBPP()<8)?(g.theme.dark?"#0ff":"#00f"):"#08f").setFont("6x8"); + g.drawString(ver[0], this.x + 2, this.y + 4, true); + g.setFontAlign(0, -1, 0).drawString(ver[1], this.x + this.width / 2, this.y + 14, true); +} }; diff --git a/apps/worldclock/ChangeLog b/apps/worldclock/ChangeLog index e922ef2a4..831dd3b5c 100644 --- a/apps/worldclock/ChangeLog +++ b/apps/worldclock/ChangeLog @@ -2,3 +2,5 @@ 0.02: Update custom.html for refactor; add README 0.03: Update for larger secondary timezone display (#610) 0.04: setUI, different screen sizes +0.05: Now update *on* the minute rather than every 15 secs + Fix rendering of single extra timezone on Bangle.js 2 diff --git a/apps/worldclock/app.js b/apps/worldclock/app.js index 84cb29874..2627e056c 100644 --- a/apps/worldclock/app.js +++ b/apps/worldclock/app.js @@ -1,5 +1,3 @@ -/* jshint esversion: 6 */ - const big = g.getWidth()>200; // Font for primary time and date const primaryTimeFontSize = big?6:5; @@ -16,8 +14,13 @@ const xcol2 = g.getWidth() - xcol1; const font = "6x8"; +/* TODO: we could totally use 'Layout' here and +avoid a whole bunch of hard-coded offsets */ + + const xyCenter = g.getWidth() / 2; const yposTime = big ? 75 : 60; +const yposTime2 = yposTime + (big ? 100 : 60); const yposDate = big ? 130 : 90; const yposWorld = big ? 170 : 120; @@ -29,41 +32,52 @@ var offsets = require("Storage").readJSON("worldclock.settings.json") || []; // TESTING CODE // Used to test offset array values during development. // Uncomment to override secondary offsets value - -// const mockOffsets = { -// zeroOffsets: [], -// oneOffset: [["UTC", 0]], -// twoOffsets: [ -// ["Tokyo", 9], -// ["UTC", 0], -// ], -// fourOffsets: [ -// ["Tokyo", 9], -// ["UTC", 0], -// ["Denver", -7], -// ["Miami", -5], -// ], -// fiveOffsets: [ -// ["Tokyo", 9], -// ["UTC", 0], -// ["Denver", -7], -// ["Chicago", -6], -// ["Miami", -5], -// ], -// }; +/* +const mockOffsets = { + zeroOffsets: [], + oneOffset: [["UTC", 0]], + twoOffsets: [ + ["Tokyo", 9], + ["UTC", 0], + ], + fourOffsets: [ + ["Tokyo", 9], + ["UTC", 0], + ["Denver", -7], + ["Miami", -5], + ], + fiveOffsets: [ + ["Tokyo", 9], + ["UTC", 0], + ["Denver", -7], + ["Chicago", -6], + ["Miami", -5], + ], +};*/ // Uncomment one at a time to test various offsets array scenarios -// offsets = mockOffsets.zeroOffsets; // should render nothing below primary time -// offsets = mockOffsets.oneOffset; // should render larger in two rows -// offsets = mockOffsets.twoOffsets; // should render two in columns -// offsets = mockOffsets.fourOffsets; // should render in columns -// offsets = mockOffsets.fiveOffsets; // should render first four in columns +//offsets = mockOffsets.zeroOffsets; // should render nothing below primary time +//offsets = mockOffsets.oneOffset; // should render larger in two rows +//offsets = mockOffsets.twoOffsets; // should render two in columns +//offsets = mockOffsets.fourOffsets; // should render in columns +//offsets = mockOffsets.fiveOffsets; // should render first four in columns // END TESTING CODE // Check settings for what type our clock should be //var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; -var secondInterval; + +// 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 doublenum(x) { return x < 10 ? "0" + x : "" + x; @@ -73,7 +87,7 @@ function getCurrentTimeFromOffset(dt, offset) { return new Date(dt.getTime() + offset * 60 * 60 * 1000); } -function drawSimpleClock() { +function draw() { // get date var d = new Date(); var da = d.toString().split(" "); @@ -111,9 +125,9 @@ function drawSimpleClock() { // For a single secondary timezone, draw it bigger and drop time zone to second line const xOffset = 30; g.setFont(font, secondaryTimeFontSize); - g.drawString(`${hours}:${minutes}`, xyCenter, yposTime + 100, true); + g.drawString(`${hours}:${minutes}`, xyCenter, yposTime2, true); g.setFont(font, secondaryTimeZoneFontSize); - g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime + 130, true); + g.drawString(offset[OFFSET_TIME_ZONE], xyCenter, yposTime2 + 30, true); // draw Day, name of month, Date g.setFont(font, secondaryTimeZoneFontSize); @@ -132,6 +146,8 @@ function drawSimpleClock() { g.drawString(`${hours}:${minutes}`, xcol2, yposWorld + index * 15, true); } }); + + queueDraw(); } // clean app screen @@ -141,18 +157,15 @@ Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); -// refesh every 15 sec when screen is on -Bangle.on("lcdPower", (on) => { - if (secondInterval) clearInterval(secondInterval); - secondInterval = undefined; +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ if (on) { - secondInterval = setInterval(drawSimpleClock, 15e3); - drawSimpleClock(); // draw immediately + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; } }); -// draw now and every 15 sec until display goes off -drawSimpleClock(); -if (Bangle.isLCDOn()) { - secondInterval = setInterval(drawSimpleClock, 15e3); -} +// draw now +draw(); diff --git a/apps/worldclock/screenshot_world.png b/apps/worldclock/screenshot_world.png new file mode 100644 index 000000000..ebae637b7 Binary files /dev/null and b/apps/worldclock/screenshot_world.png differ diff --git a/bin/create_app_supports_field.js b/bin/create_app_supports_field.js new file mode 100644 index 000000000..d6aada357 --- /dev/null +++ b/bin/create_app_supports_field.js @@ -0,0 +1,99 @@ +#!/usr/bin/nodejs +/* Quick hack to add proper 'supports' field to apps.json +*/ + +var fs = require("fs"); + +var BASEDIR = __dirname+"/../"; + +var appsFile, apps; +try { + appsFile = fs.readFileSync(BASEDIR+"apps.json").toString(); +} catch (e) { + ERROR("apps.json not found"); +} +try{ + apps = JSON.parse(appsFile); +} catch (e) { + console.log(e); + var m = e.toString().match(/in JSON at position (\d+)/); + if (m) { + var char = parseInt(m[1]); + console.log("==============================================="); + console.log("LINE "+appsFile.substr(0,char).split("\n").length); + console.log("==============================================="); + console.log(appsFile.substr(char-10, 20)); + console.log("==============================================="); + } + console.log(m); + ERROR("apps.json not valid JSON"); + +} + +apps = apps.map((app,appIdx) => { + if (app.supports) return app; // already sorted + var tags = []; + if (app.tags) tags = app.tags.split(",").map(t=>t.trim()); + var supportsB1 = true; + var supportsB2 = false; + if (tags.includes("b2")) { + tags = tags.filter(x=>x!="b2"); + supportsB2 = true; + } + if (tags.includes("bno2")) { + tags = tags.filter(x=>x!="bno2"); + supportsB2 = false; + } + if (tags.includes("bno1")) { + tags = tags.filter(x=>x!="bno1"); + supportsB1 = false; + } + app.tags = tags.join(","); + app.supports = []; + if (supportsB1) app.supports.push("BANGLEJS"); + if (supportsB2) app.supports.push("BANGLEJS2"); + return app; +}); + +// search for screenshots +apps = apps.map((app,appIdx) => { + if (app.screenshots) return app; // already sorted + + var files = require("fs").readdirSync(__dirname+"/../apps/"+app.id); + var screenshots = files.filter(fn=>fn.startsWith("screenshot") && fn.endsWith(".png")); + if (screenshots.length) + app.screenshots = screenshots.map(fn => ({url:fn})); + return app; +}); + +var KEY_ORDER = [ + "id","name","shortName","version","description","icon","screenshots","type","tags","supports", + "dependencies", "readme", "custom", "customConnect", "interface", + "allow_emulator", "storage", "data", "sortorder" +]; + +var JS = JSON.stringify; +var json = "[\n "+apps.map(app=>{ + var keys = KEY_ORDER.filter(k=>k in app); + Object.keys(app).forEach(k=>{ + if (!KEY_ORDER.includes(k)) + throw new Error(`Key named ${k} not known!`); + }); + //var keys = Object.keys(app); // don't re-order + + return "{\n "+keys.map(k=>{ + var js = JS(app[k]); + if (k=="storage") { + if (app.storage.length) + js = "[\n "+app.storage.map(s=>JS(s)).join(",\n ")+"\n ]"; + else + js = "[]"; + } + return JS(k)+": "+js; + }).join(",\n ")+"\n }"; +}).join(",\n ")+"\n]\n"; + +//console.log(json); + +console.log("new apps.json written"); +fs.writeFileSync(BASEDIR+"apps.json", json); diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 4e22dd168..4bc2a70b2 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -12,6 +12,7 @@ var ROOTDIR = path.join(__dirname, '..'); var APPDIR = ROOTDIR+'/apps'; var APPJSON = ROOTDIR+'/apps.json'; var OUTFILE = ROOTDIR+'/firmware.js'; +var DEVICE = "BANGLEJS"; var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", "about","alarm","widbat","widbt","welcome" @@ -61,7 +62,8 @@ Promise.all(APPS.map(appid => { if (app===undefined) throw new Error(`App ${appid} not found`); return AppInfo.getFiles(app, { fileGetter : fileGetter, - settings : SETTINGS + settings : SETTINGS, + device : { id : DEVICE } }).then(files => { appfiles = appfiles.concat(files); }); diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 2cb993d00..14ced9ef8 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -29,8 +29,8 @@ if (DEVICE=="BANGLEJS") { } else if (DEVICE=="BANGLEJS2") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs2_storage_default.c'); var APPS = [ // IDs of apps to install - "boot","launchb2","s7clk","setting", - "about","alarm","widlock","widbat","widbt" + "boot","launch","antonclk","setting", + "about","alarm","health","widlock","widbat","widbt","widid","welcome" ]; } else { console.log("USAGE:"); @@ -102,7 +102,13 @@ function fileGetter(url) { fs.writeFileSync(url, code); } } - return Promise.resolve(fs.readFileSync(url).toString("binary")); + var blob = fs.readFileSync(url); + var data; + if (url.endsWith(".js") || url.endsWith(".json")) + data = blob.toString(); // allow JS/etc to be written in UTF-8 + else + data = blob.toString("binary") + return Promise.resolve(data); } // If file should be evaluated, try and do it... @@ -132,7 +138,8 @@ Promise.all(APPS.map(appid => { if (app===undefined) throw new Error(`App ${appid} not found`); return AppInfo.getFiles(app, { fileGetter : fileGetter, - settings : SETTINGS + settings : SETTINGS, + device : { id : DEVICE } }).then(files => { appfiles = appfiles.concat(files); }); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index b98aa9ef3..a84d26efd 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -50,11 +50,12 @@ try{ } const APP_KEYS = [ - 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', - 'sortorder', 'readme', 'custom', 'customConnect', 'interface', 'storage', 'data', 'allow_emulator', + 'id', 'name', 'shortName', 'version', 'icon', 'screenshots', 'description', 'tags', 'type', + 'sortorder', 'readme', 'custom', 'customConnect', 'interface', 'storage', 'data', + 'supports', 'allow_emulator', 'dependencies' ]; -const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite']; +const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; @@ -81,6 +82,14 @@ apps.forEach((app,appIdx) => { if (!app.name) ERROR(`App ${app.id} has no name`); 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`); + else { + app.supports.forEach(dev => { + if (!["BANGLEJS","BANGLEJS2"].includes(dev)) + ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`); + }); + } + if (!app.version) WARN(`App ${app.id} has no version`); else { if (!fs.existsSync(appDir+"ChangeLog")) { @@ -98,6 +107,13 @@ apps.forEach((app,appIdx) => { 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.screenshots) { + if (!Array.isArray(app.screenshots)) ERROR(`App ${app.id} screenshots is not an array`); + app.screenshots.forEach(screenshot => { + if (!fs.existsSync(appDir+screenshot.url)) + ERROR(`App ${app.id} screenshot file ${screenshot.url} not found`); + }); + } 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`); @@ -105,8 +121,8 @@ apps.forEach((app,appIdx) => { if (app.dependencies) { if (("object"==typeof app.dependencies) && !Array.isArray(app.dependencies)) { Object.keys(app.dependencies).forEach(dependency => { - if (app.dependencies[dependency]!="type") - ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' right now`); + if (!["type","app"].includes(app.dependencies[dependency])) + ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`); }); } else ERROR(`App ${app.id} 'dependencies' must be an object`); @@ -117,7 +133,7 @@ apps.forEach((app,appIdx) => { if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); 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 (fileNames.includes(file.name)) + 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`); fileNames.push(file.name); allFiles.push({app: app.id, file: file.name}); @@ -126,6 +142,7 @@ apps.forEach((app,appIdx) => { 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.evaluate) { try { acorn.parse("("+fileContents+")"); @@ -156,7 +173,7 @@ apps.forEach((app,appIdx) => { } } for (const key in file) { - if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`); } }); let dataNames = []; diff --git a/bin/thumbnailer.js b/bin/thumbnailer.js new file mode 100755 index 000000000..b6862741a --- /dev/null +++ b/bin/thumbnailer.js @@ -0,0 +1,164 @@ +#!/usr/bin/node + +/* +var EMULATOR = "banglejs2"; +var DEVICEID = "BANGLEJS2"; +*/ +var EMULATOR = "banglejs1"; +var DEVICEID = "BANGLEJS"; + +var singleAppId; + +if (process.argv.length!=3 && process.argv.length!=2) { + console.log("USAGE:"); + console.log(" bin/thumbnailer.js"); + console.log(" - all thumbnails"); + console.log(" bin/thumbnailer.js APP_ID"); + console.log(" - just one app"); + process.exit(1); +} +if (process.argv.length==3) + singleAppId = process.argv[2]; + +if (!require("fs").existsSync(__dirname + "/../../EspruinoWebIDE")) { + console.log("You need to:"); + console.log(" git clone https://github.com/espruino/EspruinoWebIDE"); + console.log("At the same level as this project"); + process.exit(1); +} + +eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emulator_"+EMULATOR+".js").toString()); +eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emu_"+EMULATOR+".js").toString()); +eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/common.js").toString()); + +var SETTINGS = { + pretokenise : true +}; +var Const = { +}; +module = undefined; +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")); + +/* we factory reset ONCE, get this, then we can use it to reset +state quickly for each new app */ +var factoryFlashMemory = new Uint8Array(FLASH_SIZE); +// Log of messages from app +var appLog = ""; +// List of apps that errored +var erroredApps = []; + +jsRXCallback = function() {}; +jsUpdateGfx = function() {}; + +function ERROR(s) { + console.error(s); + process.exit(1); +} + +function onConsoleOutput(txt) { + appLog += txt + "\n"; +} + +function getThumbnail(appId, imageFn) { + console.log("Thumbnail for "+appId); + var app = apps.find(a=>a.id==appId); + if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); + if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); + + + return new Promise(resolve => { + AppInfo.getFiles(app, { + fileGetter:function(url) { + console.log(__dirname+"/"+url); + return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); + }, + settings : SETTINGS, + device : { id : DEVICEID } + }).then(files => { + console.log("AppInfo returned");//, files); + flashMemory.set(factoryFlashMemory); + jsTransmitString("reset()\n"); + console.log("Uploading..."); + jsTransmitString("g.clear()\n"); + var command = files.map(f=>f.cmd).join("\n")+"\n"; + command += `load("${appId}.app.js")\n`; + appLog = ""; + jsTransmitString(command); + console.log("Done."); + jsStopIdle(); + + var rgba = new Uint8Array(GFX_WIDTH*GFX_HEIGHT*4); + jsGetGfxContents(rgba); + var rgba32 = new Uint32Array(rgba.buffer); + var firstPixel = rgba32[0]; + var blankImage = rgba32.every(col=>col==firstPixel) + + if (appLog.indexOf("Uncaught")>=0) + erroredApps.push( { id : app.id, log : appLog } ); + + if (!blankImage) { + var Jimp = require("jimp"); + let image = new Jimp(GFX_WIDTH, GFX_HEIGHT, function (err, image) { + if (err) throw err; + let buffer = image.bitmap.data; + buffer.set(rgba); + image.write(imageFn, (err) => { + if (err) throw err; + console.log("Image written as "+imageFn); + resolve(true); + }); + }); + } else { + console.log("Image is empty"); + resolve(false); + } + + }); + }); +} + +var screenshots = []; + +// wait until loaded... +setTimeout(function() { + console.log("Loaded..."); + jsInit(); + jsIdle(); + console.log("Factory reset"); + jsTransmitString("Bangle.factoryReset()\n"); + factoryFlashMemory.set(flashMemory); + console.log("Ready!"); + + if (singleAppId) { + getThumbnail(singleAppId, "screenshots/"+singleAppId+"-"+EMULATOR+".png"); + return; + } + + var appList = apps.filter(app => (!app.type || app.type=="clock") && !app.custom); + appList = appList.filter(app => !app.screenshots && app.supports.includes(DEVICEID)); + + var promise = Promise.resolve(); + appList.forEach(app => { + promise = promise.then(() => { + var imageFile = "screenshots/"+app.id+"-"+EMULATOR+".png"; + return getThumbnail(app.id, imageFile).then(ok => { + screenshots.push({ + id : app.id, + url : imageFile, + version: app.version + }); + }); + }); + }); + + promise.then(function() { + console.log("Complete!"); + require("fs").writeFileSync("screenshots.json", JSON.stringify(screenshots,null,2)); + console.log("Errored Apps", erroredApps); + }); + + +}); diff --git a/core b/core index 0fd608f08..59f80bb52 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 0fd608f085deff9b39f2db3559ecc88edb232aba +Subproject commit 59f80bb52a38da12cb272f9106cb3951b49dab2e diff --git a/css/main.css b/css/main.css index 0dbe8da14..82f6fbcfe 100644 --- a/css/main.css +++ b/css/main.css @@ -23,15 +23,48 @@ .filter-nav { display: inline-block; } +.device-nav { + display: inline-block; +} .sort-nav { float: right; } + +.app-tile { + position: relative; + border-bottom: 1px solid #EEE; + margin-bottom: 4px; + min-height: 8em; +} + .tile-content { position: relative; } .link-github { position:absolute; top: 36px; left: -24px; } -.btn-favourite { - color: red; +.tile-screenshot { + position:absolute;bottom:1em;right:1em; + width:4em;height:4em; + padding:2px;border:1px solid black; + cursor:pointer; +} + +.btn.btn-favourite { color: red; } +.btn.btn-favourite:hover { color: red; } + +.icon.icon-emulator { text-indent: 0px; } /*override spectre*/ +.icon.icon-emulator { + content: url("data:image/svg+xml,%3Csvg fill='rgb(87, 85, 217)' xmlns='http://www.w3.org/2000/svg' viewBox='4 4 40 40' width='1em' height='1em'%3E%3Cpath d='M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z'/%3E%3C/svg%3E"); +} +.icon.icon-favourite { text-indent: 0px; } /*override spectre*/ +.icon.icon-favourite::before { + content: "\02661"; /* 0x2661 = empty heart; 0x2606 = empty star */ +} +.icon.icon-favourite-active::before { + content: "\02665"; /* 0x2665 = solid heart; 0x2605 = solid star */ +} +.icon.icon-interface {text-indent: 0px;font-size: 130%;vertical-align: -30%;} /*override spectre*/ +.icon.icon-interface::before { + content: "\01F5AB"; } diff --git a/css/spectre-exp.min.css b/css/spectre-exp.min.css index 942cf59bf..d3137743a 100644 --- a/css/spectre-exp.min.css +++ b/css/spectre-exp.min.css @@ -1 +1 @@ -/*! Spectre.css Experimentals v0.5.8 | MIT License | github.com/picturepan2/spectre */.form-autocomplete{position:relative}.form-autocomplete .form-autocomplete-input{align-content:flex-start;display:flex;display:-ms-flexbox;-ms-flex-line-pack:start;-ms-flex-wrap:wrap;flex-wrap:wrap;height:auto;min-height:1.6rem;padding:.1rem}.form-autocomplete .form-autocomplete-input.is-focused{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-autocomplete .form-autocomplete-input .form-input{border-color:transparent;box-shadow:none;display:inline-block;-ms-flex:1 0 auto;flex:1 0 auto;height:1.2rem;line-height:.8rem;margin:.1rem;width:auto}.form-autocomplete .menu{left:0;position:absolute;top:100%;width:100%}.form-autocomplete.autocomplete-oneline .form-autocomplete-input{-ms-flex-wrap:nowrap;flex-wrap:nowrap;overflow-x:auto}.form-autocomplete.autocomplete-oneline .chip{-ms-flex:1 0 auto;flex:1 0 auto}.calendar{border:.05rem solid #dadee4;border-radius:.1rem;display:block;min-width:280px}.calendar .calendar-nav{align-items:center;background:#f7f8f9;border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flex;display:-ms-flexbox;-ms-flex-align:center;font-size:.9rem;padding:.4rem}.calendar .calendar-body,.calendar .calendar-header{display:flex;display:-ms-flexbox;-ms-flex-pack:center;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:center;padding:.4rem 0}.calendar .calendar-body .calendar-date,.calendar .calendar-header .calendar-date{-ms-flex:0 0 14.28%;flex:0 0 14.28%;max-width:14.28%}.calendar .calendar-header{background:#f7f8f9;border-bottom:.05rem solid #dadee4;color:#bcc3ce;font-size:.7rem;text-align:center}.calendar .calendar-body{color:#66758c}.calendar .calendar-date{border:0;padding:.2rem}.calendar .calendar-date .date-item{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:0 0;border:.05rem solid transparent;border-radius:50%;color:#66758c;cursor:pointer;font-size:.7rem;height:1.4rem;line-height:1rem;outline:0;padding:.1rem;position:relative;text-align:center;text-decoration:none;transition:background .2s,border .2s,box-shadow .2s,color .2s;vertical-align:middle;white-space:nowrap;width:1.4rem}.calendar .calendar-date .date-item.date-today{border-color:#e5e5f9;color:#5755d9}.calendar .calendar-date .date-item:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.calendar .calendar-date .date-item:focus,.calendar .calendar-date .date-item:hover{background:#fefeff;border-color:#e5e5f9;color:#5755d9;text-decoration:none}.calendar .calendar-date .date-item.active,.calendar .calendar-date .date-item:active{background:#4b48d6;border-color:#3634d2;color:#fff}.calendar .calendar-date .date-item.badge::after{position:absolute;right:3px;top:3px;transform:translate(50%,-50%)}.calendar .calendar-date .calendar-event.disabled,.calendar .calendar-date .calendar-event:disabled,.calendar .calendar-date .date-item.disabled,.calendar .calendar-date .date-item:disabled{cursor:default;opacity:.25;pointer-events:none}.calendar .calendar-date.next-month .calendar-event,.calendar .calendar-date.next-month .date-item,.calendar .calendar-date.prev-month .calendar-event,.calendar .calendar-date.prev-month .date-item{opacity:.25}.calendar .calendar-range{position:relative}.calendar .calendar-range::before{background:#f1f1fc;content:"";height:1.4rem;left:0;position:absolute;right:0;top:50%;transform:translateY(-50%)}.calendar .calendar-range.range-start::before{left:50%}.calendar .calendar-range.range-end::before{right:50%}.calendar .calendar-range.range-end .date-item,.calendar .calendar-range.range-start .date-item{background:#4b48d6;border-color:#3634d2;color:#fff}.calendar .calendar-range .date-item{color:#5755d9}.calendar.calendar-lg .calendar-body{padding:0}.calendar.calendar-lg .calendar-body .calendar-date{border-bottom:.05rem solid #dadee4;border-right:.05rem solid #dadee4;display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column;height:5.5rem;padding:0}.calendar.calendar-lg .calendar-body .calendar-date:nth-child(7n){border-right:0}.calendar.calendar-lg .calendar-body .calendar-date:nth-last-child(-n+7){border-bottom:0}.calendar.calendar-lg .date-item{align-self:flex-end;-ms-flex-item-align:end;height:1.4rem;margin-right:.2rem;margin-top:.2rem}.calendar.calendar-lg .calendar-range::before{top:19px}.calendar.calendar-lg .calendar-range.range-start::before{left:auto;width:19px}.calendar.calendar-lg .calendar-range.range-end::before{right:19px}.calendar.calendar-lg .calendar-events{flex-grow:1;-ms-flex-positive:1;line-height:1;overflow-y:auto;padding:.2rem}.calendar.calendar-lg .calendar-event{border-radius:.1rem;display:block;font-size:.7rem;margin:.1rem auto;overflow:hidden;padding:3px 4px;text-overflow:ellipsis;white-space:nowrap}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-container .carousel-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-container .carousel-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-container .carousel-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-container .carousel-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-container .carousel-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-container .carousel-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-container .carousel-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-container .carousel-item:nth-of-type(8){animation:carousel-slidein .75s ease-in-out 1;opacity:1;z-index:100}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-nav .nav-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-nav .nav-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-nav .nav-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-nav .nav-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-nav .nav-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-nav .nav-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-nav .nav-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-nav .nav-item:nth-of-type(8){color:#f7f8f9}.carousel{background:#f7f8f9;display:block;overflow:hidden;-webkit-overflow-scrolling:touch;position:relative;width:100%;z-index:1}.carousel .carousel-container{height:100%;left:0;position:relative}.carousel .carousel-container::before{content:"";display:block;padding-bottom:56.25%}.carousel .carousel-container .carousel-item{animation:carousel-slideout 1s ease-in-out 1;height:100%;left:0;margin:0;opacity:0;position:absolute;top:0;width:100%}.carousel .carousel-container .carousel-item:hover .item-next,.carousel .carousel-container .carousel-item:hover .item-prev{opacity:1}.carousel .carousel-container .item-next,.carousel .carousel-container .item-prev{background:rgba(247,248,249,.25);border-color:rgba(247,248,249,.5);color:#f7f8f9;opacity:0;position:absolute;top:50%;transform:translateY(-50%);transition:all .4s;z-index:100}.carousel .carousel-container .item-prev{left:1rem}.carousel .carousel-container .item-next{right:1rem}.carousel .carousel-nav{bottom:.4rem;display:flex;display:-ms-flexbox;-ms-flex-pack:center;justify-content:center;left:50%;position:absolute;transform:translateX(-50%);width:10rem;z-index:100}.carousel .carousel-nav .nav-item{color:rgba(247,248,249,.5);display:block;-ms-flex:1 0 auto;flex:1 0 auto;height:1.6rem;margin:.2rem;max-width:2.5rem;position:relative}.carousel .carousel-nav .nav-item::before{background:currentColor;content:"";display:block;height:.1rem;position:absolute;top:.5rem;width:100%}@keyframes carousel-slidein{0%{transform:translateX(100%)}100%{transform:translateX(0)}}@keyframes carousel-slideout{0%{opacity:1;transform:translateX(0)}100%{opacity:1;transform:translateX(-50%)}}.comparison-slider{height:50vh;overflow:hidden;-webkit-overflow-scrolling:touch;position:relative;width:100%}.comparison-slider .comparison-after,.comparison-slider .comparison-before{height:100%;left:0;margin:0;overflow:hidden;position:absolute;top:0}.comparison-slider .comparison-after img,.comparison-slider .comparison-before img{height:100%;object-fit:cover;object-position:left center;position:absolute;width:100%}.comparison-slider .comparison-before{width:100%;z-index:1}.comparison-slider .comparison-before .comparison-label{right:.8rem}.comparison-slider .comparison-after{max-width:100%;min-width:0;z-index:2}.comparison-slider .comparison-after::before{background:0 0;content:"";cursor:default;height:100%;left:0;position:absolute;right:.8rem;top:0;z-index:1}.comparison-slider .comparison-after::after{background:currentColor;border-radius:50%;box-shadow:0 -5px,0 5px;color:#fff;content:"";height:3px;position:absolute;right:.4rem;top:50%;transform:translate(50%,-50%);width:3px}.comparison-slider .comparison-after .comparison-label{left:.8rem}.comparison-slider .comparison-resizer{animation:first-run 1.5s 1 ease-in-out;cursor:ew-resize;height:.8rem;left:0;max-width:100%;min-width:.8rem;opacity:0;outline:0;position:relative;resize:horizontal;top:50%;transform:translateY(-50%) scaleY(30);width:0}.comparison-slider .comparison-label{background:rgba(48,55,66,.5);bottom:.8rem;color:#fff;padding:.2rem .4rem;position:absolute;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@keyframes first-run{0%{width:0}25%{width:2.4rem}50%{width:.8rem}75%{width:1.2rem}100%{width:0}}.filter .filter-tag#tag-0:checked~.filter-nav .chip[for=tag-0],.filter .filter-tag#tag-1:checked~.filter-nav .chip[for=tag-1],.filter .filter-tag#tag-2:checked~.filter-nav .chip[for=tag-2],.filter .filter-tag#tag-3:checked~.filter-nav .chip[for=tag-3],.filter .filter-tag#tag-4:checked~.filter-nav .chip[for=tag-4],.filter .filter-tag#tag-5:checked~.filter-nav .chip[for=tag-5],.filter .filter-tag#tag-6:checked~.filter-nav .chip[for=tag-6],.filter .filter-tag#tag-7:checked~.filter-nav .chip[for=tag-7],.filter .filter-tag#tag-8:checked~.filter-nav .chip[for=tag-8]{background:#5755d9;color:#fff}.filter .filter-tag#tag-1:checked~.filter-body .filter-item:not([data-tag~=tag-1]),.filter .filter-tag#tag-2:checked~.filter-body .filter-item:not([data-tag~=tag-2]),.filter .filter-tag#tag-3:checked~.filter-body .filter-item:not([data-tag~=tag-3]),.filter .filter-tag#tag-4:checked~.filter-body .filter-item:not([data-tag~=tag-4]),.filter .filter-tag#tag-5:checked~.filter-body .filter-item:not([data-tag~=tag-5]),.filter .filter-tag#tag-6:checked~.filter-body .filter-item:not([data-tag~=tag-6]),.filter .filter-tag#tag-7:checked~.filter-body .filter-item:not([data-tag~=tag-7]),.filter .filter-tag#tag-8:checked~.filter-body .filter-item:not([data-tag~=tag-8]){display:none}.filter .filter-nav{margin:.4rem 0}.filter .filter-body{display:flex;display:-ms-flexbox;-ms-flex-wrap:wrap;flex-wrap:wrap}.meter{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#f7f8f9;border:0;border-radius:.1rem;display:block;height:.8rem;width:100%}.meter::-webkit-meter-inner-element{display:block}.meter::-webkit-meter-bar,.meter::-webkit-meter-even-less-good-value,.meter::-webkit-meter-optimum-value,.meter::-webkit-meter-suboptimum-value{border-radius:.1rem}.meter::-webkit-meter-bar{background:#f7f8f9}.meter::-webkit-meter-optimum-value{background:#32b643}.meter::-webkit-meter-suboptimum-value{background:#ffb700}.meter::-webkit-meter-even-less-good-value{background:#e85600}.meter:-moz-meter-optimum,.meter:-moz-meter-sub-optimum,.meter:-moz-meter-sub-sub-optimum,.meter::-moz-meter-bar{border-radius:.1rem}.meter:-moz-meter-optimum::-moz-meter-bar{background:#32b643}.meter:-moz-meter-sub-optimum::-moz-meter-bar{background:#ffb700}.meter:-moz-meter-sub-sub-optimum::-moz-meter-bar{background:#e85600}.off-canvas{display:flex;display:-ms-flexbox;-ms-flex-flow:nowrap;flex-flow:nowrap;height:100%;position:relative;width:100%}.off-canvas .off-canvas-toggle{display:block;left:.4rem;position:absolute;top:.4rem;transition:none;z-index:1}.off-canvas .off-canvas-sidebar{background:#f7f8f9;bottom:0;left:0;min-width:10rem;overflow-y:auto;position:fixed;top:0;transform:translateX(-100%);transition:transform .25s;z-index:200}.off-canvas .off-canvas-content{-ms-flex:1 1 auto;flex:1 1 auto;height:100%;padding:.4rem .4rem .4rem 4rem}.off-canvas .off-canvas-overlay{background:rgba(48,55,66,.1);border-color:transparent;border-radius:0;bottom:0;display:none;height:100%;left:0;position:fixed;right:0;top:0;width:100%}.off-canvas .off-canvas-sidebar.active,.off-canvas .off-canvas-sidebar:target{transform:translateX(0)}.off-canvas .off-canvas-sidebar.active~.off-canvas-overlay,.off-canvas .off-canvas-sidebar:target~.off-canvas-overlay{display:block;z-index:100}@media (min-width:960px){.off-canvas.off-canvas-sidebar-show .off-canvas-toggle{display:none}.off-canvas.off-canvas-sidebar-show .off-canvas-sidebar{-ms-flex:0 0 auto;flex:0 0 auto;position:relative;transform:none}.off-canvas.off-canvas-sidebar-show .off-canvas-overlay{display:none!important}}.parallax{display:block;height:auto;position:relative;width:auto}.parallax .parallax-content{box-shadow:0 1rem 2.1rem rgba(48,55,66,.3);height:auto;transform:perspective(1000px);transform-style:preserve-3d;transition:all .4s ease;width:100%}.parallax .parallax-content::before{content:"";display:block;height:100%;left:0;position:absolute;top:0;width:100%}.parallax .parallax-front{align-items:center;color:#fff;display:flex;display:-ms-flexbox;-ms-flex-align:center;-ms-flex-pack:center;height:100%;justify-content:center;left:0;position:absolute;text-align:center;text-shadow:0 0 20px rgba(48,55,66,.75);top:0;transform:translateZ(50px) scale(.95);transition:transform .4s;width:100%;z-index:1}.parallax .parallax-top-left{height:50%;left:0;outline:0;position:absolute;top:0;width:50%;z-index:100}.parallax .parallax-top-left:focus~.parallax-content,.parallax .parallax-top-left:hover~.parallax-content{transform:perspective(1000px) rotateX(3deg) rotateY(-3deg)}.parallax .parallax-top-left:focus~.parallax-content::before,.parallax .parallax-top-left:hover~.parallax-content::before{background:linear-gradient(135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-left:focus~.parallax-content .parallax-front,.parallax .parallax-top-left:hover~.parallax-content .parallax-front{transform:translate3d(4.5px,4.5px,50px) scale(.95)}.parallax .parallax-top-right{height:50%;outline:0;position:absolute;right:0;top:0;width:50%;z-index:100}.parallax .parallax-top-right:focus~.parallax-content,.parallax .parallax-top-right:hover~.parallax-content{transform:perspective(1000px) rotateX(3deg) rotateY(3deg)}.parallax .parallax-top-right:focus~.parallax-content::before,.parallax .parallax-top-right:hover~.parallax-content::before{background:linear-gradient(-135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-right:focus~.parallax-content .parallax-front,.parallax .parallax-top-right:hover~.parallax-content .parallax-front{transform:translate3d(-4.5px,4.5px,50px) scale(.95)}.parallax .parallax-bottom-left{bottom:0;height:50%;left:0;outline:0;position:absolute;width:50%;z-index:100}.parallax .parallax-bottom-left:focus~.parallax-content,.parallax .parallax-bottom-left:hover~.parallax-content{transform:perspective(1000px) rotateX(-3deg) rotateY(-3deg)}.parallax .parallax-bottom-left:focus~.parallax-content::before,.parallax .parallax-bottom-left:hover~.parallax-content::before{background:linear-gradient(45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-left:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-left:hover~.parallax-content .parallax-front{transform:translate3d(4.5px,-4.5px,50px) scale(.95)}.parallax .parallax-bottom-right{bottom:0;height:50%;outline:0;position:absolute;right:0;width:50%;z-index:100}.parallax .parallax-bottom-right:focus~.parallax-content,.parallax .parallax-bottom-right:hover~.parallax-content{transform:perspective(1000px) rotateX(-3deg) rotateY(3deg)}.parallax .parallax-bottom-right:focus~.parallax-content::before,.parallax .parallax-bottom-right:hover~.parallax-content::before{background:linear-gradient(-45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-right:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-right:hover~.parallax-content .parallax-front{transform:translate3d(-4.5px,-4.5px,50px) scale(.95)}.progress{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#eef0f3;border:0;border-radius:.1rem;color:#5755d9;height:.2rem;position:relative;width:100%}.progress::-webkit-progress-bar{background:0 0;border-radius:.1rem}.progress::-webkit-progress-value{background:#5755d9;border-radius:.1rem}.progress::-moz-progress-bar{background:#5755d9;border-radius:.1rem}.progress:indeterminate{animation:progress-indeterminate 1.5s linear infinite;background:#eef0f3 linear-gradient(to right,#5755d9 30%,#eef0f3 30%) top left/150% 150% no-repeat}.progress:indeterminate::-moz-progress-bar{background:0 0}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}.slider{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:0 0;display:block;height:1.2rem;width:100%}.slider:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2);outline:0}.slider.tooltip:not([data-tooltip])::after{content:attr(value)}.slider::-webkit-slider-thumb{-webkit-appearance:none;background:#5755d9;border:0;border-radius:50%;height:.6rem;margin-top:-.25rem;transition:transform .2s;width:.6rem}.slider::-moz-range-thumb{background:#5755d9;border:0;border-radius:50%;height:.6rem;transition:transform .2s;width:.6rem}.slider::-ms-thumb{background:#5755d9;border:0;border-radius:50%;height:.6rem;transition:transform .2s;width:.6rem}.slider:active::-webkit-slider-thumb{transform:scale(1.25)}.slider:active::-moz-range-thumb{transform:scale(1.25)}.slider:active::-ms-thumb{transform:scale(1.25)}.slider.disabled::-webkit-slider-thumb,.slider:disabled::-webkit-slider-thumb{background:#f7f8f9;transform:scale(1)}.slider.disabled::-moz-range-thumb,.slider:disabled::-moz-range-thumb{background:#f7f8f9;transform:scale(1)}.slider.disabled::-ms-thumb,.slider:disabled::-ms-thumb{background:#f7f8f9;transform:scale(1)}.slider::-webkit-slider-runnable-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-moz-range-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-ms-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-ms-fill-lower{background:#5755d9}.timeline .timeline-item{display:flex;display:-ms-flexbox;margin-bottom:1.2rem;position:relative}.timeline .timeline-item::before{background:#dadee4;content:"";height:100%;left:11px;position:absolute;top:1.2rem;width:2px}.timeline .timeline-item .timeline-left{-ms-flex:0 0 auto;flex:0 0 auto}.timeline .timeline-item .timeline-content{-ms-flex:1 1 auto;flex:1 1 auto;padding:2px 0 2px .8rem}.timeline .timeline-item .timeline-icon{align-items:center;border-radius:50%;color:#fff;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-pack:center;height:1.2rem;justify-content:center;text-align:center;width:1.2rem}.timeline .timeline-item .timeline-icon::before{border:.1rem solid #5755d9;border-radius:50%;content:"";display:block;height:.4rem;left:.4rem;position:absolute;top:.4rem;width:.4rem}.timeline .timeline-item .timeline-icon.icon-lg{background:#5755d9;line-height:1.2rem}.timeline .timeline-item .timeline-icon.icon-lg::before{content:none}.viewer-360{align-items:center;display:flex;display:-ms-flexbox;-ms-flex-align:center;-ms-flex-direction:column;flex-direction:column}.viewer-360 .viewer-slider[max="36"][value="1"]+.viewer-image{background-position-y:0}.viewer-360 .viewer-slider[max="36"][value="2"]+.viewer-image{background-position-y:2.8571428571%}.viewer-360 .viewer-slider[max="36"][value="3"]+.viewer-image{background-position-y:5.7142857143%}.viewer-360 .viewer-slider[max="36"][value="4"]+.viewer-image{background-position-y:8.5714285714%}.viewer-360 .viewer-slider[max="36"][value="5"]+.viewer-image{background-position-y:11.4285714286%}.viewer-360 .viewer-slider[max="36"][value="6"]+.viewer-image{background-position-y:14.2857142857%}.viewer-360 .viewer-slider[max="36"][value="7"]+.viewer-image{background-position-y:17.1428571429%}.viewer-360 .viewer-slider[max="36"][value="8"]+.viewer-image{background-position-y:20%}.viewer-360 .viewer-slider[max="36"][value="9"]+.viewer-image{background-position-y:22.8571428571%}.viewer-360 .viewer-slider[max="36"][value="10"]+.viewer-image{background-position-y:25.7142857143%}.viewer-360 .viewer-slider[max="36"][value="11"]+.viewer-image{background-position-y:28.5714285714%}.viewer-360 .viewer-slider[max="36"][value="12"]+.viewer-image{background-position-y:31.4285714286%}.viewer-360 .viewer-slider[max="36"][value="13"]+.viewer-image{background-position-y:34.2857142857%}.viewer-360 .viewer-slider[max="36"][value="14"]+.viewer-image{background-position-y:37.1428571429%}.viewer-360 .viewer-slider[max="36"][value="15"]+.viewer-image{background-position-y:40%}.viewer-360 .viewer-slider[max="36"][value="16"]+.viewer-image{background-position-y:42.8571428571%}.viewer-360 .viewer-slider[max="36"][value="17"]+.viewer-image{background-position-y:45.7142857143%}.viewer-360 .viewer-slider[max="36"][value="18"]+.viewer-image{background-position-y:48.5714285714%}.viewer-360 .viewer-slider[max="36"][value="19"]+.viewer-image{background-position-y:51.4285714286%}.viewer-360 .viewer-slider[max="36"][value="20"]+.viewer-image{background-position-y:54.2857142857%}.viewer-360 .viewer-slider[max="36"][value="21"]+.viewer-image{background-position-y:57.1428571429%}.viewer-360 .viewer-slider[max="36"][value="22"]+.viewer-image{background-position-y:60%}.viewer-360 .viewer-slider[max="36"][value="23"]+.viewer-image{background-position-y:62.8571428571%}.viewer-360 .viewer-slider[max="36"][value="24"]+.viewer-image{background-position-y:65.7142857143%}.viewer-360 .viewer-slider[max="36"][value="25"]+.viewer-image{background-position-y:68.5714285714%}.viewer-360 .viewer-slider[max="36"][value="26"]+.viewer-image{background-position-y:71.4285714286%}.viewer-360 .viewer-slider[max="36"][value="27"]+.viewer-image{background-position-y:74.2857142857%}.viewer-360 .viewer-slider[max="36"][value="28"]+.viewer-image{background-position-y:77.1428571429%}.viewer-360 .viewer-slider[max="36"][value="29"]+.viewer-image{background-position-y:80%}.viewer-360 .viewer-slider[max="36"][value="30"]+.viewer-image{background-position-y:82.8571428571%}.viewer-360 .viewer-slider[max="36"][value="31"]+.viewer-image{background-position-y:85.7142857143%}.viewer-360 .viewer-slider[max="36"][value="32"]+.viewer-image{background-position-y:88.5714285714%}.viewer-360 .viewer-slider[max="36"][value="33"]+.viewer-image{background-position-y:91.4285714286%}.viewer-360 .viewer-slider[max="36"][value="34"]+.viewer-image{background-position-y:94.2857142857%}.viewer-360 .viewer-slider[max="36"][value="35"]+.viewer-image{background-position-y:97.1428571429%}.viewer-360 .viewer-slider[max="36"][value="36"]+.viewer-image{background-position-y:100%}.viewer-360 .viewer-slider{cursor:ew-resize;-ms-flex-order:2;margin:1rem;order:2;width:60%}.viewer-360 .viewer-image{background-position-y:0;background-repeat:no-repeat;background-size:100%;-ms-flex-order:1;max-width:100%;order:1} \ No newline at end of file +/*! Spectre.css Experimentals v0.5.9 | MIT License | github.com/picturepan2/spectre */.form-autocomplete{position:relative}.form-autocomplete .form-autocomplete-input{align-content:flex-start;display:-ms-flexbox;display:flex;-ms-flex-line-pack:start;-ms-flex-wrap:wrap;flex-wrap:wrap;height:auto;min-height:1.6rem;padding:.1rem}.form-autocomplete .form-autocomplete-input.is-focused{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-autocomplete .form-autocomplete-input .form-input{border-color:transparent;box-shadow:none;display:inline-block;-ms-flex:1 0 auto;flex:1 0 auto;height:1.2rem;line-height:.8rem;margin:.1rem;width:auto}.form-autocomplete .menu{left:0;position:absolute;top:100%;width:100%}.form-autocomplete.autocomplete-oneline .form-autocomplete-input{-ms-flex-wrap:nowrap;flex-wrap:nowrap;overflow-x:auto}.form-autocomplete.autocomplete-oneline .chip{-ms-flex:1 0 auto;flex:1 0 auto}.calendar{border:.05rem solid #dadee4;border-radius:.1rem;display:block;min-width:280px}.calendar .calendar-nav{align-items:center;background:#f7f8f9;border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:-ms-flexbox;display:flex;-ms-flex-align:center;font-size:.9rem;padding:.4rem}.calendar .calendar-body,.calendar .calendar-header{display:-ms-flexbox;display:flex;-ms-flex-pack:center;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:center;padding:.4rem 0}.calendar .calendar-body .calendar-date,.calendar .calendar-header .calendar-date{-ms-flex:0 0 14.28%;flex:0 0 14.28%;max-width:14.28%}.calendar .calendar-header{background:#f7f8f9;border-bottom:.05rem solid #dadee4;color:#bcc3ce;font-size:.7rem;text-align:center}.calendar .calendar-body{color:#66758c}.calendar .calendar-date{border:0;padding:.2rem}.calendar .calendar-date .date-item{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:0 0;border:.05rem solid transparent;border-radius:50%;color:#66758c;cursor:pointer;font-size:.7rem;height:1.4rem;line-height:1rem;outline:0;padding:.1rem;position:relative;text-align:center;text-decoration:none;transition:background .2s,border .2s,box-shadow .2s,color .2s;vertical-align:middle;white-space:nowrap;width:1.4rem}.calendar .calendar-date .date-item.date-today{border-color:#e5e5f9;color:#5755d9}.calendar .calendar-date .date-item:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.calendar .calendar-date .date-item:focus,.calendar .calendar-date .date-item:hover{background:#fefeff;border-color:#e5e5f9;color:#5755d9;text-decoration:none}.calendar .calendar-date .date-item.active,.calendar .calendar-date .date-item:active{background:#4b48d6;border-color:#3634d2;color:#fff}.calendar .calendar-date .date-item.badge::after{position:absolute;right:3px;top:3px;transform:translate(50%,-50%)}.calendar .calendar-date .calendar-event.disabled,.calendar .calendar-date .calendar-event:disabled,.calendar .calendar-date .date-item.disabled,.calendar .calendar-date .date-item:disabled{cursor:default;opacity:.25;pointer-events:none}.calendar .calendar-date.next-month .calendar-event,.calendar .calendar-date.next-month .date-item,.calendar .calendar-date.prev-month .calendar-event,.calendar .calendar-date.prev-month .date-item{opacity:.25}.calendar .calendar-range{position:relative}.calendar .calendar-range::before{background:#f1f1fc;content:"";height:1.4rem;left:0;position:absolute;right:0;top:50%;transform:translateY(-50%)}.calendar .calendar-range.range-start::before{left:50%}.calendar .calendar-range.range-end::before{right:50%}.calendar .calendar-range.range-end .date-item,.calendar .calendar-range.range-start .date-item{background:#4b48d6;border-color:#3634d2;color:#fff}.calendar .calendar-range .date-item{color:#5755d9}.calendar.calendar-lg .calendar-body{padding:0}.calendar.calendar-lg .calendar-body .calendar-date{border-bottom:.05rem solid #dadee4;border-right:.05rem solid #dadee4;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:5.5rem;padding:0}.calendar.calendar-lg .calendar-body .calendar-date:nth-child(7n){border-right:0}.calendar.calendar-lg .calendar-body .calendar-date:nth-last-child(-n+7){border-bottom:0}.calendar.calendar-lg .date-item{align-self:flex-end;-ms-flex-item-align:end;height:1.4rem;margin-right:.2rem;margin-top:.2rem}.calendar.calendar-lg .calendar-range::before{top:19px}.calendar.calendar-lg .calendar-range.range-start::before{left:auto;width:19px}.calendar.calendar-lg .calendar-range.range-end::before{right:19px}.calendar.calendar-lg .calendar-events{flex-grow:1;-ms-flex-positive:1;line-height:1;overflow-y:auto;padding:.2rem}.calendar.calendar-lg .calendar-event{border-radius:.1rem;display:block;font-size:.7rem;margin:.1rem auto;overflow:hidden;padding:3px 4px;text-overflow:ellipsis;white-space:nowrap}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-container .carousel-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-container .carousel-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-container .carousel-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-container .carousel-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-container .carousel-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-container .carousel-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-container .carousel-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-container .carousel-item:nth-of-type(8){animation:carousel-slidein .75s ease-in-out 1;opacity:1;z-index:100}.carousel .carousel-locator:nth-of-type(1):checked~.carousel-nav .nav-item:nth-of-type(1),.carousel .carousel-locator:nth-of-type(2):checked~.carousel-nav .nav-item:nth-of-type(2),.carousel .carousel-locator:nth-of-type(3):checked~.carousel-nav .nav-item:nth-of-type(3),.carousel .carousel-locator:nth-of-type(4):checked~.carousel-nav .nav-item:nth-of-type(4),.carousel .carousel-locator:nth-of-type(5):checked~.carousel-nav .nav-item:nth-of-type(5),.carousel .carousel-locator:nth-of-type(6):checked~.carousel-nav .nav-item:nth-of-type(6),.carousel .carousel-locator:nth-of-type(7):checked~.carousel-nav .nav-item:nth-of-type(7),.carousel .carousel-locator:nth-of-type(8):checked~.carousel-nav .nav-item:nth-of-type(8){color:#f7f8f9}.carousel{background:#f7f8f9;display:block;overflow:hidden;-webkit-overflow-scrolling:touch;position:relative;width:100%;z-index:1}.carousel .carousel-container{height:100%;left:0;position:relative}.carousel .carousel-container::before{content:"";display:block;padding-bottom:56.25%}.carousel .carousel-container .carousel-item{animation:carousel-slideout 1s ease-in-out 1;height:100%;left:0;margin:0;opacity:0;position:absolute;top:0;width:100%}.carousel .carousel-container .carousel-item:hover .item-next,.carousel .carousel-container .carousel-item:hover .item-prev{opacity:1}.carousel .carousel-container .item-next,.carousel .carousel-container .item-prev{background:rgba(247,248,249,.25);border-color:rgba(247,248,249,.5);color:#f7f8f9;opacity:0;position:absolute;top:50%;transform:translateY(-50%);transition:all .4s;z-index:100}.carousel .carousel-container .item-prev{left:1rem}.carousel .carousel-container .item-next{right:1rem}.carousel .carousel-nav{bottom:.4rem;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;left:50%;position:absolute;transform:translateX(-50%);width:10rem;z-index:100}.carousel .carousel-nav .nav-item{color:rgba(247,248,249,.5);display:block;-ms-flex:1 0 auto;flex:1 0 auto;height:1.6rem;margin:.2rem;max-width:2.5rem;position:relative}.carousel .carousel-nav .nav-item::before{background:currentColor;content:"";display:block;height:.1rem;position:absolute;top:.5rem;width:100%}@keyframes carousel-slidein{0%{transform:translateX(100%)}100%{transform:translateX(0)}}@keyframes carousel-slideout{0%{opacity:1;transform:translateX(0)}100%{opacity:1;transform:translateX(-50%)}}.comparison-slider{height:50vh;overflow:hidden;-webkit-overflow-scrolling:touch;position:relative;width:100%}.comparison-slider .comparison-after,.comparison-slider .comparison-before{height:100%;left:0;margin:0;overflow:hidden;position:absolute;top:0}.comparison-slider .comparison-after img,.comparison-slider .comparison-before img{height:100%;object-fit:cover;object-position:left center;position:absolute;width:100%}.comparison-slider .comparison-before{width:100%;z-index:1}.comparison-slider .comparison-before .comparison-label{right:.8rem}.comparison-slider .comparison-after{max-width:100%;min-width:0;z-index:2}.comparison-slider .comparison-after::before{background:0 0;content:"";cursor:default;height:100%;left:0;position:absolute;right:.8rem;top:0;z-index:1}.comparison-slider .comparison-after::after{background:currentColor;border-radius:50%;box-shadow:0 -5px,0 5px;color:#fff;content:"";height:3px;pointer-events:none;position:absolute;right:.4rem;top:50%;transform:translate(50%,-50%);width:3px}.comparison-slider .comparison-after .comparison-label{left:.8rem}.comparison-slider .comparison-resizer{animation:first-run 1.5s 1 ease-in-out;cursor:ew-resize;height:.8rem;left:0;max-width:100%;min-width:.8rem;opacity:0;outline:0;position:relative;resize:horizontal;top:50%;transform:translateY(-50%) scaleY(30);width:0}.comparison-slider .comparison-label{background:rgba(48,55,66,.5);bottom:.8rem;color:#fff;padding:.2rem .4rem;position:absolute;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@keyframes first-run{0%{width:0}25%{width:2.4rem}50%{width:.8rem}75%{width:1.2rem}100%{width:0}}.filter .filter-tag#tag-0:checked~.filter-nav .chip[for=tag-0],.filter .filter-tag#tag-1:checked~.filter-nav .chip[for=tag-1],.filter .filter-tag#tag-2:checked~.filter-nav .chip[for=tag-2],.filter .filter-tag#tag-3:checked~.filter-nav .chip[for=tag-3],.filter .filter-tag#tag-4:checked~.filter-nav .chip[for=tag-4],.filter .filter-tag#tag-5:checked~.filter-nav .chip[for=tag-5],.filter .filter-tag#tag-6:checked~.filter-nav .chip[for=tag-6],.filter .filter-tag#tag-7:checked~.filter-nav .chip[for=tag-7],.filter .filter-tag#tag-8:checked~.filter-nav .chip[for=tag-8]{background:#5755d9;color:#fff}.filter .filter-tag#tag-1:checked~.filter-body .filter-item:not([data-tag~=tag-1]),.filter .filter-tag#tag-2:checked~.filter-body .filter-item:not([data-tag~=tag-2]),.filter .filter-tag#tag-3:checked~.filter-body .filter-item:not([data-tag~=tag-3]),.filter .filter-tag#tag-4:checked~.filter-body .filter-item:not([data-tag~=tag-4]),.filter .filter-tag#tag-5:checked~.filter-body .filter-item:not([data-tag~=tag-5]),.filter .filter-tag#tag-6:checked~.filter-body .filter-item:not([data-tag~=tag-6]),.filter .filter-tag#tag-7:checked~.filter-body .filter-item:not([data-tag~=tag-7]),.filter .filter-tag#tag-8:checked~.filter-body .filter-item:not([data-tag~=tag-8]){display:none}.filter .filter-nav{margin:.4rem 0}.filter .filter-body{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.meter{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#f7f8f9;border:0;border-radius:.1rem;display:block;height:.8rem;width:100%}.meter::-webkit-meter-inner-element{display:block}.meter::-webkit-meter-bar,.meter::-webkit-meter-even-less-good-value,.meter::-webkit-meter-optimum-value,.meter::-webkit-meter-suboptimum-value{border-radius:.1rem}.meter::-webkit-meter-bar{background:#f7f8f9}.meter::-webkit-meter-optimum-value{background:#32b643}.meter::-webkit-meter-suboptimum-value{background:#ffb700}.meter::-webkit-meter-even-less-good-value{background:#e85600}.meter:-moz-meter-optimum,.meter:-moz-meter-sub-optimum,.meter:-moz-meter-sub-sub-optimum,.meter::-moz-meter-bar{border-radius:.1rem}.meter:-moz-meter-optimum::-moz-meter-bar{background:#32b643}.meter:-moz-meter-sub-optimum::-moz-meter-bar{background:#ffb700}.meter:-moz-meter-sub-sub-optimum::-moz-meter-bar{background:#e85600}.off-canvas{display:-ms-flexbox;display:flex;-ms-flex-flow:nowrap;flex-flow:nowrap;height:100%;position:relative;width:100%}.off-canvas .off-canvas-toggle{display:block;left:.4rem;position:absolute;top:.4rem;transition:none;z-index:1}.off-canvas .off-canvas-sidebar{background:#f7f8f9;bottom:0;left:0;min-width:10rem;overflow-y:auto;position:fixed;top:0;transform:translateX(-100%);transition:transform .25s;z-index:200}.off-canvas .off-canvas-content{-ms-flex:1 1 auto;flex:1 1 auto;height:100%;padding:.4rem .4rem .4rem 4rem}.off-canvas .off-canvas-overlay{background:rgba(48,55,66,.1);border-color:transparent;border-radius:0;bottom:0;display:none;height:100%;left:0;position:fixed;right:0;top:0;width:100%}.off-canvas .off-canvas-sidebar.active,.off-canvas .off-canvas-sidebar:target{transform:translateX(0)}.off-canvas .off-canvas-sidebar.active~.off-canvas-overlay,.off-canvas .off-canvas-sidebar:target~.off-canvas-overlay{display:block;z-index:100}@media (min-width:960px){.off-canvas.off-canvas-sidebar-show .off-canvas-toggle{display:none}.off-canvas.off-canvas-sidebar-show .off-canvas-sidebar{-ms-flex:0 0 auto;flex:0 0 auto;position:relative;transform:none}.off-canvas.off-canvas-sidebar-show .off-canvas-overlay{display:none!important}}.parallax{display:block;height:auto;position:relative;width:auto}.parallax .parallax-content{box-shadow:0 1rem 2.1rem rgba(48,55,66,.3);height:auto;transform:perspective(1000px);transform-style:preserve-3d;transition:all .4s ease;width:100%}.parallax .parallax-content::before{content:"";display:block;height:100%;left:0;position:absolute;top:0;width:100%}.parallax .parallax-front{align-items:center;color:#fff;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-pack:center;height:100%;justify-content:center;left:0;position:absolute;text-align:center;text-shadow:0 0 20px rgba(48,55,66,.75);top:0;transform:translateZ(50px) scale(.95);transition:transform .4s;width:100%;z-index:1}.parallax .parallax-top-left{height:50%;left:0;outline:0;position:absolute;top:0;width:50%;z-index:100}.parallax .parallax-top-left:focus~.parallax-content,.parallax .parallax-top-left:hover~.parallax-content{transform:perspective(1000px) rotateX(3deg) rotateY(-3deg)}.parallax .parallax-top-left:focus~.parallax-content::before,.parallax .parallax-top-left:hover~.parallax-content::before{background:linear-gradient(135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-left:focus~.parallax-content .parallax-front,.parallax .parallax-top-left:hover~.parallax-content .parallax-front{transform:translate3d(4.5px,4.5px,50px) scale(.95)}.parallax .parallax-top-right{height:50%;outline:0;position:absolute;right:0;top:0;width:50%;z-index:100}.parallax .parallax-top-right:focus~.parallax-content,.parallax .parallax-top-right:hover~.parallax-content{transform:perspective(1000px) rotateX(3deg) rotateY(3deg)}.parallax .parallax-top-right:focus~.parallax-content::before,.parallax .parallax-top-right:hover~.parallax-content::before{background:linear-gradient(-135deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-top-right:focus~.parallax-content .parallax-front,.parallax .parallax-top-right:hover~.parallax-content .parallax-front{transform:translate3d(-4.5px,4.5px,50px) scale(.95)}.parallax .parallax-bottom-left{bottom:0;height:50%;left:0;outline:0;position:absolute;width:50%;z-index:100}.parallax .parallax-bottom-left:focus~.parallax-content,.parallax .parallax-bottom-left:hover~.parallax-content{transform:perspective(1000px) rotateX(-3deg) rotateY(-3deg)}.parallax .parallax-bottom-left:focus~.parallax-content::before,.parallax .parallax-bottom-left:hover~.parallax-content::before{background:linear-gradient(45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-left:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-left:hover~.parallax-content .parallax-front{transform:translate3d(4.5px,-4.5px,50px) scale(.95)}.parallax .parallax-bottom-right{bottom:0;height:50%;outline:0;position:absolute;right:0;width:50%;z-index:100}.parallax .parallax-bottom-right:focus~.parallax-content,.parallax .parallax-bottom-right:hover~.parallax-content{transform:perspective(1000px) rotateX(-3deg) rotateY(3deg)}.parallax .parallax-bottom-right:focus~.parallax-content::before,.parallax .parallax-bottom-right:hover~.parallax-content::before{background:linear-gradient(-45deg,rgba(255,255,255,.35) 0,transparent 50%)}.parallax .parallax-bottom-right:focus~.parallax-content .parallax-front,.parallax .parallax-bottom-right:hover~.parallax-content .parallax-front{transform:translate3d(-4.5px,-4.5px,50px) scale(.95)}.progress{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#eef0f3;border:0;border-radius:.1rem;color:#5755d9;height:.2rem;position:relative;width:100%}.progress::-webkit-progress-bar{background:0 0;border-radius:.1rem}.progress::-webkit-progress-value{background:#5755d9;border-radius:.1rem}.progress::-moz-progress-bar{background:#5755d9;border-radius:.1rem}.progress:indeterminate{animation:progress-indeterminate 1.5s linear infinite;background:#eef0f3 linear-gradient(to right,#5755d9 30%,#eef0f3 30%) top left/150% 150% no-repeat}.progress:indeterminate::-moz-progress-bar{background:0 0}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}.slider{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:0 0;display:block;height:1.2rem;width:100%}.slider:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2);outline:0}.slider.tooltip:not([data-tooltip])::after{content:attr(value)}.slider::-webkit-slider-thumb{-webkit-appearance:none;background:#5755d9;border:0;border-radius:50%;height:.6rem;margin-top:-.25rem;-webkit-transition:transform .2s;transition:transform .2s;width:.6rem}.slider::-moz-range-thumb{background:#5755d9;border:0;border-radius:50%;height:.6rem;-moz-transition:transform .2s;transition:transform .2s;width:.6rem}.slider::-ms-thumb{background:#5755d9;border:0;border-radius:50%;height:.6rem;-ms-transition:transform .2s;transition:transform .2s;width:.6rem}.slider:active::-webkit-slider-thumb{transform:scale(1.25)}.slider:active::-moz-range-thumb{transform:scale(1.25)}.slider:active::-ms-thumb{transform:scale(1.25)}.slider.disabled::-webkit-slider-thumb,.slider:disabled::-webkit-slider-thumb{background:#f7f8f9;transform:scale(1)}.slider.disabled::-moz-range-thumb,.slider:disabled::-moz-range-thumb{background:#f7f8f9;transform:scale(1)}.slider.disabled::-ms-thumb,.slider:disabled::-ms-thumb{background:#f7f8f9;transform:scale(1)}.slider::-webkit-slider-runnable-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-moz-range-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-ms-track{background:#eef0f3;border-radius:.1rem;height:.1rem;width:100%}.slider::-ms-fill-lower{background:#5755d9}.timeline .timeline-item{display:-ms-flexbox;display:flex;margin-bottom:1.2rem;position:relative}.timeline .timeline-item::before{background:#dadee4;content:"";height:100%;left:11px;position:absolute;top:1.2rem;width:2px}.timeline .timeline-item .timeline-left{-ms-flex:0 0 auto;flex:0 0 auto}.timeline .timeline-item .timeline-content{-ms-flex:1 1 auto;flex:1 1 auto;padding:2px 0 2px .8rem}.timeline .timeline-item .timeline-icon{align-items:center;border-radius:50%;color:#fff;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-pack:center;height:1.2rem;justify-content:center;text-align:center;width:1.2rem}.timeline .timeline-item .timeline-icon::before{border:.1rem solid #5755d9;border-radius:50%;content:"";display:block;height:.4rem;left:.4rem;position:absolute;top:.4rem;width:.4rem}.timeline .timeline-item .timeline-icon.icon-lg{background:#5755d9;line-height:1.2rem}.timeline .timeline-item .timeline-icon.icon-lg::before{content:none}.viewer-360{align-items:center;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-direction:column;flex-direction:column}.viewer-360 .viewer-slider[max="36"][value="1"]+.viewer-image{background-position-y:0}.viewer-360 .viewer-slider[max="36"][value="2"]+.viewer-image{background-position-y:2.8571428571%}.viewer-360 .viewer-slider[max="36"][value="3"]+.viewer-image{background-position-y:5.7142857143%}.viewer-360 .viewer-slider[max="36"][value="4"]+.viewer-image{background-position-y:8.5714285714%}.viewer-360 .viewer-slider[max="36"][value="5"]+.viewer-image{background-position-y:11.4285714286%}.viewer-360 .viewer-slider[max="36"][value="6"]+.viewer-image{background-position-y:14.2857142857%}.viewer-360 .viewer-slider[max="36"][value="7"]+.viewer-image{background-position-y:17.1428571429%}.viewer-360 .viewer-slider[max="36"][value="8"]+.viewer-image{background-position-y:20%}.viewer-360 .viewer-slider[max="36"][value="9"]+.viewer-image{background-position-y:22.8571428571%}.viewer-360 .viewer-slider[max="36"][value="10"]+.viewer-image{background-position-y:25.7142857143%}.viewer-360 .viewer-slider[max="36"][value="11"]+.viewer-image{background-position-y:28.5714285714%}.viewer-360 .viewer-slider[max="36"][value="12"]+.viewer-image{background-position-y:31.4285714286%}.viewer-360 .viewer-slider[max="36"][value="13"]+.viewer-image{background-position-y:34.2857142857%}.viewer-360 .viewer-slider[max="36"][value="14"]+.viewer-image{background-position-y:37.1428571429%}.viewer-360 .viewer-slider[max="36"][value="15"]+.viewer-image{background-position-y:40%}.viewer-360 .viewer-slider[max="36"][value="16"]+.viewer-image{background-position-y:42.8571428571%}.viewer-360 .viewer-slider[max="36"][value="17"]+.viewer-image{background-position-y:45.7142857143%}.viewer-360 .viewer-slider[max="36"][value="18"]+.viewer-image{background-position-y:48.5714285714%}.viewer-360 .viewer-slider[max="36"][value="19"]+.viewer-image{background-position-y:51.4285714286%}.viewer-360 .viewer-slider[max="36"][value="20"]+.viewer-image{background-position-y:54.2857142857%}.viewer-360 .viewer-slider[max="36"][value="21"]+.viewer-image{background-position-y:57.1428571429%}.viewer-360 .viewer-slider[max="36"][value="22"]+.viewer-image{background-position-y:60%}.viewer-360 .viewer-slider[max="36"][value="23"]+.viewer-image{background-position-y:62.8571428571%}.viewer-360 .viewer-slider[max="36"][value="24"]+.viewer-image{background-position-y:65.7142857143%}.viewer-360 .viewer-slider[max="36"][value="25"]+.viewer-image{background-position-y:68.5714285714%}.viewer-360 .viewer-slider[max="36"][value="26"]+.viewer-image{background-position-y:71.4285714286%}.viewer-360 .viewer-slider[max="36"][value="27"]+.viewer-image{background-position-y:74.2857142857%}.viewer-360 .viewer-slider[max="36"][value="28"]+.viewer-image{background-position-y:77.1428571429%}.viewer-360 .viewer-slider[max="36"][value="29"]+.viewer-image{background-position-y:80%}.viewer-360 .viewer-slider[max="36"][value="30"]+.viewer-image{background-position-y:82.8571428571%}.viewer-360 .viewer-slider[max="36"][value="31"]+.viewer-image{background-position-y:85.7142857143%}.viewer-360 .viewer-slider[max="36"][value="32"]+.viewer-image{background-position-y:88.5714285714%}.viewer-360 .viewer-slider[max="36"][value="33"]+.viewer-image{background-position-y:91.4285714286%}.viewer-360 .viewer-slider[max="36"][value="34"]+.viewer-image{background-position-y:94.2857142857%}.viewer-360 .viewer-slider[max="36"][value="35"]+.viewer-image{background-position-y:97.1428571429%}.viewer-360 .viewer-slider[max="36"][value="36"]+.viewer-image{background-position-y:100%}.viewer-360 .viewer-slider{cursor:ew-resize;-ms-flex-order:2;margin:1rem;order:2;width:60%}.viewer-360 .viewer-image{background-position-y:0;background-repeat:no-repeat;background-size:100%;-ms-flex-order:1;max-width:100%;order:1} \ No newline at end of file diff --git a/css/spectre-icons.min.css b/css/spectre-icons.min.css index 9b6167caa..0276f7b84 100644 --- a/css/spectre-icons.min.css +++ b/css/spectre-icons.min.css @@ -1 +1 @@ -/*! Spectre.css Icons v0.5.8 | MIT License | github.com/picturepan2/spectre */.icon{box-sizing:border-box;display:inline-block;font-size:inherit;font-style:normal;height:1em;position:relative;text-indent:-9999px;vertical-align:middle;width:1em}.icon::after,.icon::before{content:"";display:block;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%)}.icon.icon-2x{font-size:1.6rem}.icon.icon-3x{font-size:2.4rem}.icon.icon-4x{font-size:3.2rem}.accordion .icon,.btn .icon,.menu .icon,.toast .icon{vertical-align:-10%}.btn-lg .icon{vertical-align:-15%}.icon-arrow-down::before,.icon-arrow-left::before,.icon-arrow-right::before,.icon-arrow-up::before,.icon-back::before,.icon-downward::before,.icon-forward::before,.icon-upward::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.65em;width:.65em}.icon-arrow-down::before{transform:translate(-50%,-75%) rotate(225deg)}.icon-arrow-left::before{transform:translate(-25%,-50%) rotate(-45deg)}.icon-arrow-right::before{transform:translate(-75%,-50%) rotate(135deg)}.icon-arrow-up::before{transform:translate(-50%,-25%) rotate(45deg)}.icon-back::after,.icon-forward::after{background:currentColor;height:.1rem;width:.8em}.icon-downward::after,.icon-upward::after{background:currentColor;height:.8em;width:.1rem}.icon-back::after{left:55%}.icon-back::before{transform:translate(-50%,-50%) rotate(-45deg)}.icon-downward::after{top:45%}.icon-downward::before{transform:translate(-50%,-50%) rotate(-135deg)}.icon-forward::after{left:45%}.icon-forward::before{transform:translate(-50%,-50%) rotate(135deg)}.icon-upward::after{top:55%}.icon-upward::before{transform:translate(-50%,-50%) rotate(45deg)}.icon-caret::before{border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.3em solid currentColor;height:0;transform:translate(-50%,-25%);width:0}.icon-menu::before{background:currentColor;box-shadow:0 -.35em,0 .35em;height:.1rem;width:100%}.icon-apps::before{background:currentColor;box-shadow:-.35em -.35em,-.35em 0,-.35em .35em,0 -.35em,0 .35em,.35em -.35em,.35em 0,.35em .35em;height:3px;width:3px}.icon-resize-horiz::after,.icon-resize-horiz::before,.icon-resize-vert::after,.icon-resize-vert::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.45em;width:.45em}.icon-resize-horiz::before,.icon-resize-vert::before{transform:translate(-50%,-90%) rotate(45deg)}.icon-resize-horiz::after,.icon-resize-vert::after{transform:translate(-50%,-10%) rotate(225deg)}.icon-resize-horiz::before{transform:translate(-90%,-50%) rotate(-45deg)}.icon-resize-horiz::after{transform:translate(-10%,-50%) rotate(135deg)}.icon-more-horiz::before,.icon-more-vert::before{background:currentColor;border-radius:50%;box-shadow:-.4em 0,.4em 0;height:3px;width:3px}.icon-more-vert::before{box-shadow:0 -.4em,0 .4em}.icon-cross::before,.icon-minus::before,.icon-plus::before{background:currentColor;height:.1rem;width:100%}.icon-cross::after,.icon-plus::after{background:currentColor;height:100%;width:.1rem}.icon-cross::before{width:100%}.icon-cross::after{height:100%}.icon-cross::after,.icon-cross::before{transform:translate(-50%,-50%) rotate(45deg)}.icon-check::before{border:.1rem solid currentColor;border-right:0;border-top:0;height:.5em;transform:translate(-50%,-75%) rotate(-45deg);width:.9em}.icon-stop{border:.1rem solid currentColor;border-radius:50%}.icon-stop::before{background:currentColor;height:.1rem;transform:translate(-50%,-50%) rotate(45deg);width:1em}.icon-shutdown{border:.1rem solid currentColor;border-radius:50%;border-top-color:transparent}.icon-shutdown::before{background:currentColor;content:"";height:.5em;top:.1em;width:.1rem}.icon-refresh::before{border:.1rem solid currentColor;border-radius:50%;border-right-color:transparent;height:1em;width:1em}.icon-refresh::after{border:.2em solid currentColor;border-left-color:transparent;border-top-color:transparent;height:0;left:80%;top:20%;width:0}.icon-search::before{border:.1rem solid currentColor;border-radius:50%;height:.75em;left:5%;top:5%;transform:translate(0,0) rotate(45deg);width:.75em}.icon-search::after{background:currentColor;height:.1rem;left:80%;top:80%;transform:translate(-50%,-50%) rotate(45deg);width:.4em}.icon-edit::before{border:.1rem solid currentColor;height:.4em;transform:translate(-40%,-60%) rotate(-45deg);width:.85em}.icon-edit::after{border:.15em solid currentColor;border-right-color:transparent;border-top-color:transparent;height:0;left:5%;top:95%;transform:translate(0,-100%);width:0}.icon-delete::before{border:.1rem solid currentColor;border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top:0;height:.75em;top:60%;width:.75em}.icon-delete::after{background:currentColor;box-shadow:-.25em .2em,.25em .2em;height:.1rem;top:.05rem;width:.5em}.icon-share{border:.1rem solid currentColor;border-radius:.1rem;border-right:0;border-top:0}.icon-share::before{border:.1rem solid currentColor;border-left:0;border-top:0;height:.4em;left:100%;top:.25em;transform:translate(-125%,-50%) rotate(-45deg);width:.4em}.icon-share::after{border:.1rem solid currentColor;border-bottom:0;border-radius:75% 0;border-right:0;height:.5em;width:.6em}.icon-flag::before{background:currentColor;height:1em;left:15%;width:.1rem}.icon-flag::after{border:.1rem solid currentColor;border-bottom-right-radius:.1rem;border-left:0;border-top-right-radius:.1rem;height:.65em;left:60%;top:35%;width:.8em}.icon-bookmark::before{border:.1rem solid currentColor;border-bottom:0;border-top-left-radius:.1rem;border-top-right-radius:.1rem;height:.9em;width:.8em}.icon-bookmark::after{border:.1rem solid currentColor;border-bottom:0;border-left:0;border-radius:.1rem;height:.5em;transform:translate(-50%,35%) rotate(-45deg) skew(15deg,15deg);width:.5em}.icon-download,.icon-upload{border-bottom:.1rem solid currentColor}.icon-download::before,.icon-upload::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.5em;transform:translate(-50%,-60%) rotate(-135deg);width:.5em}.icon-download::after,.icon-upload::after{background:currentColor;height:.6em;top:40%;width:.1rem}.icon-upload::before{transform:translate(-50%,-60%) rotate(45deg)}.icon-upload::after{top:50%}.icon-copy::before{border:.1rem solid currentColor;border-bottom:0;border-radius:.1rem;border-right:0;height:.8em;left:40%;top:35%;width:.8em}.icon-copy::after{border:.1rem solid currentColor;border-radius:.1rem;height:.8em;left:60%;top:60%;width:.8em}.icon-time{border:.1rem solid currentColor;border-radius:50%}.icon-time::before{background:currentColor;height:.4em;transform:translate(-50%,-75%);width:.1rem}.icon-time::after{background:currentColor;height:.3em;transform:translate(-50%,-75%) rotate(90deg);transform-origin:50% 90%;width:.1rem}.icon-mail::before{border:.1rem solid currentColor;border-radius:.1rem;height:.8em;width:1em}.icon-mail::after{border:.1rem solid currentColor;border-right:0;border-top:0;height:.5em;transform:translate(-50%,-90%) rotate(-45deg) skew(10deg,10deg);width:.5em}.icon-people::before{border:.1rem solid currentColor;border-radius:50%;height:.45em;top:25%;width:.45em}.icon-people::after{border:.1rem solid currentColor;border-radius:50% 50% 0 0;height:.4em;top:75%;width:.9em}.icon-message{border:.1rem solid currentColor;border-bottom:0;border-radius:.1rem;border-right:0}.icon-message::before{border:.1rem solid currentColor;border-bottom-right-radius:.1rem;border-left:0;border-top:0;height:.8em;left:65%;top:40%;width:.7em}.icon-message::after{background:currentColor;border-radius:.1rem;height:.3em;left:10%;top:100%;transform:translate(0,-90%) rotate(45deg);width:.1rem}.icon-photo{border:.1rem solid currentColor;border-radius:.1rem}.icon-photo::before{border:.1rem solid currentColor;border-radius:50%;height:.25em;left:35%;top:35%;width:.25em}.icon-photo::after{border:.1rem solid currentColor;border-bottom:0;border-left:0;height:.5em;left:60%;transform:translate(-50%,25%) rotate(-45deg);width:.5em}.icon-link::after,.icon-link::before{border:.1rem solid currentColor;border-radius:5em 0 0 5em;border-right:0;height:.5em;width:.75em}.icon-link::before{transform:translate(-70%,-45%) rotate(-45deg)}.icon-link::after{transform:translate(-30%,-55%) rotate(135deg)}.icon-location::before{border:.1rem solid currentColor;border-radius:50% 50% 50% 0;height:.8em;transform:translate(-50%,-60%) rotate(-45deg);width:.8em}.icon-location::after{border:.1rem solid currentColor;border-radius:50%;height:.2em;transform:translate(-50%,-80%);width:.2em}.icon-emoji{border:.1rem solid currentColor;border-radius:50%}.icon-emoji::before{border-radius:50%;box-shadow:-.17em -.1em,.17em -.1em;height:.15em;width:.15em}.icon-emoji::after{border:.1rem solid currentColor;border-bottom-color:transparent;border-radius:50%;border-right-color:transparent;height:.5em;transform:translate(-50%,-40%) rotate(-135deg);width:.5em} \ No newline at end of file +/*! Spectre.css Icons v0.5.9 | MIT License | github.com/picturepan2/spectre */.icon{box-sizing:border-box;display:inline-block;font-size:inherit;font-style:normal;height:1em;position:relative;text-indent:-9999px;vertical-align:middle;width:1em}.icon::after,.icon::before{content:"";display:block;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%)}.icon.icon-2x{font-size:1.6rem}.icon.icon-3x{font-size:2.4rem}.icon.icon-4x{font-size:3.2rem}.accordion .icon,.btn .icon,.menu .icon,.toast .icon{vertical-align:-10%}.btn-lg .icon{vertical-align:-15%}.icon-arrow-down::before,.icon-arrow-left::before,.icon-arrow-right::before,.icon-arrow-up::before,.icon-back::before,.icon-downward::before,.icon-forward::before,.icon-upward::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.65em;width:.65em}.icon-arrow-down::before{transform:translate(-50%,-75%) rotate(225deg)}.icon-arrow-left::before{transform:translate(-25%,-50%) rotate(-45deg)}.icon-arrow-right::before{transform:translate(-75%,-50%) rotate(135deg)}.icon-arrow-up::before{transform:translate(-50%,-25%) rotate(45deg)}.icon-back::after,.icon-forward::after{background:currentColor;height:.1rem;width:.8em}.icon-downward::after,.icon-upward::after{background:currentColor;height:.8em;width:.1rem}.icon-back::after{left:55%}.icon-back::before{transform:translate(-50%,-50%) rotate(-45deg)}.icon-downward::after{top:45%}.icon-downward::before{transform:translate(-50%,-50%) rotate(-135deg)}.icon-forward::after{left:45%}.icon-forward::before{transform:translate(-50%,-50%) rotate(135deg)}.icon-upward::after{top:55%}.icon-upward::before{transform:translate(-50%,-50%) rotate(45deg)}.icon-caret::before{border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.3em solid currentColor;height:0;transform:translate(-50%,-25%);width:0}.icon-menu::before{background:currentColor;box-shadow:0 -.35em,0 .35em;height:.1rem;width:100%}.icon-apps::before{background:currentColor;box-shadow:-.35em -.35em,-.35em 0,-.35em .35em,0 -.35em,0 .35em,.35em -.35em,.35em 0,.35em .35em;height:3px;width:3px}.icon-resize-horiz::after,.icon-resize-horiz::before,.icon-resize-vert::after,.icon-resize-vert::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.45em;width:.45em}.icon-resize-horiz::before,.icon-resize-vert::before{transform:translate(-50%,-90%) rotate(45deg)}.icon-resize-horiz::after,.icon-resize-vert::after{transform:translate(-50%,-10%) rotate(225deg)}.icon-resize-horiz::before{transform:translate(-90%,-50%) rotate(-45deg)}.icon-resize-horiz::after{transform:translate(-10%,-50%) rotate(135deg)}.icon-more-horiz::before,.icon-more-vert::before{background:currentColor;border-radius:50%;box-shadow:-.4em 0,.4em 0;height:3px;width:3px}.icon-more-vert::before{box-shadow:0 -.4em,0 .4em}.icon-cross::before,.icon-minus::before,.icon-plus::before{background:currentColor;height:.1rem;width:100%}.icon-cross::after,.icon-plus::after{background:currentColor;height:100%;width:.1rem}.icon-cross::before{width:100%}.icon-cross::after{height:100%}.icon-cross::after,.icon-cross::before{transform:translate(-50%,-50%) rotate(45deg)}.icon-check::before{border:.1rem solid currentColor;border-right:0;border-top:0;height:.5em;transform:translate(-50%,-75%) rotate(-45deg);width:.9em}.icon-stop{border:.1rem solid currentColor;border-radius:50%}.icon-stop::before{background:currentColor;height:.1rem;transform:translate(-50%,-50%) rotate(45deg);width:1em}.icon-shutdown{border:.1rem solid currentColor;border-radius:50%;border-top-color:transparent}.icon-shutdown::before{background:currentColor;content:"";height:.5em;top:.1em;width:.1rem}.icon-refresh::before{border:.1rem solid currentColor;border-radius:50%;border-right-color:transparent;height:1em;width:1em}.icon-refresh::after{border:.2em solid currentColor;border-left-color:transparent;border-top-color:transparent;height:0;left:80%;top:20%;width:0}.icon-search::before{border:.1rem solid currentColor;border-radius:50%;height:.75em;left:5%;top:5%;transform:translate(0,0) rotate(45deg);width:.75em}.icon-search::after{background:currentColor;height:.1rem;left:80%;top:80%;transform:translate(-50%,-50%) rotate(45deg);width:.4em}.icon-edit::before{border:.1rem solid currentColor;height:.4em;transform:translate(-40%,-60%) rotate(-45deg);width:.85em}.icon-edit::after{border:.15em solid currentColor;border-right-color:transparent;border-top-color:transparent;height:0;left:5%;top:95%;transform:translate(0,-100%);width:0}.icon-delete::before{border:.1rem solid currentColor;border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top:0;height:.75em;top:60%;width:.75em}.icon-delete::after{background:currentColor;box-shadow:-.25em .2em,.25em .2em;height:.1rem;top:.05rem;width:.5em}.icon-share{border:.1rem solid currentColor;border-radius:.1rem;border-right:0;border-top:0}.icon-share::before{border:.1rem solid currentColor;border-left:0;border-top:0;height:.4em;left:100%;top:.25em;transform:translate(-125%,-50%) rotate(-45deg);width:.4em}.icon-share::after{border:.1rem solid currentColor;border-bottom:0;border-radius:75% 0;border-right:0;height:.5em;width:.6em}.icon-flag::before{background:currentColor;height:1em;left:15%;width:.1rem}.icon-flag::after{border:.1rem solid currentColor;border-bottom-right-radius:.1rem;border-left:0;border-top-right-radius:.1rem;height:.65em;left:60%;top:35%;width:.8em}.icon-bookmark::before{border:.1rem solid currentColor;border-bottom:0;border-top-left-radius:.1rem;border-top-right-radius:.1rem;height:.9em;width:.8em}.icon-bookmark::after{border:.1rem solid currentColor;border-bottom:0;border-left:0;border-radius:.1rem;height:.5em;transform:translate(-50%,35%) rotate(-45deg) skew(15deg,15deg);width:.5em}.icon-download,.icon-upload{border-bottom:.1rem solid currentColor}.icon-download::before,.icon-upload::before{border:.1rem solid currentColor;border-bottom:0;border-right:0;height:.5em;transform:translate(-50%,-60%) rotate(-135deg);width:.5em}.icon-download::after,.icon-upload::after{background:currentColor;height:.6em;top:40%;width:.1rem}.icon-upload::before{transform:translate(-50%,-60%) rotate(45deg)}.icon-upload::after{top:50%}.icon-copy::before{border:.1rem solid currentColor;border-bottom:0;border-radius:.1rem;border-right:0;height:.8em;left:40%;top:35%;width:.8em}.icon-copy::after{border:.1rem solid currentColor;border-radius:.1rem;height:.8em;left:60%;top:60%;width:.8em}.icon-time{border:.1rem solid currentColor;border-radius:50%}.icon-time::before{background:currentColor;height:.4em;transform:translate(-50%,-75%);width:.1rem}.icon-time::after{background:currentColor;height:.3em;transform:translate(-50%,-75%) rotate(90deg);transform-origin:50% 90%;width:.1rem}.icon-mail::before{border:.1rem solid currentColor;border-radius:.1rem;height:.8em;width:1em}.icon-mail::after{border:.1rem solid currentColor;border-right:0;border-top:0;height:.5em;transform:translate(-50%,-90%) rotate(-45deg) skew(10deg,10deg);width:.5em}.icon-people::before{border:.1rem solid currentColor;border-radius:50%;height:.45em;top:25%;width:.45em}.icon-people::after{border:.1rem solid currentColor;border-radius:50% 50% 0 0;height:.4em;top:75%;width:.9em}.icon-message{border:.1rem solid currentColor;border-bottom:0;border-radius:.1rem;border-right:0}.icon-message::before{border:.1rem solid currentColor;border-bottom-right-radius:.1rem;border-left:0;border-top:0;height:.8em;left:65%;top:40%;width:.7em}.icon-message::after{background:currentColor;border-radius:.1rem;height:.3em;left:10%;top:100%;transform:translate(0,-90%) rotate(45deg);width:.1rem}.icon-photo{border:.1rem solid currentColor;border-radius:.1rem}.icon-photo::before{border:.1rem solid currentColor;border-radius:50%;height:.25em;left:35%;top:35%;width:.25em}.icon-photo::after{border:.1rem solid currentColor;border-bottom:0;border-left:0;height:.5em;left:60%;transform:translate(-50%,25%) rotate(-45deg);width:.5em}.icon-link::after,.icon-link::before{border:.1rem solid currentColor;border-radius:5em 0 0 5em;border-right:0;height:.5em;width:.75em}.icon-link::before{transform:translate(-70%,-45%) rotate(-45deg)}.icon-link::after{transform:translate(-30%,-55%) rotate(135deg)}.icon-location::before{border:.1rem solid currentColor;border-radius:50% 50% 50% 0;height:.8em;transform:translate(-50%,-60%) rotate(-45deg);width:.8em}.icon-location::after{border:.1rem solid currentColor;border-radius:50%;height:.2em;transform:translate(-50%,-80%);width:.2em}.icon-emoji{border:.1rem solid currentColor;border-radius:50%}.icon-emoji::before{border-radius:50%;box-shadow:-.17em -.1em,.17em -.1em;height:.15em;width:.15em}.icon-emoji::after{border:.1rem solid currentColor;border-bottom-color:transparent;border-radius:50%;border-right-color:transparent;height:.5em;transform:translate(-50%,-40%) rotate(-135deg);width:.5em} \ No newline at end of file diff --git a/css/spectre.min.css b/css/spectre.min.css index 8df0bf64f..0fe23d9c0 100644 --- a/css/spectre.min.css +++ b/css/spectre.min.css @@ -1 +1 @@ -/*! Spectre.css v0.5.8 | MIT License | github.com/picturepan2/spectre */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}hr{box-sizing:content-box;height:0;overflow:visible}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}address{font-style:normal}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:"SF Mono","Segoe UI Mono","Roboto Mono",Menlo,Courier,monospace;font-size:1em}dfn{font-style:italic}small{font-size:80%;font-weight:400}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}fieldset{border:0;margin:0;padding:0}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item;outline:0}canvas{display:inline-block}template{display:none}[hidden]{display:none}*,::after,::before{box-sizing:inherit}html{box-sizing:border-box;font-size:20px;line-height:1.5;-webkit-tap-highlight-color:transparent}body{background:#fff;color:#3b4351;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;font-size:.8rem;overflow-x:hidden;text-rendering:optimizeLegibility}a{color:#5755d9;outline:0;text-decoration:none}a:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}a.active,a:active,a:focus,a:hover{color:#302ecd;text-decoration:underline}a:visited{color:#807fe2}h1,h2,h3,h4,h5,h6{color:inherit;font-weight:500;line-height:1.2;margin-bottom:.5em;margin-top:0}.h1,.h2,.h3,.h4,.h5,.h6{font-weight:500}.h1,h1{font-size:2rem}.h2,h2{font-size:1.6rem}.h3,h3{font-size:1.4rem}.h4,h4{font-size:1.2rem}.h5,h5{font-size:1rem}.h6,h6{font-size:.8rem}p{margin:0 0 1.2rem}a,ins,u{-webkit-text-decoration-skip:ink edges;text-decoration-skip:ink edges}abbr[title]{border-bottom:.05rem dotted;cursor:help;text-decoration:none}kbd{background:#303742;border-radius:.1rem;color:#fff;font-size:.7rem;line-height:1.25;padding:.1rem .2rem}mark{background:#ffe9b3;border-bottom:.05rem solid #ffd367;border-radius:.1rem;color:#3b4351;padding:.05rem .1rem 0}blockquote{border-left:.1rem solid #dadee4;margin-left:0;padding:.4rem .8rem}blockquote p:last-child{margin-bottom:0}ol,ul{margin:.8rem 0 .8rem .8rem;padding:0}ol ol,ol ul,ul ol,ul ul{margin:.8rem 0 .8rem .8rem}ol li,ul li{margin-top:.4rem}ul{list-style:disc inside}ul ul{list-style-type:circle}ol{list-style:decimal inside}ol ol{list-style-type:lower-alpha}dl dt{font-weight:700}dl dd{margin:.4rem 0 .8rem 0}.lang-zh,.lang-zh-hans,html:lang(zh),html:lang(zh-Hans){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",sans-serif}.lang-zh-hant,html:lang(zh-Hant){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang TC","Hiragino Sans CNS","Microsoft JhengHei","Helvetica Neue",sans-serif}.lang-ja,html:lang(ja){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Hiragino Sans","Hiragino Kaku Gothic Pro","Yu Gothic",YuGothic,Meiryo,"Helvetica Neue",sans-serif}.lang-ko,html:lang(ko){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Malgun Gothic","Helvetica Neue",sans-serif}.lang-cjk ins,.lang-cjk u,:lang(ja) ins,:lang(ja) u,:lang(zh) ins,:lang(zh) u{border-bottom:.05rem solid;text-decoration:none}.lang-cjk del+del,.lang-cjk del+s,.lang-cjk ins+ins,.lang-cjk ins+u,.lang-cjk s+del,.lang-cjk s+s,.lang-cjk u+ins,.lang-cjk u+u,:lang(ja) del+del,:lang(ja) del+s,:lang(ja) ins+ins,:lang(ja) ins+u,:lang(ja) s+del,:lang(ja) s+s,:lang(ja) u+ins,:lang(ja) u+u,:lang(zh) del+del,:lang(zh) del+s,:lang(zh) ins+ins,:lang(zh) ins+u,:lang(zh) s+del,:lang(zh) s+s,:lang(zh) u+ins,:lang(zh) u+u{margin-left:.125em}.table{border-collapse:collapse;border-spacing:0;text-align:left;width:100%}.table.table-striped tbody tr:nth-of-type(odd){background:#f7f8f9}.table tbody tr.active,.table.table-striped tbody tr.active{background:#eef0f3}.table.table-hover tbody tr:hover{background:#eef0f3}.table.table-scroll{display:block;overflow-x:auto;padding-bottom:.75rem;white-space:nowrap}.table td,.table th{border-bottom:.05rem solid #dadee4;padding:.6rem .4rem}.table th{border-bottom-width:.1rem}.btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:.05rem solid #5755d9;border-radius:.1rem;color:#5755d9;cursor:pointer;display:inline-block;font-size:.8rem;height:1.8rem;line-height:1.2rem;outline:0;padding:.25rem .4rem;text-align:center;text-decoration:none;transition:background .2s,border .2s,box-shadow .2s,color .2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.btn:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.btn:focus,.btn:hover{background:#f1f1fc;border-color:#4b48d6;text-decoration:none}.btn.active,.btn:active{background:#4b48d6;border-color:#3634d2;color:#fff;text-decoration:none}.btn.active.loading::after,.btn:active.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.disabled,.btn:disabled,.btn[disabled]{cursor:default;opacity:.5;pointer-events:none}.btn.btn-primary{background:#5755d9;border-color:#4b48d6;color:#fff}.btn.btn-primary:focus,.btn.btn-primary:hover{background:#4240d4;border-color:#3634d2;color:#fff}.btn.btn-primary.active,.btn.btn-primary:active{background:#3a38d2;border-color:#302ecd;color:#fff}.btn.btn-primary.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-success{background:#32b643;border-color:#2faa3f;color:#fff}.btn.btn-success:focus{box-shadow:0 0 0 .1rem rgba(50,182,67,.2)}.btn.btn-success:focus,.btn.btn-success:hover{background:#30ae40;border-color:#2da23c;color:#fff}.btn.btn-success.active,.btn.btn-success:active{background:#2a9a39;border-color:#278e34;color:#fff}.btn.btn-success.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-error{background:#e85600;border-color:#d95000;color:#fff}.btn.btn-error:focus{box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.btn.btn-error:focus,.btn.btn-error:hover{background:#de5200;border-color:#cf4d00;color:#fff}.btn.btn-error.active,.btn.btn-error:active{background:#c44900;border-color:#b54300;color:#fff}.btn.btn-error.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-link{background:0 0;border-color:transparent;color:#5755d9}.btn.btn-link.active,.btn.btn-link:active,.btn.btn-link:focus,.btn.btn-link:hover{color:#302ecd}.btn.btn-sm{font-size:.7rem;height:1.4rem;padding:.05rem .3rem}.btn.btn-lg{font-size:.9rem;height:2rem;padding:.35rem .6rem}.btn.btn-block{display:block;width:100%}.btn.btn-action{padding-left:0;padding-right:0;width:1.8rem}.btn.btn-action.btn-sm{width:1.4rem}.btn.btn-action.btn-lg{width:2rem}.btn.btn-clear{background:0 0;border:0;color:currentColor;height:1rem;line-height:.8rem;margin-left:.2rem;margin-right:-2px;opacity:1;padding:.1rem;text-decoration:none;width:1rem}.btn.btn-clear:focus,.btn.btn-clear:hover{background:rgba(247,248,249,.5);opacity:.95}.btn.btn-clear::before{content:"\2715"}.btn-group{display:inline-flex;display:-ms-inline-flexbox;-ms-flex-wrap:wrap;flex-wrap:wrap}.btn-group .btn{-ms-flex:1 0 auto;flex:1 0 auto}.btn-group .btn:first-child:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group .btn:not(:first-child):not(:last-child){border-radius:0;margin-left:-.05rem}.btn-group .btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-.05rem}.btn-group .btn.active,.btn-group .btn:active,.btn-group .btn:focus,.btn-group .btn:hover{z-index:1}.btn-group.btn-group-block{display:flex;display:-ms-flexbox}.btn-group.btn-group-block .btn{-ms-flex:1 0 0;flex:1 0 0}.form-group:not(:last-child){margin-bottom:.4rem}fieldset{margin-bottom:.8rem}legend{font-size:.9rem;font-weight:500;margin-bottom:.8rem}.form-label{display:block;line-height:1.2rem;padding:.3rem 0}.form-label.label-sm{font-size:.7rem;padding:.1rem 0}.form-label.label-lg{font-size:.9rem;padding:.4rem 0}.form-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-image:none;border:.05rem solid #bcc3ce;border-radius:.1rem;color:#3b4351;display:block;font-size:.8rem;height:1.8rem;line-height:1.2rem;max-width:100%;outline:0;padding:.25rem .4rem;position:relative;transition:background .2s,border .2s,box-shadow .2s,color .2s;width:100%}.form-input:focus{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-input:-ms-input-placeholder{color:#bcc3ce}.form-input::-ms-input-placeholder{color:#bcc3ce}.form-input::placeholder{color:#bcc3ce}.form-input.input-sm{font-size:.7rem;height:1.4rem;padding:.05rem .3rem}.form-input.input-lg{font-size:.9rem;height:2rem;padding:.35rem .6rem}.form-input.input-inline{display:inline-block;vertical-align:middle;width:auto}.form-input[type=file]{height:auto}textarea.form-input,textarea.form-input.input-lg,textarea.form-input.input-sm{height:auto}.form-input-hint{color:#bcc3ce;font-size:.7rem;margin-top:.2rem}.has-success .form-input-hint,.is-success+.form-input-hint{color:#32b643}.has-error .form-input-hint,.is-error+.form-input-hint{color:#e85600}.form-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:.05rem solid #bcc3ce;border-radius:.1rem;color:inherit;font-size:.8rem;height:1.8rem;line-height:1.2rem;outline:0;padding:.25rem .4rem;vertical-align:middle;width:100%}.form-select:focus{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-select::-ms-expand{display:none}.form-select.select-sm{font-size:.7rem;height:1.4rem;padding:.05rem 1.1rem .05rem .3rem}.form-select.select-lg{font-size:.9rem;height:2rem;padding:.35rem 1.4rem .35rem .6rem}.form-select[multiple],.form-select[size]{height:auto;padding:.25rem .4rem}.form-select[multiple] option,.form-select[size] option{padding:.1rem .2rem}.form-select:not([multiple]):not([size]){background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center/.4rem .5rem;padding-right:1.2rem}.has-icon-left,.has-icon-right{position:relative}.has-icon-left .form-icon,.has-icon-right .form-icon{height:.8rem;margin:0 .25rem;position:absolute;top:50%;transform:translateY(-50%);width:.8rem;z-index:2}.has-icon-left .form-icon{left:.05rem}.has-icon-left .form-input{padding-left:1.3rem}.has-icon-right .form-icon{right:.05rem}.has-icon-right .form-input{padding-right:1.3rem}.form-checkbox,.form-radio,.form-switch{display:block;line-height:1.2rem;margin:.2rem 0;min-height:1.4rem;padding:.1rem .4rem .1rem 1.2rem;position:relative}.form-checkbox input,.form-radio input,.form-switch input{clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;position:absolute;width:1px}.form-checkbox input:focus+.form-icon,.form-radio input:focus+.form-icon,.form-switch input:focus+.form-icon{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-checkbox input:checked+.form-icon,.form-radio input:checked+.form-icon,.form-switch input:checked+.form-icon{background:#5755d9;border-color:#5755d9}.form-checkbox .form-icon,.form-radio .form-icon,.form-switch .form-icon{border:.05rem solid #bcc3ce;cursor:pointer;display:inline-block;position:absolute;transition:background .2s,border .2s,box-shadow .2s,color .2s}.form-checkbox.input-sm,.form-radio.input-sm,.form-switch.input-sm{font-size:.7rem;margin:0}.form-checkbox.input-lg,.form-radio.input-lg,.form-switch.input-lg{font-size:.9rem;margin:.3rem 0}.form-checkbox .form-icon,.form-radio .form-icon{background:#fff;height:.8rem;left:0;top:.3rem;width:.8rem}.form-checkbox input:active+.form-icon,.form-radio input:active+.form-icon{background:#eef0f3}.form-checkbox .form-icon{border-radius:.1rem}.form-checkbox input:checked+.form-icon::before{background-clip:padding-box;border:.1rem solid #fff;border-left-width:0;border-top-width:0;content:"";height:9px;left:50%;margin-left:-3px;margin-top:-6px;position:absolute;top:50%;transform:rotate(45deg);width:6px}.form-checkbox input:indeterminate+.form-icon{background:#5755d9;border-color:#5755d9}.form-checkbox input:indeterminate+.form-icon::before{background:#fff;content:"";height:2px;left:50%;margin-left:-5px;margin-top:-1px;position:absolute;top:50%;width:10px}.form-radio .form-icon{border-radius:50%}.form-radio input:checked+.form-icon::before{background:#fff;border-radius:50%;content:"";height:6px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:6px}.form-switch{padding-left:2rem}.form-switch .form-icon{background:#bcc3ce;background-clip:padding-box;border-radius:.45rem;height:.9rem;left:0;top:.25rem;width:1.6rem}.form-switch .form-icon::before{background:#fff;border-radius:50%;content:"";display:block;height:.8rem;left:0;position:absolute;top:0;transition:background .2s,border .2s,box-shadow .2s,color .2s,left .2s;width:.8rem}.form-switch input:checked+.form-icon::before{left:14px}.form-switch input:active+.form-icon::before{background:#f7f8f9}.input-group{display:flex;display:-ms-flexbox}.input-group .input-group-addon{background:#f7f8f9;border:.05rem solid #bcc3ce;border-radius:.1rem;line-height:1.2rem;padding:.25rem .4rem;white-space:nowrap}.input-group .input-group-addon.addon-sm{font-size:.7rem;padding:.05rem .3rem}.input-group .input-group-addon.addon-lg{font-size:.9rem;padding:.35rem .6rem}.input-group .form-input,.input-group .form-select{-ms-flex:1 1 auto;flex:1 1 auto;width:1%}.input-group .input-group-btn{z-index:1}.input-group .form-input:first-child:not(:last-child),.input-group .form-select:first-child:not(:last-child),.input-group .input-group-addon:first-child:not(:last-child),.input-group .input-group-btn:first-child:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.input-group .form-input:not(:first-child):not(:last-child),.input-group .form-select:not(:first-child):not(:last-child),.input-group .input-group-addon:not(:first-child):not(:last-child),.input-group .input-group-btn:not(:first-child):not(:last-child){border-radius:0;margin-left:-.05rem}.input-group .form-input:last-child:not(:first-child),.input-group .form-select:last-child:not(:first-child),.input-group .input-group-addon:last-child:not(:first-child),.input-group .input-group-btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-.05rem}.input-group .form-input:focus,.input-group .form-select:focus,.input-group .input-group-addon:focus,.input-group .input-group-btn:focus{z-index:2}.input-group .form-select{width:auto}.input-group.input-inline{display:inline-flex;display:-ms-inline-flexbox}.form-input.is-success,.form-select.is-success,.has-success .form-input,.has-success .form-select{background:#f9fdfa;border-color:#32b643}.form-input.is-success:focus,.form-select.is-success:focus,.has-success .form-input:focus,.has-success .form-select:focus{box-shadow:0 0 0 .1rem rgba(50,182,67,.2)}.form-input.is-error,.form-select.is-error,.has-error .form-input,.has-error .form-select{background:#fffaf7;border-color:#e85600}.form-input.is-error:focus,.form-select.is-error:focus,.has-error .form-input:focus,.has-error .form-select:focus{box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-checkbox.is-error .form-icon,.form-radio.is-error .form-icon,.form-switch.is-error .form-icon,.has-error .form-checkbox .form-icon,.has-error .form-radio .form-icon,.has-error .form-switch .form-icon{border-color:#e85600}.form-checkbox.is-error input:checked+.form-icon,.form-radio.is-error input:checked+.form-icon,.form-switch.is-error input:checked+.form-icon,.has-error .form-checkbox input:checked+.form-icon,.has-error .form-radio input:checked+.form-icon,.has-error .form-switch input:checked+.form-icon{background:#e85600;border-color:#e85600}.form-checkbox.is-error input:focus+.form-icon,.form-radio.is-error input:focus+.form-icon,.form-switch.is-error input:focus+.form-icon,.has-error .form-checkbox input:focus+.form-icon,.has-error .form-radio input:focus+.form-icon,.has-error .form-switch input:focus+.form-icon{border-color:#e85600;box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-checkbox.is-error input:indeterminate+.form-icon,.has-error .form-checkbox input:indeterminate+.form-icon{background:#e85600;border-color:#e85600}.form-input:not(:placeholder-shown):invalid{border-color:#e85600}.form-input:not(:placeholder-shown):invalid:focus{background:#fffaf7;box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-input:not(:placeholder-shown):invalid+.form-input-hint{color:#e85600}.form-input.disabled,.form-input:disabled,.form-select.disabled,.form-select:disabled{background-color:#eef0f3;cursor:not-allowed;opacity:.5}.form-input[readonly]{background-color:#f7f8f9}input.disabled+.form-icon,input:disabled+.form-icon{background:#eef0f3;cursor:not-allowed;opacity:.5}.form-switch input.disabled+.form-icon::before,.form-switch input:disabled+.form-icon::before{background:#fff}.form-horizontal{padding:.4rem 0}.form-horizontal .form-group{display:flex;display:-ms-flexbox;-ms-flex-wrap:wrap;flex-wrap:wrap}.form-inline{display:inline-block}.label{background:#eef0f3;border-radius:.1rem;color:#455060;display:inline-block;line-height:1.25;padding:.1rem .2rem}.label.label-rounded{border-radius:5rem;padding-left:.4rem;padding-right:.4rem}.label.label-primary{background:#5755d9;color:#fff}.label.label-secondary{background:#f1f1fc;color:#5755d9}.label.label-success{background:#32b643;color:#fff}.label.label-warning{background:#ffb700;color:#fff}.label.label-error{background:#e85600;color:#fff}code{background:#fcf2f2;border-radius:.1rem;color:#d73e48;font-size:85%;line-height:1.25;padding:.1rem .2rem}.code{border-radius:.1rem;color:#3b4351;position:relative}.code::before{color:#bcc3ce;content:attr(data-lang);font-size:.7rem;position:absolute;right:.4rem;top:.1rem}.code code{background:#f7f8f9;color:inherit;display:block;line-height:1.5;overflow-x:auto;padding:1rem;width:100%}.img-responsive{display:block;height:auto;max-width:100%}.img-fit-cover{object-fit:cover}.img-fit-contain{object-fit:contain}.video-responsive{display:block;overflow:hidden;padding:0;position:relative;width:100%}.video-responsive::before{content:"";display:block;padding-bottom:56.25%}.video-responsive embed,.video-responsive iframe,.video-responsive object{border:0;bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%}video.video-responsive{height:auto;max-width:100%}video.video-responsive::before{content:none}.video-responsive-4-3::before{padding-bottom:75%}.video-responsive-1-1::before{padding-bottom:100%}.figure{margin:0 0 .4rem 0}.figure .figure-caption{color:#66758c;margin-top:.4rem}.container{margin-left:auto;margin-right:auto;padding-left:.4rem;padding-right:.4rem;width:100%}.container.grid-xl{max-width:1296px}.container.grid-lg{max-width:976px}.container.grid-md{max-width:856px}.container.grid-sm{max-width:616px}.container.grid-xs{max-width:496px}.show-lg,.show-md,.show-sm,.show-xl,.show-xs{display:none!important}.columns{display:flex;display:-ms-flexbox;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-left:-.4rem;margin-right:-.4rem}.columns.col-gapless{margin-left:0;margin-right:0}.columns.col-gapless>.column{padding-left:0;padding-right:0}.columns.col-oneline{-ms-flex-wrap:nowrap;flex-wrap:nowrap;overflow-x:auto}.column{-ms-flex:1;flex:1;max-width:100%;padding-left:.4rem;padding-right:.4rem}.column.col-1,.column.col-10,.column.col-11,.column.col-12,.column.col-2,.column.col-3,.column.col-4,.column.col-5,.column.col-6,.column.col-7,.column.col-8,.column.col-9,.column.col-auto{-ms-flex:none;flex:none}.col-12{width:100%}.col-11{width:91.66666667%}.col-10{width:83.33333333%}.col-9{width:75%}.col-8{width:66.66666667%}.col-7{width:58.33333333%}.col-6{width:50%}.col-5{width:41.66666667%}.col-4{width:33.33333333%}.col-3{width:25%}.col-2{width:16.66666667%}.col-1{width:8.33333333%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:none;width:auto}.col-mx-auto{margin-left:auto;margin-right:auto}.col-ml-auto{margin-left:auto}.col-mr-auto{margin-right:auto}@media (max-width:1280px){.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{-ms-flex:none;flex:none}.col-xl-12{width:100%}.col-xl-11{width:91.66666667%}.col-xl-10{width:83.33333333%}.col-xl-9{width:75%}.col-xl-8{width:66.66666667%}.col-xl-7{width:58.33333333%}.col-xl-6{width:50%}.col-xl-5{width:41.66666667%}.col-xl-4{width:33.33333333%}.col-xl-3{width:25%}.col-xl-2{width:16.66666667%}.col-xl-1{width:8.33333333%}.col-xl-auto{width:auto}.hide-xl{display:none!important}.show-xl{display:block!important}}@media (max-width:960px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto{-ms-flex:none;flex:none}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-auto{width:auto}.hide-lg{display:none!important}.show-lg{display:block!important}}@media (max-width:840px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto{-ms-flex:none;flex:none}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-auto{width:auto}.hide-md{display:none!important}.show-md{display:block!important}}@media (max-width:600px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto{-ms-flex:none;flex:none}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-auto{width:auto}.hide-sm{display:none!important}.show-sm{display:block!important}}@media (max-width:480px){.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-auto{-ms-flex:none;flex:none}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-auto{width:auto}.hide-xs{display:none!important}.show-xs{display:block!important}}.hero{display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:justify;justify-content:space-between;padding-bottom:4rem;padding-top:4rem}.hero.hero-sm{padding-bottom:2rem;padding-top:2rem}.hero.hero-lg{padding-bottom:8rem;padding-top:8rem}.hero .hero-body{padding:.4rem}.navbar{align-items:stretch;display:flex;display:-ms-flexbox;-ms-flex-align:stretch;-ms-flex-pack:justify;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:space-between}.navbar .navbar-section{align-items:center;display:flex;display:-ms-flexbox;-ms-flex:1 0 0;flex:1 0 0;-ms-flex-align:center}.navbar .navbar-section:not(:first-child):last-child{-ms-flex-pack:end;justify-content:flex-end}.navbar .navbar-center{align-items:center;display:flex;display:-ms-flexbox;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-align:center}.navbar .navbar-brand{font-size:.9rem;text-decoration:none}.accordion input:checked~.accordion-header .icon,.accordion[open] .accordion-header .icon{transform:rotate(90deg)}.accordion input:checked~.accordion-body,.accordion[open] .accordion-body{max-height:50rem}.accordion .accordion-header{display:block;padding:.2rem .4rem}.accordion .accordion-header .icon{transition:transform .25s}.accordion .accordion-body{margin-bottom:.4rem;max-height:0;overflow:hidden;transition:max-height .25s}summary.accordion-header::-webkit-details-marker{display:none}.avatar{background:#5755d9;border-radius:50%;color:rgba(255,255,255,.85);display:inline-block;font-size:.8rem;font-weight:300;height:1.6rem;line-height:1.25;margin:0;position:relative;vertical-align:middle;width:1.6rem}.avatar.avatar-xs{font-size:.4rem;height:.8rem;width:.8rem}.avatar.avatar-sm{font-size:.6rem;height:1.2rem;width:1.2rem}.avatar.avatar-lg{font-size:1.2rem;height:2.4rem;width:2.4rem}.avatar.avatar-xl{font-size:1.6rem;height:3.2rem;width:3.2rem}.avatar img{border-radius:50%;height:100%;position:relative;width:100%;z-index:1}.avatar .avatar-icon,.avatar .avatar-presence{background:#fff;bottom:14.64%;height:50%;padding:.1rem;position:absolute;right:14.64%;transform:translate(50%,50%);width:50%;z-index:2}.avatar .avatar-presence{background:#bcc3ce;border-radius:50%;box-shadow:0 0 0 .1rem #fff;height:.5em;width:.5em}.avatar .avatar-presence.online{background:#32b643}.avatar .avatar-presence.busy{background:#e85600}.avatar .avatar-presence.away{background:#ffb700}.avatar[data-initial]::before{color:currentColor;content:attr(data-initial);left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);z-index:1}.badge{position:relative;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge]::after{background:#5755d9;background-clip:padding-box;border-radius:.5rem;box-shadow:0 0 0 .1rem #fff;color:#fff;content:attr(data-badge);display:inline-block;transform:translate(-.05rem,-.5rem)}.badge[data-badge]::after{font-size:.7rem;height:.9rem;line-height:1;min-width:.9rem;padding:.1rem .2rem;text-align:center;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge=""]::after{height:6px;min-width:6px;padding:0;width:6px}.badge.btn::after{position:absolute;right:0;top:0;transform:translate(50%,-50%)}.badge.avatar::after{position:absolute;right:14.64%;top:14.64%;transform:translate(50%,-50%);z-index:100}.breadcrumb{list-style:none;margin:.2rem 0;padding:.2rem 0}.breadcrumb .breadcrumb-item{color:#66758c;display:inline-block;margin:0;padding:.2rem 0}.breadcrumb .breadcrumb-item:not(:last-child){margin-right:.2rem}.breadcrumb .breadcrumb-item:not(:last-child) a{color:#66758c}.breadcrumb .breadcrumb-item:not(:first-child)::before{color:#66758c;content:"/";padding-right:.4rem}.bar{background:#eef0f3;border-radius:.1rem;display:flex;display:-ms-flexbox;-ms-flex-wrap:nowrap;flex-wrap:nowrap;height:.8rem;width:100%}.bar.bar-sm{height:.2rem}.bar .bar-item{background:#5755d9;color:#fff;display:block;-ms-flex-negative:0;flex-shrink:0;font-size:.7rem;height:100%;line-height:.8rem;position:relative;text-align:center;width:0}.bar .bar-item:first-child{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.bar .bar-item:last-child{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem;-ms-flex-negative:1;flex-shrink:1}.bar-slider{height:.1rem;margin:.4rem 0;position:relative}.bar-slider .bar-item{left:0;padding:0;position:absolute}.bar-slider .bar-item:not(:last-child):first-child{background:#eef0f3;z-index:1}.bar-slider .bar-slider-btn{background:#5755d9;border:0;border-radius:50%;height:.6rem;padding:0;position:absolute;right:0;top:50%;transform:translate(50%,-50%);width:.6rem}.bar-slider .bar-slider-btn:active{box-shadow:0 0 0 .1rem #5755d9}.card{background:#fff;border:.05rem solid #dadee4;border-radius:.1rem;display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column}.card .card-body,.card .card-footer,.card .card-header{padding:.8rem;padding-bottom:0}.card .card-body:last-child,.card .card-footer:last-child,.card .card-header:last-child{padding-bottom:.8rem}.card .card-body{-ms-flex:1 1 auto;flex:1 1 auto}.card .card-image{padding-top:.8rem}.card .card-image:first-child{padding-top:0}.card .card-image:first-child img{border-top-left-radius:.1rem;border-top-right-radius:.1rem}.card .card-image:last-child img{border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem}.chip{align-items:center;background:#eef0f3;border-radius:5rem;display:inline-flex;display:-ms-inline-flexbox;-ms-flex-align:center;font-size:90%;height:1.2rem;line-height:.8rem;margin:.1rem;max-width:320px;overflow:hidden;padding:.2rem .4rem;text-decoration:none;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.chip.active{background:#5755d9;color:#fff}.chip .avatar{margin-left:-.4rem;margin-right:.2rem}.chip .btn-clear{border-radius:50%;transform:scale(.75)}.dropdown{display:inline-block;position:relative}.dropdown .menu{animation:slide-down .15s ease 1;display:none;left:0;max-height:50vh;overflow-y:auto;position:absolute;top:100%}.dropdown.dropdown-right .menu{left:auto;right:0}.dropdown .dropdown-toggle:focus+.menu,.dropdown .menu:hover,.dropdown.active .menu{display:block}.dropdown .btn-group .dropdown-toggle:nth-last-child(2){border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.empty{background:#f7f8f9;border-radius:.1rem;color:#66758c;padding:3.2rem 1.6rem;text-align:center}.empty .empty-icon{margin-bottom:.8rem}.empty .empty-subtitle,.empty .empty-title{margin:.4rem auto}.empty .empty-action{margin-top:.8rem}.menu{background:#fff;border-radius:.1rem;box-shadow:0 .05rem .2rem rgba(48,55,66,.3);list-style:none;margin:0;min-width:180px;padding:.4rem;transform:translateY(.2rem);z-index:300}.menu.menu-nav{background:0 0;box-shadow:none}.menu .menu-item{margin-top:0;padding:0 .4rem;position:relative;text-decoration:none}.menu .menu-item>a{border-radius:.1rem;color:inherit;display:block;margin:0 -.4rem;padding:.2rem .4rem;text-decoration:none}.menu .menu-item>a:focus,.menu .menu-item>a:hover{background:#f1f1fc;color:#5755d9}.menu .menu-item>a.active,.menu .menu-item>a:active{background:#f1f1fc;color:#5755d9}.menu .menu-item .form-checkbox,.menu .menu-item .form-radio,.menu .menu-item .form-switch{margin:.1rem 0}.menu .menu-item+.menu-item{margin-top:.2rem}.menu .menu-badge{align-items:center;display:flex;display:-ms-flexbox;-ms-flex-align:center;height:100%;position:absolute;right:0;top:0}.menu .menu-badge .label{margin-right:.4rem}.modal{align-items:center;bottom:0;display:none;-ms-flex-align:center;-ms-flex-pack:center;justify-content:center;left:0;opacity:0;overflow:hidden;padding:.4rem;position:fixed;right:0;top:0}.modal.active,.modal:target{display:flex;display:-ms-flexbox;opacity:1;z-index:400}.modal.active .modal-overlay,.modal:target .modal-overlay{background:rgba(247,248,249,.75);bottom:0;cursor:default;display:block;left:0;position:absolute;right:0;top:0}.modal.active .modal-container,.modal:target .modal-container{animation:slide-down .2s ease 1;z-index:1}.modal.modal-sm .modal-container{max-width:320px;padding:0 .4rem}.modal.modal-lg .modal-overlay{background:#fff}.modal.modal-lg .modal-container{box-shadow:none;max-width:960px}.modal-container{background:#fff;border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(48,55,66,.3);display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column;max-height:75vh;max-width:640px;padding:0 .8rem;width:100%}.modal-container.modal-fullheight{max-height:100vh}.modal-container .modal-header{color:#303742;padding:.8rem}.modal-container .modal-body{overflow-y:auto;padding:.8rem;position:relative}.modal-container .modal-footer{padding:.8rem;text-align:right}.nav{display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column;list-style:none;margin:.2rem 0}.nav .nav-item a{color:#66758c;padding:.2rem .4rem;text-decoration:none}.nav .nav-item a:focus,.nav .nav-item a:hover{color:#5755d9}.nav .nav-item.active>a{color:#505c6e;font-weight:700}.nav .nav-item.active>a:focus,.nav .nav-item.active>a:hover{color:#5755d9}.nav .nav{margin-bottom:.4rem;margin-left:.8rem}.pagination{display:flex;display:-ms-flexbox;list-style:none;margin:.2rem 0;padding:.2rem 0}.pagination .page-item{margin:.2rem .05rem}.pagination .page-item span{display:inline-block;padding:.2rem .2rem}.pagination .page-item a{border-radius:.1rem;display:inline-block;padding:.2rem .4rem;text-decoration:none}.pagination .page-item a:focus,.pagination .page-item a:hover{color:#5755d9}.pagination .page-item.disabled a{cursor:default;opacity:.5;pointer-events:none}.pagination .page-item.active a{background:#5755d9;color:#fff}.pagination .page-item.page-next,.pagination .page-item.page-prev{-ms-flex:1 0 50%;flex:1 0 50%}.pagination .page-item.page-next{text-align:right}.pagination .page-item .page-item-title{margin:0}.pagination .page-item .page-item-subtitle{margin:0;opacity:.5}.panel{border:.05rem solid #dadee4;border-radius:.1rem;display:flex;display:-ms-flexbox;-ms-flex-direction:column;flex-direction:column}.panel .panel-footer,.panel .panel-header{-ms-flex:0 0 auto;flex:0 0 auto;padding:.8rem}.panel .panel-nav{-ms-flex:0 0 auto;flex:0 0 auto}.panel .panel-body{-ms-flex:1 1 auto;flex:1 1 auto;overflow-y:auto;padding:0 .8rem}.popover{display:inline-block;position:relative}.popover .popover-container{left:50%;opacity:0;padding:.4rem;position:absolute;top:0;transform:translate(-50%,-50%) scale(0);transition:transform .2s;width:320px;z-index:300}.popover :focus+.popover-container,.popover:hover .popover-container{display:block;opacity:1;transform:translate(-50%,-100%) scale(1)}.popover.popover-right .popover-container{left:100%;top:50%}.popover.popover-right :focus+.popover-container,.popover.popover-right:hover .popover-container{transform:translate(0,-50%) scale(1)}.popover.popover-bottom .popover-container{left:50%;top:100%}.popover.popover-bottom :focus+.popover-container,.popover.popover-bottom:hover .popover-container{transform:translate(-50%,0) scale(1)}.popover.popover-left .popover-container{left:0;top:50%}.popover.popover-left :focus+.popover-container,.popover.popover-left:hover .popover-container{transform:translate(-100%,-50%) scale(1)}.popover .card{border:0;box-shadow:0 .2rem .5rem rgba(48,55,66,.3)}.step{display:flex;display:-ms-flexbox;-ms-flex-wrap:nowrap;flex-wrap:nowrap;list-style:none;margin:.2rem 0;width:100%}.step .step-item{-ms-flex:1 1 0;flex:1 1 0;margin-top:0;min-height:1rem;position:relative;text-align:center}.step .step-item:not(:first-child)::before{background:#5755d9;content:"";height:2px;left:-50%;position:absolute;top:9px;width:100%}.step .step-item a{color:#5755d9;display:inline-block;padding:20px 10px 0;text-decoration:none}.step .step-item a::before{background:#5755d9;border:.1rem solid #fff;border-radius:50%;content:"";display:block;height:.6rem;left:50%;position:absolute;top:.2rem;transform:translateX(-50%);width:.6rem;z-index:1}.step .step-item.active a::before{background:#fff;border:.1rem solid #5755d9}.step .step-item.active~.step-item::before{background:#dadee4}.step .step-item.active~.step-item a{color:#bcc3ce}.step .step-item.active~.step-item a::before{background:#dadee4}.tab{align-items:center;border-bottom:.05rem solid #dadee4;display:flex;display:-ms-flexbox;-ms-flex-align:center;-ms-flex-wrap:wrap;flex-wrap:wrap;list-style:none;margin:.2rem 0 .15rem 0}.tab .tab-item{margin-top:0}.tab .tab-item a{border-bottom:.1rem solid transparent;color:inherit;display:block;margin:0 .4rem 0 0;padding:.4rem .2rem .3rem .2rem;text-decoration:none}.tab .tab-item a:focus,.tab .tab-item a:hover{color:#5755d9}.tab .tab-item a.active,.tab .tab-item.active a{border-bottom-color:#5755d9;color:#5755d9}.tab .tab-item.tab-action{-ms-flex:1 0 auto;flex:1 0 auto;text-align:right}.tab .tab-item .btn-clear{margin-top:-.2rem}.tab.tab-block .tab-item{-ms-flex:1 0 0;flex:1 0 0;text-align:center}.tab.tab-block .tab-item a{margin:0}.tab.tab-block .tab-item .badge[data-badge]::after{position:absolute;right:.1rem;top:.1rem;transform:translate(0,0)}.tab:not(.tab-block) .badge{padding-right:0}.tile{align-content:space-between;align-items:flex-start;display:flex;display:-ms-flexbox;-ms-flex-align:start;-ms-flex-line-pack:justify}.tile .tile-action,.tile .tile-icon{-ms-flex:0 0 auto;flex:0 0 auto}.tile .tile-content{-ms-flex:1 1 auto;flex:1 1 auto}.tile .tile-content:not(:first-child){padding-left:.4rem}.tile .tile-content:not(:last-child){padding-right:.4rem}.tile .tile-subtitle,.tile .tile-title{line-height:1.2rem}.tile.tile-centered{align-items:center;-ms-flex-align:center}.tile.tile-centered .tile-content{overflow:hidden}.tile.tile-centered .tile-subtitle,.tile.tile-centered .tile-title{margin-bottom:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast{background:rgba(48,55,66,.95);border:.05rem solid #303742;border-color:#303742;border-radius:.1rem;color:#fff;display:block;padding:.4rem;width:100%}.toast.toast-primary{background:rgba(87,85,217,.95);border-color:#5755d9}.toast.toast-success{background:rgba(50,182,67,.95);border-color:#32b643}.toast.toast-warning{background:rgba(255,183,0,.95);border-color:#ffb700}.toast.toast-error{background:rgba(232,86,0,.95);border-color:#e85600}.toast a{color:#fff;text-decoration:underline}.toast a.active,.toast a:active,.toast a:focus,.toast a:hover{opacity:.75}.toast .btn-clear{margin:.1rem}.toast p:last-child{margin-bottom:0}.tooltip{position:relative}.tooltip::after{background:rgba(48,55,66,.95);border-radius:.1rem;bottom:100%;color:#fff;content:attr(data-tooltip);display:block;font-size:.7rem;left:50%;max-width:320px;opacity:0;overflow:hidden;padding:.2rem .4rem;pointer-events:none;position:absolute;text-overflow:ellipsis;transform:translate(-50%,.4rem);transition:opacity .2s,transform .2s;white-space:pre;z-index:300}.tooltip:focus::after,.tooltip:hover::after{opacity:1;transform:translate(-50%,-.2rem)}.tooltip.disabled,.tooltip[disabled]{pointer-events:auto}.tooltip.tooltip-right::after{bottom:50%;left:100%;transform:translate(-.2rem,50%)}.tooltip.tooltip-right:focus::after,.tooltip.tooltip-right:hover::after{transform:translate(.2rem,50%)}.tooltip.tooltip-bottom::after{bottom:auto;top:100%;transform:translate(-50%,-.4rem)}.tooltip.tooltip-bottom:focus::after,.tooltip.tooltip-bottom:hover::after{transform:translate(-50%,.2rem)}.tooltip.tooltip-left::after{bottom:50%;left:auto;right:100%;transform:translate(.4rem,50%)}.tooltip.tooltip-left:focus::after,.tooltip.tooltip-left:hover::after{transform:translate(-.2rem,50%)}@keyframes loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes slide-down{0%{opacity:0;transform:translateY(-1.6rem)}100%{opacity:1;transform:translateY(0)}}.text-primary{color:#5755d9!important}a.text-primary:focus,a.text-primary:hover{color:#4240d4}a.text-primary:visited{color:#6c6ade}.text-secondary{color:#e5e5f9!important}a.text-secondary:focus,a.text-secondary:hover{color:#d1d0f4}a.text-secondary:visited{color:#fafafe}.text-gray{color:#bcc3ce!important}a.text-gray:focus,a.text-gray:hover{color:#adb6c4}a.text-gray:visited{color:#cbd0d9}.text-light{color:#fff!important}a.text-light:focus,a.text-light:hover{color:#f2f2f2}a.text-light:visited{color:#fff}.text-dark{color:#3b4351!important}a.text-dark:focus,a.text-dark:hover{color:#303742}a.text-dark:visited{color:#455060}.text-success{color:#32b643!important}a.text-success:focus,a.text-success:hover{color:#2da23c}a.text-success:visited{color:#39c94b}.text-warning{color:#ffb700!important}a.text-warning:focus,a.text-warning:hover{color:#e6a500}a.text-warning:visited{color:#ffbe1a}.text-error{color:#e85600!important}a.text-error:focus,a.text-error:hover{color:#cf4d00}a.text-error:visited{color:#ff6003}.bg-primary{background:#5755d9!important;color:#fff}.bg-secondary{background:#f1f1fc!important}.bg-dark{background:#303742!important;color:#fff}.bg-gray{background:#f7f8f9!important}.bg-success{background:#32b643!important;color:#fff}.bg-warning{background:#ffb700!important;color:#fff}.bg-error{background:#e85600!important;color:#fff}.c-hand{cursor:pointer}.c-move{cursor:move}.c-zoom-in{cursor:zoom-in}.c-zoom-out{cursor:zoom-out}.c-not-allowed{cursor:not-allowed}.c-auto{cursor:auto}.d-block{display:block}.d-inline{display:inline}.d-inline-block{display:inline-block}.d-flex{display:flex;display:-ms-flexbox}.d-inline-flex{display:inline-flex;display:-ms-inline-flexbox}.d-hide,.d-none{display:none!important}.d-visible{visibility:visible}.d-invisible{visibility:hidden}.text-hide{background:0 0;border:0;color:transparent;font-size:0;line-height:0;text-shadow:none}.text-assistive{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.divider,.divider-vert{display:block;position:relative}.divider-vert[data-content]::after,.divider[data-content]::after{background:#fff;color:#bcc3ce;content:attr(data-content);display:inline-block;font-size:.7rem;padding:0 .4rem;transform:translateY(-.65rem)}.divider{border-top:.05rem solid #f1f3f5;height:.05rem;margin:.4rem 0}.divider[data-content]{margin:.8rem 0}.divider-vert{display:block;padding:.8rem}.divider-vert::before{border-left:.05rem solid #dadee4;bottom:.4rem;content:"";display:block;left:50%;position:absolute;top:.4rem;transform:translateX(-50%)}.divider-vert[data-content]::after{left:50%;padding:.2rem 0;position:absolute;top:50%;transform:translate(-50%,-50%)}.loading{color:transparent!important;min-height:.8rem;pointer-events:none;position:relative}.loading::after{animation:loading .5s infinite linear;border:.1rem solid #5755d9;border-radius:50%;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:.8rem;left:50%;margin-left:-.4rem;margin-top:-.4rem;position:absolute;top:50%;width:.8rem;z-index:1}.loading.loading-lg{min-height:2rem}.loading.loading-lg::after{height:1.6rem;margin-left:-.8rem;margin-top:-.8rem;width:1.6rem}.clearfix::after{clear:both;content:"";display:table}.float-left{float:left!important}.float-right{float:right!important}.p-relative{position:relative!important}.p-absolute{position:absolute!important}.p-fixed{position:fixed!important}.p-sticky{position:sticky!important;position:-webkit-sticky!important}.p-centered{display:block;float:none;margin-left:auto;margin-right:auto}.flex-centered{align-items:center;display:flex;display:-ms-flexbox;-ms-flex-align:center;-ms-flex-pack:center;justify-content:center}.m-0{margin:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mr-0{margin-right:0!important}.mt-0{margin-top:0!important}.mx-0{margin-left:0!important;margin-right:0!important}.my-0{margin-bottom:0!important;margin-top:0!important}.m-1{margin:.2rem!important}.mb-1{margin-bottom:.2rem!important}.ml-1{margin-left:.2rem!important}.mr-1{margin-right:.2rem!important}.mt-1{margin-top:.2rem!important}.mx-1{margin-left:.2rem!important;margin-right:.2rem!important}.my-1{margin-bottom:.2rem!important;margin-top:.2rem!important}.m-2{margin:.4rem!important}.mb-2{margin-bottom:.4rem!important}.ml-2{margin-left:.4rem!important}.mr-2{margin-right:.4rem!important}.mt-2{margin-top:.4rem!important}.mx-2{margin-left:.4rem!important;margin-right:.4rem!important}.my-2{margin-bottom:.4rem!important;margin-top:.4rem!important}.p-0{padding:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.pr-0{padding-right:0!important}.pt-0{padding-top:0!important}.px-0{padding-left:0!important;padding-right:0!important}.py-0{padding-bottom:0!important;padding-top:0!important}.p-1{padding:.2rem!important}.pb-1{padding-bottom:.2rem!important}.pl-1{padding-left:.2rem!important}.pr-1{padding-right:.2rem!important}.pt-1{padding-top:.2rem!important}.px-1{padding-left:.2rem!important;padding-right:.2rem!important}.py-1{padding-bottom:.2rem!important;padding-top:.2rem!important}.p-2{padding:.4rem!important}.pb-2{padding-bottom:.4rem!important}.pl-2{padding-left:.4rem!important}.pr-2{padding-right:.4rem!important}.pt-2{padding-top:.4rem!important}.px-2{padding-left:.4rem!important;padding-right:.4rem!important}.py-2{padding-bottom:.4rem!important;padding-top:.4rem!important}.s-rounded{border-radius:.1rem}.s-circle{border-radius:50%}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-normal{font-weight:400}.text-bold{font-weight:700}.text-italic{font-style:italic}.text-large{font-size:1.2em}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-clip{overflow:hidden;text-overflow:clip;white-space:nowrap}.text-break{-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto;word-break:break-word;word-wrap:break-word} \ No newline at end of file +/*! Spectre.css v0.5.9 | MIT License | github.com/picturepan2/spectre */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}hr{box-sizing:content-box;height:0;overflow:visible}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}address{font-style:normal}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:"SF Mono","Segoe UI Mono","Roboto Mono",Menlo,Courier,monospace;font-size:1em}dfn{font-style:italic}small{font-size:80%;font-weight:400}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}fieldset{border:0;margin:0;padding:0}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item;outline:0}canvas{display:inline-block}template{display:none}[hidden]{display:none}*,::after,::before{box-sizing:inherit}html{box-sizing:border-box;font-size:20px;line-height:1.5;-webkit-tap-highlight-color:transparent}body{background:#fff;color:#3b4351;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;font-size:.8rem;overflow-x:hidden;text-rendering:optimizeLegibility}a{color:#5755d9;outline:0;text-decoration:none}a:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}a.active,a:active,a:focus,a:hover{color:#302ecd;text-decoration:underline}a:visited{color:#807fe2}h1,h2,h3,h4,h5,h6{color:inherit;font-weight:500;line-height:1.2;margin-bottom:.5em;margin-top:0}.h1,.h2,.h3,.h4,.h5,.h6{font-weight:500}.h1,h1{font-size:2rem}.h2,h2{font-size:1.6rem}.h3,h3{font-size:1.4rem}.h4,h4{font-size:1.2rem}.h5,h5{font-size:1rem}.h6,h6{font-size:.8rem}p{margin:0 0 1.2rem}a,ins,u{-webkit-text-decoration-skip:ink edges;text-decoration-skip:ink edges}abbr[title]{border-bottom:.05rem dotted;cursor:help;text-decoration:none}kbd{background:#303742;border-radius:.1rem;color:#fff;font-size:.7rem;line-height:1.25;padding:.1rem .2rem}mark{background:#ffe9b3;border-bottom:.05rem solid #ffd367;border-radius:.1rem;color:#3b4351;padding:.05rem .1rem 0}blockquote{border-left:.1rem solid #dadee4;margin-left:0;padding:.4rem .8rem}blockquote p:last-child{margin-bottom:0}ol,ul{margin:.8rem 0 .8rem .8rem;padding:0}ol ol,ol ul,ul ol,ul ul{margin:.8rem 0 .8rem .8rem}ol li,ul li{margin-top:.4rem}ul{list-style:disc inside}ul ul{list-style-type:circle}ol{list-style:decimal inside}ol ol{list-style-type:lower-alpha}dl dt{font-weight:700}dl dd{margin:.4rem 0 .8rem 0}.lang-zh,.lang-zh-hans,html:lang(zh),html:lang(zh-Hans){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",sans-serif}.lang-zh-hant,html:lang(zh-Hant){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang TC","Hiragino Sans CNS","Microsoft JhengHei","Helvetica Neue",sans-serif}.lang-ja,html:lang(ja){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Hiragino Sans","Hiragino Kaku Gothic Pro","Yu Gothic",YuGothic,Meiryo,"Helvetica Neue",sans-serif}.lang-ko,html:lang(ko){font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Malgun Gothic","Helvetica Neue",sans-serif}.lang-cjk ins,.lang-cjk u,:lang(ja) ins,:lang(ja) u,:lang(zh) ins,:lang(zh) u{border-bottom:.05rem solid;text-decoration:none}.lang-cjk del+del,.lang-cjk del+s,.lang-cjk ins+ins,.lang-cjk ins+u,.lang-cjk s+del,.lang-cjk s+s,.lang-cjk u+ins,.lang-cjk u+u,:lang(ja) del+del,:lang(ja) del+s,:lang(ja) ins+ins,:lang(ja) ins+u,:lang(ja) s+del,:lang(ja) s+s,:lang(ja) u+ins,:lang(ja) u+u,:lang(zh) del+del,:lang(zh) del+s,:lang(zh) ins+ins,:lang(zh) ins+u,:lang(zh) s+del,:lang(zh) s+s,:lang(zh) u+ins,:lang(zh) u+u{margin-left:.125em}.table{border-collapse:collapse;border-spacing:0;text-align:left;width:100%}.table.table-striped tbody tr:nth-of-type(odd){background:#f7f8f9}.table tbody tr.active,.table.table-striped tbody tr.active{background:#eef0f3}.table.table-hover tbody tr:hover{background:#eef0f3}.table.table-scroll{display:block;overflow-x:auto;padding-bottom:.75rem;white-space:nowrap}.table td,.table th{border-bottom:.05rem solid #dadee4;padding:.6rem .4rem}.table th{border-bottom-width:.1rem}.btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:.05rem solid #5755d9;border-radius:.1rem;color:#5755d9;cursor:pointer;display:inline-block;font-size:.8rem;height:1.8rem;line-height:1.2rem;outline:0;padding:.25rem .4rem;text-align:center;text-decoration:none;transition:background .2s,border .2s,box-shadow .2s,color .2s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.btn:focus{box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.btn:focus,.btn:hover{background:#f1f1fc;border-color:#4b48d6;text-decoration:none}.btn.active,.btn:active{background:#4b48d6;border-color:#3634d2;color:#fff;text-decoration:none}.btn.active.loading::after,.btn:active.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.disabled,.btn:disabled,.btn[disabled]{cursor:default;opacity:.5;pointer-events:none}.btn.btn-primary{background:#5755d9;border-color:#4b48d6;color:#fff}.btn.btn-primary:focus,.btn.btn-primary:hover{background:#4240d4;border-color:#3634d2;color:#fff}.btn.btn-primary.active,.btn.btn-primary:active{background:#3a38d2;border-color:#302ecd;color:#fff}.btn.btn-primary.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-success{background:#32b643;border-color:#2faa3f;color:#fff}.btn.btn-success:focus{box-shadow:0 0 0 .1rem rgba(50,182,67,.2)}.btn.btn-success:focus,.btn.btn-success:hover{background:#30ae40;border-color:#2da23c;color:#fff}.btn.btn-success.active,.btn.btn-success:active{background:#2a9a39;border-color:#278e34;color:#fff}.btn.btn-success.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-error{background:#e85600;border-color:#d95000;color:#fff}.btn.btn-error:focus{box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.btn.btn-error:focus,.btn.btn-error:hover{background:#de5200;border-color:#cf4d00;color:#fff}.btn.btn-error.active,.btn.btn-error:active{background:#c44900;border-color:#b54300;color:#fff}.btn.btn-error.loading::after{border-bottom-color:#fff;border-left-color:#fff}.btn.btn-link{background:0 0;border-color:transparent;color:#5755d9}.btn.btn-link.active,.btn.btn-link:active,.btn.btn-link:focus,.btn.btn-link:hover{color:#302ecd}.btn.btn-sm{font-size:.7rem;height:1.4rem;padding:.05rem .3rem}.btn.btn-lg{font-size:.9rem;height:2rem;padding:.35rem .6rem}.btn.btn-block{display:block;width:100%}.btn.btn-action{padding-left:0;padding-right:0;width:1.8rem}.btn.btn-action.btn-sm{width:1.4rem}.btn.btn-action.btn-lg{width:2rem}.btn.btn-clear{background:0 0;border:0;color:currentColor;height:1rem;line-height:.8rem;margin-left:.2rem;margin-right:-2px;opacity:1;padding:.1rem;text-decoration:none;width:1rem}.btn.btn-clear:focus,.btn.btn-clear:hover{background:rgba(247,248,249,.5);opacity:.95}.btn.btn-clear::before{content:"\2715"}.btn-group{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.btn-group .btn{-ms-flex:1 0 auto;flex:1 0 auto}.btn-group .btn:first-child:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group .btn:not(:first-child):not(:last-child){border-radius:0;margin-left:-.05rem}.btn-group .btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-.05rem}.btn-group .btn.active,.btn-group .btn:active,.btn-group .btn:focus,.btn-group .btn:hover{z-index:1}.btn-group.btn-group-block{display:-ms-flexbox;display:flex}.btn-group.btn-group-block .btn{-ms-flex:1 0 0;flex:1 0 0}.form-group:not(:last-child){margin-bottom:.4rem}fieldset{margin-bottom:.8rem}legend{font-size:.9rem;font-weight:500;margin-bottom:.8rem}.form-label{display:block;line-height:1.2rem;padding:.3rem 0}.form-label.label-sm{font-size:.7rem;padding:.1rem 0}.form-label.label-lg{font-size:.9rem;padding:.4rem 0}.form-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;background-image:none;border:.05rem solid #bcc3ce;border-radius:.1rem;color:#3b4351;display:block;font-size:.8rem;height:1.8rem;line-height:1.2rem;max-width:100%;outline:0;padding:.25rem .4rem;position:relative;transition:background .2s,border .2s,box-shadow .2s,color .2s;width:100%}.form-input:focus{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-input:-ms-input-placeholder{color:#bcc3ce}.form-input::-ms-input-placeholder{color:#bcc3ce}.form-input::placeholder{color:#bcc3ce}.form-input.input-sm{font-size:.7rem;height:1.4rem;padding:.05rem .3rem}.form-input.input-lg{font-size:.9rem;height:2rem;padding:.35rem .6rem}.form-input.input-inline{display:inline-block;vertical-align:middle;width:auto}.form-input[type=file]{height:auto}textarea.form-input,textarea.form-input.input-lg,textarea.form-input.input-sm{height:auto}.form-input-hint{color:#bcc3ce;font-size:.7rem;margin-top:.2rem}.has-success .form-input-hint,.is-success+.form-input-hint{color:#32b643}.has-error .form-input-hint,.is-error+.form-input-hint{color:#e85600}.form-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:.05rem solid #bcc3ce;border-radius:.1rem;color:inherit;font-size:.8rem;height:1.8rem;line-height:1.2rem;outline:0;padding:.25rem .4rem;vertical-align:middle;width:100%}.form-select:focus{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-select::-ms-expand{display:none}.form-select.select-sm{font-size:.7rem;height:1.4rem;padding:.05rem 1.1rem .05rem .3rem}.form-select.select-lg{font-size:.9rem;height:2rem;padding:.35rem 1.4rem .35rem .6rem}.form-select[multiple],.form-select[size]{height:auto;padding:.25rem .4rem}.form-select[multiple] option,.form-select[size] option{padding:.1rem .2rem}.form-select:not([multiple]):not([size]){background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center/.4rem .5rem;padding-right:1.2rem}.has-icon-left,.has-icon-right{position:relative}.has-icon-left .form-icon,.has-icon-right .form-icon{height:.8rem;margin:0 .25rem;position:absolute;top:50%;transform:translateY(-50%);width:.8rem;z-index:2}.has-icon-left .form-icon{left:.05rem}.has-icon-left .form-input{padding-left:1.3rem}.has-icon-right .form-icon{right:.05rem}.has-icon-right .form-input{padding-right:1.3rem}.form-checkbox,.form-radio,.form-switch{display:block;line-height:1.2rem;margin:.2rem 0;min-height:1.4rem;padding:.1rem .4rem .1rem 1.2rem;position:relative}.form-checkbox input,.form-radio input,.form-switch input{clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;position:absolute;width:1px}.form-checkbox input:focus+.form-icon,.form-radio input:focus+.form-icon,.form-switch input:focus+.form-icon{border-color:#5755d9;box-shadow:0 0 0 .1rem rgba(87,85,217,.2)}.form-checkbox input:checked+.form-icon,.form-radio input:checked+.form-icon,.form-switch input:checked+.form-icon{background:#5755d9;border-color:#5755d9}.form-checkbox .form-icon,.form-radio .form-icon,.form-switch .form-icon{border:.05rem solid #bcc3ce;cursor:pointer;display:inline-block;position:absolute;transition:background .2s,border .2s,box-shadow .2s,color .2s}.form-checkbox.input-sm,.form-radio.input-sm,.form-switch.input-sm{font-size:.7rem;margin:0}.form-checkbox.input-lg,.form-radio.input-lg,.form-switch.input-lg{font-size:.9rem;margin:.3rem 0}.form-checkbox .form-icon,.form-radio .form-icon{background:#fff;height:.8rem;left:0;top:.3rem;width:.8rem}.form-checkbox input:active+.form-icon,.form-radio input:active+.form-icon{background:#eef0f3}.form-checkbox .form-icon{border-radius:.1rem}.form-checkbox input:checked+.form-icon::before{background-clip:padding-box;border:.1rem solid #fff;border-left-width:0;border-top-width:0;content:"";height:9px;left:50%;margin-left:-3px;margin-top:-6px;position:absolute;top:50%;transform:rotate(45deg);width:6px}.form-checkbox input:indeterminate+.form-icon{background:#5755d9;border-color:#5755d9}.form-checkbox input:indeterminate+.form-icon::before{background:#fff;content:"";height:2px;left:50%;margin-left:-5px;margin-top:-1px;position:absolute;top:50%;width:10px}.form-radio .form-icon{border-radius:50%}.form-radio input:checked+.form-icon::before{background:#fff;border-radius:50%;content:"";height:6px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:6px}.form-switch{padding-left:2rem}.form-switch .form-icon{background:#bcc3ce;background-clip:padding-box;border-radius:.45rem;height:.9rem;left:0;top:.25rem;width:1.6rem}.form-switch .form-icon::before{background:#fff;border-radius:50%;content:"";display:block;height:.8rem;left:0;position:absolute;top:0;transition:background .2s,border .2s,box-shadow .2s,color .2s,left .2s;width:.8rem}.form-switch input:checked+.form-icon::before{left:14px}.form-switch input:active+.form-icon::before{background:#f7f8f9}.input-group{display:-ms-flexbox;display:flex}.input-group .input-group-addon{background:#f7f8f9;border:.05rem solid #bcc3ce;border-radius:.1rem;line-height:1.2rem;padding:.25rem .4rem;white-space:nowrap}.input-group .input-group-addon.addon-sm{font-size:.7rem;padding:.05rem .3rem}.input-group .input-group-addon.addon-lg{font-size:.9rem;padding:.35rem .6rem}.input-group .form-input,.input-group .form-select{-ms-flex:1 1 auto;flex:1 1 auto;width:1%}.input-group .input-group-btn{z-index:1}.input-group .form-input:first-child:not(:last-child),.input-group .form-select:first-child:not(:last-child),.input-group .input-group-addon:first-child:not(:last-child),.input-group .input-group-btn:first-child:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.input-group .form-input:not(:first-child):not(:last-child),.input-group .form-select:not(:first-child):not(:last-child),.input-group .input-group-addon:not(:first-child):not(:last-child),.input-group .input-group-btn:not(:first-child):not(:last-child){border-radius:0;margin-left:-.05rem}.input-group .form-input:last-child:not(:first-child),.input-group .form-select:last-child:not(:first-child),.input-group .input-group-addon:last-child:not(:first-child),.input-group .input-group-btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-.05rem}.input-group .form-input:focus,.input-group .form-select:focus,.input-group .input-group-addon:focus,.input-group .input-group-btn:focus{z-index:2}.input-group .form-select{width:auto}.input-group.input-inline{display:-ms-inline-flexbox;display:inline-flex}.form-input.is-success,.form-select.is-success,.has-success .form-input,.has-success .form-select{background:#f9fdfa;border-color:#32b643}.form-input.is-success:focus,.form-select.is-success:focus,.has-success .form-input:focus,.has-success .form-select:focus{box-shadow:0 0 0 .1rem rgba(50,182,67,.2)}.form-input.is-error,.form-select.is-error,.has-error .form-input,.has-error .form-select{background:#fffaf7;border-color:#e85600}.form-input.is-error:focus,.form-select.is-error:focus,.has-error .form-input:focus,.has-error .form-select:focus{box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-checkbox.is-error .form-icon,.form-radio.is-error .form-icon,.form-switch.is-error .form-icon,.has-error .form-checkbox .form-icon,.has-error .form-radio .form-icon,.has-error .form-switch .form-icon{border-color:#e85600}.form-checkbox.is-error input:checked+.form-icon,.form-radio.is-error input:checked+.form-icon,.form-switch.is-error input:checked+.form-icon,.has-error .form-checkbox input:checked+.form-icon,.has-error .form-radio input:checked+.form-icon,.has-error .form-switch input:checked+.form-icon{background:#e85600;border-color:#e85600}.form-checkbox.is-error input:focus+.form-icon,.form-radio.is-error input:focus+.form-icon,.form-switch.is-error input:focus+.form-icon,.has-error .form-checkbox input:focus+.form-icon,.has-error .form-radio input:focus+.form-icon,.has-error .form-switch input:focus+.form-icon{border-color:#e85600;box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-checkbox.is-error input:indeterminate+.form-icon,.has-error .form-checkbox input:indeterminate+.form-icon{background:#e85600;border-color:#e85600}.form-input:not(:-ms-input-placeholder):invalid{border-color:#e85600}.form-input:not(:placeholder-shown):invalid{border-color:#e85600}.form-input:not(:-ms-input-placeholder):invalid:focus{background:#fffaf7;box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-input:not(:placeholder-shown):invalid:focus{background:#fffaf7;box-shadow:0 0 0 .1rem rgba(232,86,0,.2)}.form-input:not(:-ms-input-placeholder):invalid+.form-input-hint{color:#e85600}.form-input:not(:placeholder-shown):invalid+.form-input-hint{color:#e85600}.form-input.disabled,.form-input:disabled,.form-select.disabled,.form-select:disabled{background-color:#eef0f3;cursor:not-allowed;opacity:.5}.form-input[readonly]{background-color:#f7f8f9}input.disabled+.form-icon,input:disabled+.form-icon{background:#eef0f3;cursor:not-allowed;opacity:.5}.form-switch input.disabled+.form-icon::before,.form-switch input:disabled+.form-icon::before{background:#fff}.form-horizontal{padding:.4rem 0}.form-horizontal .form-group{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.form-inline{display:inline-block}.label{background:#eef0f3;border-radius:.1rem;color:#455060;display:inline-block;line-height:1.25;padding:.1rem .2rem}.label.label-rounded{border-radius:5rem;padding-left:.4rem;padding-right:.4rem}.label.label-primary{background:#5755d9;color:#fff}.label.label-secondary{background:#f1f1fc;color:#5755d9}.label.label-success{background:#32b643;color:#fff}.label.label-warning{background:#ffb700;color:#fff}.label.label-error{background:#e85600;color:#fff}code{background:#fcf2f2;border-radius:.1rem;color:#d73e48;font-size:85%;line-height:1.25;padding:.1rem .2rem}.code{border-radius:.1rem;color:#3b4351;position:relative}.code::before{color:#bcc3ce;content:attr(data-lang);font-size:.7rem;position:absolute;right:.4rem;top:.1rem}.code code{background:#f7f8f9;color:inherit;display:block;line-height:1.5;overflow-x:auto;padding:1rem;width:100%}.img-responsive{display:block;height:auto;max-width:100%}.img-fit-cover{object-fit:cover}.img-fit-contain{object-fit:contain}.video-responsive{display:block;overflow:hidden;padding:0;position:relative;width:100%}.video-responsive::before{content:"";display:block;padding-bottom:56.25%}.video-responsive embed,.video-responsive iframe,.video-responsive object{border:0;bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%}video.video-responsive{height:auto;max-width:100%}video.video-responsive::before{content:none}.video-responsive-4-3::before{padding-bottom:75%}.video-responsive-1-1::before{padding-bottom:100%}.figure{margin:0 0 .4rem 0}.figure .figure-caption{color:#66758c;margin-top:.4rem}.container{margin-left:auto;margin-right:auto;padding-left:.4rem;padding-right:.4rem;width:100%}.container.grid-xl{max-width:1296px}.container.grid-lg{max-width:976px}.container.grid-md{max-width:856px}.container.grid-sm{max-width:616px}.container.grid-xs{max-width:496px}.show-lg,.show-md,.show-sm,.show-xl,.show-xs{display:none!important}.cols,.columns{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-left:-.4rem;margin-right:-.4rem}.cols.col-gapless,.columns.col-gapless{margin-left:0;margin-right:0}.cols.col-gapless>.column,.columns.col-gapless>.column{padding-left:0;padding-right:0}.cols.col-oneline,.columns.col-oneline{-ms-flex-wrap:nowrap;flex-wrap:nowrap;overflow-x:auto}.column,[class~=col-]{-ms-flex:1;flex:1;max-width:100%;padding-left:.4rem;padding-right:.4rem}.column.col-1,.column.col-10,.column.col-11,.column.col-12,.column.col-2,.column.col-3,.column.col-4,.column.col-5,.column.col-6,.column.col-7,.column.col-8,.column.col-9,.column.col-auto,[class~=col-].col-1,[class~=col-].col-10,[class~=col-].col-11,[class~=col-].col-12,[class~=col-].col-2,[class~=col-].col-3,[class~=col-].col-4,[class~=col-].col-5,[class~=col-].col-6,[class~=col-].col-7,[class~=col-].col-8,[class~=col-].col-9,[class~=col-].col-auto{-ms-flex:none;flex:none}.col-12{width:100%}.col-11{width:91.66666667%}.col-10{width:83.33333333%}.col-9{width:75%}.col-8{width:66.66666667%}.col-7{width:58.33333333%}.col-6{width:50%}.col-5{width:41.66666667%}.col-4{width:33.33333333%}.col-3{width:25%}.col-2{width:16.66666667%}.col-1{width:8.33333333%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;max-width:none;width:auto}.col-mx-auto{margin-left:auto;margin-right:auto}.col-ml-auto{margin-left:auto}.col-mr-auto{margin-right:auto}@media (max-width:1280px){.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{-ms-flex:none;flex:none}.col-xl-12{width:100%}.col-xl-11{width:91.66666667%}.col-xl-10{width:83.33333333%}.col-xl-9{width:75%}.col-xl-8{width:66.66666667%}.col-xl-7{width:58.33333333%}.col-xl-6{width:50%}.col-xl-5{width:41.66666667%}.col-xl-4{width:33.33333333%}.col-xl-3{width:25%}.col-xl-2{width:16.66666667%}.col-xl-1{width:8.33333333%}.col-xl-auto{width:auto}.hide-xl{display:none!important}.show-xl{display:block!important}}@media (max-width:960px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto{-ms-flex:none;flex:none}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-auto{width:auto}.hide-lg{display:none!important}.show-lg{display:block!important}}@media (max-width:840px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto{-ms-flex:none;flex:none}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-auto{width:auto}.hide-md{display:none!important}.show-md{display:block!important}}@media (max-width:600px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto{-ms-flex:none;flex:none}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-auto{width:auto}.hide-sm{display:none!important}.show-sm{display:block!important}}@media (max-width:480px){.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-auto{-ms-flex:none;flex:none}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-auto{width:auto}.hide-xs{display:none!important}.show-xs{display:block!important}}.hero{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:justify;justify-content:space-between;padding-bottom:4rem;padding-top:4rem}.hero.hero-sm{padding-bottom:2rem;padding-top:2rem}.hero.hero-lg{padding-bottom:8rem;padding-top:8rem}.hero .hero-body{padding:.4rem}.navbar{align-items:stretch;display:-ms-flexbox;display:flex;-ms-flex-align:stretch;-ms-flex-pack:justify;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:space-between}.navbar .navbar-section{align-items:center;display:-ms-flexbox;display:flex;-ms-flex:1 0 0;flex:1 0 0;-ms-flex-align:center}.navbar .navbar-section:not(:first-child):last-child{-ms-flex-pack:end;justify-content:flex-end}.navbar .navbar-center{align-items:center;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-align:center}.navbar .navbar-brand{font-size:.9rem;text-decoration:none}.accordion input:checked~.accordion-header>.icon:first-child,.accordion[open] .accordion-header>.icon:first-child{transform:rotate(90deg)}.accordion input:checked~.accordion-body,.accordion[open] .accordion-body{max-height:50rem}.accordion .accordion-header{display:block;padding:.2rem .4rem}.accordion .accordion-header .icon{transition:transform .25s}.accordion .accordion-body{margin-bottom:.4rem;max-height:0;overflow:hidden;transition:max-height .25s}summary.accordion-header::-webkit-details-marker{display:none}.avatar{background:#5755d9;border-radius:50%;color:rgba(255,255,255,.85);display:inline-block;font-size:.8rem;font-weight:300;height:1.6rem;line-height:1.25;margin:0;position:relative;vertical-align:middle;width:1.6rem}.avatar.avatar-xs{font-size:.4rem;height:.8rem;width:.8rem}.avatar.avatar-sm{font-size:.6rem;height:1.2rem;width:1.2rem}.avatar.avatar-lg{font-size:1.2rem;height:2.4rem;width:2.4rem}.avatar.avatar-xl{font-size:1.6rem;height:3.2rem;width:3.2rem}.avatar img{border-radius:50%;height:100%;position:relative;width:100%;z-index:1}.avatar .avatar-icon,.avatar .avatar-presence{background:#fff;bottom:14.64%;height:50%;padding:.1rem;position:absolute;right:14.64%;transform:translate(50%,50%);width:50%;z-index:2}.avatar .avatar-presence{background:#bcc3ce;border-radius:50%;box-shadow:0 0 0 .1rem #fff;height:.5em;width:.5em}.avatar .avatar-presence.online{background:#32b643}.avatar .avatar-presence.busy{background:#e85600}.avatar .avatar-presence.away{background:#ffb700}.avatar[data-initial]::before{color:currentColor;content:attr(data-initial);left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);z-index:1}.badge{position:relative;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge]::after{background:#5755d9;background-clip:padding-box;border-radius:.5rem;box-shadow:0 0 0 .1rem #fff;color:#fff;content:attr(data-badge);display:inline-block;transform:translate(-.05rem,-.5rem)}.badge[data-badge]::after{font-size:.7rem;height:.9rem;line-height:1;min-width:.9rem;padding:.1rem .2rem;text-align:center;white-space:nowrap}.badge:not([data-badge])::after,.badge[data-badge=""]::after{height:6px;min-width:6px;padding:0;width:6px}.badge.btn::after{position:absolute;right:0;top:0;transform:translate(50%,-50%)}.badge.avatar::after{position:absolute;right:14.64%;top:14.64%;transform:translate(50%,-50%);z-index:100}.breadcrumb{list-style:none;margin:.2rem 0;padding:.2rem 0}.breadcrumb .breadcrumb-item{color:#66758c;display:inline-block;margin:0;padding:.2rem 0}.breadcrumb .breadcrumb-item:not(:last-child){margin-right:.2rem}.breadcrumb .breadcrumb-item:not(:last-child) a{color:#66758c}.breadcrumb .breadcrumb-item:not(:first-child)::before{color:#66758c;content:"/";padding-right:.4rem}.bar{background:#eef0f3;border-radius:.1rem;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;height:.8rem;width:100%}.bar.bar-sm{height:.2rem}.bar .bar-item{background:#5755d9;color:#fff;display:block;-ms-flex-negative:0;flex-shrink:0;font-size:.7rem;height:100%;line-height:.8rem;position:relative;text-align:center;width:0}.bar .bar-item:first-child{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.bar .bar-item:last-child{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem;-ms-flex-negative:1;flex-shrink:1}.bar-slider{height:.1rem;margin:.4rem 0;position:relative}.bar-slider .bar-item{left:0;padding:0;position:absolute}.bar-slider .bar-item:not(:last-child):first-child{background:#eef0f3;z-index:1}.bar-slider .bar-slider-btn{background:#5755d9;border:0;border-radius:50%;height:.6rem;padding:0;position:absolute;right:0;top:50%;transform:translate(50%,-50%);width:.6rem}.bar-slider .bar-slider-btn:active{box-shadow:0 0 0 .1rem #5755d9}.card{background:#fff;border:.05rem solid #dadee4;border-radius:.1rem;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card .card-body,.card .card-footer,.card .card-header{padding:.8rem;padding-bottom:0}.card .card-body:last-child,.card .card-footer:last-child,.card .card-header:last-child{padding-bottom:.8rem}.card .card-body{-ms-flex:1 1 auto;flex:1 1 auto}.card .card-image{padding-top:.8rem}.card .card-image:first-child{padding-top:0}.card .card-image:first-child img{border-top-left-radius:.1rem;border-top-right-radius:.1rem}.card .card-image:last-child img{border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem}.chip{align-items:center;background:#eef0f3;border-radius:5rem;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;font-size:90%;height:1.2rem;line-height:.8rem;margin:.1rem;max-width:320px;overflow:hidden;padding:.2rem .4rem;text-decoration:none;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.chip.active{background:#5755d9;color:#fff}.chip .avatar{margin-left:-.4rem;margin-right:.2rem}.chip .btn-clear{border-radius:50%;transform:scale(.75)}.dropdown{display:inline-block;position:relative}.dropdown .menu{animation:slide-down .15s ease 1;display:none;left:0;max-height:50vh;overflow-y:auto;position:absolute;top:100%}.dropdown.dropdown-right .menu{left:auto;right:0}.dropdown .dropdown-toggle:focus+.menu,.dropdown .menu:hover,.dropdown.active .menu{display:block}.dropdown .btn-group .dropdown-toggle:nth-last-child(2){border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.empty{background:#f7f8f9;border-radius:.1rem;color:#66758c;padding:3.2rem 1.6rem;text-align:center}.empty .empty-icon{margin-bottom:.8rem}.empty .empty-subtitle,.empty .empty-title{margin:.4rem auto}.empty .empty-action{margin-top:.8rem}.menu{background:#fff;border-radius:.1rem;box-shadow:0 .05rem .2rem rgba(48,55,66,.3);list-style:none;margin:0;min-width:180px;padding:.4rem;transform:translateY(.2rem);z-index:300}.menu.menu-nav{background:0 0;box-shadow:none}.menu .menu-item{margin-top:0;padding:0 .4rem;position:relative;text-decoration:none}.menu .menu-item>a{border-radius:.1rem;color:inherit;display:block;margin:0 -.4rem;padding:.2rem .4rem;text-decoration:none}.menu .menu-item>a:focus,.menu .menu-item>a:hover{background:#f1f1fc;color:#5755d9}.menu .menu-item>a.active,.menu .menu-item>a:active{background:#f1f1fc;color:#5755d9}.menu .menu-item .form-checkbox,.menu .menu-item .form-radio,.menu .menu-item .form-switch{margin:.1rem 0}.menu .menu-item+.menu-item{margin-top:.2rem}.menu .menu-badge{align-items:center;display:-ms-flexbox;display:flex;-ms-flex-align:center;height:100%;position:absolute;right:0;top:0}.menu .menu-badge .label{margin-right:.4rem}.modal{align-items:center;bottom:0;display:none;-ms-flex-align:center;-ms-flex-pack:center;justify-content:center;left:0;opacity:0;overflow:hidden;padding:.4rem;position:fixed;right:0;top:0}.modal.active,.modal:target{display:-ms-flexbox;display:flex;opacity:1;z-index:400}.modal.active .modal-overlay,.modal:target .modal-overlay{background:rgba(247,248,249,.75);bottom:0;cursor:default;display:block;left:0;position:absolute;right:0;top:0}.modal.active .modal-container,.modal:target .modal-container{animation:slide-down .2s ease 1;z-index:1}.modal.modal-sm .modal-container{max-width:320px;padding:0 .4rem}.modal.modal-lg .modal-overlay{background:#fff}.modal.modal-lg .modal-container{box-shadow:none;max-width:960px}.modal-container{background:#fff;border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(48,55,66,.3);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;max-height:75vh;max-width:640px;padding:0 .8rem;width:100%}.modal-container.modal-fullheight{max-height:100vh}.modal-container .modal-header{color:#303742;padding:.8rem}.modal-container .modal-body{overflow-y:auto;padding:.8rem;position:relative}.modal-container .modal-footer{padding:.8rem;text-align:right}.nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;list-style:none;margin:.2rem 0}.nav .nav-item a{color:#66758c;padding:.2rem .4rem;text-decoration:none}.nav .nav-item a:focus,.nav .nav-item a:hover{color:#5755d9}.nav .nav-item.active>a{color:#505c6e;font-weight:700}.nav .nav-item.active>a:focus,.nav .nav-item.active>a:hover{color:#5755d9}.nav .nav{margin-bottom:.4rem;margin-left:.8rem}.pagination{display:-ms-flexbox;display:flex;list-style:none;margin:.2rem 0;padding:.2rem 0}.pagination .page-item{margin:.2rem .05rem}.pagination .page-item span{display:inline-block;padding:.2rem .2rem}.pagination .page-item a{border-radius:.1rem;display:inline-block;padding:.2rem .4rem;text-decoration:none}.pagination .page-item a:focus,.pagination .page-item a:hover{color:#5755d9}.pagination .page-item.disabled a{cursor:default;opacity:.5;pointer-events:none}.pagination .page-item.active a{background:#5755d9;color:#fff}.pagination .page-item.page-next,.pagination .page-item.page-prev{-ms-flex:1 0 50%;flex:1 0 50%}.pagination .page-item.page-next{text-align:right}.pagination .page-item .page-item-title{margin:0}.pagination .page-item .page-item-subtitle{margin:0;opacity:.5}.panel{border:.05rem solid #dadee4;border-radius:.1rem;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.panel .panel-footer,.panel .panel-header{-ms-flex:0 0 auto;flex:0 0 auto;padding:.8rem}.panel .panel-nav{-ms-flex:0 0 auto;flex:0 0 auto}.panel .panel-body{-ms-flex:1 1 auto;flex:1 1 auto;overflow-y:auto;padding:0 .8rem}.popover{display:inline-block;position:relative}.popover .popover-container{left:50%;opacity:0;padding:.4rem;position:absolute;top:0;transform:translate(-50%,-50%) scale(0);transition:transform .2s;width:320px;z-index:300}.popover :focus+.popover-container,.popover:hover .popover-container{display:block;opacity:1;transform:translate(-50%,-100%) scale(1)}.popover.popover-right .popover-container{left:100%;top:50%}.popover.popover-right :focus+.popover-container,.popover.popover-right:hover .popover-container{transform:translate(0,-50%) scale(1)}.popover.popover-bottom .popover-container{left:50%;top:100%}.popover.popover-bottom :focus+.popover-container,.popover.popover-bottom:hover .popover-container{transform:translate(-50%,0) scale(1)}.popover.popover-left .popover-container{left:0;top:50%}.popover.popover-left :focus+.popover-container,.popover.popover-left:hover .popover-container{transform:translate(-100%,-50%) scale(1)}.popover .card{border:0;box-shadow:0 .2rem .5rem rgba(48,55,66,.3)}.step{display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;list-style:none;margin:.2rem 0;width:100%}.step .step-item{-ms-flex:1 1 0;flex:1 1 0;margin-top:0;min-height:1rem;position:relative;text-align:center}.step .step-item:not(:first-child)::before{background:#5755d9;content:"";height:2px;left:-50%;position:absolute;top:9px;width:100%}.step .step-item a{color:#5755d9;display:inline-block;padding:20px 10px 0;text-decoration:none}.step .step-item a::before{background:#5755d9;border:.1rem solid #fff;border-radius:50%;content:"";display:block;height:.6rem;left:50%;position:absolute;top:.2rem;transform:translateX(-50%);width:.6rem;z-index:1}.step .step-item.active a::before{background:#fff;border:.1rem solid #5755d9}.step .step-item.active~.step-item::before{background:#dadee4}.step .step-item.active~.step-item a{color:#bcc3ce}.step .step-item.active~.step-item a::before{background:#dadee4}.tab{align-items:center;border-bottom:.05rem solid #dadee4;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-wrap:wrap;flex-wrap:wrap;list-style:none;margin:.2rem 0 .15rem 0}.tab .tab-item{margin-top:0}.tab .tab-item a{border-bottom:.1rem solid transparent;color:inherit;display:block;margin:0 .4rem 0 0;padding:.4rem .2rem .3rem .2rem;text-decoration:none}.tab .tab-item a:focus,.tab .tab-item a:hover{color:#5755d9}.tab .tab-item a.active,.tab .tab-item.active a{border-bottom-color:#5755d9;color:#5755d9}.tab .tab-item.tab-action{-ms-flex:1 0 auto;flex:1 0 auto;text-align:right}.tab .tab-item .btn-clear{margin-top:-.2rem}.tab.tab-block .tab-item{-ms-flex:1 0 0;flex:1 0 0;text-align:center}.tab.tab-block .tab-item a{margin:0}.tab.tab-block .tab-item .badge[data-badge]::after{position:absolute;right:.1rem;top:.1rem;transform:translate(0,0)}.tab:not(.tab-block) .badge{padding-right:0}.tile{align-content:space-between;align-items:flex-start;display:-ms-flexbox;display:flex;-ms-flex-align:start;-ms-flex-line-pack:justify}.tile .tile-action,.tile .tile-icon{-ms-flex:0 0 auto;flex:0 0 auto}.tile .tile-content{-ms-flex:1 1 auto;flex:1 1 auto}.tile .tile-content:not(:first-child){padding-left:.4rem}.tile .tile-content:not(:last-child){padding-right:.4rem}.tile .tile-subtitle,.tile .tile-title{line-height:1.2rem}.tile.tile-centered{align-items:center;-ms-flex-align:center}.tile.tile-centered .tile-content{overflow:hidden}.tile.tile-centered .tile-subtitle,.tile.tile-centered .tile-title{margin-bottom:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast{background:rgba(48,55,66,.95);border:.05rem solid #303742;border-color:#303742;border-radius:.1rem;color:#fff;display:block;padding:.4rem;width:100%}.toast.toast-primary{background:rgba(87,85,217,.95);border-color:#5755d9}.toast.toast-success{background:rgba(50,182,67,.95);border-color:#32b643}.toast.toast-warning{background:rgba(255,183,0,.95);border-color:#ffb700}.toast.toast-error{background:rgba(232,86,0,.95);border-color:#e85600}.toast a{color:#fff;text-decoration:underline}.toast a.active,.toast a:active,.toast a:focus,.toast a:hover{opacity:.75}.toast .btn-clear{margin:.1rem}.toast p:last-child{margin-bottom:0}.tooltip{position:relative}.tooltip::after{background:rgba(48,55,66,.95);border-radius:.1rem;bottom:100%;color:#fff;content:attr(data-tooltip);display:block;font-size:.7rem;left:50%;max-width:320px;opacity:0;overflow:hidden;padding:.2rem .4rem;pointer-events:none;position:absolute;text-overflow:ellipsis;transform:translate(-50%,.4rem);transition:opacity .2s,transform .2s;white-space:pre;z-index:300}.tooltip:focus::after,.tooltip:hover::after{opacity:1;transform:translate(-50%,-.2rem)}.tooltip.disabled,.tooltip[disabled]{pointer-events:auto}.tooltip.tooltip-right::after{bottom:50%;left:100%;transform:translate(-.2rem,50%)}.tooltip.tooltip-right:focus::after,.tooltip.tooltip-right:hover::after{transform:translate(.2rem,50%)}.tooltip.tooltip-bottom::after{bottom:auto;top:100%;transform:translate(-50%,-.4rem)}.tooltip.tooltip-bottom:focus::after,.tooltip.tooltip-bottom:hover::after{transform:translate(-50%,.2rem)}.tooltip.tooltip-left::after{bottom:50%;left:auto;right:100%;transform:translate(.4rem,50%)}.tooltip.tooltip-left:focus::after,.tooltip.tooltip-left:hover::after{transform:translate(-.2rem,50%)}@keyframes loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes slide-down{0%{opacity:0;transform:translateY(-1.6rem)}100%{opacity:1;transform:translateY(0)}}.text-primary{color:#5755d9!important}a.text-primary:focus,a.text-primary:hover{color:#4240d4}a.text-primary:visited{color:#6c6ade}.text-secondary{color:#e5e5f9!important}a.text-secondary:focus,a.text-secondary:hover{color:#d1d0f4}a.text-secondary:visited{color:#fafafe}.text-gray{color:#bcc3ce!important}a.text-gray:focus,a.text-gray:hover{color:#adb6c4}a.text-gray:visited{color:#cbd0d9}.text-light{color:#fff!important}a.text-light:focus,a.text-light:hover{color:#f2f2f2}a.text-light:visited{color:#fff}.text-dark{color:#3b4351!important}a.text-dark:focus,a.text-dark:hover{color:#303742}a.text-dark:visited{color:#455060}.text-success{color:#32b643!important}a.text-success:focus,a.text-success:hover{color:#2da23c}a.text-success:visited{color:#39c94b}.text-warning{color:#ffb700!important}a.text-warning:focus,a.text-warning:hover{color:#e6a500}a.text-warning:visited{color:#ffbe1a}.text-error{color:#e85600!important}a.text-error:focus,a.text-error:hover{color:#cf4d00}a.text-error:visited{color:#ff6003}.bg-primary{background:#5755d9!important;color:#fff}.bg-secondary{background:#f1f1fc!important}.bg-dark{background:#303742!important;color:#fff}.bg-gray{background:#f7f8f9!important}.bg-success{background:#32b643!important;color:#fff}.bg-warning{background:#ffb700!important;color:#fff}.bg-error{background:#e85600!important;color:#fff}.c-hand{cursor:pointer}.c-move{cursor:move}.c-zoom-in{cursor:zoom-in}.c-zoom-out{cursor:zoom-out}.c-not-allowed{cursor:not-allowed}.c-auto{cursor:auto}.d-block{display:block}.d-inline{display:inline}.d-inline-block{display:inline-block}.d-flex{display:-ms-flexbox;display:flex}.d-inline-flex{display:-ms-inline-flexbox;display:inline-flex}.d-hide,.d-none{display:none!important}.d-visible{visibility:visible}.d-invisible{visibility:hidden}.text-hide{background:0 0;border:0;color:transparent;font-size:0;line-height:0;text-shadow:none}.text-assistive{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.divider,.divider-vert{display:block;position:relative}.divider-vert[data-content]::after,.divider[data-content]::after{background:#fff;color:#bcc3ce;content:attr(data-content);display:inline-block;font-size:.7rem;padding:0 .4rem;transform:translateY(-.65rem)}.divider{border-top:.05rem solid #f1f3f5;height:.05rem;margin:.4rem 0}.divider[data-content]{margin:.8rem 0}.divider-vert{display:block;padding:.8rem}.divider-vert::before{border-left:.05rem solid #dadee4;bottom:.4rem;content:"";display:block;left:50%;position:absolute;top:.4rem;transform:translateX(-50%)}.divider-vert[data-content]::after{left:50%;padding:.2rem 0;position:absolute;top:50%;transform:translate(-50%,-50%)}.loading{color:transparent!important;min-height:.8rem;pointer-events:none;position:relative}.loading::after{animation:loading .5s infinite linear;background:0 0;border:.1rem solid #5755d9;border-radius:50%;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:.8rem;left:50%;margin-left:-.4rem;margin-top:-.4rem;opacity:1;padding:0;position:absolute;top:50%;width:.8rem;z-index:1}.loading.loading-lg{min-height:2rem}.loading.loading-lg::after{height:1.6rem;margin-left:-.8rem;margin-top:-.8rem;width:1.6rem}.clearfix::after{clear:both;content:"";display:table}.float-left{float:left!important}.float-right{float:right!important}.p-relative{position:relative!important}.p-absolute{position:absolute!important}.p-fixed{position:fixed!important}.p-sticky{position:-webkit-sticky!important;position:sticky!important}.p-centered{display:block;float:none;margin-left:auto;margin-right:auto}.flex-centered{align-items:center;display:-ms-flexbox;display:flex;-ms-flex-align:center;-ms-flex-pack:center;justify-content:center}.m-0{margin:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mr-0{margin-right:0!important}.mt-0{margin-top:0!important}.mx-0{margin-left:0!important;margin-right:0!important}.my-0{margin-bottom:0!important;margin-top:0!important}.m-1{margin:.2rem!important}.mb-1{margin-bottom:.2rem!important}.ml-1{margin-left:.2rem!important}.mr-1{margin-right:.2rem!important}.mt-1{margin-top:.2rem!important}.mx-1{margin-left:.2rem!important;margin-right:.2rem!important}.my-1{margin-bottom:.2rem!important;margin-top:.2rem!important}.m-2{margin:.4rem!important}.mb-2{margin-bottom:.4rem!important}.ml-2{margin-left:.4rem!important}.mr-2{margin-right:.4rem!important}.mt-2{margin-top:.4rem!important}.mx-2{margin-left:.4rem!important;margin-right:.4rem!important}.my-2{margin-bottom:.4rem!important;margin-top:.4rem!important}.p-0{padding:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.pr-0{padding-right:0!important}.pt-0{padding-top:0!important}.px-0{padding-left:0!important;padding-right:0!important}.py-0{padding-bottom:0!important;padding-top:0!important}.p-1{padding:.2rem!important}.pb-1{padding-bottom:.2rem!important}.pl-1{padding-left:.2rem!important}.pr-1{padding-right:.2rem!important}.pt-1{padding-top:.2rem!important}.px-1{padding-left:.2rem!important;padding-right:.2rem!important}.py-1{padding-bottom:.2rem!important;padding-top:.2rem!important}.p-2{padding:.4rem!important}.pb-2{padding-bottom:.4rem!important}.pl-2{padding-left:.4rem!important}.pr-2{padding-right:.4rem!important}.pt-2{padding-top:.4rem!important}.px-2{padding-left:.4rem!important;padding-right:.4rem!important}.py-2{padding-bottom:.4rem!important;padding-top:.4rem!important}.s-rounded{border-radius:.1rem}.s-circle{border-radius:50%}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-normal{font-weight:400}.text-bold{font-weight:700}.text-italic{font-style:italic}.text-large{font-size:1.2em}.text-small{font-size:.9em}.text-tiny{font-size:.8em}.text-muted{opacity:.8}.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-clip{overflow:hidden;text-overflow:clip;white-space:nowrap}.text-break{-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto;word-break:break-word;word-wrap:break-word} \ No newline at end of file diff --git a/defaultapps.json b/defaultapps_banglejs1.json similarity index 100% rename from defaultapps.json rename to defaultapps_banglejs1.json diff --git a/defaultapps_banglejs2.json b/defaultapps_banglejs2.json new file mode 100644 index 000000000..04bd44504 --- /dev/null +++ b/defaultapps_banglejs2.json @@ -0,0 +1 @@ +["boot","launch","antonclk","health","setting","about","widbat","widbt","widlock","widid"] diff --git a/index.html b/index.html index a5ae7bff0..e7c7c31cd 100644 --- a/index.html +++ b/index.html @@ -60,6 +60,17 @@
+
@@ -125,6 +136,10 @@ Pretokenise apps before upload (smaller, faster apps) +
diff --git a/loader.js b/loader.js index 6528ffc98..a28f7fe78 100644 --- a/loader.js +++ b/loader.js @@ -14,6 +14,10 @@ if (window.location.host=="banglejs.com") { var RECOMMENDED_VERSION = "2v10"; // could check http://www.espruino.com/json/BANGLEJS.json for this +// We're only interested in Bangles +DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS")); + +// Set up source code URL (function() { let username = "espruino"; let githubMatch = window.location.href.match(/\/(\w+)\.github\.io/); @@ -21,16 +25,169 @@ var RECOMMENDED_VERSION = "2v10"; Const.APP_SOURCECODE_URL = `https://github.com/${username}/BangleApps/tree/master/apps`; })(); +// When a device is found, filter the apps accordingly function onFoundDeviceInfo(deviceId, deviceVersion) { - if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { - showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); - } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}). You can update with the instructions here` ,"warning", 20000); - } + var fwURL = "#"; if (deviceId == "BANGLEJS") { + fwURL = "https://www.espruino.com/Bangle.js#firmware-updates"; Const.MESSAGE_RELOAD = 'Hold BTN3\nto reload'; } if (deviceId == "BANGLEJS2") { + fwURL = "https://www.espruino.com/Bangle.js2#firmware-updates"; Const.MESSAGE_RELOAD = 'Hold button\nto reload'; } + + if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { + showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); + } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { + showToast(`You're using an old Bangle.js firmware (${deviceVersion}). You can update with the instructions here` ,"warning", 20000); + } + + + // check against features shown? + filterAppsForDevice(deviceId); + /* if we'd saved a device ID but this device is different, ensure + we ask again next time */ + var savedDeviceId = getSavedDeviceId(); + if (savedDeviceId!==undefined && savedDeviceId!=deviceId) + setSavedDeviceId(undefined); +} + +var originalAppJSON = undefined; +function filterAppsForDevice(deviceId) { + if (originalAppJSON===undefined && appJSON.length) + originalAppJSON = appJSON; + + var device = DEVICEINFO.find(d=>d.id==deviceId); + // set the device dropdown + document.querySelector(".devicetype-nav span").innerText = device ? device.name : "All apps"; + + if (!device) { + if (deviceId!==undefined) + showToast(`Device ID ${deviceId} not recognised. Some apps may not work`, "warning"); + appJSON = originalAppJSON; + } else { + // Now filter apps + appJSON = originalAppJSON.filter(app => { + var supported = ["BANGLEJS"]; + if (!app.supports) { + console.log(`App ${app.id} doesn't include a 'supports' field - ignoring`); + return false; + } + if (app.supports.includes(deviceId)) return true; + //console.log(`Dropping ${app.id} because ${deviceId} is not in supported list ${app.supports.join(",")}`); + return false; + }); + } + refreshLibrary(); +} + +// If 'remember' was checked in the window below, this is the device +function getSavedDeviceId() { + let deviceId = localStorage.getItem("deviceId"); + if (("string"==typeof deviceId) && DEVICEINFO.find(d=>d.id == deviceId)) + return deviceId; + return undefined; +} + +function setSavedDeviceId(deviceId) { + localStorage.setItem("deviceId", deviceId); +} + +// At boot, show a window to choose which type of device you have... +window.addEventListener('load', (event) => { + let deviceId = getSavedDeviceId() + if (deviceId !== undefined) return; // already chosen + + var html = `
+ ${DEVICEINFO.map(d=>` +
+
+
+
${d.name}
+ +
+
+ ${d.name} +
+
+
`).join("\n")} +
+
+
+ +
+
+
`; + showPrompt("Which Bangle.js?",html,{},false); + htmlToArray(document.querySelectorAll(".devicechooser")).forEach(button => { + button.addEventListener("click",event => { + let rememberDevice = document.getElementById("remember_device").checked; + + let button = event.currentTarget; + let deviceId = button.getAttribute("deviceid"); + hidePrompt(); + console.log("Chosen device", deviceId); + setSavedDeviceId(rememberDevice ? deviceId : undefined); + filterAppsForDevice(deviceId); + }); + }); +}); + +window.addEventListener('load', (event) => { + // Hook onto device chooser dropdown + htmlToArray(document.querySelectorAll(".devicetype-nav .menu-item")).forEach(button => { + button.addEventListener("click", event => { + var a = event.target; + var deviceId = a.getAttribute("dt")||undefined; + filterAppsForDevice(deviceId); // also sets the device dropdown + setSavedDeviceId(undefined); // ask at startup next time + document.querySelector(".devicetype-nav span").innerText = a.innerText; + }); + }); + + // Button to install all default apps in one go + document.getElementById("installdefault").addEventListener("click",event=>{ + getInstalledApps().then(() => { + if (device.id == "BANGLEJS") + return httpGet("defaultapps_banglejs1.json"); + if (device.id == "BANGLEJS2") + return httpGet("defaultapps_banglejs2.json"); + throw new Error("Unknown device "+device.id); + }).then(json=>{ + return installMultipleApps(JSON.parse(json), "default"); + }).catch(err=>{ + Progress.hide({sticky:true}); + showToast("App Install failed, "+err,"error"); + }); + }); +}); + +function onAppJSONLoaded() { + let deviceId = getSavedDeviceId() + if (deviceId !== undefined) + filterAppsForDevice(deviceId); + + return new Promise(resolve => { + httpGet("screenshots.json").then(screenshotJSON=>{ + var screenshots = []; + try { + screenshots = JSON.parse(screenshotJSON); + } catch(e) { + console.error("Screenshot JSON Corrupted", e); + } + screenshots.forEach(s => { + var app = appJSON.find(a=>a.id==s.id); + if (!app) return; + if (!app.screenshots) app.screenshots = []; + app.screenshots.push({url:s.url}); + }) + }).catch(err=>{ + console.log("No screenshots.json found"); + resolve(); + }); + }); } diff --git a/modules/Layout.js b/modules/Layout.js index 03aa6249b..6dc4b6368 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -29,7 +29,9 @@ layoutObject has: * `undefined` - blank, can be used for padding * `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required * `"btn"` - a button, with value `label` and callback `cb` - * `"img"` - an image where `src` is an image, or a function which is called to return an image to draw + optional `src` specifies an image (like img) in which case label is ignored + * `"img"` - an image where `src` is an image, or a function which is called to return an image to draw. + optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal layout, `c` is an array of more `layoutObject` * `"v"` - Veritical layout, `c` is an array of more `layoutObject` @@ -80,12 +82,12 @@ function Layout(layout, options) { this._l = this.l = layout; // Do we have >1 physical buttons? this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; - this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0; options = options || {}; this.lazy = options.lazy || false; var btnList; + Bangle.setUI(); // remove all existing input handlers if (process.env.HWVERSION!=2) { // no touchscreen, find any buttons in 'layout' btnList = []; @@ -107,9 +109,7 @@ function Layout(layout, options) { delete this.buttons[s].selected; this.render(this.buttons[s]); } - s += dir; - if (s<0) s+=lh; - if (s>=l) s-=l; + s = (s+l+dir) % l; if (this.buttons[s]) { this.buttons[s].selected = 1; this.render(this.buttons[s]); @@ -131,7 +131,7 @@ function Layout(layout, options) { if (this.b[btn].cb) this.b[btn].cb(e); } // enough physical buttons - let btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns); + let btnHeight = Math.floor(Bangle.appRect.h / this.physBtns); if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch); Bangle.btnWatch = []; if (this.physBtns > 2 && buttons.length==1) @@ -158,6 +158,7 @@ function Layout(layout, options) { } } if (process.env.HWVERSION==2) { + // Handler for touch events function touchHandler(l,e) { if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) { @@ -169,14 +170,23 @@ function Layout(layout, options) { Bangle.on('touch',Bangle.touchHandler); } - // add IDs + // recurse over layout doing some fixing up if needed var ll = this; - function idRecurser(l) { + function recurser(l) { + // add IDs if (l.id) ll[l.id] = l; + // fix type up if (!l.type) l.type=""; - if (l.c) l.c.forEach(idRecurser); + // FIXME ':'/fsz not needed in new firmwares - Font:12 is handled internally + // fix fonts for pre-2v11 firmware + if (l.font && l.font.includes(":")) { + var f = l.font.split(":"); + l.font = f[0]; + l.fsz = f[1]; + } + if (l.c) l.c.forEach(recurser); } - idRecurser(layout); + recurser(this._l); this.updateNeeded = true; } @@ -237,10 +247,8 @@ Layout.prototype.render = function (l) { g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1)); } }, "btn":function(l){ - var x = l.x+(0|l.pad); - var y = l.y+(0|l.pad); - var w = l.w-(l.pad<<1); - var h = l.h-(l.pad<<1); + var x = l.x+(0|l.pad), y = l.y+(0|l.pad), + w = l.w-(l.pad<<1), h = l.h-(l.pad<<1); var poly = [ x,y+4, x+4,y, @@ -251,10 +259,12 @@ Layout.prototype.render = function (l) { x+4,y+h-1, x,y+h-5, x,y+4 - ]; - g.setColor(l.selected?g.theme.bgH:g.theme.bg2).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly).setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); + ], bg = l.selected?g.theme.bgH:g.theme.bg2; + g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); + if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); + else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); }, "img":function(l){ - g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad)); + g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad), l.scale?{scale:l.scale}:undefined); }, "custom":function(l){ l.render(l); },"h":function(l) { l.c.forEach(render); }, @@ -335,10 +345,6 @@ Layout.prototype.debug = function(l,c) { }; Layout.prototype.update = function() { delete this.updateNeeded; - var l = this._l; - var w = g.getWidth(); - var y = this.yOffset; - var h = g.getHeight()-y; // update sizes function updateMin(l) {"ram" cb[l.type](l); @@ -352,12 +358,6 @@ Layout.prototype.update = function() { "txt" : function(l) { if (l.font.endsWith("%")) l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100); - // FIXME ':'/fsz not needed in new firmwares - it's handled internally - if (l.font.includes(":")) { - var f = l.font.split(":"); - l.font = f[0]; - l.fsz = f[1]; - } if (l.wrap) { l._h = l._w = 0; } else { @@ -365,12 +365,13 @@ Layout.prototype.update = function() { l._w = m.width; l._h = m.height; } }, "btn": function(l) { - l._h = 32; - l._w = 20 + l.label.length*12; + var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont("6x8",2).stringMetrics(l.label); + l._h = 16 + m.height; + l._w = 20 + m.width; }, "img": function(l) { - var m = g.imageMetrics("function"==typeof l.src?l.src():l.src); // get width and height out of image - l._w = m.width; - l._h = m.height; + var m = g.imageMetrics("function"==typeof l.src?l.src():l.src), s=l.scale||1; // get width and height out of image + l._w = m.width*s; + l._h = m.height*s; }, "": function(l) { // size should already be set up in width/height l._w = 0; @@ -393,18 +394,19 @@ Layout.prototype.update = function() { if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1; } }; + + var l = this._l; updateMin(l); - // center - if (l.fillx || l.filly) { - l.w = w; - l.h = h; - l.x = 0; - l.y = y; - } else { + if (l.fillx || l.filly) { // fill all + l.w = Bangle.appRect.w; + l.h = Bangle.appRect.h; + l.x = Bangle.appRect.x; + l.y = Bangle.appRect.y; + } else { // or center l.w = l._w; l.h = l._h; - l.x = (w-l.w)>>1; - l.y = y+((h-l.h)>>1); + l.x = (Bangle.appRect.w-l.w)>>1; + l.y = Bangle.appRect.y+((Bangle.appRect.h-l.h)>>1); } // layout children this.layout(l);