Merge pull request #1 from espruino/master

Updating to latest version
pull/470/head
Richard Hopkins 2020-05-17 17:41:37 +01:00 committed by GitHub
commit e975bad1ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 976 additions and 180 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
package-lock.json
.DS_Store
*.js.bak
appdates.csv

View File

@ -14,3 +14,7 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* Added espruinotools.js for pretokenisation
* Included image and compression tools in repo
* Added better upload of large files (incl. compression)
* URL fetch is now async
* Adding '#search' after the URL (when not the name of a 'filter' chip) will set up search for that term
* If `bin/pre-publish.sh` has been run and recent.csv created, add 'Sort By' chip
* New 'espruinotools' which fixes pretokenise issue when ID follows ID (fix #416)

View File

@ -6,7 +6,7 @@ Bangle.js App Loader (and 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/)
**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
submitting code to this repository you confirm that you are happy with it being MIT licensed,
and that it is not licensed in another way that would make this impossible.
@ -203,7 +203,7 @@ and which gives information about the app for the Launcher.
// added by BangleApps loader on upload - lists all files
// that belong to the app so it can be deleted
"data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*"
// added by BangleApps loader on upload - lists files that
// added by BangleApps loader on upload - lists files that
// the app might write, so they can be deleted on uninstall
// typically these files are not uploaded, but created by the app
// these can include '*' or '?' wildcards
@ -251,7 +251,7 @@ and which gives information about the app for the Launcher.
"storageFile":true // if supplied, file is treated as storageFile
},
{"wildcard":"appid.data.*" // wildcard of filenames used in storage
}, // this is mutually exclusive with using "name"
}, // this is mutually exclusive with using "name"
],
"sortorder" : 0, // optional - choose where in the list this goes.
// this should only really be used to put system
@ -341,9 +341,12 @@ See [apps/gpsrec/interface.html](the GPS Recorder) for a full example.
Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings".
To do so, the app needs to include a `settings.js` file, containing a single function
that handles configuring the app.
When the app settings are opened, this function is called with one
When the app settings are opened, this function is called with one
argument, `back`: a callback to return to the settings menu.
Usually it will save any information in `app.json` where `app` is the name
of your app - so you should change the example accordingly.
Example `settings.js`
```js
// make sure to enclose the function in parentheses
@ -352,7 +355,7 @@ Example `settings.js`
function save(key, value) {
settings[key] = value;
require('Storage').write('app.json',settings);
}
}
const appMenu = {
'': {'title': 'App Settings'},
'< Back': back,

View File

@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
"version":"0.16",
"version":"0.17",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
"type":"bootloader",
@ -78,7 +78,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.08",
"version":"0.09",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -108,7 +108,7 @@
{ "id": "mclock",
"name": "Morphing Clock",
"icon": "clock-morphing.png",
"version":"0.05",
"version":"0.06",
"description": "7 segment clock that morphs between minutes and hours",
"tags": "clock",
"type":"clock",
@ -168,7 +168,7 @@
"name": "Image background clock",
"shortName":"Image Clock",
"icon": "app.png",
"version":"0.05",
"version":"0.06",
"description": "A clock with an image as a background",
"tags": "clock",
"type" : "clock",
@ -589,7 +589,7 @@
"id": "ncstart",
"name": "NCEU Startup",
"icon": "start.png",
"version":"0.05",
"version":"0.06",
"description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome",
"storage": [
@ -1272,7 +1272,7 @@
"name": "Battery Chart",
"shortName":"Battery Chart",
"icon": "app.png",
"version":"0.09",
"version":"0.10",
"readme": "README.md",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool",
@ -1421,7 +1421,7 @@
"id": "metronome",
"name": "Metronome",
"icon": "metronome_icon.png",
"version": "0.04",
"version": "0.05",
"readme": "README.md",
"description": "Makes the watch blinking and vibrating with a given rate",
"tags": "tool",
@ -1435,7 +1435,8 @@
"name": "metronome.img",
"url": "metronome-icon.js",
"evaluate": true
}
},
{"name":"metronome.settings.js","url":"settings.js"}
]
},
{ "id": "blackjack",
@ -1469,7 +1470,7 @@
"name": "Round clock with seconds, minutes and date",
"shortName":"Round Clock",
"icon": "app.png",
"version":"0.02",
"version":"0.03",
"description": "Designed round clock with ticks for minutes and seconds and heart rate indication",
"tags": "clock",
"type": "clock",
@ -1572,7 +1573,7 @@
"id": "largeclock",
"name": "Large Clock",
"icon": "largeclock.png",
"version": "0.02",
"version": "0.03",
"description": "A readable and informational digital watch, with date, seconds and moon phase",
"readme": "README.md",
"tags": "clock",
@ -1591,12 +1592,10 @@
{
"name": "largeclock.settings.js",
"url": "settings.js"
},
{
"name": "largeclock.json",
"url": "largeclock.json",
"evaluate": true
}
],
"data": [
{"name":"largeclock.json"}
]
},
{ "id": "smtswch",
@ -1617,6 +1616,18 @@
{"name":"switch-off.img","url":"switch-off.js","evaluate":true}
]
},
{ "id": "miplant",
"name": "Xiaomi Plant Sensor",
"shortName":"Mi Plant",
"icon": "app.png",
"version":"0.01",
"description": "Reads and displays data from Xiaomi bluetooth plant moisture sensors",
"tags": "xiaomi,mi,plant,ble,bluetooth",
"storage": [
{"name":"miplant.app.js","url":"app.js"},
{"name":"miplant.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "simpletimer",
"name": "Timer",
@ -1699,7 +1710,7 @@
"version": "0.01",
"description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.",
"tags": "clock",
"readme": "README.md",
"readme": "README.md",
"type": "clock",
"allow_emulator":true,
"storage": [
@ -1708,7 +1719,46 @@
{ "name": "gallifr.settings.js", "url": "settings.js" }
],
"data": [
{"name":"app.json"}
{"name":"gallifr.json"}
]
},
{ "id": "rndmclk",
"name": "Random Clock Loader",
"icon": "rndmclk.png",
"version":"0.02",
"description": "Load a different clock whenever the LCD is switched on.",
"readme": "README.md",
"tags": "widget,clock",
"type":"widget",
"storage": [
{"name":"rndmclk.wid.js","url":"widget.js"}
]
},
{ "id": "dotmatrixclock",
"name": "Dotmatrix Clock",
"icon": "dotmatrixclock.png",
"version":"0.01",
"description": "A clear white-on-blue dotmatrix simulated clock",
"tags": "clock,dotmatrix,retro",
"type": "clock",
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"dotmatrixclock.app.js","url":"app.js"},
{"name":"dotmatrixclock.img","url":"dotmatrixclock-icon.js","evaluate":true}
]
},
{
"id": "jbm8b",
"name": "Magic 8 Ball",
"shortName": "Magic 8 Ball",
"icon": "app.png",
"description": "A simple fortune telling app",
"tags": "game",
"storage": [
{ "name": "jbm8b.app.js", "url": "app.js" },
{ "name": "jbm8b.img", "url": "app-icon.js", "evaluate": true }
],
"version": "0.03"
}
]
]

View File

@ -11,4 +11,4 @@
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}
]
}
}

View File

@ -6,4 +6,5 @@
0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon
0.08: Fix for Home button in the app and README added.
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state
0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381)

View File

@ -1,6 +1,7 @@
(() => {
let recordingInterval = null;
const Storage = require("Storage");
const switchableConsumers = {
none: 0,
lcd: 1,
@ -14,53 +15,44 @@
const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60 * 1000; //For testing
const recordingInterval10S = 10 * 1000; //For testing
var recordingInterval = null;
var compassEventReceived = false;
var gpsEventReceived = false;
var hrmEventReceived = false;
// draw your widget
function draw() {
let x = this.x;
let y = this.y;
g.setColor(0, 1, 0);
g.fillPoly([x + 5, y, x + 5, y + 4, x + 1, y + 4, x + 1, y + 20, x + 18, y + 20, x + 18, y + 4, x + 13, y + 4, x + 13, y], true);
g.setColor(0, 0, 0);
g.drawPoly([x + 5, y + 6, x + 8, y + 12, x + 13, y + 12, x + 16, y + 18], false);
g.reset();
// void
}
function onMag() {
function batteryChartOnMag() {
compassEventReceived = true;
// Stop handling events when no longer necessarry
Bangle.removeListener("mag", onMag);
Bangle.removeListener("mag", batteryChartOnMag);
}
function onGps() {
function batterChartOnGps() {
gpsEventReceived = true;
Bangle.removeListener("GPS", onGps);
Bangle.removeListener("GPS", batterChartOnGps);
}
function onHrm() {
function batteryChartOnHrm() {
hrmEventReceived = true;
Bangle.removeListener("HRM", onHrm);
Bangle.removeListener("HRM", batteryChartOnHrm);
}
function getEnabledConsumersValue() {
// Wait for an event from each of the devices to see if they are switched on
var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', onMag);
Bangle.on('GPS', onGps);
Bangle.on('HRM', onHrm);
Bangle.on('mag', batteryChartOnMag);
Bangle.on('GPS', batterChartOnGps);
Bangle.on('HRM', batteryChartOnHrm);
// Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => {
Bangle.removeAllListeners();
Bangle.removeListener('mag', batteryChartOnMag);
Bangle.removeListener('GPS', batterChartOnGps);
Bangle.removeListener('HRM', batteryChartOnHrm);
}, 2000);
if (Bangle.isLCDOn())
@ -112,14 +104,20 @@
}
function reload() {
WIDGETS["batchart"].width = 24;
console.log("Reloading BatteryChart widget");
WIDGETS["batchart"].width = 0;
if (recordingInterval) {
clearInterval(recordingInterval);
recordingInterval = null;
}
recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
}
// add the widget
WIDGETS["batchart"] = {
area: "tl", width: 24, draw: draw, reload: reload
area: "tl", width: 0, draw: draw, reload: reload
};
reload();

View File

@ -15,3 +15,4 @@
0.14: Move welcome loaders to *.boot.js
0.15: Added BLE HID option for Joystick and bare Keyboard
0.16: Detect out of memory errors and draw them onto the bottom of the screen in red
0.17: Don't modify beep/buzz behaviour if firmware does it automatically

View File

@ -21,20 +21,22 @@ if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
// Don't disconnect if something is already connected to us
if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep();
// Set time, vibrate, beep, etc
if (!s.vibrate) Bangle.buzz=Promise.resolve;
if (s.beep===false) Bangle.beep=Promise.resolve;
else if (s.beep=="vib") Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) Bangle.buzz=Promise.resolve;
if (s.beep===false) Bangle.beep=Promise.resolve;
else if (s.beep=="vib") Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};
}
Bangle.setLCDTimeout(s.timeout);
if (!s.timeout) Bangle.setLCDPower(1);
E.setTimeZone(s.timezone);

View File

@ -0,0 +1 @@
0.01: Create dotmatrix clock app

View File

@ -0,0 +1,28 @@
# Dotmatrix clock
A clock face simulating the classic dotmatrix displays. Shows time, date, compass, and heart rate.
![](dotmatrix-clock-screen-shot.png)
## Features
* Easy to read digits
* Simulated white-on-blue dotmatrix display
* Compass
* Heart rate monitor
* Multiple colour palletes, swipe to change
## Usage
### Sensor readings
When the display is activated by 'flipping' the watch up, the compass and heart sensors will be activated automatically, but if
you activate the LCD through a button press, then the sensors will remain off until you press button-1.
### Colours
The display defaults to blue, but you can change this to orange by swiping the screen
## Requests
If you have any feature requests, please send an email to the author paulcockrell@gmail.com`

354
apps/dotmatrixclock/app.js Executable file
View File

@ -0,0 +1,354 @@
/**
* BangleJS DotMatrixCLOCK
*
* + Original Author: Paul Cockrell https://github.com/paulcockrell
* + Created: May 2020
*/
const storage = require('Storage');
const settings = (storage.readJSON('setting.json', 1) || {});
const is12Hour = settings["12hour"] || false;
const timeout = settings.timeout || 20;
const font7x7 = {
"empty": "00000000",
"0": "3E61514945433E",
"1": "1808080808081C",
"2": "7E01013E40407F",
"3": "7E01013E01017E",
"4": "4141417F010101",
"5": "7F40407E01017E",
"6": "3E40407E41413E",
"7": "3F010202040408",
"8": "3E41413E41413E",
"9": "3E41413F01013E",
};
const font5x5 = {
"empty": "00000000",
"-": "0000FF0000",
"0": "0E1915130E",
"1": "0C0404040E",
"2": "1E010E101F",
"3": "1E010E011E",
"4": "11111F0101",
"5": "1F101E011E",
"6": "0E101E110E",
"7": "1F01020408",
"8": "0E110E110E",
"9": "0E110F010E",
"A": "040A0E1111",
"B": "1E111E111E",
"C": "0F1010100F",
"D": "1E1111111E",
"E": "1F101E101F",
"F": "1F101E1010",
"G": "0F1013110E",
"H": "11111F1111",
"I": "0E0404040E",
"J": "1F0404140C",
"L": "101010101F",
"M": "111B151111",
"N": "1119151311",
"O": "0E1111110E",
"P": "1E111E1010",
"R": "1E111E1111",
"S": "0F100E011E",
"T": "1F04040404",
"U": "111111110E",
"V": "1111110A04",
"W": "111115150A",
"Y": "110A040404",
};
// Char renderer
const COLORS = {
blue: {
BG: "#0297fe",
DARK: "#3b3ce8",
LIGHT: "#E9ffff",
},
orange: {
BG: "#f7b336",
DARK: "#ac721e",
LIGHT: "#f6fc0f",
}
};
let selectedColor = "blue";
let displayTimeoutRef, sensorTimeoutRef;
// Example
// binToHex(["0111110", "1000000", "1000000", "1111110", "1000001", "1000001", "0111110"])
function binToHex(bins) {
return bins.map(bin => ("00" + (parseInt(bin, 2).toString(16))).substr(-2).toUpperCase()).join("");
}
// Example
// hexToBin("3E40407E41413E")
function hexToBin(hexStr) {
const regEx = new RegExp("..", "g");
const bin = hexStr
.replace(regEx, el => el + '_')
.slice(0, -1)
.split('_')
.map(hex => ("00000000" + (parseInt(hex, 16)).toString(2)).substr(-8));
return bin;
}
function drawPixel(opts) {
g.setColor(opts.color);
g.fillRect(opts.x, opts.y, opts.x + opts.w, opts.y + opts.h);
}
function drawGrid(pos, dims, charAsBin, opts) {
const defaultOpts = {
pxlW: 5,
pxlH: 5,
gap: 1,
offColor: COLORS[selectedColor].DARK,
onColor: COLORS[selectedColor].LIGHT
};
const pxl = Object.assign({}, defaultOpts, opts);
for (let rowY = 0; rowY < dims.rows; rowY++) {
const y = pos.y + ((pxl.pxlH + pxl.gap) * rowY);
for (let colX = 7; colX > (7 - dims.cols); colX--) {
const x = pos.x + ((pxl.pxlW + pxl.gap) * colX);
const color = (charAsBin && parseInt(charAsBin[rowY][colX])) ? pxl.onColor : pxl.offColor;
drawPixel({
x: x,
y: y,
w: pxl.pxlW,
h: pxl.pxlH,
color: color,
});
}
}
}
function drawFont(str, font, x, y) {
let fontMap, rows, cols;
switch(font) {
case "7x7":
fontMap = font7x7;
rows = cols = 7;
break;
case "5x5":
fontMap = font5x5;
rows = cols = 5;
break;
default:
throw "Unknown font type: " + font;
}
const pxlW = 2;
const pxlH = 2;
const gap = 2;
const gutter = 3;
const charArr = str.split("");
const gridWidthTotal = (rows * (pxlW + gap)) + gutter;
for (let i = 0; i < charArr.length; i++) {
const charAsBin = fontMap.hasOwnProperty(charArr[i])?
hexToBin(fontMap[charArr[i]]):
fontMap.empty;
drawGrid(
{x: x + (i * gridWidthTotal), y: y},
{rows: rows, cols: cols},
charAsBin,
{pxlW: pxlW, pxlH: pxlH, gap: gap}
);
}
}
function drawTitles() {
g.setColor("#ffffff");
g.setFont("6x8");
g.drawString("COMPASS", 52, 49);
g.drawString("HEART", 122, 49);
g.drawString("TIME", 52, 94);
g.drawString("DATE", 52, 144);
}
function drawCompass(lastHeading) {
const directions = [
'N',
'NE',
'E',
'SE',
'S',
'SW',
'W',
'NW'
];
const cps = Bangle.getCompass();
let angle = cps.heading;
let heading = angle?
directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]:
"-- ";
heading = (heading + " ").slice(0, 3);
if (lastHeading != heading) drawFont(heading, "5x5", 40, 67);
setTimeout(drawCompass.bind(null, heading), 1000 * 2);
}
function drawHeart(hrm) {
drawFont((" " + (hrm ? hrm.bpm : "---")).slice(-3), "5x5", 109, 67);
}
function drawTime(lastHrs, lastMns, toggle) {
const date = new Date();
const h = date.getHours();
const hrs = ("00" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2);
const mns = ("00" + date.getMinutes()).substr(-2);
if (lastHrs != hrs) {
drawFont(hrs, "7x7", 48, 109);
}
if (lastMns != mns) {
drawFont(mns, "7x7", 124, 109);
}
const color = toggle? COLORS[selectedColor].LIGHT : COLORS[selectedColor].DARK;
// This should toggle on/off per second
drawPixel({
color: color,
x: 118, y: 118,
w: 2, h: 2,
});
drawPixel({
color: color,
x: 118, y: 125,
w: 2, h: 2,
});
setTimeout(drawTime.bind(null, hrs, mns, !toggle), 1000);
}
function drawDate(lastDate) {
const locale = require('locale');
const date = new Date();
if (lastDate != date.toISOString().split('T')[0]) {
const dow = locale.dow(date, 1).toUpperCase();
const dayNum = ("00" + date.getDate()).slice(-2);
const mon = locale.month(date).toUpperCase().slice(0, 3);
const yr = date.getFullYear().toString().slice(-2);
drawFont(dow + " " + dayNum, "5x5", 40, 159);
drawFont(mon + " " + yr, "5x5", 40, 189);
}
setTimeout(drawDate.bind(null, date.toISOString().split('T')), 1000 * 60);
}
function setSensors(state) {
// Already reading sensors and trying to activate sensors, do nothing
if (sensorTimeoutRef && state === 1) return;
// If we are activating the sensors, turn them off again in one minute
if (state === 1) {
sensorTimeoutRef = setTimeout(() => { setSensors(0); }, 1000 * 60);
} else {
if (sensorTimeoutRef) {
clearInterval(sensorTimeoutRef);
sensorTimeoutRef = null;
}
// Bit nasty, but we only redraw the heart value on sensor callback
// but we want to blank out when sensor is off, but no callback for
// that so force redraw here
drawHeart();
}
Bangle.setHRMPower(state);
Bangle.setCompassPower(state);
}
function drawScreen() {
g.setBgColor(COLORS[selectedColor].BG);
g.clearRect(0, 24, g.getWidth(), g.getHeight());
// Draw components
drawTitles();
drawCompass();
drawHeart();
drawTime();
drawDate();
}
function clearTimers(){
if (displayTimeoutRef) {
clearInterval(displayTimeoutRef);
displayTimeoutRef = null;
}
if (sensorTimeoutRef) {
clearInterval(sensorTimeoutRef);
sensorTimeoutRef = null;
}
}
function resetDisplayTimeout() {
if (displayTimeoutRef) clearInterval(displayTimeoutRef);
Bangle.setLCDPower(true);
displayTimeoutRef = setTimeout(() => {
if (Bangle.isLCDOn()) Bangle.setLCDPower(false);
clearTimers();
}, 1000 * timeout);
}
// Turn sensors on
setSensors(1);
// Reset screen
g.clear();
// Load and draw widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Draw screen
drawScreen();
resetDisplayTimeout();
// Setup callbacks
Bangle.on('swipe', (sDir) => {
selectedColor = selectedColor === "blue" ? "orange" : "blue";
resetDisplayTimeout();
drawScreen();
});
Bangle.on('HRM', drawHeart);
setWatch(() => {
setSensors(1);
resetDisplayTimeout();
}, BTN1, {repeat: true, edge: "falling"});
setWatch(() => {
setSensors(0);
clearTimers();
Bangle.setLCDMode();
Bangle.showLauncher();
}, BTN2, {repeat: false, edge: "falling"});
Bangle.on('lcdPower', (on) => {
if(on) {
resetDisplayTimeout();
} else {
clearTimers();
setSensors(0);
}
});
Bangle.on('faceUp', (up) => {
if (up && !Bangle.isLCDOn()) {
setSensors(1);
resetDisplayTimeout();
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AAmBAEgrFAAZelFo+s1krAEcAE4IuFHIIJBAEXXE4KSEF84nCF4WBgErBYoAfEoaTBF42zF8PXF5QNBi4AgMIYv/F9nX64CDAw4ACl8vBIgGGF/4AOEgKPfI4xfoF96P/R/6PdACAv/F/4v/F/4v/F8HX68Xl8vAwIDCBIQADBIQQDBoQQDF/4AOGQqPbLAxmGL5gGDF/4AfF/6PRBIQQDSwwv/ABwoCR7xYGMwxfhF94AeF/4vr1nXBoIAf64mCF4gJEF8IkCF4YABFYQLDAEItBwIuCF9InBF4iSBwMrAEgnBFwgACXsIADFo4ABqwAkFQg="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -201,7 +201,7 @@ function showSortAppsManually() {
function setSortorder(app, val) {
app = store.readJSON(app.id + '.info', 1);
app.sortorder = val;
store.writeJSON(app.id + '.info', app);
store.write(app.id + '.info', JSON.stringify(app));
}
function getAppsList() {

View File

@ -10,7 +10,7 @@ const cirRad = 2*Math.PI;
const proportion = 0.3; // relative size of hour hand
const thickness = 4; // thickness of decorative lines
// retrieve settings from menu
let settings = require('Storage').readJSON('app.json',1)||{};
let settings = require('Storage').readJSON('gallifr.json',1)||{};
const decoration = !settings.decoration;
const widgets = !settings.widgets;
if (widgets) {
@ -133,21 +133,21 @@ const drawDecoration = () => {
drawSegment(params);
params = {
fromX: 0.4,
fromY: 0.2,
fromY: 0.2,
toX: 0.6,
toY: 0.1
};
drawThickLine(params);
params = {
fromX: -0.2,
fromY: -0.05,
fromY: -0.05,
toX: -0.7,
toY: -0.7
};
drawThickLine(params);
params = {
fromX: -0.3,
fromY: 0.05,
fromY: 0.05,
toX: -0.95,
toY: -0.3
};
@ -166,7 +166,7 @@ const drawMinuteHand = () => {
break;
case "blue":
g.setColor(0,0,1);
break;
break;
case "80s":
g.setColor(1,0,0);
break;
@ -203,7 +203,7 @@ const drawClockFace = () => {
break;
case "blue":
g.setColor(0,0.3,0.8);
break;
break;
case "80s":
g.setColor(1,1,1);
break;

View File

@ -1,11 +1,11 @@
// make sure to enclose the function in parentheses
(function (back) {
let settings = require('Storage').readJSON('app.json',1)||{};
let settings = require('Storage').readJSON('gallifr.json',1)||{};
let colours = ["green","red","blue","80s"];
let onoff = ["on","off"];
function save(key, value) {
settings[key] = value;
require('Storage').write('app.json',settings);
require('Storage').writeJSON('gallifr.json',settings);
}
const appMenu = {
'': {'title': 'Clock Settings'},
@ -21,13 +21,13 @@
min:0,max:1,
format: m => onoff[m],
onchange: m => {save('widgets', m)}
},
},
'Decoration': {
value: 0|settings['decoration'],
min:0,max:1,
format: m => onoff[m],
onchange: m => {save('decoration', m)}
}
}
};
E.showMenu(appMenu)
})
})

3
apps/jbm8b/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: First working version
0.02: Added delay in replying for dramatic effect
0.03: Fixed apps.json entry

1
apps/jbm8b/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhBC/AGMrq2B1gAEwNWlYthq2s64AKGYIydFpoAEGLUrFqIADqxcXFqhiDFymBFy7GCF1owTRjCSVlYudeiGsF7/XlaNqSKBeP1mBwJxQMBReO1gaEleBMDBLN1hAC1hhBAoIwNCwQAGlZINqxvFGAIXOSBAXQN4hPBC5yQIVBxfBCAgvQSBC+NFAYRDMwJHOF654DqxkBYooALF6+sbIhkEF8Z3CRIWBR6AvXFAzvQF6wnIYQJgNd5AWNdoLoGBBAvPO5pfYH4IvUUwS/GVBzXBYCpHCq2s1mBDwKOWDwRgNPAwVVMCRLCwIABCZ6OJJSAATLxZgRACJeLAAMrFz9WFxiRgRpoADwIub1guQGDmsXhqSfRiL0G1jqkMRYxRwKLUGK2sFryVEq2B1gAEwNWFkIA/AH4A/AH4AQ"))

80
apps/jbm8b/app.js Normal file
View File

@ -0,0 +1,80 @@
const affirmative = [
'It is\ncertain.',
'It is\ndicededly\nso.',
'Without\na doubt.',
'Yes\ndefinitely.',
'You may\nrely\non it.',
'As I see,\nit yes.',
'Most\nlikely.',
'Outlook\ngood.',
'Yes.',
'Signs point\nto yes.'
];
const nonCommittal = [
'Reply hazy,\ntry again.',
'Ask again\nlater.',
'Better not\ntell you\nnow.',
'Cannot\npredict\nnow.',
'Concentrate\nand\nask again.'
];
const negative = [
'Don\'t\ncount on it.',
'My reply\nis no.',
'My sources\nsay no.',
'Outlook\nis not\nso\ngood.',
'Very\ndoubtful.'
];
const title = 'Magic 8 Ball';
const answers = [affirmative, nonCommittal, negative];
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
function predict() {
// affirmative, negative or non-committal
let max = answers.length;
const a = Math.floor(getRandomArbitrary(0, max));
// sets max compared to answer category
max = answers[a].length;
const b = Math.floor(getRandomArbitrary(0, max));
// get the answer
const response = answers[a][b];
return response;
}
function draw(msg) {
// console.log(msg);
g.clear();
E.showMessage(msg, title);
}
function reply(button) {
const theButton = (typeof button === 'undefined' || isNaN(button)) ? 1 : button;
const timer = Math.floor(getRandomArbitrary(0, theButton) * 1000);
// Thinking...
draw('...');
setTimeout('draw(predict());', timer);
}
function ask() {
draw('Ask me a\nYes or No\nquestion\nand\ntouch the\nscreen');
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
ask();
// Event Handlers
Bangle.on('touch', (button) => reply(button));
setWatch(ask, BTN1, { repeat: true, edge: "falling" });
setWatch(reply, BTN3, { repeat: true, edge: "falling" });
// Back to launcher
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });

BIN
apps/jbm8b/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,2 +1,3 @@
0.01: Init
0.02: fix 3/4 moon orientation
0.03: Change `largeclock.json` to 'data' file to allow settings to be preserved

View File

@ -9,10 +9,8 @@ const moonX = 215;
const moonY = 50;
const settings = require("Storage").readJSON("largeclock.json", 1);
const BTN1app = settings.BTN1;
const BTN3app = settings.BTN3;
console.log("BTN1app", BTN1app);
console.log("BTN3app", BTN3app);
const BTN1app = settings.BTN1 || "";
const BTN3app = settings.BTN3 || "";
function drawMoon(d) {
const BLACK = 0,
@ -174,14 +172,14 @@ Bangle.setLCDMode();
// Show launcher when middle button pressed
clearWatch();
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
setWatch(
if (BTN1app) setWatch(
function() {
load(BTN1app);
},
BTN1,
{ repeat: false, edge: "rising" }
);
setWatch(
if (BTN3app) setWatch(
function() {
load(BTN3app);
},

View File

@ -34,7 +34,7 @@
function onchange(v) {
settings[btn] = v;
s.write("largeclock.json", settings);
s.writeJSON("largeclock.json", settings);
}
const btnMenu = {

View File

@ -3,3 +3,4 @@
0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279)
0.05: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast
Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365)
0.06: Support 12 hour time

View File

@ -1,3 +1,4 @@
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
var locale = require("locale");
var CHARW = 34; // how tall are digits?
var CHARP = 2; // how chunky are digits?
@ -146,7 +147,7 @@ function drawDigits(lastText,thisText,n) {
x+=s+p+7;
}
}
function drawSeconds() {
function drawEverythingElse() {
var x = (CHARW + CHARP + 6)*5;
var y = Y + 2*CHARW + CHARP;
var d = new Date();
@ -154,6 +155,8 @@ function drawSeconds() {
g.setFont("6x8");
g.setFontAlign(-1,-1);
g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true);
// meridian
if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x, Y + 4, true);
// date
g.setFontAlign(0,-1);
var date = locale.date(d,false);
@ -164,13 +167,15 @@ function drawSeconds() {
function showTime() {
if (animInterval) return; // in animation - quit
var d = new Date();
var t = (" "+d.getHours()).substr(-2)+":"+
var hours = d.getHours();
if (is12Hour) hours = ((hours + 11) % 12) + 1;
var t = (" "+hours).substr(-2)+":"+
("0"+d.getMinutes()).substr(-2);
var l = lastTime;
// same - don't animate
if (t==l || l=="-----") {
drawDigits(l,t,0);
drawSeconds();
drawEverythingElse();
lastTime = t;
return;
}

View File

@ -2,3 +2,4 @@
0.02: Watch vibrates with every beat
0.03: Uses mean of three time intervalls to calculate bmp
0.04: App shows instructions, Widgets remain visible, color changed
0.05: Buzz intensity and beats per bar can be changed via settings-app

View File

@ -8,6 +8,7 @@ This metronome makes your watch blink and vibrate with a given rate.
* Use `BTN1` to increase the bmp value by one.
* Use `BTN3` to decrease the bmp value by one.
* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`.
* Intensity of buzzing and the beats per bar (default 4) can be changed with the settings-app. The first beat per bar will be marked in red.
## Attributions

View File

@ -6,31 +6,40 @@ var tindex=0; //index to iterate through time_diffs
Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app
const storage = require("Storage");
const SETTINGS_FILE = 'metronome.settings.json';
//return setting
function setting(key) {
//define default settings
const DEFAULTS = {
'beatsperbar': 4,
'buzzintens': 0.75,
};
if (!settings) { loadSettings(); }
return (key in settings) ? settings[key] : DEFAULTS[key];
}
//load settings
let settings;
function loadSettings() {
settings = storage.readJSON(SETTINGS_FILE, 1) || {};
}
function changecolor() {
const maxColors = 2;
const colors = {
0: { value: 0xFFFF, name: "White" },
// 1: { value: 0x000F, name: "Navy" },
// 2: { value: 0x03E0, name: "DarkGreen" },
// 3: { value: 0x03EF, name: "DarkCyan" },
// 4: { value: 0x7800, name: "Maroon" },
// 5: { value: 0x780F, name: "Purple" },
// 6: { value: 0x7BE0, name: "Olive" },
// 7: { value: 0xC618, name: "LightGray" },
// 8: { value: 0x7BEF, name: "DarkGrey" },
// 9: { value: 0x001F, name: "Blue" },
// 10: { value: 0x07E0, name: "Green" },
// 11: { value: 0x07FF, name: "Cyan" },
1: { value: 0xF800, name: "Red" },
// 13: { value: 0xF81F, name: "Magenta" },
// 14: { value: 0xFFE0, name: "Yellow" },
// 15: { value: 0xFFFF, name: "White" },
// 16: { value: 0xFD20, name: "Orange" },
// 17: { value: 0xAFE5, name: "GreenYellow" },
// 18: { value: 0xF81F, name: "Pink" },
const colors = {
0: { value: 0xF800, name: "Red" },
1: { value: 0xFFFF, name: "White" },
2: { value: 0x9492, name: "gray" },
3: { value: 0xFFFF, name: "White" },
4: { value: 0x9492, name: "gray" },
5: { value: 0xFFFF, name: "White" },
6: { value: 0x9492, name: "gray" },
7: { value: 0xFFFF, name: "White" },
};
g.setColor(colors[cindex].value);
if (cindex == maxColors-1) {
if (cindex == setting('beatsperbar')-1) {
cindex = 0;
}
else {
@ -42,11 +51,16 @@ function changecolor() {
function updateScreen() {
g.clearRect(0, 50, 250, 150);
changecolor();
Bangle.buzz(50, 0.75);
try {
Bangle.buzz(50, setting('buzzintens'));
}
catch(err) {
}
g.setFont("Vector",48);
g.drawString(Math.floor(bpm)+"bpm", 5, 60);
}
Bangle.on('touch', function(button) {
// setting bpm by tapping the screen. Uses the mean time difference between several tappings.
if (tindex < time_diffs.length) {

View File

@ -0,0 +1,48 @@
// This file should contain exactly one function, which shows the app's settings
/**
* @param {function} back Use back() to return to settings menu
*/
(function(back) {
const SETTINGS_FILE = 'metronome.settings.json';
// initialize with default settings...
let s = {
'beatsperbar': 4,
'buzzintens': 0.75,
};
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage');
const saved = storage.readJSON(SETTINGS_FILE, 1) || {};
for (const key in saved) {
s[key] = saved[key];
}
// creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) {
return function(value) {
s[key] = value;
storage.write(SETTINGS_FILE, s);
};
}
const menu = {
'': { 'title': 'Metronome' },
'< Back': back,
'beats per bar': {
value: s.beatsperbar,
min: 1,
max: 8,
step: 1,
onchange: save('beatsperbar'),
},
'buzz intensity': {
value: s.buzzintens,
min: 0.0,
max: 1.0,
step: 0.25,
onchange: save('buzzintens'),
},
};
E.showMenu(menu);
});

1
apps/miplant/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

1
apps/miplant/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+4AA/AH4A/AAPQ64AQ44ZKBYwvc6/QF9wwFF9XXF73I4IAH44/F54vdCpYQESAYvm5Avm44ADA4Yvmc44v/F/4v/F/4v/F/4v+zmjv4ABF9GrwoACrYvn2WGFwgABSh4AV0QtDFwYvmFwlhF9wuDF9QuEF82jFw4vmMIQuFv4vuEDOi0d/WgV/0YNGBIN/LrSvCABAkECAI4GAChKBF5QABCIYEDADKpCsIvJLQQ0EGoShJMBwACSJYvEB5GiMCYxJF4LuCLorTLF6IxGQILuBMYgAGC4QwP0YwHYwQbCF4K1CL4lhCohfQMBBiCFQQvEAgIsFAATxWSQyPDAYYSIHgRgZE4IsBF4QGCCI6NRMBboFXgTTIFyYwIPYWcGAb2CCI2iF6qwBD4aqERYQABIQ+cFywALLwgAqXoYvrSAQROA=="))

74
apps/miplant/app.js Normal file
View File

@ -0,0 +1,74 @@
function getImgHum() {
return require("heatshrink").decompress(atob("jUoxH+AEtlsoYYDS4ZYDAYaVDLAYFDSQYHDSIZYDBIaPDLAYLDRoZYDBoaLDLAYPDRIZYDCIaHDLAYTDQoZYDCoaDDOQYXAA+JxIYX1utDSwYBAAIzYGiwZUTgpODQpzPGGgY3OdI4aRDIIaMDJIYCDIztDGRwaJP5oaWDAwaRDBAbOC5YcKB5I="));
}
function getImgTemp() {
return require("heatshrink").decompress(atob("iUqxH+AA2sAAQLHCBASMCAoSLCPOBAAQRfI/5Hn3YACy4ACCL4ADCL5H/I/AQHCRAQJCQwQLCQgQNCQYRQCB4A/ADaPjYqTpSCRYQGCZALFA"));
}
function getImgFert() {
return require("heatshrink").decompress(atob("kklxH+AC+FwtbDbAfFAAVbEbgiGEbYiHEbQiEsIjiEQYjeEQiPdEQrXdEdKnTAAJsMD6QlJFZAAIGAIkPEaIkCrdhEaR9MT4gkLFAyjMYoojNUZ4jFEoxrGEBCJDEZSWEEZdhCwpsKJQiJFAgYgGEQwjLD4QjFCRD+KCAylGQ4gjXVhAiPEhAKDJIwiQEowIEEQo2GERgAKEYwAcEUQkDEL9VAAgHFETgAIDJwePEZwdTE5ggdMJt6AAQEEqwRMABYQDAAwkBF5AkKEBQAPEUR6ESAQicJIX+A=="));
}
var deviceInfo = {};
function parseDevice(device) {
var d = new DataView(device.serviceData["fe95"]);
var frame = d.getUint16(0,true);
var offset = 5;
if (frame&16) offset+=6; // mac address
if (frame&32) offset+=1; // capabilitities
if (frame&64) { // event
var l = d.getUint8(offset+2);
var code = d.getUint16(offset,true);
if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id};
event = deviceInfo[device.id];
switch (code) {
case 0x1004: event.temperature = d.getInt16(offset+3,true)/10; break;
case 0x1006: event.humidity = d.getInt16(offset+3)/10; break;
case 0x100D:
event.temperature = d.getInt16(offset+3,true)/10;
event.humidity = d.getInt16(offset+5)/10; break;
case 0x1008: event.moisture = d.getUint8(offset+3); break;
case 0x1009: event.fertility = d.getUint16(offset+3,true)/10; break;
// case 0x1007: break; // 3 bytes? got 84,0,0 or 68,0,0
default: event.code = code;
event.raw = new Uint8Array(d.buffer, offset+3, l);
break;
}
//print(event);
show(event);
}
}
/*
eg. {
"id": "c4:7c:8d:6a:ac:79 public",
"temperature": 16.6, "code": 4103,
"raw": new Uint8Array([246, 0, 0]),
"moisture": 46, "fertility": 20.8 }
*/
function show(event) {
g.reset().setFont("6x8");
var y = 45 + 50*Object.keys(deviceInfo).indexOf(event.id);
g.drawString(event.id.substr(0,17),0,y);
g.drawImage(getImgHum(),0,y+15);
g.setFont("6x8",2);
var t = (event.moisture===undefined) ? "?" : event.moisture;
g.drawString((t+" ").substr(0,3),35,y+25,true);
g.drawImage(getImgFert(),80,y+15);
t = Math.round(event.fertility) || "?";
g.drawString((t+" ").substr(0,3), 120, y+25, true);
g.drawImage(getImgTemp(),160,y+15);
t = Math.round(event.temperature) || "?";
g.drawString((t+" ").substr(0,3), 180, y+25, true);
g.flip();
}
g.clear();
g.setFont("6x8",2).setFontAlign(0,-1).drawString("Scanning...",120,24);
Bangle.loadWidgets()
Bangle.drawWidgets()
NRF.setScan(parseDevice, { filters: [{serviceData:{"fe95":{}}}], timeout: 2000 });

BIN
apps/miplant/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -6,3 +6,4 @@
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings
0.05: Don't overwrite existing settings on app update
0.06: Allow welcome to run after a fresh install

View File

@ -1,11 +1,8 @@
(function() {
let s = require('Storage').readJSON('ncstart.json', 1)
|| require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
let s = require('Storage').readJSON('ncstart.json', 1) || {};
if (!s.welcomed) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('ncstart.json', s)
require('Storage').write('ncstart.json', {welcomed: true})
load('ncstart.app.js')
})
}

2
apps/rndmclk/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New widget
0.02: Less invasive, change default clock setting instead of directly loading the new clock (no longer breaks Gadgetbridge notifications)

6
apps/rndmclk/README.md Normal file
View File

@ -0,0 +1,6 @@
# Summary
Random Clock is a widget that will randomly show one of the installed watch faces each time the LCD is turned on.
# How it works
Everytime the LCD is turned off, the widget randomly changes the clock. When you long press BTN 3 the next time,
you might (or might not, it's random after all) see another watch face.

BIN
apps/rndmclk/rndmclk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

35
apps/rndmclk/widget.js Normal file
View File

@ -0,0 +1,35 @@
(() => {
let currentClock = "";
/**
* Random value between zero (inclusive) and max (exclusive)
* @param {int} max
*/
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function loadRandomClock() {
// Find available clock apps (same way as in the bootloader)
var clockApps = require("Storage").list(/\.info$/).map(app => require("Storage").readJSON(app, 1) || {}).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder);
if (clockApps && clockApps.length > 0) {
var clockIndex = getRandomInt(clockApps.length);
// Only update the file if the clock really change to be nice to the FLASH mem
if (clockApps[clockIndex].src != currentClock) {
currentClock = clockApps[clockIndex].src;
settings = require("Storage").readJSON('setting.json', 1);
settings.clock = clockApps[clockIndex].src;
require("Storage").write('setting.json', settings);
}
}
}
Bangle.on('lcdPower', (on) => {
if (!on) {
loadRandomClock();
}
});
})();

View File

@ -8,3 +8,6 @@
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings
0.08: Don't overwrite existing settings on app update
0.09: Allow welcome to run after a fresh install
More useful app menu
BTN2 now goes to menu on release

View File

@ -285,7 +285,7 @@ setWatch(()=>{
if (sceneNumber == scenes.length-1) {
load();
}
}, BTN2, {repeat:true,edge:"rising"});
}, BTN2, {repeat:true,edge:"falling"});
setWatch(()=>move(-1), BTN1, {repeat:true});
(function migrateSettings(){

View File

@ -1,11 +1,8 @@
(function() {
let s = require('Storage').readJSON('welcome.json', 1)
|| require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('welcome.app.js')) {
let s = require('Storage').readJSON('welcome.json', 1) || {};
if (!s.welcomed) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('welcome.json', {welcomed: "yes"})
require('Storage').write('welcome.json', {welcomed: true})
load('welcome.app.js')
})
}

View File

@ -3,12 +3,16 @@
|| require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({
'': { 'title': 'Welcome App' },
'Run on Next Boot': {
'Run next boot': {
value: !settings.welcomed,
format: v => v ? 'OK' : 'No',
format: v => v ? 'Yes' : 'No',
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
},
'Run Now': () => load('welcome.app.js'),
'Turn off & run next': () => {
require('Storage').write('welcome.json', {welcomed: false});
Bangle.off();
},
'< Back': back,
})
})

18
bin/pre-publish.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
cd `dirname $0`/..
nodejs bin/sanitycheck.js || exit 1
echo "Sanity check passed."
echo "Finding app dates..."
# Create list of:
# appid,created_time,modified_time
cd apps
for appfolder in *; do
echo "$appfolder,$(git log --follow --format=%ai -- $appfolder | tail -n 1),$(git log --follow --format=%ai -- $appfolder | head -n 1)" ;
done | grep -v _example_ | grep -v unknown.png > ../appdates.csv
cd ..
echo "Ready to publish"

View File

@ -40,6 +40,12 @@
.chip {
cursor: pointer;
}
.filter-nav {
display: inline-block;
}
.sort-nav {
float: right;
}
.tile-content { position: relative; }
.link-github {
position:absolute;
@ -88,17 +94,26 @@
</div>
<div class="container bangle-tab" id="librarycontainer">
<div class="filter-nav">
<label class="chip active" filterid="">All</label>
<label class="chip" filterid="clock">Clocks</label>
<label class="chip" filterid="game">Games</label>
<label class="chip" filterid="tool">Tools</label>
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
<div>
<div class="filter-nav">
<label class="chip active" filterid="">Default</label>
<label class="chip" filterid="clock">Clocks</label>
<label class="chip" filterid="game">Games</label>
<label class="chip" filterid="tool">Tools</label>
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="sort-nav hidden">
<span>Sort by:</span>
<label class="chip active" sortid="">None</label>
<label class="chip" sortid="created">New</label>
<label class="chip" sortid="modified">Updated</label>
</div>
</div>
<div class="panel">
<div class="panel" style="clear:both">
<div class="panel-header">
<div class="input-group" id="searchform">
<input class="form-input" type="text" placeholder="Keywords...">

View File

@ -1,10 +1,13 @@
if (typeof btoa==="undefined")
function btoa(d) { return Buffer.from(d).toString('base64'); }
// Converts a string into most efficient way to send to Espruino (either json, base64, or compressed base64)
function toJS(txt) {
var json = JSON.stringify(txt);
var b64 = "atob("+JSON.stringify(btoa(json))+")";
var js = b64.length < json.length ? b64 : json;
if (heatshrink) {
if (typeof heatshrink !== "undefined") {
var ua = new Uint8Array(txt.length);
for (var i=0;i<txt.length;i++) ua[i] = txt.charCodeAt(i);
var c = heatshrink.compress(ua);

View File

@ -1,6 +1,6 @@
// EspruinoTools bundle (https://github.com/espruino/EspruinoTools)
// Created with https://github.com/espruino/EspruinoWebIDE/blob/gh-pages/extras/create_espruinotools_js.sh
// Based on EspruinoWebIDE 0.73.4
// Based on EspruinoWebIDE 0.73.7
/**
Copyright 2014 Gordon Williams (gw@pur3.co.uk)
@ -23,6 +23,7 @@ var Espruino;
*
* Common processors are:
*
* jsCodeChanged - called when the code in the editor changes with {code}
* sending - sending code to Espruino (no data)
* transformForEspruino - transform code ready to be sent to Espruino
* transformModuleForEspruino({code,name})
@ -123,6 +124,7 @@ Espruino.Core.Status = {
hasProgress : function() { return false; },
incrementProgress : function(amt) {}
};
var acorn = (function(){ var exports={};var module={};
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
@ -3988,6 +3990,7 @@ exports.nonASCIIwhitespace = nonASCIIwhitespace;
Object.defineProperty(exports, '__esModule', { value: true });
})));
return exports;})();
/**
Copyright 2014 Gordon Williams (gw@pur3.co.uk)
@ -4123,7 +4126,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
} else if (isIn(chNum,ch)) { // NUMBER
type = "NUMBER";
var chRange = chNum;
if (ch=="0") { // Handle
if (ch=="0") { // Handle
s+=ch;
nextCh();
if ("xXoObB".indexOf(ch)>=0) {
@ -4132,12 +4135,12 @@ Object.defineProperty(exports, '__esModule', { value: true });
if (ch=="x" || ch=="X") chRange="0123456789ABCDEFabcdef";
s+=ch;
nextCh();
}
}
}
while (isIn(chRange,ch) || ch==".") {
s+=ch;
nextCh();
}
}
} else if (isIn(chQuotes,ch)) { // STRING
type = "STRING";
var q = ch;
@ -4507,8 +4510,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
fileLoader.click();
}
/* Save a file with a save file dialog. callback(savedFileName) only called in chrome app case when we knopw the filename*/
function fileSaveDialog(data, filename, callback) {
// Save a file with a save file dialog
function fileSaveDialog(data, filename) {
function errorHandler() {
Espruino.Core.Notifications.error("Error Saving", true);
}
@ -4524,7 +4527,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
writer.onwriteend = function(e) {
writer.onwriteend = function(e) {
console.log('FileWriter: complete');
if (callback) callback(writableFileEntry.name);
};
console.log('FileWriter: writing');
writer.write(blob);
@ -4535,8 +4537,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
}, errorHandler);
});
} else {
var rawdata = new Uint8Array(data.length);
for (var i=0;i<data.length;i++) rawdata[i]=data.charCodeAt(i);
var a = document.createElement("a"),
file = new Blob([data], {type: "text/plain"});
file = new Blob([rawdata.buffer], {type: "text/plain"});
var url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
@ -4765,6 +4769,16 @@ Object.defineProperty(exports, '__esModule', { value: true });
console.log("GET chrome.storage.sync = "+JSON.stringify(value));
callback(value);
});
} else if (typeof window !== 'undefined' && window.localStorage) {
var data = {};
var value = window.localStorage.getItem("CONFIG");
console.log("GET window.localStorage = "+JSON.stringify(value));
try {
data = JSON.parse(value);
} catch (e) {
console.log("Invalid config data");
}
callback(data);
} else if (typeof document != "undefined") {
var data = {};
var cookie = document.cookie;
@ -4786,8 +4800,11 @@ Object.defineProperty(exports, '__esModule', { value: true });
function _set(data) {
if (typeof chrome !== 'undefined' && chrome.storage) {
console.log("SET chrome.storage.sync = "+JSON.stringify(data));
console.log("SET chrome.storage.sync = "+JSON.stringify(data,null,2));
chrome.storage.sync.set({ CONFIGS : data });
} else if (typeof window !== 'undefined' && window.localStorage) {
console.log("SET window.localStorage = "+JSON.stringify(data,null,2));
window.localStorage.setItem("CONFIG",JSON.stringify(data));
} else if (typeof document != "undefined") {
document.cookie = "CONFIG="+btoa(JSON.stringify(data));
}
@ -4811,7 +4828,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
addSection("General", { sortOrder:100, description: "General Web IDE Settings" });
addSection("Communications", { sortOrder:200, description: "Settings for communicating with the Espruino Board" });
addSection("Board", { sortOrder:300, description: "Settings for the Espruino Board itself" });
}
function add(name, options) {
@ -5076,6 +5092,7 @@ To add a new serial device, you must add an object to
}
}
var portInfo = { port:serialPort };
connectionInfo = undefined;
flowControlXOFF = false;
currentDevice = portToDevice[serialPort];
@ -5088,7 +5105,6 @@ To add a new serial device, you must add an object to
connectionInfo = cInfo;
connectedPort = serialPort;
console.log("Connected", cInfo);
var portInfo = { port:serialPort };
if (connectionInfo.portName)
portInfo.portName = connectionInfo.portName;
Espruino.callProcessor("connected", portInfo, function() {
@ -5127,8 +5143,8 @@ To add a new serial device, you must add an object to
sendingBinary = false;
flowControlXOFF = false;
Espruino.callProcessor("disconnected", undefined, function() {
disconnectCallback();
Espruino.callProcessor("disconnected", portInfo, function() {
disconnectCallback(portInfo);
});
});
};
@ -5608,9 +5624,6 @@ To add a new serial device, you must add an object to
// When code is sent to Espruino, search it for modules and add extra code required to load them
Espruino.addProcessor("transformForEspruino", function(code, callback) {
if (Espruino.Config.ROLLUP) {
return loadModulesRollup(code, callback);
}
loadModules(code, callback);
});
@ -5787,24 +5800,8 @@ To add a new serial device, you must add an object to
callback(loadedModuleData.join("\n") + "\n" + code);
});
}
}
};
function loadModulesRollup(code, callback) {
rollupTools.loadModulesRollup(code)
.then(generated => {
const minified = generated.code;
console.log('rollup: '+minified.length+' bytes');
// FIXME: needs warnings?
Espruino.Core.Notifications.info('Rollup no errors. Bundling ' + code.length + ' bytes to ' + minified.length + ' bytes');
callback(minified);
})
.catch(err => {
console.log('rollup:error', err);
Espruino.Core.Notifications.error("Rollup errors - Bundling failed: " + String(err).trim());
callback(code);
});
}
Espruino.Core.Modules = {
init : init
@ -6436,18 +6433,18 @@ To add a new serial device, you must add an object to
This Source Code is subject to the terms of the Mozilla Public
License, v2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
------------------------------------------------------------------
Try and get any URLS that are from GitHub
------------------------------------------------------------------
**/
"use strict";
(function(){
function init() {
Espruino.addProcessor("getURL", getGitHub);
Espruino.addProcessor("getURL", getGitHub);
}
function getGitHub(data, callback) {
var match = data.url.match(/^https?:\/\/github.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.*)$/);
if (match) {
@ -6457,7 +6454,7 @@ To add a new serial device, you must add an object to
branch : match[3],
path : match[4]
};
var url = "https://raw.githubusercontent.com/"+git.owner+"/"+git.repo+"/"+git.branch+"/"+git.path;
console.log("Found GitHub", JSON.stringify(git));
callback({url: url});
@ -6484,6 +6481,7 @@ To add a new serial device, you must add an object to
(function(){
if (typeof acorn == "undefined") {
console.log("pretokenise: needs acorn, disabling.");
return;
}
function init() {
@ -6587,11 +6585,12 @@ To add a new serial device, you must add an object to
var tp = "?";
if (tk.type.label=="template" || tk.type.label=="string") tp="STRING";
if (tk.type.label=="num") tp="NUMBER";
if (tk.type.keyword) tp="ID";
if (tk.type.keyword || tk.type.label=="name") tp="ID";
if (tp=="?" && tk.start+1==tk.end) tp="CHAR";
return {
startIdx : tk.start,
endIdx : tk.end,
str : code.substr(tk.start, tk.end),
str : code.substring(tk.start, tk.end),
type : tp
};
}};
@ -6802,5 +6801,7 @@ Espruino.transform = function(code, options) {
});
};
if ("undefined"==typeof document) Espruino.init();
if ("undefined"!=typeof module)
module.exports = Espruino;

View File

@ -1,5 +1,6 @@
var appJSON = []; // List of apps and info from apps.json
var appsInstalled = []; // list of app JSON
var appSortInfo = {}; // list of data to sort by, from appdates.csv { created, modified }
var files = []; // list of files on Bangle
var DEFAULTSETTINGS = {
pretokenise : true,
@ -19,6 +20,19 @@ httpGet("apps.json").then(apps=>{
refreshFilter();
});
httpGet("appdates.csv").then(csv=>{
document.querySelector(".sort-nav").classList.remove("hidden");
csv.split("\n").forEach(line=>{
var l = line.split(",");
appSortInfo[l[0]] = {
created : Date.parse(l[1]),
modified : Date.parse(l[2])
};
});
}).catch(err=>{
console.log("No recent.csv - app sort disabled");
});
// =========================================== Top Navigation
function showChangeLog(appid) {
var app = appNameToApp(appid);
@ -182,11 +196,12 @@ function showTab(tabname) {
// =========================================== Library
var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value);
var chips = Array.from(document.querySelectorAll('.filter-nav .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 = '';
var activeSort = '';
var currentSearch = activeFilter ? '' : hash;
function refreshFilter(){
var filtersContainer = document.querySelector("#librarycontainer .filter-nav");
@ -194,6 +209,12 @@ function refreshFilter(){
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active');
else filtersContainer.querySelector('.chip[filterid]').classList.add('active');
}
function refreshSort(){
var sortContainer = document.querySelector("#librarycontainer .sort-nav");
sortContainer.querySelector('.active').classList.remove('active');
if(activeSort) sortContainer.querySelector('.chip[sortid="'+activeSort+'"]').classList.add('active');
else sortContainer.querySelector('.chip[sortid]').classList.add('active');
}
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
var visibleApps = appJSON;
@ -202,7 +223,7 @@ function refreshLibrary() {
if (activeFilter) {
if ( activeFilter == "favourites" ) {
visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length));
}else{
} else {
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
}
}
@ -211,6 +232,13 @@ function refreshLibrary() {
visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch));
}
if (activeSort) {
visibleApps = visibleApps.slice(); // clone the array so sort doesn't mess with original
if (activeSort=="created" || activeSort=="modified") {
visibleApps = visibleApps.sort((a,b) => appSortInfo[b.id][activeSort] - appSortInfo[a.id][activeSort]);
} else throw new Error("Unknown sort type "+activeSort);
}
panelbody.innerHTML = visibleApps.map((app,idx) => {
var appInstalled = appsInstalled.find(a=>a.id==app.id);
var version = getVersionInfo(app, appInstalled);
@ -580,12 +608,22 @@ filtersContainer.addEventListener('click', ({ target }) => {
});
var librarySearchInput = document.querySelector("#searchform input");
librarySearchInput.value = currentSearch;
librarySearchInput.addEventListener('input', evt => {
currentSearch = evt.target.value.toLowerCase();
refreshLibrary();
});
var sortContainer = document.querySelector("#librarycontainer .sort-nav");
sortContainer.addEventListener('click', ({ target }) => {
if (target.classList.contains('active')) return;
activeSort = target.getAttribute('sortid') || '';
refreshSort();
refreshLibrary();
window.location.hash = activeFilter;
});
// =========================================== About
if (window.location.host=="banglejs.com") {

View File

@ -37,7 +37,10 @@ function httpGet(url) {
});
oReq.addEventListener("error", () => reject());
oReq.addEventListener("abort", () => reject());
oReq.open("GET", url);
oReq.open("GET", url, true);
oReq.onerror = function () {
reject("HTTP Request failed");
};
oReq.send();
});
}