Merge branch 'master' of github.com:espruino/BangleApps into pushups

pull/3669/head
frederic wagner 2024-01-29 12:50:30 +01:00
commit 26cf4fe444
44 changed files with 997 additions and 191 deletions

1
apps/angles/ChangeLog Normal file
View File

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

49
apps/angles/app.js Normal file
View File

@ -0,0 +1,49 @@
g.clear().setRotation(1);
// g.setRotation ALSO changes accelerometer axes
var avrAngle = undefined;
var history = [];
var R = Bangle.appRect;
var W = g.getWidth();
var H = g.getHeight();
var relativeTo = undefined;
function draw(v) {
if (v===undefined) v = Bangle.getAccel();
// current angle
var d = Math.sqrt(v.y*v.y + v.z*v.z);
var ang = Math.atan2(-v.x, d)*180/Math.PI;
// Median filter
if (history.length > 10) history.shift(); // pull old reading off the start
history.push(ang);
avrAngle = history.slice().sort()[(history.length-1)>>1]; // median filter
// Render
var x = R.x + R.w/2;
var y = R.y + R.h/2;
g.reset().clearRect(R).setFontAlign(0,0);
var displayAngle = avrAngle;
g.setFont("6x15").drawString("ANGLE (DEGREES)", x, R.y2-8);
if (relativeTo!==undefined) {
g.drawString("RELATIVE TO", x,y-50);
g.setFont("Vector:30").drawString(relativeTo.toFixed(1),x,y-30);
y += 20;
displayAngle = displayAngle-relativeTo;
}
g.setFont("Vector:60").drawString(displayAngle.toFixed(1),x,y);
}
draw();
Bangle.on('accel',draw);
// Pressing the button turns relative angle on/off
Bangle.setUI({
mode : "custom",
btn : function(n) {
if (relativeTo===undefined)
relativeTo = avrAngle;
else
relativeTo = undefined;
draw();
}
});

1
apps/angles/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA"))

BIN
apps/angles/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

15
apps/angles/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "angles",
"name": "Angles (Spirit Level)",
"shortName": "Angles",
"version": "0.01",
"description": "Shows Angle or Relative angle in degrees (Digital Protractor/Inclinometer). Place Bangle sideways against a surface with the button facing away for best readings.",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],
"tags": "tool",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"angles.app.js","url":"app.js"},
{"name":"angles.img","url":"icon.js","evaluate":true}
]
}

BIN
apps/angles/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -2,3 +2,4 @@
0.02: Allow boot exceptions, e.g. to load DST
0.03: Permit exceptions to load in low-power mode, e.g. daylight saving time.
Also avoid polluting global scope.
0.04: Enhance menu: enable bluetooth, visit settings & visit recovery

View File

@ -61,14 +61,13 @@ var reload = function () {
nextDraw = undefined;
},
btn: function () {
E.showPrompt("Restore watch to full power?").then(function (v) {
if (v) {
drainedRestore();
}
else {
reload();
}
});
var menu = {
"Restore to full power": drainedRestore,
"Enable BLE": function () { return NRF.wake(); },
"Settings": function () { return load("setting.app.js"); },
"Recovery": function () { return Bangle.showRecoveryMenu(); },
};
E.showMenu(menu);
}
});
Bangle.CLOCK = 1;

View File

@ -79,13 +79,13 @@ const reload = () => {
nextDraw = undefined;
},
btn: () => {
E.showPrompt("Restore watch to full power?").then(v => {
if(v){
drainedRestore();
}else{
reload();
}
})
const menu = {
"Restore to full power": drainedRestore,
"Enable BLE": () => NRF.wake(),
"Settings": () => load("setting.app.js"),
"Recovery": () => Bangle.showRecoveryMenu(),
};
E.showMenu(menu);
}
});
Bangle.CLOCK=1;

View File

@ -1,12 +1,10 @@
{
"id": "drained",
"name": "Drained",
"version": "0.03",
"version": "0.04",
"description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals",
"readme": "README.md",
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [

2
apps/intervals/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First Release
0.02: Changing resolution to seconds instead of 5 seconds

View File

@ -149,9 +149,9 @@ function showMenu()
"START" : function() { startSession(); },
"Sets" : { value : settings.sets,min:0,max:20,step:1,onchange : v => { settings.sets=v; } },
"Work minutes" : { value : settings.workmin,min:0,max:59,step:1,onchange : v => { settings.workmin=v; } },
"Work seconds" : { value : settings.workseg,min:0,max:59,step:5,onchange : v => { settings.workseg=v; } },
"Work seconds" : { value : settings.workseg,min:0,max:59,step:1,onchange : v => { settings.workseg=v; } },
"Rest minutes" : { value : settings.restmin,min:0,max:59,step:1,onchange : v => { settings.restmin=v; } },
"Rest seconds" : { value : settings.restseg,min:0,max:59,step:5,onchange : v => { settings.restseg=v; } },
"Rest seconds" : { value : settings.restseg,min:0,max:59,step:1,onchange : v => { settings.restseg=v; } },
"Signal type" : { value : settings.buzz,format : v => v?"Buzz":"Beep",onchange : v => { settings.buzz=v; }}
};

View File

@ -2,7 +2,7 @@
"id": "intervals",
"name": "Intervals App",
"shortName": "Intervals",
"version": "0.01",
"version": "0.02",
"description": "Intervals for training. It is possible to configure work time and rest time and number of sets.",
"icon": "intervals.png",
"tags": "",

View File

@ -4,3 +4,4 @@
Support for fast loading
0.04: Localisation request: added Miles and AM/PM
0.05: Prevent exceptions from halting the draw cycle
0.06: Fix Settings page to ensure that the currently set distance is displayed (not 0.75)

View File

@ -2,7 +2,7 @@
"id": "pebbled",
"name": "Pebble Clock with distance",
"shortName": "Pebble + distance",
"version": "0.05",
"version": "0.06",
"description": "Fork of Pebble Clock with distance in KM. Both step count and the distance are on the main screen. Default step length = 0.75m (can be changed in settings).",
"readme": "README.md",
"icon": "pebbled.png",

View File

@ -36,7 +36,7 @@
},
},
'Step length': {
value: 0.75 || s.avStep,
value: s.avStep || 0.75,
min: 0.2,
max: 1.5,
step: 0.01,

View File

@ -2,3 +2,4 @@
0.02: Major speed improvement. Added more stars. Up to 500!
0.03: Added more stars and constellations. Now it shows 20 constellations.
0.04: Use default Bangle formatter for booleans
0.05: Added more constellations (scorpio and aguila)

View File

@ -2,7 +2,7 @@
"id": "planetarium",
"name": "Planetarium",
"shortName": "Planetarium",
"version": "0.04",
"version": "0.05",
"description": "Planetarium showing up to 500 stars using the watch location and time",
"icon": "planetarium.png",
"tags": "",

View File

@ -38,3 +38,7 @@ Draco
e_15 131,131 70,70 382,382 e_15,382 187,187 423,423 e_16,e_16 207,207 122,122 e_17,e_17 232,232 342,342 452,452 428
Pegasus
92 85,138 54,54 85,138 92,283 85,283 389,160 85,92 258,258 297,297 83
Aguila
12 249,249 271,249 170,249 217,12 365,120 12
Scorpius
14 105,14 80,14 152,14 137,137 76,332 239,239 41,76 188,188 332,41 181,181 27

1 Orion
38 e_15 131,131 70,70 382,382 e_15,382 187,187 423,423 e_16,e_16 207,207 122,122 e_17,e_17 232,232 342,342 452,452 428
39 Pegasus
40 92 85,138 54,54 85,138 92,283 85,283 389,160 85,92 258,258 297,297 83
41 Aguila
42 12 249,249 271,249 170,249 217,12 365,120 12
43 Scorpius
44 14 105,14 80,14 152,14 137,137 76,332 239,239 41,76 188,188 332,41 181,181 27

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDAEEREREREREURERERERERERBERERERERERERERERERREREREREREREREEREREREREREREREREURERERERERERERERBERERERERERERERFEREREREREREREREREERERERERERERERREREREREREREREREREQREREREREREREURBEhEURERERERERERERBEREREREREREURBESERIkRERERERERERBERERERERERFEQhERIhEiREREREREREREERERERERERFERCERIiIiJEREREREREREQRERERERERREREERASIiEkREREREREREQRERERERERREREQQERIiEkRERERERERERBEREREREUREREQRERAiIiEkRERERERERBEREREREURERERAERERISEkRERERERERBEREREREUREREREEQAREiJEREREREREREERERERFEREREREIiIRESIUREREREREREERERERFEREREREERABERIRREREREREREERERERFEREREREQREREREhREREREREREERERERFEREREREIiJBERESFEREREREREQRERERFEREREREIiQRERERIUREREREREQRERERRERERERCIiIhEQEREhREREREREQRERERRERERERCIiIiIRERESFEREREREQRERERREREREQiIiIiIiQRESJEREREREQRERERREREREQiIiIiIiIkFBJEREREREQRERERREREREQiIiIiIiJBESEkREREREQRERERREREREQiIiIiIiIiRCIiREREREQRERERREREREQiIiIiIiIiIkQiREREREQRERERREREREQiIiIiIiIiIiIiJEREREQRERERREREREQiIiIiIyIiIiIiIkREREQRERERREREREQiIiIkQjMiIiIkIkREREQRERERREREREREREREQiIiIiJEQiIiMkQRERERREQiIkREQRESIiIiIiJCQiMzIkQRERERREIiIgAAABEBERRCMzMzIiIhARERERERFEIiIiIiIiIzMzMzMzMhAAJAEQVRERERQjMzMzMzMzMzMiIhEAAAAAEiBURBERFCIzMzIiIhERAAAAARERERERESFERBEREUERAAAAAAAAARERERERERFREUJEQhERERERFEREREREREREREREREREREJEQkEREREUREREREREREREREREREREREEkREERERFEREREREREREREREQiJEREREQkRBERERFEJERCIiRCIiIiJEIiIkREREQSERARERESREIiIiIiRERERERERERCREVSIAEREREUREREREREREREREREREREREVREBEREREURERERBFERERERERERERERVEAAREREREQAAABEREREREREUREREREEAAAAAABEREREREREREREREREAAAAAAAAAAAABERERERERERERRERERERBEREREREREREREREREREREREUREREREREREQRERERERERERERERERERERRERERERERBERERERERERER"))

196
apps/quoteclock/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/quoteclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,15 @@
{
"id": "quoteclock",
"name": "Quote Clock",
"version": "0.01",
"description": "A clock showing quotes every hour",
"icon": "app.png",
"type": "clock",
"tags": "clock,shakespeare",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"quoteclock.app.js","url":"app.js"},
{"name":"quoteclock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -14,3 +14,4 @@
0.14: cleanup code and fix fastload issue
0.15: fix draw before widget hide
0.16: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.17: Add fullscreen option (on by default) to show widgets, adjust sidebar 1 and 2 when fullscreen is off

View File

@ -10,14 +10,22 @@
* Tap top or bottom right to instantly cycle to the next sidebar
* Uses pedometer widget to get latest step count
* Dependant apps are installed when Rebble installs
* Uses the whole screen, widgets are made invisible but still run in the background
* When in fullscreen widgets are made invisible but still run in the background
* The icon is James Dean - 'Rebel Without a Cause'
## Fullscreen
![](screenshot_rebble.png)
![](screenshot_rebble2.png)
![](screenshot_rebble3.png)
![](screenshot_rebble4.png)
## With widgets
![](screenshot_rebble_w1.png)
![](screenshot_rebble_w2.png)
![](screenshot_rebble_w3.png)
## Future Enhancements
* Support for Weather Icons in the Steps Sidebar

View File

@ -2,12 +2,12 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
"version": "0.16",
"version": "0.17",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
"dependencies": {"mylocation":"app"},
"screenshots": [{"url":"screenshot_rebble.png"}],
"screenshots": [{"url":"screenshot_rebble.png"}, {"url":"screenshot_rebble2.png"}, {"url":"screenshot_rebble3.png"}, {"url":"screenshot_rebble4.png"}, {"url":"screenshot_rebble_w1.png"}, {"url":"screenshot_rebble_w2.png"}, {"url":"screenshot_rebble_w3.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],

View File

@ -40,7 +40,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
}
let loadSettings=function() {
settings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true,'sideTap':0};
settings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true, 'fullScreen': true, 'sideTap':0};
//sideTap 0 = on | 1 = sidebar1...
let tmp = require('Storage').readJSON(SETTINGS_FILE, 1) || settings;
@ -118,32 +118,60 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
if (drawCount % 60 == 0)
updateSunRiseSunSet(location.lat, location.lon);
g.reset();
g.setColor(g.theme.bg);
g.fillRect(0, 0, w2, h);
g.setColor(settings.bg);
g.fillRect(w2, 0, w, h);
if (settings.fullScreen) {
g.setColor(g.theme.bg);
g.fillRect(0, 0, w2, h);
g.setColor(settings.bg);
g.fillRect(w2, 0, w, h);
// time
g.setColor(g.theme.fg);
g.setFontKdamThmor();
g.setFontAlign(0, -1);
g.drawString(hh, w2/2, 10 + 0);
g.drawString(mm, w2/2, 10 + h/2);
// time
g.setColor(g.theme.fg);
g.setFontKdamThmor();
g.setFontAlign(0, -1);
g.drawString(hh, w2/2, 10 + 0);
g.drawString(mm, w2/2, 10 + h/2);
switch(sideBar) {
case 0:
drawSideBar1();
break;
case 1:
drawSideBar2();
break;
case 2:
drawSideBar3();
break;
switch(sideBar) {
case 0:
drawSideBar1();
break;
case 1:
drawSideBar2();
break;
case 2:
drawSideBar3();
break;
}
} else {
g.setColor(g.theme.bg);
g.fillRect(0, 24, 113, 176);
g.setColor(settings.bg);
g.fillRect(113, 24, 176, 176);
// time
g.setColor(g.theme.fg);
g.setFontKdamThmor();
g.setFontAlign(0, -1);
g.drawString(hh, 57, 24);
g.drawString(mm, 57, 100);
switch(sideBar) {
case 0:
drawSideBar1Alt();
break;
case 1:
drawSideBar2Alt();
break;
case 2:
drawSideBar3();
break;
}
}
drawCount++;
queueDraw();
}
@ -164,6 +192,16 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
drawDateAndCalendar(w3, h/2, dy, dd, mm);
}
let drawSideBar1Alt=function() {
let date = new Date();
let dy= require("date_utils").dow(date.getDay(),1).toUpperCase();
let dd= date.getDate();
let mm= require("date_utils").month(date.getMonth()+1,1).toUpperCase();
let yy = date.getFullYear();
drawDateAndCalendarAlt(145, 46, dy, dd, mm, yy);
}
let drawSideBar2=function() {
drawBattery(w2 + (w-w2-wb)/2, h/10, wb, 17);
@ -178,6 +216,14 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
setSmallFont();
g.setFontAlign(0, -1);
g.drawString(formatSteps(), w3, 7*h/8);
}
let drawSideBar2Alt=function() {
// steps
g.drawImage(boot_img, 113, 59, { scale: 1 });
setSmallFont();
g.setFontAlign(0, -1);
g.drawString(formatSteps(), 145, 122);
}
// sunrise, sunset times
@ -212,6 +258,28 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
g.setFontAlign(0, -1);
g.drawString(mm.toUpperCase(), x, y + 70);
}
let drawDateAndCalendarAlt=function(x, y, dy, dd, mm, yy) {
// day
setTextColor();
setSmallFont();
g.setFontAlign(0, -1);
g.drawString(dy.toUpperCase(), x, y);
drawCalendar(x - 18, y + 28, 35, 3, dd);
// month
setTextColor();
setSmallFont();
g.setFontAlign(0, -1);
g.drawString(mm.toUpperCase(), x, y + 70);
// year
setTextColor();
setSmallFont();
g.setFontAlign(0, -1);
g.drawString(yy, x, y + 94);
}
// at x,y width:wi thicknes:th
let drawCalendar=function(x,y,wi,th,str) {
@ -311,7 +379,10 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
delete Graphics.prototype.setFontKdamThmor;
Bangle.removeListener('charging',chargingListener);
if (settings.fullScreen) {
Bangle.removeListener('charging',chargingListener);
}
}
let main=function(){
@ -341,17 +412,17 @@ Graphics.prototype.setFontKdamThmor = function(scale) {
});
}
Bangle.on('charging',chargingListener);
Bangle.loadWidgets();
require("widget_utils").hide();
if (settings.fullScreen) {
Bangle.on('charging',chargingListener);
require("widget_utils").hide();
} else {
Bangle.drawWidgets();
}
draw();
}
main();
}

View File

@ -2,7 +2,7 @@
const SETTINGS_FILE = "rebble.json";
// initialize with default settings...
let localSettings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true, 'sideTap':0};
let localSettings = {'bg': '#0f0', 'color': 'Green', 'autoCycle': true, 'fullScreen': true, 'sideTap':0};
//sideTap 0 = on| 1= sideBar1 | 2 = ...
// ...and overwrite them with any saved values
@ -37,6 +37,14 @@
localSettings.bg = bg_code[v];
save();
},
},
'Fullscreen': {
value: localSettings.fullScreen,
onchange: (v) => {
localSettings.fullScreen = v;
save();
showMenu();
}
},
'Auto Cycle': {
value: localSettings.autoCycle,
@ -74,4 +82,4 @@
}
showMenu();
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,2 +1 @@
require("heatshrink").decompress(atob("mEwghC/AF8OoMRC6nu8kRiAuT93uGCgWBGCguCGCgsBoMUGCQuBFYMRDAIwQFQUhDAIFCFx/hiUyC4NOGAKMP8MRmYwBjwwBCxkEFAIXBiYwBC4PuC5hxCC4IwCDwPgC5gpBpxxBiMSL4QWMgQpBIIKnEFxsikcxOISODCxkDmUiLIQADFxsjUIQWELp0iLwYuQgMzkUiFydBkcyFycOoMSXoIuST4YuTB4NBZwIuSABAuPAA5dQdSQuBoIXBLwouPiUxGAguOC4imDRh3hC4wuMgBABC44WMgBxBI4wuNgBxCC4MhAoQWNC4IwBU4guOgEBFQVBiguQGAi7PGBCMPGBAuRGAoWSGAYuTAH4AcA="))
require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

1
apps/terminal/ChangeLog Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///w8h7fOm2V/8fjkf/Gt58jKf4AtiVJkmSARFICK3PlnyCJ1t03aEcBZjCBVJCP4R/CK9N8gRPpmUEaGSuRHP+1KCJ8m5AjgI4PyNZ+cAQIQCwgjKCgQRNdP4R/CL4PJAQIRWAH4ApA=="))

263
apps/terminal/app.js Normal file
View File

@ -0,0 +1,263 @@
/* Espruino VT100 JS REPL
TODO: Add option to connect to a remote BLE device
*/
var settings = Object.assign({
// default values
textSize: 1,
loopAround: 1,
oneToOne: 0,
speedScaling: 24
}, /*require('Storage').readJSON("repl.settings.json", true) || */{});
// Key Maps for Keyboard
var KEYMAPLOWER = [
"`1234567890-=\b\b",
"\tqwertyuiop[]\n\n",
"\2asdfghjkl;'#\x82\n",
"\2\\zxcvbnm,./\x80\x83\x81",
];
var KEYMAPUPPER = [
"¬!\"£$%^&*()_+\b\b",
"\tQWERTYUIOP{}\n\n",
"\2ASDFGHJKL:@~\x82\n",
"\2|ZXCVBNM<>?\x80\x83\x81",
];
var KEYIMGL = Graphics.createImage(`
# #
## #
######
## #
# #
#
###
#####
#
#
#
#
#
#
#
#
#
#
`);
KEYIMGL.transparent = 0;
var KEYIMGR = Graphics.createImage(`
#
##
#########
##
#
#######
#
#
#
#
#
#####
###
#
#
###
#####
# #
## ##
### ##### ###
## ### ##
# # #
`);
KEYIMGR.transparent = 0;
/* If a char in the keymap is >=128,
subtract 128 and look in this array for
multi-character key codes*/
var KEYEXTRA = [
String.fromCharCode(27, 91, 68), // 0x80 left
String.fromCharCode(27, 91, 67), // 0x81 right
String.fromCharCode(27, 91, 65), // 0x82 up
String.fromCharCode(27, 91, 66), // 0x83 down
String.fromCharCode(27, 91, 53, 126), // 0x84 page up
String.fromCharCode(27, 91, 54, 126), // 0x85 page down
];
// state
const R = Bangle.appRect;
var kbx = 0,
kby = 0,
kbdx = 0,
kbdy = 0,
kbShift = false,
flashToggle = false;
const PX = 12,
PY = 16,
DRAGSCALE = settings.speedScaling;
var xoff = 0,
yoff = g.getHeight() - PY * 4;
function draw() {
"ram";
var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER;
//g.drawImage(KEYIMG,0,yoff);
g.reset().setFont("6x8:2");
g.clearRect(R.x, yoff, R.x2, R.y2);
if (kbx >= 0)
g.setColor(g.theme.bgH).fillRect(xoff + kbx * PX, yoff + kby * PY, xoff + (kbx + 1) * PX - 1, yoff + (kby + 1) * PY - 1).setColor(g.theme.fg);
g.drawImage(KEYIMGL, xoff - 1, yoff + PY, { scale: 2 });
g.drawImage(KEYIMGR, xoff + PX * 12, yoff, { scale: 2 });
var replace = /[\x80\x81\x82\x83\x84\x85]/g;
g.drawString(map[0].replace(replace," "), xoff, yoff);
g.drawString(map[1].replace(replace," "), xoff, yoff + PY);
g.drawString(map[2].replace(replace," "), xoff, yoff + PY * 2);
g.drawString(map[3].replace(replace," "), xoff, yoff + PY * 3);
g.flip();
}
function startTerminal(dataOutCallback) {
g.reset().clearRect(R);
// Set up the terminal
term = require("VT100").connect(g, {
charWidth: 6,
charHeight: 8
});
term.oy = R.y;
term.h = yoff; // we added this - it's not usually part of it
term.consoleHeight = 0 | ((term.h - term.oy) / term.charH);
term.scrollDown = function() {
g.setClipRect(R.x, term.y, R.x2, term.oy + term.h);
g.scroll(0, -this.charH);
g.setClipRect(R.x, R.y, R.x2, R.y2);
this.y--;
};
term.fgCol = g.theme.fg;
term.bgCol = g.theme.bg;
draw();
var flashInterval = setInterval(() => {
flashToggle = !flashToggle;
draw();
}, 1000);
function keyPress(kbx, kby) {
var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER;
var ch = map[kby][kbx];
if (ch == "\2")
kbShift = !kbShift;
else {
if (ch.charCodeAt(0) > 127)
ch = KEYEXTRA[ch.charCodeAt(0) - 128];
dataOutCallback(ch);
}
Bangle.buzz(20);
draw();
}
Bangle.setUI({
mode: "custom",
drag: e => {
if (settings.oneToOne) {
kbx = Math.max(Math.min(Math.floor((e.x - 16) / (6 * 2)), 13), 0);
kby = Math.max(Math.min(Math.floor((e.y - 120) / (8 * 2)), 3), 0);
//print(e.y, kby, e.x, kbx);
}
if (!settings.oneToOne) {
kbdx += e.dx;
kbdy += e.dy;
var dx = Math.round(kbdx / DRAGSCALE),
dy = Math.round(kbdy / DRAGSCALE);
kbdx -= dx * DRAGSCALE;
kbdy -= dy * DRAGSCALE;
if (dx || dy) {
if (settings.loopAround) {
kbx = (kbx + dx + 15) % 15;
kby = (kby + dy + 4) % 4;
} else {
kbx = Math.max(Math.min((kbx + dx), 13), 0);
kby = Math.max(Math.min((kby + dy), 3), 0);
}
}
}
draw();
if (!e.b && e.y > Bangle.appRect.y && settings.oneToOne /*&& settings.releaseToSelect*/ )
keyPress(kbx, kby);
},
touch: () => {
if (!settings.oneToOne /*|| !settings.releaseToSelect*/ )
keyPress(kbx, kby);
}
});
let catchSwipe = () => {
E.stopEventPropagation && E.stopEventPropagation();
};
Bangle.prependListener && Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
return {
dataReceived : function(d) {
g.reset().setFont("6x8");
// USB.write(e); // optionally mirror back to the PC
// Send characters to the terminal
for (var i in d) term.char(d[i]);
// update the screen
g.flip();
}
};
}
function mainMenu() {
E.showMenu({
"":{title:"Terminal"},
/*LANG*/"JS REPL" : function() {
var t = startTerminal(function(d) {
LoopbackB.write(d);
});
LoopbackB.on('data', function(d) {
t.dataReceived(d);
});
// Now move the console to Loopback
LoopbackA.setConsole();
},
/*LANG*/"Bluetooth" : function() {
Bangle.setUI();
E.showMessage(/*LANG*/"Scanning...", /*LANG*/"Bluetooth");
NRF.findDevices(function(devices) {
if (!devices.length)
return E.showAlert("No devices found").then(() => mainMenu());
var menu = { "" : { title: /*LANG*/"Bluetooth", back : () => mainMenu() } };
devices.forEach(dev => {
var name = dev.name || dev.id.substr(0,17);
menu[name] = function() {
Bangle.setUI();
E.showMessage(/*LANG*/"Connecting...", /*LANG*/"Bluetooth");
require("ble_uart").connect(dev).then(function(uart) {
var t = startTerminal(function(d) {
uart.write(d);
});
t.dataReceived("Connected to:\n "+name+"\n")
uart.on('data', function(d) { t.dataReceived(d); });
}).catch(err => {
E.showAlert(err.toString()).then(() => mainMenu());
});
};
});
E.showMenu(menu);
}, { filters: [{ services: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'] }], timeout: 2000, active:true });
}
});
}
mainMenu();

BIN
apps/terminal/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

@ -0,0 +1,14 @@
{ "id": "terminal",
"name": "VT100 Terminal",
"shortName":"Terminal",
"version":"0.01",
"description": "Terminal and Keyboard that can be used as a REPL. You can type JS commands into Bangle.js's own REPL and execute them, or you can connect to other Bluetooth LE UART devices (like other Espruinos) and issue commands.",
"icon": "app.png",
"tags": "terminal,tool,bluetooth",
"screenshots" : [ { "url":"screenshot1.png" }, { "url":"screenshot2.png" } ],
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"terminal.app.js","url":"app.js"},
{"name":"terminal.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -2,3 +2,5 @@
0.02: Better controls, implement game over.
0.03: Implement mode and level selection screens.
0.04: Bring back old controls as "swipe" in menu, exit with button press
0.10: Major overhaul: added score, levels, bugfixes and misc, inspired by NES tetris
0.11: Save/Restore game state

View File

@ -1,7 +1,7 @@
{ "id": "tetris",
"name": "Tetris",
"shortName":"Tetris",
"version":"0.04",
"version":"0.11",
"description": "Tetris",
"icon": "tetris.png",
"readme": "README.md",
@ -12,5 +12,8 @@
"storage": [
{"name":"tetris.app.js","url":"tetris.app.js"},
{"name":"tetris.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"tetris.json"}
]
}

View File

@ -36,26 +36,45 @@ const tiles = [
const ox = 176/2 - 5*8;
const oy = 8;
const FILE = "tetris.json";
const settings = Object.assign({
/* 0 .. simulated arrows
1 .. drag piece
2 .. accelerometer. 12 lines record.
3 .. altimeter
*/
var control = 0, level = 0;
control: 0,
level: 0,
initialLevel: 0,
lines: 0,
score: 0,
pf: undefined,
ctn: Math.floor(Math.random()*7), // current tile number
ntn: Math.floor(Math.random()*7), // next tile number
ntr: Math.floor(Math.random()*4), // next tile rotation
dropInterval: undefined,
}, require('Storage').readJSON(FILE, true) || {});
var alt_start = -9999; /* For altimeter control */
/* 0 .. menu
1 .. game
2 .. game over */
var state = 0;
var pf;
var px=4, py=0;
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
function initGame() {
pf = Array(23).fill().map(()=>Array(12).fill(0)); // field is really 10x20, but adding a border for collision checks
pf[20].fill(1);
pf[21].fill(1);
pf[22].fill(1);
pf.forEach((x,i) => { pf[i][0] = 1; pf[i][11] = 1; });
settings.pf = Array(23).fill().map(()=>Array(12).fill(0)); // field is really 10x20, but adding a border for collision checks
settings.pf[20].fill(1);
settings.pf[21].fill(1);
settings.pf[22].fill(1);
settings.pf.forEach((x,i) => { settings.pf[i][0] = 1; settings.pf[i][11] = 1; });
}
function rotateTile(t, r) {
@ -71,13 +90,18 @@ function rotateTile(t, r) {
return nt;
}
var time = Date.now();
var ct = rotateTile(tiles[settings.ctn], Math.floor(Math.random()*4)); // current tile (rotated)
function drawBoundingBox() {
g.setBgColor(0, 0, 0).clear().setColor(1, 1, 1);
g.theme.bg = 0;
for (i=0; i<4; ++i) g.drawRect(ox-i-1, oy-i-1, ox+10*8+i, oy+20*8+i);
}
function drawTile (tile, n, x, y, qClear) {
function drawTile(tile, n, x, y, qClear) {
if (state != 1) // stops tile from being drawn on the game over screen
return;
if (qClear) g.setColor(0);
else g.setColor(tcols[n].r, tcols[n].g, tcols[n].b);
for (i=0; i<tile.length; ++i)
@ -90,58 +114,133 @@ function drawTile (tile, n, x, y, qClear) {
function showNext(n, r) {
var nt = rotateTile(tiles[n], r);
g.setColor(0).fillRect(176-33, 40, 176-33+33, 82);
drawTile(nt, ntn, 176-33, 40);
drawTile(nt, settings.ntn, 176-33, 40);
}
var time = Date.now();
var px=4, py=0;
var ctn = Math.floor(Math.random()*7); // current tile number
var ntn = Math.floor(Math.random()*7); // next tile number
var ntr = Math.floor(Math.random()*4); // next tile rotation
var ct = rotateTile(tiles[ctn], Math.floor(Math.random()*4)); // current tile (rotated)
var dropInterval = 450;
var nlines = 0;
function calculateSpeed() {
let step = 500;
if (settings.level <= 6) // 200-500ms
step = step - 50*settings.level;
else if (settings.level <= 13) { // 25-175ms
step = 200;
step = step - 25*(settings.level - 6);
}
else {
step = 20; // usually limited by the hardware
// levels 15+ are programmed to go faster by skipping lines
}
print(`level ${settings.level}: drop interval ${step}ms`);
if (settings.control == 3)
step = step*2;
dropInterval = step;
}
function redrawPF(ly) {
for (y=0; y<=ly; ++y)
for (x=1; x<11; ++x) {
c = pf[y][x];
c = settings.pf[y][x];
if (c>0) g.setColor(tcols[c-1].r, tcols[c-1].g, tcols[c-1].b).drawImage(block, ox+(x-1)*8, oy+y*8);
else g.setColor(0, 0, 0).fillRect(ox+(x-1)*8, oy+y*8, ox+x*8-1, oy+(y+1)*8-1);
}
}
function gameOver() {
state = 0;
g.setColor(1, 1, 1).setFontAlign(0, 1, 0).setFont("Vector",22)
.drawString("Game Over", 176/2, 76);
state = 0;
E.showAlert("Game Over").then(selectGame, print);
// this cannot allow changing game controls because it would set up duplicate events
E.showAlert("Game Over").then(newGame, print);
settings.lines = 0;
settings.score = 0;
settings.level = settings.initialLevel;
}
function redrawStats(onlyScore) {
g.setColor(0).fillRect(5, 30, 41, 60)
.setColor(1, 1, 1).drawString(settings.score.toString(), 22, 50);
if (!onlyScore) {
g.setColor(0).fillRect(5, 80, 41, 110)
.setColor(1, 1, 1).drawString(settings.level.toString(), 22, 100)
.setColor(0).fillRect(5, 130, 41, 160)
.setColor(1, 1, 1).drawString(settings.lines.toString(), 22, 150);
}
}
function insertAndCheck() {
for (y=0; y<ct.length; ++y)
for (x=0; x<ct[y].length; ++x)
if (ct[y][x]>0) pf[py+y][px+x+1] = ctn+1;
// stop pieces from falling into each other
for (let y=0; y<ct.length; ++y)
for (let x=0; x<ct[y].length; ++x)
if (ct[y][x]>0) settings.pf[py+y][px+x+1] = settings.ctn+1;
let clearCount = 0;
let linesToClear = [];
let yReal = 19; // the y for display purposes
// check for full lines
for (y=19; y>0; y--) {
for (let y=19; y>0; y--) {
var qFull = true;
for (x=1; x<11; ++x) qFull &= pf[y][x]>0;
for (let x=1; x<11; ++x) qFull &= settings.pf[y][x]>0;
if (qFull) {
nlines++;
dropInterval -= 5;
Bangle.buzz(30);
for (ny=y; ny>0; ny--) pf[ny] = JSON.parse(JSON.stringify(pf[ny-1]));
redrawPF(y);
g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
clearCount++;
linesToClear.push([y, yReal]);
print(`linesToClear.push(${y})`);
// clear the line, but do not display it yet
for (ny=y; ny>0; ny--) settings.pf[ny] = JSON.parse(JSON.stringify(settings.pf[ny-1]));
y++;
}
yReal--;
}
if (clearCount) {
settings.lines += clearCount;
let effectiveLevel = Math.max(settings.level, 1);
if (clearCount == 1) { // single
settings.score += 100 * effectiveLevel;
Bangle.buzz(80, 0.5);
}
else if (clearCount == 2) { // double
settings.score += 300 * effectiveLevel;
Bangle.buzz(80);
}
else if (clearCount == 3) { // triple
settings.score += 500 * effectiveLevel;
Bangle.buzz(200);
}
else if (clearCount >= 4) { // tetris
settings.score += 800 * effectiveLevel;
Bangle.buzz(500);
}
// the score will not be shown yet because redrawStats was not called
// clear effect
let timer = getTime();
g.setColor(0, 0, 0);
while (true) {
var rectLength = (getTime()-timer)/0.05 + 1;
if (rectLength > 6)
break;
var x1 = 6 - rectLength;
var x2 = 4 + rectLength;
for (let line of linesToClear) {
let y = line[1];
g.fillRect(ox+x1*8, oy+y*8, ox+x2*8-1, oy+(y+1)*8-1);
}
g.flip();
}
// display the cleared lines
for (let line of linesToClear) {
redrawPF(line[0]);
}
if (settings.lines != 0 && settings.lines % 10 == 0) {
settings.level++;
calculateSpeed();
}
redrawStats();
}
// spawn new tile
px = 4; py = 0;
ctn = ntn;
ntn = Math.floor(Math.random()*7);
ct = rotateTile(tiles[ctn], ntr);
ntr = Math.floor(Math.random()*4);
showNext(ntn, ntr);
settings.ctn = settings.ntn;
settings.ntn = Math.floor(Math.random()*7);
ct = rotateTile(tiles[settings.ctn], settings.ntr);
settings.ntr = Math.floor(Math.random()*4);
showNext(settings.ntn, settings.ntr);
if (!moveOk(ct, 0, 0)) {
gameOver();
}
@ -149,48 +248,76 @@ function insertAndCheck() {
function moveOk(t, dx, dy) {
var ok = true;
for (y=0; y<t.length; ++y)
for (y=0; y<t.length; ++y)
for (x=0; x<t[y].length; ++x)
if (t[y][x]*pf[py+dy+y][px+dx+x+1] > 0) ok = false;
if (t[y][x]*settings.pf[py+dy+y][px+dx+x+1] > 0) ok = false;
return ok;
}
function pauseGame() {
//print("Paused");
state = 3;
E.showMenu({
"" : { "title" : /*LANG*/"Pause menu" },
"< Back" : () => resumeGame(),
/*LANG*/"Exit" : () => {
writeSettings();
load();
},
/*LANG*/"New game": () => newGame(),
});
}
function resumeGame() {
//print("Resumed");
state = 1;
drawGame();
redrawPF(19);
}
function gameStep() {
if (state != 1)
return;
if (Date.now()-time > dropInterval) { // drop one step
time = Date.now();
if (moveOk(ct, 0, 1)) {
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
if (settings.level >= 15 && moveOk(ct, 0, 2)) {
// at level 15, pieces drop twile as quickly
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
py += 2;
}
else if (moveOk(ct, 0, 1)) {
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
py++;
}
else { // reached the bottom
insertAndCheck(ct, ctn, px, py);
insertAndCheck(ct, settings.ctn, px, py);
}
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, false);
}
}
function rotate() {
t = rotateTile(ct, 3);
if (moveOk(t, 0, 0)) {
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
ct = t;
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, false);
}
}
function move(x, y) {
if (moveOk(ct, x, y)) {
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
r = moveOk(ct, x, y);
if (r) {
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
px += x;
py += y;
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, false);
}
return r;
}
function linear(x) {
print("Linear: ", x);
//print("Linear: ", x);
let now = px / 10;
if (x < now-0.06)
move(-1, 0);
@ -198,126 +325,150 @@ function linear(x) {
move(1, 0);
}
function newGame() {
E.showMenu();
Bangle.setUI({mode : "custom", btn: () => load()});
if (control == 4) { // Swipe
function setupControls() {
if (settings.control == 4) { // Swipe
Bangle.on("touch", (e) => {
t = rotateTile(ct, 3);
if (moveOk(t, 0, 0)) {
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
ct = t;
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, false);
}
});
Bangle.on("swipe", (x,y) => {
if (y<0) y = 0;
if (moveOk(ct, x, y)) {
drawTile(ct, ctn, ox+px*8, oy+py*8, true);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, true);
px += x;
py += y;
drawTile(ct, ctn, ox+px*8, oy+py*8, false);
drawTile(ct, settings.ctn, ox+px*8, oy+py*8, false);
}
});
} else { // control != 4
if (control == 2) { // Tilt
if (settings.control == 2) { // Tilt
Bangle.on("accel", (e) => {
if (state != 1) return;
if (control != 2) return;
if (settings.control != 2) return;
print(e.x);
linear((0.2-e.x) * 2.5);
});
}
if (control == 3) { // Move
Bangle.setBarometerPower(true);
Bangle.on("pressure", (e) => {
if (state != 1) return;
if (control != 3) return;
let a = e.altitude;
if (alt_start == -9999)
alt_start = a;
a = a - alt_start;
print(e.altitude, a);
linear(a);
});
if (settings.control == 3) { // Pressure
Bangle.setBarometerPower(true);
Bangle.on("pressure", (e) => {
if (state != 1) return;
if (settings.control != 3) return;
let a = e.altitude;
if (alt_start == -9999)
alt_start = a;
a = a - alt_start;
//print(e.altitude, a);
linear(a);
});
}
Bangle.on("drag", (e) => {
let h = 176/2;
if (state == 2) {
if (e.b)
selectGame();
return;
}
if (!e.b)
return;
if (state == 0) return;
if (e.y < h) {
if (e.x < h)
rotate();
else {
let i = 0;
for (i=0; i<10; i++) {
move(0, 1);
g.flip();
}
}
} else {
if (control == 1)
linear((e.x - 20) / 156);
if (control != 0)
let h = 176/2;
if (state == 2) {
if (e.b)
selectGame();
return;
if (e.x < h)
move(-1, 0);
else
move(1, 0);
}
}
if (!e.b)
return;
if (state == 0) return;
if (e.y < h) {
if (e.x < h)
rotate();
else {
while (move(0, 1)) {
settings.score++;
g.flip();
}
redrawStats(true);
}
} else {
if (settings.control == 1)
linear((e.x - 20) / 156);
if (settings.control != 0)
return;
if (e.x < h)
move(-1, 0);
else
move(1, 0);
}
});
}
}
function newGame() {
settings.lines = 0;
settings.score = 0;
settings.level = settings.initialLevel;
initGame();
drawGame();
startGame();
}
function startGame() {
calculateSpeed();
state = 1;
var step = 450 - 50*level;
if (control == 3)
step = step*2;
dropInterval = step;
var gi = setInterval(gameStep, 50);
drawGame();
var gi = setInterval(gameStep, 20);
}
function drawGame() {
Bangle.setUI({mode : "custom", btn: () => {
if (state == 1) {
pauseGame();
}
}});
drawBoundingBox();
g.setColor(1, 1, 1).setFontAlign(0, 1, 0)
.setFont("6x15", 1).drawString("Lines", 22, 30)
.setFont("6x15", 1).drawString("Score", 22, 30)
.drawString("Level", 22, 80)
.drawString("Lines", 22, 130)
.drawString("Next", 176-22, 30);
showNext(ntn, ntr);
g.setColor(0).fillRect(5, 30, 41, 80)
.setColor(1, 1, 1).drawString(nlines.toString(), 22, 50);
redrawStats();
showNext(settings.ntn, settings.ntr);
}
function selectLevel() {
print("Level selection menu");
var menu = {};
menu["< Back"] = () => {selectGame();};
menu[/*LANG*/"Level 1"] = () => { level = 0; selectGame(); };
menu[/*LANG*/"Level 2"] = () => { level = 1; selectGame(); };
menu[/*LANG*/"Level 3"] = () => { level = 2; selectGame(); };
E.showMenu(menu);
}
function selectGame() {
state = 0;
print("Game selection menu");
//for (let i = 0; i < 100000; i++) ;
//print("Game selection menu");
var menu = {};
menu[/*LANG*/"Normal"] = () => { control = 0; newGame(); };
menu[/*LANG*/"Drag"] = () => { control = 1; newGame(); };
menu[/*LANG*/"Tilt"] = () => { control = 2; newGame(); };
menu[/*LANG*/"Move"] = () => { control = 3; newGame(); };
menu[/*LANG*/"Swipe"] = () => { control = 4; newGame(); };
menu[/*LANG*/"Level"] = () => { selectLevel(); };
menu[/*LANG*/"New game"] = () => {
setupControls();
newGame();
resumeGame();
};
if (settings.pf !== undefined) {
menu[/*LANG*/"Resume game"] = () => {
setupControls();
startGame();
resumeGame();
};
}
menu[/*LANG*/"Controls"] = {
value: settings.control,
min: 0, max: 4,
format: v => [/*LANG*/"Normal", /*LANG*/"Drag", /*LANG*/"Tilt", /*LANG*/"Pressure", /*LANG*/"Swipe"][v],
onchange: v => {
settings.control = v;
writeSettings();
}
};
menu[/*LANG*/"Level"] = {
value : 1,
min : 0,
max : 10,
wrap : true,
onchange : (l) => {
settings.initialLevel = l;
writeSettings();
}
};
E.showMenu(menu);
}

View File

@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") {
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
}
var RECOMMENDED_VERSION = "2v19";
var RECOMMENDED_VERSION = "2v20";
// could check http://www.espruino.com/json/BANGLEJS.json for this
// We're only interested in Bangles