diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..5c055c12e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +App Loader ChangeLog +==================== + +Changed for individual apps are listed in `apps/appname/ChangeLog` + +* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong diff --git a/README.md b/README.md index a0a5e5ba4..2d0b54a7d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Bangle.js App Loader (and Apps) [![Build Status](https://travis-ci.org/espruino/BangleApps.svg?branch=master)](https://travis-ci.org/espruino/BangleApps) -Try it live at [banglejs.com/apps](https://banglejs.com/apps) +* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) +* Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) ## How does it work? @@ -14,6 +15,14 @@ it with the files it sees in the watch's storage. * To upload an app, BangleAppLoader checks the files that are listed in `apps.json`, loads them, and sends them over Web Bluetooth. +## Getting Started + +Check out: + +* [Building your first Bangle.js Application](https://www.espruino.com/Bangle.js+First+App) +* [Adding an app to the Bangle.js App Loader](https://www.espruino.com/Bangle.js+App+Loader) +* [Customising the App Loader](https://www.espruino.com/Bangle.js+App+Loader+Custom) + ## What filenames are used Filenames in storage are limited to 8 characters. To @@ -21,8 +30,9 @@ easily distinguish between file types, we use the following: * `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader * `stuff.img` is an image -* `stuff.app.js` is JS code +* `stuff.app.js` is JS code for applications * `stuff.wid.js` is JS code for widgets +* `stuff.boot.js` is JS code that automatically gets run at boot time * `stuff.json` is used for JSON settings for an app ## Developing your own app diff --git a/apps.json b/apps.json index 51a8f5af5..487e3f835 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ { "id": "boot", "name": "Bootloader", "icon": "bootloader.png", - "version":"0.11", + "version":"0.13", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "tags": "tool,system", "type":"bootloader", @@ -12,6 +12,31 @@ ], "sortorder" : -10 }, + { "id": "moonphase", + "name": "Moonphase", + "icon": "app.png", + "version":"0.02", + "description": "Shows current moon phase. Now with GPS function.", + "tags": "", + "allow_emulator":true, + "storage": [ + {"name":"moonphase.app.js","url":"app.js"}, + {"name":"moonphase.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "daysl", + "name": "Days left", + "icon": "app.png", + "version":"0.02", + "description": "Shows you the days left until a certain date. Date can be set with a settings app and is written to a file.", + "tags": "", + "allow_emulator":false, + "storage": [ + {"name":"daysl.app.js","url":"app.js"}, + {"name":"daysl.img","url":"app-icon.js","evaluate":true}, + {"name":"daysl.wid.js","url":"widget.js"} + ] + }, { "id": "launch", "name": "Default Launcher", "shortName":"Launcher", @@ -40,7 +65,7 @@ { "id": "locale", "name": "Languages", "icon": "locale.png", - "version":"0.01", + "version":"0.05", "description": "Translations for different countries", "tags": "tool,system,locale,translate", "type": "locale", @@ -106,11 +131,12 @@ "name": "Default Alarm", "shortName":"Alarms", "icon": "app.png", - "version":"0.04", + "version":"0.05", "description": "Set and respond to alarms", "tags": "tool,alarm,widget", "storage": [ {"name":"alarm.app.js","url":"app.js"}, + {"name":"alarm.boot.js","url":"boot.js"}, {"name":"alarm.js","url":"alarm.js"}, {"name":"alarm.json","content":"[]"}, {"name":"alarm.img","url":"app-icon.js","evaluate":true}, @@ -133,7 +159,7 @@ { "id": "aclock", "name": "Analog Clock", "icon": "clock-analog.png", - "version":"0.02", + "version":"0.10", "description": "An Analog Clock", "tags": "clock", "type":"clock", @@ -308,6 +334,17 @@ {"name":"widbat.wid.js","url":"widget.js"} ] }, + { "id": "widbatpc", + "name": "Battery Level Widget (with percentage)", + "icon": "widget.png", + "version":"0.06", + "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", + "tags": "widget,battery", + "type":"widget", + "storage": [ + {"name":"widbatpc.wid.js","url":"widget.js"} + ] + }, { "id": "widbt", "name": "Bluetooth Widget", "icon": "widget.png", @@ -355,8 +392,8 @@ { "id": "swatch", "name": "Stopwatch", "icon": "stopwatch.png", - "version":"0.01", - "description": "Simple stopwatch with Lap Time recording", + "version":"0.03", + "description": "Simple stopwatch with Lap Time logging to a JSON file", "tags": "health", "allow_emulator":true, "storage": [ @@ -508,6 +545,19 @@ {"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true} ] }, + { "id": "dclock", + "name": "Dev Clock", + "icon": "clock-dev.png", + "version":"0.09", + "description": "A Digital Clock including timestamp (tst), beats(@), days in current month (dm) and days since new moon (l)", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"dclock.app.js","url":"clock-dev.js"}, + {"name":"dclock.img","url":"clock-dev-icon.js","evaluate":true} + ] + }, { "id": "gesture", "name": "Gesture Test", "icon": "gesture.png", @@ -611,7 +661,7 @@ { "id": "miclock", "name": "Mixed Clock", "icon": "clock-mixed.png", - "version":"0.02", + "version":"0.03", "description": "A mix of analog and digital Clock", "tags": "clock", "type":"clock", @@ -790,7 +840,7 @@ "id": "pipboy", "name": "Pipboy", "icon": "app.png", - "version": "0.01", + "version": "0.02", "description": "Pipboy themed clock", "tags": "clock", "type":"clock", @@ -823,5 +873,177 @@ "storage": [ {"name":"widid.wid.js","url":"widget.js"} ] + }, + { + "id": "grocery", + "name": "Grocery", + "icon": "grocery.png", + "version":"0.01", + "description": "Simple grocery list - Display a list of product and track if you already put them in your cart.", + "tags": "tool,outdoors", + "type": "app", + "custom":"grocery.html", + "storage": [ + {"name":"grocery"}, + {"name":"grocery.app.js"}, + {"name":"grocery.img","url":"grocery-icon.js","evaluate":true} + ] + }, + { "id": "marioclock", + "name": "Mario Clock", + "icon": "marioclock.png", + "version":"0.05", + "description": "Animated Mario clock, jumps to change the time!", + "tags": "clock,mario,retro", + "type": "clock", + "allow_emulator":true, + "storage": [ + {"name":"marioclock.app.js","url":"marioclock-app.js"}, + {"name":"marioclock.img","url":"marioclock-icon.js","evaluate":true} + ] + }, + { "id": "cliock", + "name": "Commandline-Clock", + "shortName":"CLI-Clock", + "icon": "app.png", + "version":"0.07", + "description": "Simple CLI-Styled Clock", + "tags": "clock,cli,command,bash,shell", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"cliock.app.js","url":"app.js"}, + {"name":"cliock.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "widver", + "name": "Firmware Version Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Display the version of the installed firmware in the top widget section.", + "tags": "widget,tool,system", + "type":"widget", + "storage": [ + {"name":"widver.wid.js","url":"widget.js"} + ] + }, + { "id": "barclock", + "name": "Bar Clock", + "icon": "clock-bar.png", + "version":"0.04", + "description": "A simple digital clock showing seconds as a bar", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"barclock.app.js","url":"clock-bar.js"}, + {"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true} + ] + }, + { "id": "widtbat", + "name": "Tiny Battery Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Tiny blueish battery widget, vibs and changes level color when charging", + "tags": "widget,tool,system", + "type":"widget", + "storage": [ + {"name":"widtbat.wid.js","url":"widget.js"} + ] + }, + { "id": "chrono", + "name": "Chrono", + "shortName":"Chrono", + "icon": "chrono.png", + "version":"0.01", + "description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.", + "tags": "Tools", + "storage": [ + {"name":"chrono.app.js","url":"chrono.js"}, + {"name":"chrono.img","url":"chrono-icon.js","evaluate":true} + ] + }, + { "id": "astrocalc", + "name": "Astrocalc", + "icon": "astrocalc.png", + "version":"0.01", + "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", + "tags": "app,sun,moon,cycles,tool,outdoors", + "allow_emulator":true, + "storage": [ + {"name":"astrocalc.app.js","url":"astrocalc-app.js"}, + {"name":"suncalc.js","url":"suncalc.js"}, + {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, + {"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true}, + {"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true}, + {"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true}, + {"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true}, + {"name":"full.img","url":"full-icon.js","evaluate":true}, + {"name":"new.img","url":"new-icon.js","evaluate":true}, + {"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true}, + {"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true} + ] + }, + { "id": "widhwt", + "name": "Hand Wash Timer", + "icon": "widget.png", + "version":"0.01", + "description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.", + "tags": "widget,tool", + "type":"widget", + "storage": [ + {"name":"widhwt.wid.js","url":"widget.js"} + ] + }, + { "id": "toucher", + "name": "Touch Launcher", + "shortName":"Menu", + "icon": "app.png", + "version":"0.02", + "description": "Touch enable left to right launcher.", + "tags": "tool,system,launcher", + "type":"launch", + "storage": [ + {"name":"toucher.app.js","url":"app.js"} + ], + "sortorder" : -10 + }, + { + "id": "balltastic", + "name": "Balltastic", + "icon": "app.png", + "version": "0.01", + "description": "Simple but fun ball eats dots game.", + "tags": "game,fun", + "type": "app", + "storage": [ + {"name":"balltastic.app.js","url":"app.js"}, + {"name":"balltastic.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "rpgdice", + "name": "RPG dice", + "icon": "rpgdice.png", + "version": "0.01", + "description": "Simple RPG dice rolling app.", + "tags": "game,fun", + "type": "app", + "allow_emulator": true, + "storage": [ + {"name":"rpgdice.app.js","url": "app.js"}, + {"name":"rpgdice.img","url": "app-icon.js","evaluate":true} + ] + }, + { "id": "widmp", + "name": "Moon Phase Widget", + "icon": "widget.png", + "version":"0.01", + "description": "Display the current moon phase in blueish for the northern hemisphere in eight phases", + "tags": "widget,tools", + "type":"widget", + "storage": [ + {"name":"widmp.wid.js","url":"widget.js"} + ] } ] diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog index 7819dbe2a..a179800be 100644 --- a/apps/aclock/ChangeLog +++ b/apps/aclock/ChangeLog @@ -1 +1,7 @@ 0.02: Modified for use with new bootloader and firmware +0.03: add hour ticks, remove timers +0.04: add day-date display +0.07: make date and face bigger +0.08: make dots bigger and date more readable +0.09: center date, remove box around it, internal refactor to remove redundant code. +0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js index 67061af52..419ed0933 100644 --- a/apps/aclock/clock-analog.js +++ b/apps/aclock/clock-analog.js @@ -1,94 +1,146 @@ -const p = Math.PI/2; -const PRad = Math.PI/180; +let g; +let Bangle; -let intervalRefMin = null; -let intervalRefSec = null; +// http://forum.espruino.com/conversations/345155/#comment15172813 +const locale = require('locale'); +const p = Math.PI / 2; +const pRad = Math.PI / 180; +const faceWidth = 100; // watch face radius +let timer = null; +let currentDate = new Date(); +const centerPx = g.getWidth() / 2; -let minuteDate = new Date(); -let secondDate = new Date(); +const seconds = (angle) => { + const a = angle * pRad; + const x = centerPx + Math.sin(a) * faceWidth; + const y = centerPx - Math.cos(a) * faceWidth; -function seconds(angle, r) { - const a = angle*PRad; - const x = 120+Math.sin(a)*r; - const y = 120-Math.cos(a)*r; - g.fillRect(x-1,y-1,x+1,y+1); -} -function hand(angle, r1,r2) { - const a = angle*PRad; + // if 15 degrees, make hour marker larger + const radius = (angle % 15) ? 2 : 4; + g.fillCircle(x, y, radius); +}; + +const hand = (angle, r1, r2) => { + const a = angle * pRad; const r3 = 3; - g.fillPoly([ - 120+Math.sin(a)*r1, - 120-Math.cos(a)*r1, - 120+Math.sin(a+p)*r3, - 120-Math.cos(a+p)*r3, - 120+Math.sin(a)*r2, - 120-Math.cos(a)*r2, - 120+Math.sin(a-p)*r3, - 120-Math.cos(a-p)*r3]); -} -function drawAll() { + g.fillPoly([ + Math.round(centerPx + Math.sin(a) * r1), + Math.round(centerPx - Math.cos(a) * r1), + Math.round(centerPx + Math.sin(a + p) * r3), + Math.round(centerPx - Math.cos(a + p) * r3), + Math.round(centerPx + Math.sin(a) * r2), + Math.round(centerPx - Math.cos(a) * r2), + Math.round(centerPx + Math.sin(a - p) * r3), + Math.round(centerPx - Math.cos(a - p) * r3) + ]); +}; + +const drawAll = () => { g.clear(); - secondDate = minuteDate = new Date(); + currentDate = new Date(); // draw hands first onMinute(); // draw seconds - g.setColor(0,0,0.6); - for (let i=0;i<60;i++) - seconds(360*i/60, 90); + const currentSec = currentDate.getSeconds(); + // draw all secs + + for (let i = 0; i < 60; i++) { + if (i > currentSec) { + g.setColor(0, 0, 0.6); + } else { + g.setColor(0.3, 0.3, 1); + } + seconds((360 * i) / 60); + } onSecond(); -} +}; -function onSecond() { - g.setColor(0,0,0.6); - seconds(360*secondDate.getSeconds()/60, 90); - g.setColor(1,0,0); - secondDate = new Date(); - seconds(360*secondDate.getSeconds()/60, 90); - g.setColor(1,1,1); +const resetSeconds = () => { + g.setColor(0, 0, 0.6); + for (let i = 0; i < 60; i++) { + seconds((360 * i) / 60); + } +}; -} +const onSecond = () => { + g.setColor(0.3, 0.3, 1); + seconds((360 * currentDate.getSeconds()) / 60); + if (currentDate.getSeconds() === 59) { + resetSeconds(); + onMinute(); + } + g.setColor(1, 0.7, 0.2); + currentDate = new Date(); + seconds((360 * currentDate.getSeconds()) / 60); + g.setColor(1, 1, 1); +}; -function onMinute() { - g.setColor(0,0,0); - hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50); - hand(360*minuteDate.getMinutes()/60, -10, 82); - minuteDate = new Date(); - g.setColor(1,1,1); - hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50); - hand(360*minuteDate.getMinutes()/60, -10, 82); - if(minuteDate.getHours() >= 0 && minuteDate.getMinutes() === 0) { +const drawDate = () => { + g.reset(); + g.setColor(1, 0, 0); + g.setFont('6x8', 2); + + const dayString = locale.dow(currentDate, true); + // pad left date + const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString(); + const dateDisplay = `${dayString}-${dateString}`; + // console.log(`${dayString}|${dateString}`); + // center date + const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2; + const t = centerPx + 37; + g.drawString(dateDisplay, l, t); + // console.log(l, t); +}; +const onMinute = () => { + if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) { + g.clear(); + resetSeconds(); + } + // clear existing hands + g.setColor(0, 0, 0); + // Hour + hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35); + // Minute + hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10); + + // get new date, then draw new hands + currentDate = new Date(); + g.setColor(1, 0.9, 0.9); + // Hour + hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35); + g.setColor(1, 1, 0.9); + // Minute + hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10); + if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) { Bangle.buzz(); } -} + drawDate(); +}; -function clearTimers() { - if(intervalRefMin) {clearInterval(intervalRefMin);} - if(intervalRefSec) {clearInterval(intervalRefSec);} -} +const startTimers = () => { + timer = setInterval(onSecond, 1000); +}; -function startTimers() { - minuteDate = new Date(); - secondDate = new Date(); - intervalRefSec = setInterval(onSecond,1000); - intervalRefMin = setInterval(onMinute,60*1000); - drawAll(); -} - -Bangle.on('lcdPower',function(on) { +Bangle.on('lcdPower', (on) => { if (on) { - g.clear(); - Bangle.drawWidgets(); + // g.clear(); + drawAll(); startTimers(); - }else { - clearTimers(); + Bangle.drawWidgets(); + } else { + if (timer) { + clearInterval(timer); + } } }); g.clear(); +resetSeconds(); +startTimers(); +drawAll(); Bangle.loadWidgets(); Bangle.drawWidgets(); -drawAll(); -startTimers(); + // Show launcher when middle button pressed -setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 67feb024f..be3c1513c 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fix issues with alarm scheduling 0.03: More alarm scheduling issues 0.04: Tweaks for variable size widget system +0.05: Add alarm.boot.js and move code from the bootloader diff --git a/apps/alarm/boot.js b/apps/alarm/boot.js new file mode 100644 index 000000000..709703bdd --- /dev/null +++ b/apps/alarm/boot.js @@ -0,0 +1,24 @@ +// check for alarms +(function() { + var alarms = require('Storage').readJSON('alarm.json',1)||[]; + var time = new Date(); + var active = alarms.filter(a=>a.on&&(a.last!=time.getDate())); + if (active.length) { + active = active.sort((a,b)=>a.hr-b.hr); + var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); + if (!require('Storage').read("alarm.js")) { + console.log("No alarm app!"); + require('Storage').write('alarm.json',"[]") + } else { + var t = 3600000*(active[0].hr-hr); + if (t<1000) t=1000; + /* execute alarm at the correct time. We avoid execing immediately + since this code will get called AGAIN when alarm.js is loaded. alarm.js + will then clearInterval() to get rid of this call so it can proceed + normally. */ + setTimeout(function() { + load("alarm.js"); + },t); + } + } +})() diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog new file mode 100644 index 000000000..0c8adeb61 --- /dev/null +++ b/apps/astrocalc/ChangeLog @@ -0,0 +1 @@ +0.01: Create astrocalc app diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js new file mode 100644 index 000000000..318147b13 --- /dev/null +++ b/apps/astrocalc/astrocalc-app.js @@ -0,0 +1,348 @@ +/** + * Inspired by: https://www.timeanddate.com + */ + +const SunCalc = require("suncalc.js"); + +function drawMoon(phase, x, y) { + const moonImgFiles = [ + "new", + "waxing-crescent", + "first-quarter", + "waxing-gibbous", + "full", + "waning-gibbous", + "last-quarter", + "waning-crescent", + ]; + + img = require("Storage").read(`${moonImgFiles[phase]}.img`); + // image width & height = 92px + g.drawImage(img, x - parseInt(92 / 2), y); +} + +// linear interpolation between two values a and b +// u controls amount of a/b and is in range [0.0,1.0] +function lerp(a,b,u) { + return (1-u) * a + u * b; +} + +function titlizeKey(key) { + return (key[0].toUpperCase() + key.slice(1)).match(/[A-Z][a-z]+/g).join(" "); +} + +function dateToTimeString(date) { + const hrs = ("0" + date.getHours()).substr(-2); + const mins = ("0" + date.getMinutes()).substr(-2); + const secs = ("0" + date.getMinutes()).substr(-2); + + return `${hrs}:${mins}:${secs}`; +} + +function drawTitle(key) { + const fontHeight = 16; + const x = 0; + const x2 = g.getWidth() - 1; + const y = fontHeight + 26; + const y2 = g.getHeight() - 1; + const title = titlizeKey(key); + + g.setFont("6x8", 2); + g.setFontAlign(0,-1); + g.drawString(title,(x+x2)/2,y-fontHeight-2); + g.drawLine(x,y-2,x2,y-2); +} + +/** + * @params {Number} angle Angle of point around a radius + * @params {Number} radius Radius of the point to be drawn, default 2 + * @params {Object} color Color of the point + * @params {Number} color.r Red 0-1 + * @params {Number} color.g Green 0-1 + * @params {Number} color.b Blue 0-1 + */ +function drawPoint(angle, radius, color) { + const pRad = Math.PI / 180; + const faceWidth = 80; // watch face radius + const centerPx = g.getWidth() / 2; + + const a = angle * pRad; + const x = centerPx + Math.sin(a) * faceWidth; + const y = centerPx - Math.cos(a) * faceWidth; + + if (!radius) radius = 2; + + g.setColor(color.r, color.g, color.b); + g.fillCircle(x, y + 20, radius); +} + +function drawPoints() { + const startColor = {r: 140, g: 255, b: 255}; // light blue + const endColor = {r: 0, g: 0, b: 140}; // dark turquoise + + const steps = 60; + const step_u = 1.0 / (steps / 2); + let u = 0.0; + + for (let i = 0; i < steps; i++) { + const colR = lerp(startColor.r, endColor.r, u) / 255; + const colG = lerp(startColor.g, endColor.g, u) / 255; + const colB = lerp(startColor.b, endColor.b, u) / 255; + const col = {r: colR, g: colG, b: colB}; + + if (i >= 0 && i <= 30) { + u += step_u; + } else { + u -= step_u; + } + + drawPoint((360 * i) / steps, 2, col); + } +} + +function drawData(title, obj, startX, startY) { + g.clear(); + drawTitle(title); + + let xPos, yPos; + + if (typeof(startX) === "undefined" || startX === null) { + // Center text + g.setFontAlign(0,-1); + xPos = (0 + g.getWidth() - 2) / 2; + } else { + xPos = startX; + } + + if (typeof(startY) === "undefined") { + yPos = 5; + } else { + yPos = startY; + } + + g.setFont("6x8", 1); + + Object.keys(obj).forEach((key) => { + g.drawString(`${key}: ${obj[key]}`, xPos, yPos += 20); + }); + + g.flip(); +} + +function drawMoonPositionPage(gps, title) { + const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); + + const pageData = { + Azimuth: pos.azimuth.toFixed(2), + Altitude: pos.altitude.toFixed(2), + Distance: `${pos.distance.toFixed(0)} km`, + "Parallactic Ang": pos.parallacticAngle.toFixed(2), + }; + const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); + + drawData(title, pageData, null, 80); + drawPoints(); + drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1}); + + let m = setWatch(() => { + let m = moonIndexPageMenu(gps); + }, BTN3, {repeat: false, edge: "falling"}); +} + +function drawMoonIlluminationPage(gps, title) { + const phaseNames = [ + "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", + "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent", + ]; + + const phase = SunCalc.getMoonIllumination(new Date()); + const pageData = { + Phase: phaseNames[phase.phase], + }; + + drawData(title, pageData, null, 35); + drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2); + + let m = setWatch(() => { + let m = moonIndexPageMenu(gps); + }, BTN3, {repease: false, edge: "falling"}); +} + + +function drawMoonTimesPage(gps, title) { + const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon); + + const pageData = { + Rise: dateToTimeString(times.rise), + Set: dateToTimeString(times.set), + }; + + drawData(title, pageData, null, 105); + drawPoints(); + + // Draw the moon rise position + const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); + const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); + drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + + // Draw the moon set position + const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); + const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); + drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + + let m = setWatch(() => { + let m = moonIndexPageMenu(gps); + }, BTN3, {repease: false, edge: "falling"}); +} + +function drawSunShowPage(gps, key, date) { + const pos = SunCalc.getPosition(date, gps.lat, gps.lon); + + const hrs = ("0" + date.getHours()).substr(-2); + const mins = ("0" + date.getMinutes()).substr(-2); + const secs = ("0" + date.getMinutes()).substr(-2); + const time = `${hrs}:${mins}:${secs}`; + + const azimuth = Number(pos.azimuth.toFixed(2)); + const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); + const altitude = Number(pos.altitude.toFixed(2)); + + const pageData = { + Time: time, + Altitude: altitude, + Azimumth: azimuth, + Degrees: azimuthDegrees + }; + + drawData(key, pageData, null, 85); + + drawPoints(); + + // Draw the suns position + drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0}); + + m = setWatch(() => { + m = sunIndexPageMenu(gps); + }, BTN3, {repeat: false, edge: "falling"}); + + return null; +} + +function sunIndexPageMenu(gps) { + const sunTimes = SunCalc.getTimes(new Date(), gps.lat, gps.lon); + + const sunMenu = { + "": { + "title": "-- Sun --", + }, + "Current Pos": () => { + m = E.showMenu(); + drawSunShowPage(gps, "Current Pos", new Date()); + }, + }; + + Object.keys(sunTimes).sort().reduce((menu, key) => { + const title = titlizeKey(key); + menu[title] = () => { + m = E.showMenu(); + drawSunShowPage(gps, key, sunTimes[key]); + }; + return menu; + }, sunMenu); + + sunMenu["< Back"] = () => m = indexPageMenu(gps); + + return E.showMenu(sunMenu); +} + + +function moonIndexPageMenu(gps) { + const moonMenu = { + "": { + "title": "-- Moon --", + }, + "Times": () => { + m = E.showMenu(); + drawMoonTimesPage(gps, "Times"); + }, + "Position": () => { + m = E.showMenu(); + drawMoonPositionPage(gps, "Position"); + }, + "Illumination": () => { + m = E.showMenu(); + drawMoonIlluminationPage(gps, "Illumination"); + }, + "< Back": () => m = indexPageMenu(gps), + }; + + return E.showMenu(moonMenu); +} + +function indexPageMenu(gps) { + const menu = { + "": { + "title": "Select", + }, + "Sun": () => { + m = sunIndexPageMenu(gps); + }, + "Moon": () => { + m = moonIndexPageMenu(gps); + }, + "< Exit": () => { load(); } + }; + + return E.showMenu(menu); +} + +/** + * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page + */ +function drawGPSWaitPage() { + const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")) + + g.clear(); + g.drawImage(img, 100, 50); + g.setFont("6x8", 1); + g.drawString("Astrocalc v0.01", 80, 105); + g.drawString("Locating GPS", 85, 140); + g.drawString("Please wait...", 80, 155); + g.flip(); + + const DEBUG = false; + if (DEBUG) { + const gps = { + "lat": 56.45783133333, + "lon": -3.02188583333, + "alt": 75.3, + "speed": 0.070376, + "course": NaN, + "time":new Date(), + "satellites": 4, + "fix": 1 + }; + + m = indexPageMenu(gps); + + return; + } + + Bangle.on('GPS', (gps) => { + if (gps.fix === 0) return; + + Bangle.setGPSPower(0); + Bangle.buzz(); + Bangle.setLCDPower(true); + + m = indexPageMenu(gps); + }); +} + +function init() { + Bangle.setGPSPower(1); + drawGPSWaitPage(); +} + +let m; +init(); \ No newline at end of file diff --git a/apps/astrocalc/astrocalc-icon.js b/apps/astrocalc/astrocalc-icon.js new file mode 100644 index 000000000..aa04c2805 --- /dev/null +++ b/apps/astrocalc/astrocalc-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")) \ No newline at end of file diff --git a/apps/astrocalc/astrocalc.png b/apps/astrocalc/astrocalc.png new file mode 100644 index 000000000..c26a651ec Binary files /dev/null and b/apps/astrocalc/astrocalc.png differ diff --git a/apps/astrocalc/first-quarter-icon.js b/apps/astrocalc/first-quarter-icon.js new file mode 100644 index 000000000..d88ec79b5 --- /dev/null +++ b/apps/astrocalc/first-quarter-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgI1ygf4BZM/BZMD//wCxP/8AWJ/+ACxP+CxQ6ICwP/4AWJERAWCEQ4WCERAWCEQ4WDOg4WCNA4WD/gWKRYwWDHI4WDHIwWDHI4WDHIwWEOYwWDHIwWEKAwWD/4WKKAwWEKAoWEYgwWPM4wWEM4oWQM4oWEPwwWbPwoWESowW/C34WOZ1vACxP8Cyv4CxWACyoKFCwiUFCwhmGCwh9FCwhmGCwhmFCwhPGCwgKFCwg4GCwZPGCwg4GCwY4GCwgKGCwY4GCwZxGCwjBFCwghHCwQhHCwYhHCwQhHCwRlHCwSHHCwYKICwI3HCwQKJAFAA==")) \ No newline at end of file diff --git a/apps/astrocalc/full-icon.js b/apps/astrocalc/full-icon.js new file mode 100644 index 000000000..8bc04f7fc --- /dev/null +++ b/apps/astrocalc/full-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgJC/AD8B//4BRILJBQP/+AKGn4LC4AKFh4KC/4KFgYKD/gLFv4LD8AKEj4KD/+AEJAiGEIgiFIYhFFOAQADOghlDNA0HBQv+Q4wADRYZaFLgg4GHIg4GHIY4GHIhxFOYhxGOYgKHKARPHKARPHKAZPHKATBFYgoWKMw5nDMw5nCCyx9IPwQKIPwIW/C34WJZ1sDBQ/8CwM/BY/ACxkfBY+AgEBBQ/4CwJ+IBQJ+IPoJnIMwRnIMwJQIJ4RQIJ4JQIJ4RQIBQQ5HHAQ5HHAY5HHARzHOIRzHOIbEHYIIACLgpaDEQwhFEQohEIopDENAplERYwKGOgZwEBYoKIAH4AXA==")) \ No newline at end of file diff --git a/apps/astrocalc/last-quarter-icon.js b/apps/astrocalc/last-quarter-icon.js new file mode 100644 index 000000000..b6517f66b --- /dev/null +++ b/apps/astrocalc/last-quarter-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgI0xgP8BRP/4ALI/4WJv4WJj4WJg//CxA3BCxM/CxIhCCw4hCCxAhCCw4hCCxAKCCw5lBCxEDCxSHBCxA4DCw4KCCw44DCww4DCw5xCCw44DCw5PDCw0PCxQKDCwxPDCwzBDCyRmECwxmDCyRmDCwx9ECzoKDCwyUEC34W/CyDOtn4WJgYWVgIWKj4WVPwgWFSogWGM4gWGPwYWGM4gWGM4YWGKAgWGKAYWGHIgWGKAYWHHIYWGHIYWHHIYWGHIYWHOYYWHYgQWHEQYWHEQQWIEQQWHEQQWINAQWIRYIWIOgQWIHQIWJBYIWJAFI=")) \ No newline at end of file diff --git a/apps/astrocalc/new-icon.js b/apps/astrocalc/new-icon.js new file mode 100644 index 000000000..5d610fbe1 --- /dev/null +++ b/apps/astrocalc/new-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgIGDh///4RHBQQLHg4KC/wKFgIKC//4BYt/BYfgBQkfBQf/wAsHFw4HCBwXwBQc/AwYLB4AhEIARIBEQn//gECgYiEIYJ2FIoQQBE4YzBDgd/NoguBNAUPKoo/BB4YhEEQIdCAYYiECQMHUwwHDEIweBLgMPWIwiBAQSlENwQTBDIQAFFQMDHAw5BOYN/HAwfB8ANCAAofCHA45B+EPHA4UBKQQAGMgMfUYQAFv+DJ45QCn5PHKAPDJ45QB/hmICwPnT4yhC/1/Mw5nBCxZmIM4P/PpB+BC34WEVZCsB/7CIYYIWWOX4WbfiwWL/gKHgf+n/ABY8/4YWJ/k/VhF/4LDIg/4j5nI/+APxEP+EPM48BCgN/KA5CBg5QHMwINCJ4/AgY5Hh4fBj45GHAKeBAQSfFMgIZCHAoqCv45GA4QOBEQsfDwQDDEIgSC/4iFv6dCg4iFj60Dn4iEEIKRCL4K5E/5uDh4QDDgKFEv4uDj4/EE4IRCDYIzEAwIvBAQKnFEQIADMIhFBAAayFNAIACMoZtDBYa9GFwbrHBQR2EBYoKEA==")) \ No newline at end of file diff --git a/apps/astrocalc/suncalc.js b/apps/astrocalc/suncalc.js new file mode 100644 index 000000000..6ef5aa2d0 --- /dev/null +++ b/apps/astrocalc/suncalc.js @@ -0,0 +1,328 @@ +/* + (c) 2011-2015, Vladimir Agafonkin + SunCalc is a JavaScript library for calculating sun/moon position and light phases. + https://github.com/mourner/suncalc +*/ + +(function () { 'use strict'; + +// shortcuts for easier to read formulas + +var PI = Math.PI, + sin = Math.sin, + cos = Math.cos, + tan = Math.tan, + asin = Math.asin, + atan = Math.atan2, + acos = Math.acos, + rad = PI / 180; + +// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas + + +// date/time constants and conversions + +var dayMs = 1000 * 60 * 60 * 24, + J1970 = 2440588, + J2000 = 2451545; + +function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } +function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; } +function toDays(date) { return toJulian(date) - J2000; } + + +// general calculations for position + +var e = rad * 23.4397; // obliquity of the Earth + +function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } +function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } + +function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } +function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } + +function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } + +function astroRefraction(h) { + if (h < 0) // the following formula works for positive altitudes only. + h = 0; // if h = -0.08901179 a div/0 would occur. + + // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: + return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); +} + +// general sun calculations + +function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } + +function eclipticLongitude(M) { + + var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center + P = rad * 102.9372; // perihelion of the Earth + + return M + C + P + PI; +} + +function sunCoords(d) { + + var M = solarMeanAnomaly(d), + L = eclipticLongitude(M); + + return { + dec: declination(L, 0), + ra: rightAscension(L, 0) + }; +} + + +var SunCalc = {}; + + +// calculates sun position for a given date and latitude/longitude + +SunCalc.getPosition = function (date, lat, lng) { + + var lw = rad * -lng, + phi = rad * lat, + d = toDays(date), + + c = sunCoords(d), + H = siderealTime(d, lw) - c.ra; + + return { + azimuth: azimuth(H, phi, c.dec), + altitude: altitude(H, phi, c.dec) + }; +}; + + +// sun times configuration (angle, morning name, evening name) + +var times = SunCalc.times = [ + [-0.833, 'sunrise', 'sunset' ], + [ -0.3, 'sunriseEnd', 'sunsetStart' ], + [ -6, 'dawn', 'dusk' ], + [ -12, 'nauticalDawn', 'nauticalDusk'], + [ -18, 'nightEnd', 'night' ], + [ 6, 'goldenHourEnd', 'goldenHour' ] +]; + +// adds a custom time to the times config + +SunCalc.addTime = function (angle, riseName, setName) { + times.push([angle, riseName, setName]); +}; + + +// calculations for sun times + +var J0 = 0.0009; + +function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } + +function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } +function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } + +function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } +function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } + +// returns set time for the given sun altitude +function getSetJ(h, lw, phi, dec, n, M, L) { + + var w = hourAngle(h, phi, dec), + a = approxTransit(w, lw, n); + return solarTransitJ(a, M, L); +} + + +// calculates sun times for a given date, latitude/longitude, and, optionally, +// the observer height (in meters) relative to the horizon + +SunCalc.getTimes = function (date, lat, lng, height) { + + height = height || 0; + + var lw = rad * -lng, + phi = rad * lat, + + dh = observerAngle(height), + + d = toDays(date), + n = julianCycle(d, lw), + ds = approxTransit(0, lw, n), + + M = solarMeanAnomaly(ds), + L = eclipticLongitude(M), + dec = declination(L, 0), + + Jnoon = solarTransitJ(ds, M, L), + + i, len, time, h0, Jset, Jrise; + + + var result = { + solarNoon: new Date(fromJulian(Jnoon)), + nadir: new Date(fromJulian(Jnoon - 0.5)) + }; + + for (i = 0, len = times.length; i < len; i += 1) { + time = times[i]; + h0 = (time[0] + dh) * rad; + + Jset = getSetJ(h0, lw, phi, dec, n, M, L); + Jrise = Jnoon - (Jset - Jnoon); + + result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2)); + result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2)); + } + + return result; +}; + + +// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas + +function moonCoords(d) { // geocentric ecliptic coordinates of the moon + + var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude + M = rad * (134.963 + 13.064993 * d), // mean anomaly + F = rad * (93.272 + 13.229350 * d), // mean distance + + l = L + rad * 6.289 * sin(M), // longitude + b = rad * 5.128 * sin(F), // latitude + dt = 385001 - 20905 * cos(M); // distance to the moon in km + + return { + ra: rightAscension(l, b), + dec: declination(l, b), + dist: dt + }; +} + +SunCalc.getMoonPosition = function (date, lat, lng) { + + var lw = rad * -lng, + phi = rad * lat, + d = toDays(date), + + c = moonCoords(d), + H = siderealTime(d, lw) - c.ra, + h = altitude(H, phi, c.dec), + // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); + + h = h + astroRefraction(h); // altitude correction for refraction + + return { + azimuth: azimuth(H, phi, c.dec), + altitude: h, + distance: c.dist, + parallacticAngle: pa + }; +}; + + +// calculations for illumination parameters of the moon, +// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and +// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + +// Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c + +SunCalc.getMoonIllumination = function (date) { + let month = date.getMonth(); + let year = date.getFullYear(); + let day = date.getDate(); + + let c = 0; + let e = 0; + let jd = 0; + let b = 0; + + if (month < 3) { + year--; + month += 12; + } + + ++month; + c = 365.25 * year; + e = 30.6 * month; + jd = c + e + day - 694039.09; // jd is total days elapsed + jd /= 29.5305882; // divide by the moon cycle + b = parseInt(jd); // int(jd) -> b, take integer part of jd + jd -= b; // subtract integer part to leave fractional part of original jd + b = Math.round(jd * 8); // scale fraction from 0-8 and round + + if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0 + + return {phase: b}; +}; + + +function hoursLater(date, h) { + return new Date(date.valueOf() + h * dayMs / 24); +} + +// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article + +SunCalc.getMoonTimes = function (date, lat, lng, inUTC) { + var t = date; + if (inUTC) t.setUTCHours(0, 0, 0, 0); + else t.setHours(0, 0, 0, 0); + + var hc = 0.133 * rad, + h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc, + h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; + + // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) + for (var i = 1; i <= 24; i += 2) { + h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc; + h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc; + + a = (h0 + h2) / 2 - h1; + b = (h2 - h0) / 2; + xe = -b / (2 * a); + ye = (a * xe + b) * xe + h1; + d = b * b - 4 * a * h1; + roots = 0; + + if (d >= 0) { + dx = Math.sqrt(d) / (Math.abs(a) * 2); + x1 = xe - dx; + x2 = xe + dx; + if (Math.abs(x1) <= 1) roots++; + if (Math.abs(x2) <= 1) roots++; + if (x1 < -1) x1 = x2; + } + + if (roots === 1) { + if (h0 < 0) rise = i + x1; + else set = i + x1; + + } else if (roots === 2) { + rise = i + (ye < 0 ? x2 : x1); + set = i + (ye < 0 ? x1 : x2); + } + + if (rise && set) break; + + h0 = h2; + } + + var result = {}; + + if (rise) result.rise = hoursLater(t, rise); + if (set) result.set = hoursLater(t, set); + + if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; + + return result; +}; + + +// export as Node module / AMD module / browser variable +if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; +else if (typeof define === 'function' && define.amd) define(SunCalc); +else global.SunCalc = SunCalc; + +}()); diff --git a/apps/astrocalc/waning-crescent-icon.js b/apps/astrocalc/waning-crescent-icon.js new file mode 100644 index 000000000..8ff83ab1f --- /dev/null +++ b/apps/astrocalc/waning-crescent-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgJC/ABHgBRN8BRMfwAKIg/4CxP/BRM/HBMH/wKIgP/4AhJ/ghJ/5PJ/5PJj4WJgf/+AWIv5mJHAIWJ/5mJHAJ9IHAIWJn59JHAJ9JJ4IWIh4WK/4WJJ4KUIYIKUJJ4IWIMwIWgMwIWIPoLCJCwLCICxYKBCxCUBC34W/Cya3WCxr8In78JgYWhj4WJgIWKPwP8SpXAM5IWJPwIWIKAIWJM4PgKBP+CxBQBCxA5CBRBQBYZA5CBRA5BSpA5CSpA5BCxJzBPxDEBPxIiBM5MDPxJFBM5IiBKBMBKBKLBKBMAhwKJAH4ABA=")) \ No newline at end of file diff --git a/apps/astrocalc/waning-gibbous-icon.js b/apps/astrocalc/waning-gibbous-icon.js new file mode 100644 index 000000000..2373475f4 --- /dev/null +++ b/apps/astrocalc/waning-gibbous-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgI0xgP/wAKJ/wWI///+AKHv4LBEQ8fBQP8BQ0HBQP/8A3HAAQWGn4KCHIwhDHIwhE/AhJ//AEJJQGBQZQGMoQABRQsDCwhQFQ4RnHHAgWGBQhnFHAhnFHAoWFOIhnFHAp+FJ4oWEh4WKBQp+EJ4qVEYIgWRMwwWEMwoWLVghmFVgh9GCzYKGCwaUGC34W/CxzOtn4WJgYKF/wWK8AKCgIWKj4WVPwwWDSo38BQZnG4B+JCwhnGCwhnF/AKDKA2AKBIWEHIwKEKAqrDHI4KEHIp9EHIqUEHIxmEOYp9EYgxmEEQpmFEQoKFEQhmFEQhPGNAhPFRYg4GOggKHHQSIFBYghIAFQ=")) \ No newline at end of file diff --git a/apps/astrocalc/waxing-crescent-icon.js b/apps/astrocalc/waxing-crescent-icon.js new file mode 100644 index 000000000..d89525c88 --- /dev/null +++ b/apps/astrocalc/waxing-crescent-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgJC/ABdwBRMD8ALJj+ABREB/wWJh/wBZN/4AKIg4iKn/4KBP/ERMfERMB/5FJj//NBP//hnJ/6LJ/45Jg45Kv45JCwI5Jn5zJPwI5JCwJQICwP/CxRQISoJQJSoLEICwRQICwJnICzJnIYYJ+JCzB+ICwKVJC34W/CxbOffgIWIfgXACxP8Cyv4CxWACyUDPpU/ShIWBPpIWBPpEHMxMAv5mJCwJPICwQKIYQI4IYQJPJCwI4ISgI4JSgIKICwI4Jn5xJSgLBIMwIhJg4hJMwIKJj4hJgJlJgE+BRMHBRIA+A")) \ No newline at end of file diff --git a/apps/astrocalc/waxing-gibbous-icon.js b/apps/astrocalc/waxing-gibbous-icon.js new file mode 100644 index 000000000..90ccd6f37 --- /dev/null +++ b/apps/astrocalc/waxing-gibbous-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("rlcgI1yj/4BREH/4LJ/4LJj4LB8AKGgYKB/+ABY1/BQP+BQ0PCwQuHBQX/4A4IEQ8BCwYiGn4iJJ4YiHJ4QAB+CIGAAZoFBQn8MxCLHBQg5FMwY5GMwg5GCwo5EMwhzGPog5FCwxQECwv/PpJQFSghQFCwzEECyJnECwxnDVYoWFBQpnECwx+ECzp+DCwyVEC34W/CyDOt4AKCg4KF/gWDv4WQ/AWKwAWVBQcDShMAn5mJCwx9DCwxmEgJmJgEfJ5IWGBQasGHAisFJ4gWGHAh+FHAiVGBQhnFHAp+EOIhnGYIZnGEIpQEEIxnEEIpQEEIxQDMoo5EQ4o5FFgyKDBRAiBBRAApA=")) \ No newline at end of file diff --git a/apps/balltastic/ChangeLog b/apps/balltastic/ChangeLog new file mode 100644 index 000000000..5a62086c2 --- /dev/null +++ b/apps/balltastic/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version of Balltastic released! Happy! diff --git a/apps/balltastic/app-icon.js b/apps/balltastic/app-icon.js new file mode 100644 index 000000000..f25b6e067 --- /dev/null +++ b/apps/balltastic/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEogAIkUzmciBpIVIkYWBAAUyCx0hiIXFAAMkCxhUBC4fzDAYWLiAXFAAP//5KKoMRC4UTC4k/DAPzJJERiKcCC5H/GA4uBWwp6DC4YwHCwMBDI0SMAYwHoIWBiXxdIwYCMJCjBiM/C46VDC4M/GAkRgMf//ySAQAEgKrDC4lBgMCHIQXHSwxICIwIuBAAIXIGAYABiQXBkEBTYcgC473FkQXBiETTQZ4IgECC4cholCiJGDMAIXIWgIXCmMkC4JGDJBbEDC4UACwn/mAtGSYsxilCgIXFSAqDBkMRiIFBkcxiUiC4sxXowIBC4QGBkIXBiJ2EFwsDBIPyC4ILBgMRiUyiCmJgSCC+YXDgAXDR4YuEcAn/MAIXEmcgBoXyFwjIEMAQXFkIOCUgoXF+J3CC4cxBwR1IQQx3BkUzmUSBQKkFC5IuBkVDJAJeGRwLhHFwUkC4Mxl6lFC48gFwYXCmcTOwomBC4swYIMikU0C4UxkJ3FC40xFoIXCogXBmaxDC5MyCwUiogXDmIXTJASSBC4kRU4oXDkgXFmQwDNwIWEBoIXFJAYKBZggWFC4YWCC4g7BkIWBkYWBBYYXCkYXDJAYjDkQUEEYZGEGA4XIIwwwGDAQuOGAomCFo4uGGARoBE4ZOGFxAABBwgAICxAABCyxJBGJJFJJRgVNPggsMA=")) diff --git a/apps/balltastic/app.js b/apps/balltastic/app.js new file mode 100644 index 000000000..6c1de940c --- /dev/null +++ b/apps/balltastic/app.js @@ -0,0 +1,186 @@ +Bangle.setLCDBrightness(1); +Bangle.setLCDMode("doublebuffered"); + +let points = 0; +let level = 1; +let levelSpeedStart = 0.8; +let nextLevelPoints = 20; +let levelSpeedFactor = 0.2; +let counterWidth = 10; +let gWidth = g.getWidth() - counterWidth; +let gHeight = g.getHeight(); +let counter = 160; +let counterMax = 160; +let ballDims = 20; +let ballx = g.getWidth() / 2 - ballDims; +let bally = g.getHeight() / 2 - ballDims; +let dotx = g.getWidth() / 2; +let doty = g.getWidth() / 2; +let ballBuzzTime = 5; +let ballSpeedFactor = 40; +let redrawspeed = 5; +let dotwidth = 5; +let running = false; +let drawInterval; +let xBuzzed = false; +let yBuzzed = false; + +let BALL = require("heatshrink").decompress( + atob( + "ikUyAROvkQ3v4405AIYHBGq9KpMhktz1/W7feAJAtBEZ9jhkhs0ZgkQ8lKxW+jAdB516627E4X8AIPWzelmolKlpJBjMFEYIpC4kQ0YBBqWKynTFYPe7gpE3ec6gnHkNFrXL7372u2E4WjhGCAIliqWrUIPeKoIpB7h9HoUoqWq999///FIJ3BhGDEIIBBgFBAoWCoUI3vY62aQIW7ymSJooLBEoIADwkQEYVhEoInEGIOjR4O1y/OrIrBUYdr198iH/74nF88cE4gpCA4MY8k59CzBAINrx2164nBtduufPWYIlF++/xkxNoMAAIJPBoSdB52a30ZkNGE4IvBoUpwkxLIOMyWEmAmE7+MqKbEsLLBH4P3zw1BAYJFBFIMY8sQ4cx44nB0tVHYITBEoO967lDgDDC1tVQ4QBD37xBjMmJ4I3BE4IxBPoOMuSrBHYL1BJYbrDvfPLoYBD889jMlEoMhkpJBwkRE4O+jB7B405LoJPEYYUx0xPG7/3vxvBmOnrXsdIOc6jxBE4JfBvfwHIafDFoMRgh3H99+zsUDIOMqWU2YlBAAO1/AnBToN76EhgpTBFYKPBGIIhBEovOrWliuc2YlBE4oABE4etu2UyVrpqJBMoKvBEIPnjvWze97ATBE4YPBEopRC64BC27nBzn0znTAIOlimtq21y4BCEoM1HYOMqIVBE44AB0tVCYIBEigVBE4U1GYIFBymywkwEoJzHABIRBMIIXBWoIDCqOEmOEiABCmIjPAA51BFoVSEoUwAIIZNA" + ) +); + +function reset() { + g.clear(); + level = 1; + points = 0; + ballx = g.getWidth() / 2 - ballDims; + bally = g.getHeight() / 2 - ballDims; + counter = counterMax; + createRandomDot(); + drawInterval = setInterval(play, redrawspeed); + running = true; +} + +function collide() { + try { + Bangle.buzz(ballBuzzTime, 0.8); + } catch (e) {} +} + +function createRandomDot() { + dotx = Math.floor( + Math.random() * Math.floor(gWidth - dotwidth / 2) + dotwidth / 2 + ); + doty = Math.floor( + Math.random() * Math.floor(gHeight - dotwidth / 2) + dotwidth / 2 + ); +} + +function checkIfDotEaten() { + if ( + ballx + ballDims > dotx && + ballx <= dotx + dotwidth && + bally + ballDims > doty && + bally <= doty + dotwidth + ) { + collide(); + createRandomDot(); + counter = counterMax; + points++; + + if (points % nextLevelPoints == 0) { + level++; + } + } +} + +function drawLevelText() { + g.setColor("#26b6c7"); + g.setFontAlign(0, 0); + g.setFont("4x6", 5); + g.drawString("Level " + level, 120, 80); +} + +function draw() { + //bg + g.setColor("#71c6cf"); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + //counter + drawCounter(); + + //draw level + drawLevelText(); + + //dot + g.setColor("#ff0000"); + g.fillCircle(dotx, doty, dotwidth); + + //ball + g.drawImage(BALL, ballx, bally); + + g.flip(); +} + +function drawCounter() { + g.setColor("#000000"); + g.fillRect(g.getWidth() - counterWidth, 0, g.getWidth(), gHeight); + + if(counter < 40 ) g.setColor("#fc0303"); + else if (counter < 80 ) g.setColor("#fc9803"); + else g.setColor("#0318fc"); + + g.fillRect( + g.getWidth() - counterWidth, + gHeight, + g.getWidth(), + gHeight - counter + ); +} + +function checkCollision() { + if (ballx < 0) { + ballx = 0; + if (!xBuzzed) collide(); + xBuzzed = true; + } else if (ballx > gWidth - ballDims) { + ballx = gWidth - ballDims; + if (!xBuzzed) collide(); + xBuzzed = true; + } else { + xBuzzed = false; + } + + if (bally < 0) { + bally = 0; + if (!yBuzzed) collide(); + yBuzzed = true; + } else if (bally > gHeight - ballDims) { + bally = gHeight - ballDims; + if (!yBuzzed) collide(); + yBuzzed = true; + } else { + yBuzzed = false; + } +} + +function count() { + counter -= levelSpeedStart + level * levelSpeedFactor; + if (counter <= 0) { + running = false; + clearInterval(drawInterval); + setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50); + } +} + +function accel(values) { + ballx -= values.x * ballSpeedFactor; + bally -= values.y * ballSpeedFactor; +} + +function play() { + if (running) { + accel(Bangle.getAccel()); + checkCollision(); + checkIfDotEaten(); + count(); + draw(); + } +} + +setTimeout(() => { + reset(); + drawInterval = setInterval(play, redrawspeed); + + setWatch( + () => { + if(!running) reset(); + }, + BTN1, + { repeat: true } + ); + + running = true; +}, 10); diff --git a/apps/balltastic/app.png b/apps/balltastic/app.png new file mode 100644 index 000000000..0f95e056f Binary files /dev/null and b/apps/balltastic/app.png differ diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog new file mode 100644 index 000000000..2e0fd088c --- /dev/null +++ b/apps/barclock/ChangeLog @@ -0,0 +1,4 @@ +0.01: Created Bar Clock +0.02: Apply locale, 12-hour setting +0.03: Fix dates drawing over each other at midnight +0.04: Small bugfix diff --git a/apps/barclock/clock-bar-icon.js b/apps/barclock/clock-bar-icon.js new file mode 100644 index 000000000..29bf0f481 --- /dev/null +++ b/apps/barclock/clock-bar-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AD8Mgfwh/AhgFFngHBOIM8AovMDIXA5gFFDoUAmYjDAocMSoMz/4FF//P/g1CAopTLDAIABwAFGAH4AfA")) diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js new file mode 100644 index 000000000..da436daee --- /dev/null +++ b/apps/barclock/clock-bar.js @@ -0,0 +1,166 @@ +/* jshint esversion: 6 */ +/** + * A simple digital clock showing seconds as a bar + **/ +{ + // Check settings for what type our clock should be + const is12Hour = (require('Storage').readJSON('setting.json', 1) || {})['12hour'] + let locale = require('locale') + { // add some more info to locale + let date = new Date() + date.setFullYear(1111) + date.setMonth(1, 3) // februari: months are zero-indexed + const localized = locale.date(date, true) + locale.dayFirst = /3.*2/.test(localized) + locale.hasMeridian = (locale.meridian(date) !== '') + } + const screen = { + width: g.getWidth(), + height: g.getWidth(), + middle: g.getWidth() / 2, + center: g.getHeight() / 2, + } + + // hardcoded "settings" + const settings = { + time: { + color: -1, + font: '6x8', + size: (is12Hour && locale.hasMeridian) ? 6 : 8, + middle: screen.middle, + center: screen.center, + ampm: { + color: -1, + font: '6x8', + size: 2, + }, + }, + date: { + color: -1, + font: 'Vector', + size: 20, + middle: screen.height - 20, // at bottom of screen + center: screen.center, + }, + bar: { + color: -1, + top: 155, // just below time + thickness: 6, // matches 24h time "pixel" size + }, + } + + const SECONDS_PER_MINUTE = 60 + + const timeText = function (date) { + if (!is12Hour) { + return locale.time(date, true) + } + const date12 = new Date(date.getTime()) + const hours = date12.getHours() + if (hours === 0) { + date12.setHours(12) + } else if (hours > 12) { + date12.setHours(hours - 12) + } + return locale.time(date12, true) + } + const ampmText = function (date) { + return is12Hour ? locale.meridian(date) : '' + } + + const dateText = function (date) { + const dayName = locale.dow(date, true), + month = locale.month(date, true), + day = date.getDate() + const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}` + return `${dayName} ${dayMonth}` + } + + const drawDateTime = function (date) { + const t = settings.time + g.setColor(t.color) + g.setFont(t.font, t.size) + g.setFontAlign(0, 0) // centered + g.drawString(timeText(date), t.center, t.middle, true) + if (is12Hour && locale.hasMeridian) { + const a = settings.time.ampm + g.setColor(a.color) + g.setFont(a.font, a.size) + g.setFontAlign(1, -1) // right top + // at right edge of screen, aligned with time bottom + const left = screen.width - a.size * 2, + top = t.middle + t.size - a.size + g.drawString(ampmText(date), left, top, true) + } + + const d = settings.date + g.setColor(d.color) + g.setFont(d.font, d.size) + g.setFontAlign(0, 0) // centered + g.drawString(dateText(date), d.center, d.middle, true) + } + + const drawBar = function (date) { + const b = settings.bar + const seconds = date.getSeconds() + if (seconds === 0) { + // zero-size rect stills draws one line of pixels, we don't want that + return + } + const fraction = seconds / SECONDS_PER_MINUTE, + width = fraction * screen.width + g.setColor(b.color) + g.fillRect(0, b.top, width, b.top + b.thickness) + } + + const clearScreen = function () { + g.setColor(0) + const timeTop = settings.time.middle - (settings.time.size * 4) + g.fillRect(0, timeTop, screen.width, screen.height) + } + + let lastSeconds + const tick = function () { + g.reset() + const date = new Date() + const seconds = date.getSeconds() + if (lastSeconds > seconds) { + // new minute + clearScreen() + drawDateTime(date) + } + // the bar only gets larger, so drawing on top of the previous one is fine + drawBar(date) + + lastSeconds = seconds + } + + let iTick + const start = function () { + lastSeconds = 99 // force redraw + tick() + iTick = setInterval(tick, 1000) + } + const stop = function () { + if (iTick) { + clearInterval(iTick) + iTick = undefined + } + } + + // clean app screen + g.clear() + Bangle.loadWidgets() + Bangle.drawWidgets() + // Show launcher when middle button pressed + setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'}) + + Bangle.on('lcdPower', function (on) { + if (on) { + start() + } else { + stop() + } + }) + start() +} diff --git a/apps/barclock/clock-bar.png b/apps/barclock/clock-bar.png new file mode 100644 index 000000000..a580cae69 Binary files /dev/null and b/apps/barclock/clock-bar.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 3bd9ec71c..17023a8dc 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -9,3 +9,6 @@ 0.10: Stop users calling save() (fix #125) If Debug info is set to 'show' don't move to Terminal if connected! 0.11: Added vibrate as beep workaround +0.12: Add an event on BTN2 to open launcher when no clock detected (fix #147) +0.13: Now automatically load *.boot.js at startup + Move alarm code into alarm.boot.js diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js index ad8ccb312..729b7aaf2 100644 --- a/apps/boot/boot0.js +++ b/apps/boot/boot0.js @@ -39,25 +39,7 @@ E.setTimeZone(s.timezone); delete s; // stop users doing bad things! global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); } -// check for alarms -var alarms = require('Storage').readJSON('alarm.json',1)||[]; -var time = new Date(); -var active = alarms.filter(a=>a.on&&(a.last!=time.getDate())); -if (active.length) { - active = active.sort((a,b)=>a.hr-b.hr); - var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600); - if (!require('Storage').read("alarm.js")) { - console.log("No alarm app!"); - require('Storage').write('alarm.json',"[]") - } else { - var t = 3600000*(active[0].hr-hr); - if (t<1000) t=1000; - /* execute alarm at the correct time. We avoid execing immediately - since this code will get called AGAIN when alarm.js is loaded. alarm.js - will then clearInterval() to get rid of this call so it can proceed - normally. */ - setTimeout(function() { - load("alarm.js"); - },t); - } -} +// Load *.boot.js files +var clockApps = require('Storage').list(/\.boot\.js/).map(bootFile=>{ + eval(require('Storage').read(bootFile)); +}); diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js index febc4fc19..76d655671 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -12,7 +12,11 @@ if (!settings.welcomed && require("Storage").read("welcome.js")!==undefined) { clockApp = require("Storage").read(clockApps[0].src); delete clockApps; } - if (!clockApp) clockApp='E.showMessage("No Clock Found")'; + if (!clockApp) clockApp=`E.showMessage("No Clock Found"); + setWatch(() => { + Bangle.showLauncher(); + }, BTN2, {repeat:false,edge:"falling"});) + `; delete settings; // check to see if our clock is wrong - if it is use GPS time if ((new Date()).getFullYear()==1970) { diff --git a/apps/chrono/chrono-icon.js b/apps/chrono/chrono-icon.js new file mode 100644 index 000000000..580713825 --- /dev/null +++ b/apps/chrono/chrono-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwglihGIxAWUwADBDCYTDhAXSFwQEGIxowBL4QXTx///AXWF6qnBwCTDO6EIF4KnEDwLWO/4QFx7FNdwQQEGwP4GBYUB/4QBDIYXMIgQAEDIIKCVwItJFggFEx4uKCAQUBX4QDC/B2KhASCAQP/AQQcDLpQlCLgQsCCoIGBC5IkCFon/xwxCDgIXJFwYxFHIR3ILwIkBCIeIFwQHBHgReIJAgCBOoP+MYZIHhB1EDgIRBA4ZIJC4LrEMYvoAgQXJxHvI4gtDC5OIF4QSDbYY3EC5QAKG4QXNPwg0BSBAJCIQhLCDwgXKIAwXUMo4XPFwrwKC4YOCUooVCR453DIxIXJU4IqDxwXJa45FDdgxnEC40IC4TbINQYXIRQZwDAAXv/xuBCwoXBVAgXDA4wXGSARcEC4o7BRwx4DOon+C4YiCLwxIDDAobDEYJGIGAYYBxDAD9AJDC5IwCDIYACJARGIDAapDaooWLDAZhEAoIWNMggADCqAAPA")) \ No newline at end of file diff --git a/apps/chrono/chrono.js b/apps/chrono/chrono.js new file mode 100644 index 000000000..cd50b8a22 --- /dev/null +++ b/apps/chrono/chrono.js @@ -0,0 +1,73 @@ +function msToTime(duration) { + var milliseconds = parseInt((duration % 1000) / 100), + seconds = Math.floor((duration / 1000) % 60), + minutes = Math.floor((duration / (1000 * 60)) % 60), + hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return hours + ":" + minutes + ":" + seconds; +} + + +var counter = 0; +var started = false; + +function drawInterface() { + g.clear(); + g.setFontAlign(0, 0); + g.setFont("6x8", 2); + g.drawString("+5m", g.getWidth() - 30, 30); + g.drawString("+30s", g.getWidth() - 30, g.getHeight() / 2); + g.drawString("+5s", g.getWidth() - 30, g.getHeight() - 30); + + g.setFontAlign(0, 0); // center font + g.setFont("6x8", 3); + // draw the current counter value + + g.drawString(msToTime(counter * 1000), g.getWidth() / 2 - 30, g.getHeight() / 2); + // optional - this keeps the watch LCD lit up + g.flip(); +} + +function countDown() { + if (counter > 0) { + if (started) { + counter--; + drawInterface(); + } + } else { + if (started) { + Bangle.buzz(); + } + } +} + +setWatch((p) => { + if (p.time - p.lastTime < 0.1) { + counter = 0; + started = false; + } else { + counter += 60 * 5; + } + drawInterface(); +}, BTN1, { repeat: true }); + +setWatch(() => { + counter += 30; + drawInterface(); +}, BTN2, { repeat: true }); + +setWatch(() => { + counter += 5; + drawInterface(); +}, BTN3, { repeat: true }); + +Bangle.on('touch', function (button) { + started = !started; +}); + +var interval = setInterval(countDown, 1000); +drawInterface(); \ No newline at end of file diff --git a/apps/chrono/chrono.png b/apps/chrono/chrono.png new file mode 100644 index 000000000..c1aaf180d Binary files /dev/null and b/apps/chrono/chrono.png differ diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog new file mode 100644 index 000000000..081a638f6 --- /dev/null +++ b/apps/cliock/ChangeLog @@ -0,0 +1 @@ +0.07: Submitted to App Loader diff --git a/apps/cliock/app-icon.js b/apps/cliock/app-icon.js new file mode 100644 index 000000000..fab023339 --- /dev/null +++ b/apps/cliock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("kEgwkBiIA/ACBhLB6gqKB6g//B6I4DiDqCB40QB4MBAoIXDB40BAIIPNG44PLAoQvMB5RPEB5JvEBAav1f7wA/ABoA==")) diff --git a/apps/cliock/app.js b/apps/cliock/app.js new file mode 100644 index 000000000..20086464e --- /dev/null +++ b/apps/cliock/app.js @@ -0,0 +1,51 @@ +var fontsize = 3; +var locale = require("locale"); +var marginTop = 40; +var flag = false; +var WeekDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; + +function drawAll(){ + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + updateTime(); + updateRest(new Date()); +} + +function updateRest(now){ + let date = locale.date(now,false); + writeLine(WeekDays[now.getDay()],1); + writeLine(date,2); +} +function updateTime(){ + if (!Bangle.isLCDOn()) return; + let now = new Date(); + let h = now.getHours(); + let m = now.getMinutes(); + h = h>=10?h:"0"+h; + m = m>=10?m:"0"+m; + writeLine(h+":"+m,0); + writeLine(flag?" ":"_",3); + flag = !flag; + if(now.getMinutes() == 0) + updateRest(now); +} +function writeLineStart(line){ + g.drawString(">",4,marginTop+line*30); +} +function writeLine(str,line){ + g.setFont("6x8",fontsize); + g.setColor(0,1,0); + g.setFontAlign(-1,-1); + g.clearRect(0,marginTop+line*30,((str.length+1)*20),marginTop+25+line*30); + writeLineStart(line); + g.drawString(str,25,marginTop+line*30); +} + +drawAll(); +Bangle.on('lcdPower',function(on) { + if (on) + drawAll(); +}); +var click = setInterval(updateTime, 1000); +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); diff --git a/apps/cliock/app.png b/apps/cliock/app.png new file mode 100644 index 000000000..4ad2d056d Binary files /dev/null and b/apps/cliock/app.png differ diff --git a/apps/daysl/ChangeLog b/apps/daysl/ChangeLog new file mode 100644 index 000000000..4e0b8e6cf --- /dev/null +++ b/apps/daysl/ChangeLog @@ -0,0 +1,2 @@ +0.01: New Widget! +0.02: Improved calculation, new image for app diff --git a/apps/daysl/app-icon.js b/apps/daysl/app-icon.js new file mode 100644 index 000000000..bdc0da744 --- /dev/null +++ b/apps/daysl/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgmIAH4A/AH4A/AEEAAAgGOC/4XLAgoGIDgYXTwEIBY4JEAw8YCIOAEY4+EAwwTCL44XNO5IX/C6i6LC8YABa5AXOF67vIwA5DAw5GDMhg7HjAXWIwQLFZIoGNC/4XKAH4A/AH4A/ADoA=")) \ No newline at end of file diff --git a/apps/daysl/app.js b/apps/daysl/app.js new file mode 100644 index 000000000..56f85e615 --- /dev/null +++ b/apps/daysl/app.js @@ -0,0 +1,67 @@ +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const storage = require('Storage'); +let settings; + +function updateSettings() { + storage.write('daysleft.json', settings); +} + +function resetSettings() { + settings = { + day : 17, + month : 6, + year: 1981 + }; + updateSettings(); +} + +settings = storage.readJSON('daysleft.json',1); +if (!settings) resetSettings(); + +function showMenu() { + const datemenu = { + '': { + 'title': 'Set Date', + 'predraw': function() { + datemenu.Date.value = settings.day; + datemenu.Month.value = settings.month; + datemenu.Year.value = settings.year; + } + }, + 'Day': { + value: settings.day, + min: 1, + max: 31, + step: 1, + onchange: v => { + settings.day = v; + updateSettings(); + } + }, + 'Month': { + value: settings.month, + min: 1, + max: 12, + step: 1, + onchange: v => { + settings.month = v; + updateSettings(); + } + }, + 'Year': { + value: settings.year, + step: 1, + onchange: v => { + settings.year = v; + updateSettings(); + } + } + }; + datemenu['-Exit-'] = ()=>{load();}; + return E.showMenu(datemenu); +} + +showMenu(); \ No newline at end of file diff --git a/apps/daysl/app.png b/apps/daysl/app.png new file mode 100644 index 000000000..42839ae59 Binary files /dev/null and b/apps/daysl/app.png differ diff --git a/apps/daysl/widget.js b/apps/daysl/widget.js new file mode 100644 index 000000000..6fb755d1e --- /dev/null +++ b/apps/daysl/widget.js @@ -0,0 +1,39 @@ +const storage = require('Storage'); +let settings; + +function updateSettings() { + storage.write('daysleft.json', settings); + } + + function resetSettings() { + settings = { + day : 17, + month : 6, + year: 2020 + }; + updateSettings(); + } + + settings = storage.readJSON('daysleft.json',1); + if (!settings) resetSettings(); + + var dd = settings.day, + mm = settings.month-1, //month is zero-based + yy = settings.year; + + const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + const targetDate = new Date(yy, mm, dd); + const today = new Date(); + + //create date object with today, but 00:00:00 + const currentYear = today.getFullYear(); + const currentMonth = today.getMonth(); + const currentDay = today.getDate(); + const todayMorning = new Date (currentYear, currentMonth, currentDay, 0, 0, 0); + + const diffDays = (targetDate - todayMorning) / oneDay; + +WIDGETS["daysl"]={area:"tl",width:40,draw:function(){ + g.setFont("6x8", 1); + g.drawString(diffDays,this.x+12,this.y+12); +}}; \ No newline at end of file diff --git a/apps/dclock/ChangeLog b/apps/dclock/ChangeLog new file mode 100644 index 000000000..edf7da4c2 --- /dev/null +++ b/apps/dclock/ChangeLog @@ -0,0 +1,9 @@ +0.01: branched from simple clock and added seconds +0.02: add timestamp (tst) +0.03: fix timestamp round to whole number +0.04: add iso datetime and move day of the week (d) / month names (m) +0.05: add beats (@) +0.06: tidy up +0.07: add days in current month (md) and days since new moon (l) +0.08: update icon +0.09: Use localised month and day of the week from locale diff --git a/apps/dclock/clock-dev-icon.js b/apps/dclock/clock-dev-icon.js new file mode 100644 index 000000000..f36dcaee3 --- /dev/null +++ b/apps/dclock/clock-dev-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEIf4A5/8wgf/AwUB/8gh/zA4QMCl/xA4cAichgIaBiEDgMgmECDQMAkMA+EgiYvDkQJBkcQgMQDwMggUiiECG4MikEBmQWCgURiEREQIXBCIMxkIIBAoMSiQ4BGoIABKgPykRSBI4JfC+c/iARBl8zmBfEAAUvIgIAUkbAtgalB+ADDBIKSBHgUgmYJCAAa6BmCoBAYMiBIMRC4UQmEAAoQvFmUDAYUSmcxWIKMBEQKrBOw0yh8wmcyj4nBIYQDB+cwBAQA/ABUxgUDkBqBgchkMiiUikMRgSOBkR3BkEhC4MgiQHBiADBC4UQAYMRiUxkECAAITBC4MSiUQF4MTiQTBBAIDBkcCiMxkUTAYIvCAH4A/AH4AKiIPPgMxiESgUQgECgMBdAMiiUgC48ikUBiEBiIXDGQURiIbBF48RkAvCEwIvCkERgQMBRHpDBOoRhBNoJOBJIkiKYMjgcTOoMhLQMQmMDDIMjQQInEC4MhiUSkQHCC4MAkAXCiUjiZ5UiR5jLwLaBAQJ1BAgIAMCgMxMwMgkciAoMjC5pqBRwPxCoMiiUyGBsgiBBBiESVAKzBf+YACA==")) \ No newline at end of file diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js new file mode 100644 index 000000000..d2c08726a --- /dev/null +++ b/apps/dclock/clock-dev.js @@ -0,0 +1,112 @@ +var locale = require("locale"); +/* jshint esversion: 6 */ +const timeFontSize = 4; +const dateFontSize = 3; +const smallFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; +const yposTime = 50; +const yposDate = 85; +const yposTst = 115; +const yposDml = 170; +const yposDayMonth = 195; +const yposGMT = 220; + +// Check settings for what type our clock should be +var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; + +function getUTCTime(d) { + return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d)}); +} + +function drawSimpleClock() { + // get date + var d = new Date(); + var da = d.toString().split(" "); + var dutc = getUTCTime(d); + + g.reset(); // default draw styles + // drawSting centered + g.setFontAlign(0, 0); + + // draw time + var time = da[4].split(":"); + var hours = time[0], + minutes = time[1], + seconds = time[2]; + + var meridian = ""; + 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); + } + + // Time + g.setFont(font, timeFontSize); + g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true); + g.setFont(font, smallFontSize); + g.drawString(meridian, xyCenter + 102, yposTime + 10, true); + + // Date String + g.setFont(font, dateFontSize); + g.drawString(`${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}`, xyCenter, yposDate, true); + + // Timestamp + var tst = Math.round(d.getTime()); + g.setFont(font, smallFontSize); + g.drawString(`tst:${tst}`, xyCenter, yposTst, true); + + //Days in month + var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate(); + + //Days since full moon + var knownnew = new Date(2020,02,24,09,28,0); + + // Get millisecond difference and divide down to cycles + var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53; + + // Multiply decimal component back into days since new moon + var sincenew = (cycles % 1)*29.53; + + // Draw days in month and sime since new moon + g.setFont(font, smallFontSize); + g.drawString(`md:${dom} l:${sincenew.toFixed(2)}`, xyCenter, yposDml, true); + + // draw Month name, Day of the week and beats + var beats = Math.floor((((dutc[0] + 1) % 24) + dutc[1] / 60 + dutc[2] / 3600) * 1000 / 24); + g.setFont(font, smallFontSize); + g.drawString(`m:${locale.month(d,true)} d:${locale.dow(d,true)} @${beats}`, xyCenter, yposDayMonth, true); + + // draw gmt + var gmt = da[5]; + g.setFont(font, smallFontSize); + g.drawString(gmt, xyCenter, yposGMT, true); +} + +// handle switch display on by pressing BTN1 +Bangle.on('lcdPower', function(on) { + if (on) drawSimpleClock(); +}); + +// clean app screen +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// refesh every 100 milliseconds +setInterval(drawSimpleClock, 100); + +// draw now +drawSimpleClock(); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); \ No newline at end of file diff --git a/apps/dclock/clock-dev.png b/apps/dclock/clock-dev.png new file mode 100644 index 000000000..0cfbff44c Binary files /dev/null and b/apps/dclock/clock-dev.png differ diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index 3a6dd3bfd..ad6b01d6a 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -2,4 +2,5 @@ 0.02: Increase contrast (darker notification background, white text) 0.03: Gadgetbridge widget now shows connection state 0.04: Tweaks for variable size widget system -0.05: Show incoming call notification \ No newline at end of file +0.05: Show incoming call notification + Optimize animation, limit title length diff --git a/apps/gbridge/PROTOCOL.md b/apps/gbridge/PROTOCOL.md new file mode 100644 index 000000000..1fd0ddb4a --- /dev/null +++ b/apps/gbridge/PROTOCOL.md @@ -0,0 +1,178 @@ +# Watch -> Phone + +## show toast + +``` +{ "t": "info", "msg": "message" } +``` + +t can be one of "info", "warn", "error" + +## report battery level + +``` +{ "t": "status", "bat": 30, "volt": 30 } +``` + +* bat is in range 0 to 100 +* volt is optional and should be greater than 0 + +## find phone + +``` +{ "t": "findPhone", "n": true } +``` + +n is an boolean and toggles the find phone function + +## control music player + +``` +{ "t": "music", "n": "play" } +``` + +n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind" + +## control phone call + +``` +{ "t": "call", "n": "accept"} +``` + +n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore" + +## react to notifications + +Send a response to a notification from phone + +``` +{ + "t": "notify", + "n": "dismiss", + "id": 2, + "tel": "+491234", + "msg": "message", +} +``` + +* n can be one of "dismiss", "dismiss all", "open", "mute", "reply" +* id, tel and message are optional + +# Phone -> Watch + +## show notification + +``` +{ + "t": "notify", + "id": 2, + "src": "app", + "title": "titel", + "subject": "subject", + "body": "message body", + "sender": "sender", + "tel": "+491234" + } +``` + +## notification deleted + +This event is send when the user skipped a notification + +``` +{ "t": "notify-", "id": 2 } +``` + +## set alarm + +``` +{ + "t": "alarm", + "d": [ + { "h": 13, "m": 37 }, + { "h": 8, "m": 0 } + ] +} +``` + +## call state changed + +``` +{ + "t": "call", + "cmd": "accept", + "name": "name", + "number": "+491234" +} +``` + +cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end" + +## music state changed + +``` +{ + "t": "musicstate", + "state": "play", + "position": 40, + "shuffle": 0, + "repeat": 1 +} +``` + +## set music info + +``` +{ + "t": "musicinfo", + "artist": "artist", + "album": "album", + "track": "track", + "dur": 1, + "c": 2, + "n" 3 +} +``` + +* dur is the duration of the track +* c is the track count +* n is the track number + +## find device + +``` +{ + "t": "find", + "n": true +} +``` + +n toggles find device functionality + +## set constant vibration + +``` +{ + "t": "vibrate", + "n": 2 +} +``` + +n is the intensity + +## send weather + +``` +{ + "t": "weather", + "temp": 10, + "hum": 71, + "txt": "condition", + "wind": 13, + "loc": "location" +} +``` + +* hum is the humidity +* txt is the weather condition +* loc is the location diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index 2042ea7a0..3f9c7053f 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -40,7 +40,7 @@ state.scrollPos = -size; } Bangle.setLCDOffset(state.scrollPos); - if (state.scrollPos > -size) setTimeout(anim, 10); + if (state.scrollPos > -size) setTimeout(anim, 15); } anim(); } @@ -84,7 +84,8 @@ g.setColor("#ffffff"); g.setFont("6x8", 2); - g.drawString(event.title, x, y + 25); + if (event.title) + g.drawString(event.title.slice(0,17), x, y + 25); g.setFont("6x8", 1); g.setColor("#ffffff"); diff --git a/apps/gpsrec/interface.html b/apps/gpsrec/interface.html index b87d75b3c..bc8dc48e1 100644 --- a/apps/gpsrec/interface.html +++ b/apps/gpsrec/interface.html @@ -5,32 +5,9 @@
- - + + + + diff --git a/apps/grocery/grocery.png b/apps/grocery/grocery.png new file mode 100644 index 000000000..93a29a4a6 Binary files /dev/null and b/apps/grocery/grocery.png differ diff --git a/apps/heart/interface.html b/apps/heart/interface.html index c2f115564..177e2cdfb 100644 --- a/apps/heart/interface.html +++ b/apps/heart/interface.html @@ -5,32 +5,9 @@
- - - - - - - + + + + diff --git a/appinfo.js b/js/appinfo.js similarity index 78% rename from appinfo.js rename to js/appinfo.js index 151227f45..613d15379 100644 --- a/appinfo.js +++ b/js/appinfo.js @@ -27,14 +27,19 @@ var AppInfo = { // then map each file to a command to load into storage fileContents.forEach(storageFile => { // format ready for Espruino - var js; if (storageFile.evaluate) { - js = storageFile.content.trim(); + let js = storageFile.content.trim(); if (js.endsWith(";")) js = js.slice(0,-1); - } else - js = toJS(storageFile.content); - storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${js});`; + storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${js});`; + } else { + let code = storageFile.content; + // write code in chunks, in case it is too big to fit in RAM (fix #157) + var CHUNKSIZE = 4096; + storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${toJS(code.substr(0,CHUNKSIZE))},0,${code.length});`; + for (var i=CHUNKSIZE;i reject(err)); diff --git a/comms.js b/js/comms.js similarity index 74% rename from comms.js rename to js/comms.js index de2c328b3..e2cbf0cdd 100644 --- a/comms.js +++ b/js/comms.js @@ -73,13 +73,13 @@ removeApp : app => { // expects an app structure })); }, removeAllApps : () => { - return Comms.reset("wipe").then(() => new Promise((resolve,reject) => { + return new Promise((resolve,reject) => { // Use write with newline here so we wait for it to finish - Puck.write('\x10E.showMessage("Erasing...");require("Storage").eraseAll();Bluetooth.println("OK")\n', (result,err) => { + Puck.write('\x10E.showMessage("Erasing...");require("Storage").eraseAll();Bluetooth.println("OK");reset()\n', (result,err) => { if (!result || result.trim()!="OK") return reject(err || ""); resolve(); }, true /* wait for newline */); - })); + }); }, setTime : () => { return new Promise((resolve,reject) => { @@ -147,5 +147,50 @@ readFile : (file) => { }); }); }); +}, +readStorageFile : (filename) => { // StorageFiles are different to normal storage entries + return new Promise((resolve,reject) => { + // Use "\xFF" to signal end of file (can't occur in files anyway) + var fileContent = ""; + var fileSize = undefined; + var connection = Puck.getConnection(); + connection.received = ""; + connection.cb = function(d) { + var finished = false; + var eofIndex = d.indexOf("\xFF"); + if (eofIndex>=0) { + finished = true; + d = d.substr(0,eofIndex); + } + fileContent += d; + if (fileSize === undefined) { + var newLineIdx = fileContent.indexOf("\n"); + if (newLineIdx>=0) { + fileSize = parseInt(fileContent.substr(0,newLineIdx)); + console.log("File size is "+fileSize); + fileContent = fileContent.substr(newLineIdx+1); + } + } else { + showProgress(undefined,100*fileContent.length / (fileSize||1000000)); + } + if (finished) { + hideProgress(); + connection.received = ""; + connection.cb = undefined; + resolve(fileContent); + } + }; + console.log(`Reading StorageFile ${JSON.stringify(filename)}`); + connection.write(`\x03\x10(function() { + var f = require("Storage").open(${JSON.stringify(filename)},"r"); + Bluetooth.println(f.getLength()); + var l = f.readLine(); + while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); } + Bluetooth.print("\xFF"); + })()\n`,() => { + showProgress(`Reading ${JSON.stringify(filename)}`,0); + console.log(`StorageFile read started...`); + }); + }); } }; diff --git a/index.js b/js/index.js similarity index 93% rename from index.js rename to js/index.js index bc326f3b2..b21fc907d 100644 --- a/index.js +++ b/js/index.js @@ -11,6 +11,7 @@ httpGet("apps.json").then(apps=>{ } appJSON.sort(appSorter); refreshLibrary(); + refreshFilter(); }); // Status @@ -228,6 +229,14 @@ function handleAppInterface(app) { id : msg.id }); }); + } else if (msg.type=="readstoragefile") { + Comms.readStorageFile(msg.data/*filename*/).then(function(result) { + iwin.postMessage({ + type : "readstoragefilersp", + data : result, + id : msg.id + }); + }); } }, false); iwin.postMessage({type:"init"}); @@ -250,9 +259,18 @@ function showTab(tabname) { // =========================================== Library -var activeFilter = ''; +var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value) +var hash = window.location.hash ? window.location.hash.slice(1) : ''; + +var activeFilter = !!~chips.indexOf(hash) ? hash : ''; var currentSearch = ''; +function refreshFilter(){ + var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); + filtersContainer.querySelector('.active').classList.remove('active'); + if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active') + else filtersContainer.querySelector('.chip[filterid]').classList.add('active') +} function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; @@ -333,22 +351,9 @@ function refreshLibrary() { }); } else if (icon.classList.contains("icon-menu")) { // custom HTML update - if (app.custom) { - icon.classList.remove("icon-menu"); - icon.classList.add("loading"); - handleCustomApp(app).then((appJSON) => { - if (appJSON) appsInstalled.push(appJSON); - showToast(app.name+" Uploaded!", "success"); - icon.classList.remove("loading"); - icon.classList.add("icon-delete"); - refreshMyApps(); - refreshLibrary(); - }).catch(err => { - showToast("Customise failed, "+err, "error"); - icon.classList.remove("loading"); - icon.classList.add("icon-menu"); - }); - } + icon.classList.remove("icon-menu"); + icon.classList.add("loading"); + customApp(app); } else if (icon.classList.contains("icon-delete")) { // Remove app icon.classList.remove("icon-delete"); @@ -366,6 +371,7 @@ function refreshLibrary() { }); } +refreshFilter(); refreshLibrary(); // =========================================== My Apps @@ -382,9 +388,23 @@ function removeApp(app) { }); } +function customApp(app) { + return handleCustomApp(app).then((appJSON) => { + if (appJSON) appsInstalled.push(appJSON); + showToast(app.name+" Uploaded!", "success"); + refreshMyApps(); + refreshLibrary(); + }).catch(err => { + showToast("Customise failed, "+err, "error"); + refreshMyApps(); + refreshLibrary(); + }); +} + function updateApp(app) { + if (app.custom) return customApp(app); showProgress(`Upgrading ${app.name}`,undefined,"sticky"); - Comms.removeApp(app).then(()=>{ + return Comms.removeApp(app).then(()=>{ showToast(app.name+" removed successfully. Updating...",); appsInstalled = appsInstalled.filter(a=>a.id!=app.id); return Comms.uploadApp(app); @@ -397,10 +417,13 @@ function updateApp(app) { }, err=>{ hideProgress("sticky"); showToast(app.name+" update failed, "+err,"error"); + refreshMyApps(); + refreshLibrary(); }); } + function appNameToApp(appName) { var app = appJSON.find(app=>app.id==appName); if (app) return app; @@ -506,13 +529,12 @@ Comms.watchConnectionChange(handleConnectionChange); var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); filtersContainer.addEventListener('click', ({ target }) => { - if (!target.hasAttribute('filterid')) return; if (target.classList.contains('active')) return; - activeFilter = target.getAttribute('filterid'); - filtersContainer.querySelector('.active').classList.remove('active'); - target.classList.add('active'); + activeFilter = target.getAttribute('filterid') || ''; + refreshFilter(); refreshLibrary(); + window.location.hash = activeFilter }); var librarySearchInput = document.querySelector("#searchform input"); diff --git a/utils.js b/js/utils.js similarity index 100% rename from utils.js rename to js/utils.js diff --git a/lib/interface.js b/lib/interface.js index 6156759b4..414c9d7fb 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -3,10 +3,21 @@ be used from within BangleApps See: README.md / `apps.json`: `interface` element -This exposes a 'Puck' object like the puck.js library, -and calls `onInit` when it's ready. `Puck` can be used -for sending/receiving data to the correctly connected +This exposes a 'Puck' object (a simple version of +https://github.com/espruino/EspruinoWebTools/blob/master/puck.js) +and calls `onInit` when it's ready. `Puck` can be used for +sending/receiving data to the correctly connected device with Puck.eval/write. + +Puck.write(data,callback) +Puck.eval(data,callback) + +There is also: + +Util.readStorageFile(filename,callback) +Util.eraseStorageFile(filename,callback) +Util.showModal(title) +Util.hideModal() */ var __id = 0, __idlookup = []; var Puck = { @@ -20,11 +31,48 @@ var Puck = { window.postMessage({type:"write",data:data,id:__id}); } }; + +var Util = { + readStorageFile : function(filename,callback) { + __id++; + __idlookup[__id] = callback; + window.postMessage({type:"readstoragefile",data:filename,id:__id}); + }, + eraseStorageFile : function(filename,callback) { + Puck.write(`\x10require("Storage").open(${JSON.stringify(filename)}","r").erase()\n`,callback); + }, + showModal : function(title) { + if (!Util.domModal) { + Util.domModal = document.createElement('div'); + Util.domModal.id = "status-modal"; + Util.domModal.classList.add("modal"); + Util.domModal.classList.add("active"); + Util.domModal.innerHTML = ` + `; + document.body.appendChild(Util.domModal); + } + Util.domModal.querySelector(".content").innerHTML = title; + Util.domModal.classList.add("active"); + }, + hideModal : function() { + if (!Util.domModal) return; + Util.domModal.classList.remove("active"); + } +}; window.addEventListener("message", function(event) { var msg = event.data; if (msg.type=="init") { onInit(); - } else if (msg.type=="evalrsp" || msg.type=="writersp") { + } else if (msg.type=="evalrsp" || msg.type=="writersp"|| msg.type=="readstoragefilersp") { var cb = __idlookup[msg.id]; delete __idlookup[msg.id]; cb(msg.data);