Merge remote-tracking branch 'upstream/master'
commit
99e1d1a8df
|
|
@ -85,6 +85,7 @@
|
|||
<label class="chip tooltip" filterid="ram" data-tooltip="Apps that don't save anything to flash memory">Online</label>
|
||||
<label class="chip tooltip" filterid="clkinfo" data-tooltip="Info displayed on clocks, or clocks with info">Clock Info</label>
|
||||
<label class="chip tooltip" filterid="health" data-tooltip="Apps for your health">Health</label>
|
||||
<label class="chip tooltip" filterid="fonts" data-tooltip="Extra fonts for non-latin languages">Fonts</label>
|
||||
<label class="chip tooltip" filterid="favourites" data-tooltip="Apps that you've liked ❤️">Favourites</label>
|
||||
</div>
|
||||
<div class="sort-nav hidden">
|
||||
|
|
|
|||
|
|
@ -55,3 +55,4 @@
|
|||
to select an alarm in the main menu.
|
||||
0.50: Bangle.js 2: Long touch of alarm in main menu toggle it on/off. Touching the icon on
|
||||
the right will do the same.
|
||||
0.51: Fix long-touch to enable alarm/timer not updating time (fix #3804)
|
||||
|
|
|
|||
|
|
@ -87,11 +87,10 @@ function showMainMenu(scroll, group, scrollback) {
|
|||
};
|
||||
const getGroups = settings.showGroup && !group;
|
||||
const groups = getGroups ? {} : undefined;
|
||||
var showAlarm;
|
||||
const getIcon = (e)=>{return e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff);};
|
||||
|
||||
alarms.forEach((e, index) => {
|
||||
showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
|
||||
const showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
|
||||
if(showAlarm) {
|
||||
const label = trimLabel(getLabel(e),40);
|
||||
menu[label] = {
|
||||
|
|
@ -99,6 +98,7 @@ function showMainMenu(scroll, group, scrollback) {
|
|||
onchange: (v, touch) => {
|
||||
if (touch && (2==touch.type || 145<touch.x)) { // Long touch or touched icon.
|
||||
e.on = v;
|
||||
if (e.on) prepareForSave(e, index);
|
||||
saveAndReload();
|
||||
} else {
|
||||
setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller?scroller.scroll:undefined, group);
|
||||
|
|
@ -328,6 +328,14 @@ function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) {
|
|||
}
|
||||
}
|
||||
|
||||
function prepareForSave(alarm, alarmIndex) {
|
||||
if (alarm.timer) {
|
||||
prepareTimerForSave(alarm, alarmIndex, require("time_utils").decodeTime(alarm.timer));
|
||||
} else {
|
||||
prepareAlarmForSave(alarm, alarmIndex, require("time_utils").decodeTime(alarm.t));
|
||||
}
|
||||
}
|
||||
|
||||
function saveAndReload() {
|
||||
// Before saving revert the dow to the standard format (alarms only!)
|
||||
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
|
||||
|
|
@ -574,13 +582,7 @@ function enableAll(on) {
|
|||
if (confirm) {
|
||||
alarms.forEach((alarm, i) => {
|
||||
alarm.on = on;
|
||||
if (on) {
|
||||
if (alarm.timer) {
|
||||
prepareTimerForSave(alarm, i, require("time_utils").decodeTime(alarm.timer));
|
||||
} else {
|
||||
prepareAlarmForSave(alarm, i, require("time_utils").decodeTime(alarm.t));
|
||||
}
|
||||
}
|
||||
if (on) prepareForSave(alarm, i);
|
||||
});
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.50",
|
||||
"version": "0.51",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm",
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps.
|
||||
0.03: Added option to display weather.
|
||||
0.04: Added option to display live updates of step count.
|
||||
0.05: Reset graphics before initial clearing of the screen. Helps in some
|
||||
situations if using fastload utils.
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@
|
|||
/* Startup Process
|
||||
------------------------------------------------------------------------------*/
|
||||
|
||||
g.clear();
|
||||
g.clear(1);
|
||||
drawAll();
|
||||
startTimers();
|
||||
registerEvents();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "edgeclk",
|
||||
"name": "Edge Clock",
|
||||
"shortName": "Edge Clock",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Crisp clock with perfect readability.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@
|
|||
* @param _button 1 for left half, 2 for right half
|
||||
* @param xy postion on screen
|
||||
*/
|
||||
let onTouch = function (_button: number, xy: { x: number, y: number } | undefined) {
|
||||
let onTouch = function (_button, xy) {
|
||||
// Determine which grid cell was tapped
|
||||
let x: number = Math.floor((xy!.x - 12) / ((g.getWidth() - 24) / config.display.rows));
|
||||
if (x < 0) x = 0;
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies TouchCallback;
|
||||
|
||||
let page: number = 0;
|
||||
let nPages: number; // Set when setting folder
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: first release
|
||||
0.02: memory leak fix; color changes to better align with VSCode color scheme; logo is transparent
|
||||
0.03: Fixed redrawing of commas
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ let redraw = function() {
|
|||
if (!(key in valsArrs)) continue;
|
||||
let valsArr = valsArrs[key];
|
||||
if (value === valsArr.text) continue; // No need to update
|
||||
if (valsArr.endComma) value = value.slice(0, -1);
|
||||
valsArrs[key].text = value;
|
||||
|
||||
// Clear prev values
|
||||
|
|
@ -239,7 +240,7 @@ let redraw = function() {
|
|||
g.drawString(value, valsArr.x, valsArr.y);
|
||||
if (valsArr.endComma){
|
||||
g.setColor(clrs.brackets);
|
||||
g.drawString(',', valsArr.Banglex + g.stringWidth(value), valsArr.y);
|
||||
g.drawString(',', valsArr.x + g.stringWidth(value), valsArr.y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "jsonclock",
|
||||
"name": "JsonClock",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "JSON view of the time, date, steps, battery, and sunrise and sunset times",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"dark.png"}],
|
||||
|
|
|
|||
|
|
@ -475,12 +475,6 @@ module.exports = {
|
|||
"no-undef"
|
||||
]
|
||||
},
|
||||
"apps/warpdrive/app.js": {
|
||||
"hash": "c2f9113c4d298a3021ec4bc0bc5f5d1bcd88267b4fa2acc03ae17d6447ed7d00",
|
||||
"rules": [
|
||||
"no-undef"
|
||||
]
|
||||
},
|
||||
"apps/usgs/settings.js": {
|
||||
"hash": "00ee672a6920f5667bfbd2988fd2853cfd579895a843ae036a00028dcb13878d",
|
||||
"rules": [
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
0.02: Show elapsed time on pause screen
|
||||
0.03: Avoid initial GPS skew and allow reset of time/splits
|
||||
0.04: Bump exstats module - show active time, not elapsed
|
||||
0.05: Fix menu display - don't draw over the menu and vice-versa. Require
|
||||
double-tap for menu
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ A running pace app, useful for races. Will also record your splits and display t
|
|||
Drag up/down on the pause menu to scroll through your splits.
|
||||
Press the button to pause/resume - when resumed, pressing the button will pause instantly, regardless of whether the screen is locked.
|
||||
|
||||
Double tap the pause screen to access a menu for finer control
|
||||
|
||||
# Todo
|
||||
|
||||
- Load splits on app start, button to reset (exs is always reset)
|
||||
- Show total time on pause screen
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
});
|
||||
var S_1 = require("Storage");
|
||||
var drawTimeout_1;
|
||||
var menuShown_1 = false;
|
||||
var splits_1 = [];
|
||||
var splitOffset_1 = 0, splitOffsetPx_1 = 0;
|
||||
var GPS_TIMEOUT_MS_1 = 30000;
|
||||
|
|
@ -113,12 +114,10 @@
|
|||
};
|
||||
var pauseRun_1 = function () {
|
||||
exs_1.stop();
|
||||
Bangle.setGPSPower(0, "pace");
|
||||
draw_1();
|
||||
};
|
||||
var resumeRun_1 = function () {
|
||||
exs_1.resume();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
g.clearRect(Bangle.appRect);
|
||||
layout_1.forgetLazyState();
|
||||
draw_1();
|
||||
|
|
@ -129,6 +128,12 @@
|
|||
else
|
||||
resumeRun_1();
|
||||
};
|
||||
var hideMenu_1 = function () {
|
||||
if (!menuShown_1)
|
||||
return;
|
||||
Bangle.setUI();
|
||||
menuShown_1 = false;
|
||||
};
|
||||
exs_1.start();
|
||||
exs_1.stats.dist.on("notify", function (dist) {
|
||||
var prev = { time: 0, dist: 0 };
|
||||
|
|
@ -156,7 +161,7 @@
|
|||
});
|
||||
setWatch(function () { return onButton_1(); }, BTN1, { repeat: true });
|
||||
Bangle.on('drag', function (e) {
|
||||
if (exs_1.state.active || e.b === 0)
|
||||
if (exs_1.state.active || e.b === 0 || menuShown_1)
|
||||
return;
|
||||
splitOffsetPx_1 -= e.dy;
|
||||
if (splitOffsetPx_1 > 20) {
|
||||
|
|
@ -174,9 +179,10 @@
|
|||
Bangle.on('twist', function () {
|
||||
Bangle.setBacklight(1);
|
||||
});
|
||||
Bangle.on('tap', function (_e) {
|
||||
if (exs_1.state.active)
|
||||
Bangle.on('tap', function (e) {
|
||||
if (exs_1.state.active || menuShown_1 || !e.double)
|
||||
return;
|
||||
menuShown_1 = true;
|
||||
var menu = {
|
||||
"": {
|
||||
remove: function () {
|
||||
|
|
@ -184,22 +190,23 @@
|
|||
},
|
||||
},
|
||||
"< Back": function () {
|
||||
Bangle.setUI();
|
||||
hideMenu_1();
|
||||
},
|
||||
"Zero time": function () {
|
||||
exs_1.start();
|
||||
exs_1.stop();
|
||||
Bangle.setUI();
|
||||
hideMenu_1();
|
||||
},
|
||||
"Clear splits": function () {
|
||||
splits_1.splice(0, splits_1.length);
|
||||
Bangle.setUI();
|
||||
hideMenu_1();
|
||||
},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
});
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
g.clearRect(Bangle.appRect);
|
||||
draw_1();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const exs = require("exstats").getStats(
|
|||
const S = require("Storage");
|
||||
|
||||
let drawTimeout: TimeoutId | undefined;
|
||||
let menuShown = false;
|
||||
|
||||
type Dist = number & { brand: 'dist' };
|
||||
type Time = number & { brand: 'time' };
|
||||
|
|
@ -151,13 +152,11 @@ const drawSplit = (i: number, y: number, pace: number | string) =>
|
|||
|
||||
const pauseRun = () => {
|
||||
exs.stop();
|
||||
Bangle.setGPSPower(0, "pace")
|
||||
draw();
|
||||
};
|
||||
|
||||
const resumeRun = () => {
|
||||
exs.resume();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
|
||||
g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine
|
||||
layout.forgetLazyState();
|
||||
|
|
@ -171,6 +170,12 @@ const onButton = () => {
|
|||
resumeRun();
|
||||
};
|
||||
|
||||
const hideMenu = () => {
|
||||
if (!menuShown) return;
|
||||
Bangle.setUI(); // calls `remove`, which handles redrawing
|
||||
menuShown = false;
|
||||
}
|
||||
|
||||
exs.start(); // aka reset
|
||||
|
||||
exs.stats.dist.on("notify", (dist) => {
|
||||
|
|
@ -209,7 +214,7 @@ Bangle.on('lock', locked => {
|
|||
setWatch(() => onButton(), BTN1, { repeat: true });
|
||||
|
||||
Bangle.on('drag', e => {
|
||||
if (exs.state.active || e.b === 0) return;
|
||||
if (exs.state.active || e.b === 0 || menuShown) return;
|
||||
|
||||
splitOffsetPx -= e.dy;
|
||||
if (splitOffsetPx > 20) {
|
||||
|
|
@ -226,9 +231,11 @@ Bangle.on('twist', () => {
|
|||
Bangle.setBacklight(1);
|
||||
});
|
||||
|
||||
Bangle.on('tap', _e => {
|
||||
if(exs.state.active) return;
|
||||
Bangle.on('tap', e => {
|
||||
// require a double tap, to avoid picking up menu "< Back" taps
|
||||
if(exs.state.active || menuShown || !e.double) return;
|
||||
|
||||
menuShown = true;
|
||||
const menu: Menu = {
|
||||
"": {
|
||||
remove: () => {
|
||||
|
|
@ -236,16 +243,16 @@ Bangle.on('tap', _e => {
|
|||
},
|
||||
},
|
||||
"< Back": () => {
|
||||
Bangle.setUI(); // calls `remove`, which handles redrawing
|
||||
hideMenu();
|
||||
},
|
||||
"Zero time": () => {
|
||||
exs.start(); // calls reset
|
||||
exs.stop(); // re-pauses
|
||||
Bangle.setUI();
|
||||
hideMenu();
|
||||
},
|
||||
"Clear splits": () => {
|
||||
splits.splice(0, splits.length);
|
||||
Bangle.setUI();
|
||||
hideMenu();
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -254,6 +261,7 @@ Bangle.on('tap', _e => {
|
|||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
|
||||
g.clearRect(Bangle.appRect);
|
||||
draw();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pace",
|
||||
"name": "Pace",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Show pace and time running splits",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@
|
|||
0.10: Trigger `remove` callbacks when ending the menu
|
||||
0.11: Add options for natural scroll and disabling wrap-around
|
||||
0.12: Fix bug where settings would behave as if all were set to false
|
||||
0.13: Update to new touch-event handling
|
||||
0.14: Fix bug in handling changes to `Bangle.appRect`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
var _a, _b;
|
||||
var S = (require("Storage").readJSON("promenu.settings.json", true) || {});
|
||||
(_a = S.naturalScroll) !== null && _a !== void 0 ? _a : (S.naturalScroll = false);
|
||||
(_b = S.wrapAround) !== null && _b !== void 0 ? _b : (S.wrapAround = true);
|
||||
var prosettings = (require("Storage").readJSON("promenu.settings.json", true) || {});
|
||||
(_a = prosettings.naturalScroll) !== null && _a !== void 0 ? _a : (prosettings.naturalScroll = false);
|
||||
(_b = prosettings.wrapAround) !== null && _b !== void 0 ? _b : (prosettings.wrapAround = true);
|
||||
E.showMenu = function (items) {
|
||||
var RectRnd = function (x1, y1, x2, y2, r) {
|
||||
var pp = [];
|
||||
|
|
@ -20,14 +20,7 @@ E.showMenu = function (items) {
|
|||
var menuItems = Object.keys(items).filter(function (x) { return x.length; });
|
||||
var fontHeight = options.fontHeight || 25;
|
||||
var selected = options.scroll || options.selected || 0;
|
||||
var ar = Bangle.appRect;
|
||||
g.reset().clearRect(ar);
|
||||
var x = ar.x;
|
||||
var x2 = ar.x2;
|
||||
var y = ar.y;
|
||||
var y2 = ar.y2 - 12;
|
||||
if (options.title)
|
||||
y += 22;
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
var lastIdx = 0;
|
||||
var selectEdit = undefined;
|
||||
var scroller = {
|
||||
|
|
@ -36,6 +29,7 @@ E.showMenu = function (items) {
|
|||
var nameScroller = null;
|
||||
var drawLine = function (name, v, item, idx, x, y, nameScroll) {
|
||||
if (nameScroll === void 0) { nameScroll = 0; }
|
||||
var x2 = Bangle.appRect.x2;
|
||||
var hl = (idx === selected && !selectEdit);
|
||||
if (g.theme.dark) {
|
||||
fillRectRnd(x, y, x2, y + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg + 40);
|
||||
|
|
@ -74,6 +68,12 @@ E.showMenu = function (items) {
|
|||
};
|
||||
var l = {
|
||||
draw: function (rowmin, rowmax) {
|
||||
var _a = Bangle.appRect, x = _a.x, x2 = _a.x2, y = _a.y, y2 = _a.y2;
|
||||
if (y === 0)
|
||||
y = 24;
|
||||
if (options.title)
|
||||
y += 22;
|
||||
y2 -= 12;
|
||||
if (nameScroller)
|
||||
clearInterval(nameScroller), nameScroller = null;
|
||||
var rows = 0 | Math.min((y2 - y) / fontHeight, menuItems.length);
|
||||
|
|
@ -133,10 +133,10 @@ E.showMenu = function (items) {
|
|||
g.setColor((idx < menuItems.length) ? g.theme.fg : g.theme.bg).fillPoly([72, 166, 104, 166, 88, 174]);
|
||||
g.flip();
|
||||
},
|
||||
select: function () {
|
||||
select: function (evt) {
|
||||
var item = items[menuItems[selected]];
|
||||
if (typeof item === "function") {
|
||||
item();
|
||||
item(evt);
|
||||
}
|
||||
else if (typeof item === "object") {
|
||||
if (typeof item.value === "number") {
|
||||
|
|
@ -146,12 +146,12 @@ E.showMenu = function (items) {
|
|||
if (typeof item.value === "boolean")
|
||||
item.value = !item.value;
|
||||
if (item.onchange)
|
||||
item.onchange(item.value);
|
||||
item.onchange(item.value, evt);
|
||||
}
|
||||
l.draw();
|
||||
}
|
||||
},
|
||||
move: function (dir) {
|
||||
move: function (dir, evt) {
|
||||
var item = selectEdit;
|
||||
if (typeof item === "object" && typeof item.value === "number") {
|
||||
var orig = item.value;
|
||||
|
|
@ -162,13 +162,13 @@ E.showMenu = function (items) {
|
|||
item.value = item.wrap ? item.min : item.max;
|
||||
if (item.value !== orig) {
|
||||
if (item.onchange)
|
||||
item.onchange(item.value);
|
||||
item.onchange(item.value, evt);
|
||||
l.draw(selected, selected);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var lastSelected = selected;
|
||||
if (S.wrapAround) {
|
||||
if (prosettings.wrapAround) {
|
||||
selected = (selected + dir + menuItems.length) % menuItems.length;
|
||||
}
|
||||
else {
|
||||
|
|
@ -198,6 +198,12 @@ E.showMenu = function (items) {
|
|||
};
|
||||
Bangle.on('swipe', onSwipe);
|
||||
}
|
||||
var cb = function (dir, evt) {
|
||||
if (dir)
|
||||
l.move(prosettings.naturalScroll ? -dir : dir, evt);
|
||||
else
|
||||
l.select(evt);
|
||||
};
|
||||
Bangle.setUI({
|
||||
mode: "updown",
|
||||
back: back,
|
||||
|
|
@ -208,11 +214,9 @@ E.showMenu = function (items) {
|
|||
Bangle.removeListener("swipe", onSwipe);
|
||||
(_a = options.remove) === null || _a === void 0 ? void 0 : _a.call(options);
|
||||
},
|
||||
}, function (dir) {
|
||||
if (dir)
|
||||
l.move(S.naturalScroll ? -dir : dir);
|
||||
else
|
||||
l.select();
|
||||
});
|
||||
touch: (function (_button, xy) {
|
||||
cb(void 0, xy);
|
||||
}),
|
||||
}, cb);
|
||||
return l;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ const enum Consts {
|
|||
NAME_SCROLL_PAD = 5,
|
||||
}
|
||||
|
||||
const S = (require("Storage").readJSON("promenu.settings.json", true) || {}) as PromenuSettings;
|
||||
S.naturalScroll ??= false;
|
||||
S.wrapAround ??= true;
|
||||
const prosettings = (require("Storage").readJSON("promenu.settings.json", true) || {}) as PromenuSettings;
|
||||
prosettings.naturalScroll ??= false;
|
||||
prosettings.wrapAround ??= true;
|
||||
|
||||
E.showMenu = (items?: Menu): MenuInstance => {
|
||||
const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => {
|
||||
|
|
@ -34,15 +34,7 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
|
||||
let selected = options.scroll || options.selected || 0;
|
||||
|
||||
const ar = Bangle.appRect;
|
||||
g.reset().clearRect(ar);
|
||||
|
||||
const x = ar.x;
|
||||
const x2 = ar.x2;
|
||||
let y = ar.y;
|
||||
const y2 = ar.y2 - 12; // padding at end for arrow
|
||||
if (options.title)
|
||||
y += 22;
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
|
||||
let lastIdx = 0;
|
||||
let selectEdit: undefined | ActualMenuItem = undefined;
|
||||
|
|
@ -61,6 +53,7 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
y: number,
|
||||
nameScroll: number = 0,
|
||||
) => {
|
||||
const { x2 } = Bangle.appRect;
|
||||
const hl = (idx === selected && !selectEdit);
|
||||
if(g.theme.dark){
|
||||
fillRectRnd(x, y, x2, y + fontHeight - 3, 7, hl ? g.theme.bgH : g.theme.bg + 40);
|
||||
|
|
@ -114,6 +107,13 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
|
||||
const l = {
|
||||
draw: (rowmin?: number, rowmax?: number) => {
|
||||
// always refresh appRect, a back button may have appeared
|
||||
let { x, x2, y, y2 } = Bangle.appRect;
|
||||
if(y === 0) y = 24; // always bump down for widgets/back button
|
||||
|
||||
if (options.title) y += 22;
|
||||
y2 -= 12; // padding at end for arrow
|
||||
|
||||
if (nameScroller) clearInterval(nameScroller), nameScroller = null;
|
||||
let rows = 0|Math.min((y2 - y) / fontHeight, menuItems.length);
|
||||
let idx = E.clip(selected - (rows>>1), 0, menuItems.length - rows);
|
||||
|
|
@ -175,11 +175,11 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
g.setColor((idx < menuItems.length)?g.theme.fg:g.theme.bg).fillPoly([72, 166, 104, 166, 88, 174]);
|
||||
g.flip();
|
||||
},
|
||||
select: () => {
|
||||
select: (evt: TouchCallbackXY | undefined) => {
|
||||
const item = items![menuItems[selected]] as ActualMenuItem;
|
||||
|
||||
if (typeof item === "function") {
|
||||
item();
|
||||
item(evt);
|
||||
} else if (typeof item === "object") {
|
||||
if (typeof item.value === "number") {
|
||||
selectEdit = selectEdit ? undefined : item;
|
||||
|
|
@ -188,12 +188,12 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
item.value = !item.value;
|
||||
|
||||
if (item.onchange)
|
||||
item.onchange(item.value as boolean);
|
||||
item.onchange(item.value as boolean, evt);
|
||||
}
|
||||
l.draw();
|
||||
}
|
||||
},
|
||||
move: (dir: number) => {
|
||||
move: (dir: number, evt: TouchCallbackXY | undefined) => {
|
||||
const item = selectEdit;
|
||||
|
||||
if (typeof item === "object" && typeof item.value === "number") {
|
||||
|
|
@ -209,14 +209,14 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
|
||||
if (item.value !== orig) {
|
||||
if (item.onchange)
|
||||
item.onchange(item.value);
|
||||
item.onchange(item.value, evt);
|
||||
|
||||
l.draw(selected, selected);
|
||||
}
|
||||
|
||||
} else {
|
||||
const lastSelected = selected;
|
||||
if (S.wrapAround) {
|
||||
if (prosettings.wrapAround) {
|
||||
selected = (selected + dir + /*keep +ve*/menuItems.length) % menuItems.length;
|
||||
} else {
|
||||
selected = E.clip(selected + dir, 0, menuItems.length - 1);
|
||||
|
|
@ -247,6 +247,11 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
Bangle.on('swipe', onSwipe);
|
||||
}
|
||||
|
||||
const cb = (dir?: 1 | -1, evt?: TouchCallbackXY) => {
|
||||
if (dir) l.move(prosettings.naturalScroll ? -dir : dir, evt);
|
||||
else l.select(evt);
|
||||
};
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "updown",
|
||||
back,
|
||||
|
|
@ -255,11 +260,13 @@ E.showMenu = (items?: Menu): MenuInstance => {
|
|||
Bangle.removeListener("swipe", onSwipe);
|
||||
options.remove?.();
|
||||
},
|
||||
} as SetUIArg<"updown">,
|
||||
dir => {
|
||||
if (dir) l.move(S.naturalScroll ? -dir : dir);
|
||||
else l.select();
|
||||
});
|
||||
touch: ((_button, xy) => {
|
||||
// since we've specified options.touch,
|
||||
// we need to pass through all taps since the default
|
||||
// touchHandler isn't installed in setUI
|
||||
cb(void 0, xy);
|
||||
}) satisfies TouchCallback,
|
||||
} as SetUIArg<"updown">, cb);
|
||||
|
||||
return l;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "promenu",
|
||||
"name": "Pro Menu",
|
||||
"version": "0.12",
|
||||
"version": "0.14",
|
||||
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
|
||||
"icon": "icon.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Allow scanning of QR codes from camera or file
|
||||
0.05: Change brightness on touch
|
||||
0.06: Add ability to generate contact info (MeCard format) QR code
|
||||
0.07: Add custom appname (for storing multiple QR codes)
|
||||
|
|
|
|||
|
|
@ -17,61 +17,61 @@
|
|||
<hr>
|
||||
|
||||
<div id="srcText">
|
||||
<p>Text/URL: <input type="text" id="text" class="form-input" value="http://www.espruino.com"></p>
|
||||
<p>Text/URL: <input type="text" id="text" class="form-input" value="http://www.espruino.com"></p>
|
||||
</div>
|
||||
|
||||
<div id="srcScanCam">
|
||||
<div>
|
||||
<video id="qrVideo" align="center" width="50%"></video>
|
||||
</div>
|
||||
<div>
|
||||
<select id="camList">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button id="flashToggle">Flash: <span id="flashState">off</span></button>
|
||||
</div>
|
||||
<br>
|
||||
Detected QR code:
|
||||
<span id="camQrResult">None</span>
|
||||
<br>
|
||||
<button id="startButton" class="btn btn-primary">Start</button>
|
||||
<button id="stopButton" class="btn btn-primary">Stop</button>
|
||||
<div>
|
||||
<video id="qrVideo" align="center" width="50%"></video>
|
||||
</div>
|
||||
<div>
|
||||
<select id="camList">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button id="flashToggle">Flash: <span id="flashState">off</span></button>
|
||||
</div>
|
||||
<br>
|
||||
Detected QR code:
|
||||
<span id="camQrResult">None</span>
|
||||
<br>
|
||||
<button id="startButton" class="btn btn-primary">Start</button>
|
||||
<button id="stopButton" class="btn btn-primary">Stop</button>
|
||||
</div>
|
||||
|
||||
<div id="srcScanFile">
|
||||
<input type="file" id="fileSelector">
|
||||
<br>
|
||||
Detected QR code:
|
||||
<span id="fileQrResult">None</span>
|
||||
<input type="file" id="fileSelector">
|
||||
<br>
|
||||
Detected QR code:
|
||||
<span id="fileQrResult">None</span>
|
||||
</div>
|
||||
|
||||
<div id="srcWifi">
|
||||
<p>Wifi name: <input type="text" id="ssid" class="form-input" value=""></p>
|
||||
<p>Wifi password: <input type="password" id="password" class="form-input" value=""></p>
|
||||
<div class="form-group">
|
||||
<label for="encryption" class="control-label">Encryption</label>
|
||||
<div class="input-group">
|
||||
<select name="encryption" id="encryption" class="form-control">
|
||||
<option value="WPA">WPA/WPA2</option>
|
||||
<option value="WEP">WEP</option>
|
||||
<option value="nopass">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<p>Wifi name: <input type="text" id="ssid" class="form-input" value=""></p>
|
||||
<p>Wifi password: <input type="password" id="password" class="form-input" value=""></p>
|
||||
<div class="form-group">
|
||||
<label for="encryption" class="control-label">Encryption</label>
|
||||
<div class="input-group">
|
||||
<select name="encryption" id="encryption" class="form-control">
|
||||
<option value="WPA">WPA/WPA2</option>
|
||||
<option value="WEP">WEP</option>
|
||||
<option value="nopass">None</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="hidden" name="hidden"/>
|
||||
<label for="hidden">Wifi is hidden</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="hidden" name="hidden"/>
|
||||
<label for="hidden">Wifi is hidden</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="srcMeCard">
|
||||
<p>First Name: <input type="text" id="meNameFirst" class="form-input" value=""></p>
|
||||
<p>Last Name: <input type="text" id="meNameLast" class="form-input" value=""></p>
|
||||
<p>Phone Number: <input type="text" id="mePhoneNumber" class="form-input" value=""></p>
|
||||
<p>Email: <input type="text" id="meEmail" class="form-input" value=""></p>
|
||||
<p>Website: <input type="text" id="meWebsite" class="form-input" value=""></p>
|
||||
<p>First Name: <input type="text" id="meNameFirst" class="form-input" value=""></p>
|
||||
<p>Last Name: <input type="text" id="meNameLast" class="form-input" value=""></p>
|
||||
<p>Phone Number: <input type="text" id="mePhoneNumber" class="form-input" value=""></p>
|
||||
<p>Email: <input type="text" id="meEmail" class="form-input" value=""></p>
|
||||
<p>Website: <input type="text" id="meWebsite" class="form-input" value=""></p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
|
@ -82,34 +82,35 @@
|
|||
<p>Additional options:</p>
|
||||
<input type="checkbox" id="preventIntegerScaling" name="preventIntegerScaling"/>
|
||||
<label for="preventIntegerScaling">Prevent integer scaling</label></br>
|
||||
<input type="checkbox" id="preventBrightnessChangeOnTouch" name="preventBrightnessChangeOnTouch"/>
|
||||
<label for="preventBrightnessChangeOnTouch">Prevent brightness change on touch</label></br>
|
||||
<input type="checkbox" id="boostBacklight" name="boostBacklight"/>
|
||||
<label for="boostBacklight">Set initial backlight to max. while QR is shown</label></br>
|
||||
<input type="checkbox" id="stayOn" name="stayOn"/>
|
||||
<label for="stayOn">Do not lock or dim while showing QR</label></br>
|
||||
<input type="checkbox" id="hideDescription" name="hideDescription"/>
|
||||
<label for="hideDescription">Hide Description</label></br>
|
||||
<label for="description">Replace default description:</label>
|
||||
<input type="text" id="description" class="form-input" value="">
|
||||
<label for="correction">Error correction level:</label>
|
||||
<div class="input-group">
|
||||
<input type="checkbox" id="preventBrightnessChangeOnTouch" name="preventBrightnessChangeOnTouch"/>
|
||||
<label for="preventBrightnessChangeOnTouch">Prevent brightness change on touch</label></br>
|
||||
<input type="checkbox" id="boostBacklight" name="boostBacklight"/>
|
||||
<label for="boostBacklight">Set initial backlight to max. while QR is shown</label></br>
|
||||
<input type="checkbox" id="stayOn" name="stayOn"/>
|
||||
<label for="stayOn">Do not lock or dim while showing QR</label></br>
|
||||
<input type="checkbox" id="hideDescription" name="hideDescription"/>
|
||||
<label for="hideDescription">Hide Description</label></br>
|
||||
<label for="description">Replace default description:</label>
|
||||
<input type="text" id="description" class="form-input" value="">
|
||||
<label for="correction">Error correction level:</label>
|
||||
<div class="input-group">
|
||||
<select name="correction" id="correction" class="form-control">
|
||||
<option value="1">L - Low - 7%</option>
|
||||
<option value="0">M - Medium - 15%</option>
|
||||
<option value="3">Q - Quartile - 25%</option>
|
||||
<option value="2">H - High - 30%</option>
|
||||
<option value="1">L - Low - 7%</option>
|
||||
<option value="0">M - Medium - 15%</option>
|
||||
<option value="3">Q - Quartile - 25%</option>
|
||||
<option value="2">H - High - 30%</option>
|
||||
</select>
|
||||
</div>
|
||||
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
|
||||
</div>
|
||||
<label for="appname">App name</label>
|
||||
<input type="text" id="appname" class="form-input" value="qrcode">
|
||||
<button id="upload" class="btn btn-primary">Upload</button>
|
||||
|
||||
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<script src="../../core/lib/qrcode.min.js"></script><!-- https://davidshimjs.github.io/qrcodejs/ -->
|
||||
<script src="../../webtools/heatshrink.js"></script>
|
||||
<script src="../../webtools/imageconverter.js"></script>
|
||||
<script src="./qr-scanner.umd.min.js"></script><!-- https://github.com/nimiq/qr-scanner -->
|
||||
<script>
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
<script src="../../core/lib/qrcode.min.js"></script><!-- https://davidshimjs.github.io/qrcodejs/ -->
|
||||
<script src="../../webtools/heatshrink.js"></script>
|
||||
<script src="../../webtools/imageconverter.js"></script>
|
||||
<script src="./qr-scanner.umd.min.js"></script><!-- https://github.com/nimiq/qr-scanner -->
|
||||
<script>
|
||||
var targetSize = 200;
|
||||
var deviceWidth = targetSize;
|
||||
var deviceHeight = targetSize;
|
||||
|
|
@ -120,190 +121,190 @@
|
|||
|
||||
|
||||
function onInit(device) {
|
||||
console.info("onInit" + device);
|
||||
if (device && device.info && device.info.g) {
|
||||
deviceWidth = device.info.g.width;
|
||||
deviceHeight = device.info.g.height;
|
||||
}
|
||||
refreshQRCode();
|
||||
console.info("onInit" + device);
|
||||
if (device && device.info && device.info.g) {
|
||||
deviceWidth = device.info.g.width;
|
||||
deviceHeight = device.info.g.height;
|
||||
}
|
||||
refreshQRCode();
|
||||
}
|
||||
|
||||
const updateFlashAvailability = () => {
|
||||
scanner.hasFlash().then(hasFlash => {
|
||||
document.getElementById('flashToggle').style.display = hasFlash ? 'inline-block' : 'none';
|
||||
});
|
||||
scanner.hasFlash().then(hasFlash => {
|
||||
document.getElementById('flashToggle').style.display = hasFlash ? 'inline-block' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
function setResult(label, result) {
|
||||
console.info("setResult " + result);
|
||||
label.textContent = result;
|
||||
scanner.stop();
|
||||
refreshQRCode();
|
||||
console.info("setResult " + result);
|
||||
label.textContent = result;
|
||||
scanner.stop();
|
||||
refreshQRCode();
|
||||
}
|
||||
|
||||
function initQrScanner() {
|
||||
console.info("initQrScanner");
|
||||
QrScanner.WORKER_PATH = './qr-scanner-worker.min.js';
|
||||
if (scanner == null) {
|
||||
scanner = new QrScanner(document.getElementById('qrVideo'), result => setResult(document.getElementById('camQrResult'), result), error => {
|
||||
document.getElementById('camQrResult').textContent = error;
|
||||
document.getElementById('camQrResult').style.color = 'inherit';
|
||||
});
|
||||
}
|
||||
console.info("initQrScanner");
|
||||
QrScanner.WORKER_PATH = './qr-scanner-worker.min.js';
|
||||
if (scanner == null) {
|
||||
scanner = new QrScanner(document.getElementById('qrVideo'), result => setResult(document.getElementById('camQrResult'), result), error => {
|
||||
document.getElementById('camQrResult').textContent = error;
|
||||
document.getElementById('camQrResult').style.color = 'inherit';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initQrCam(){
|
||||
scanner.start().then(() => {
|
||||
scanner.start().then(() => {
|
||||
updateFlashAvailability();
|
||||
QrScanner.listCameras(true).then(cameras => cameras.forEach(camera => {
|
||||
const option = document.createElement('option');
|
||||
option.value = camera.id;
|
||||
option.text = camera.label;
|
||||
document.getElementById('camList').add(option);
|
||||
}));
|
||||
});
|
||||
const option = document.createElement('option');
|
||||
option.value = camera.id;
|
||||
option.text = camera.label;
|
||||
document.getElementById('camList').add(option);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function toggleVis(id){
|
||||
console.info("Got id", id);
|
||||
["srcScanFile", "srcText", "srcWifi", "srcScanCam", "srcMeCard"].forEach(function (item){
|
||||
document.getElementById(item).style.display = "none";
|
||||
});
|
||||
if (id != undefined && id != null) document.getElementById(id).style.display = "block";
|
||||
refreshQRCode();
|
||||
console.info("Got id", id);
|
||||
["srcScanFile", "srcText", "srcWifi", "srcScanCam", "srcMeCard"].forEach(function (item){
|
||||
document.getElementById(item).style.display = "none";
|
||||
});
|
||||
if (id != undefined && id != null) document.getElementById(id).style.display = "block";
|
||||
refreshQRCode();
|
||||
}
|
||||
|
||||
toggleVis("srcText");
|
||||
|
||||
//https://github.com/evgeni/qifi/blob/gh-pages/index.html#L168
|
||||
function escapeString (string) {
|
||||
var to_escape = ['\\', ';', ',', ':', '"'];
|
||||
var hex_only = /^[0-9a-f]+$/i;
|
||||
var output = "";
|
||||
for (var i=0; i<string.length; i++) {
|
||||
var to_escape = ['\\', ';', ',', ':', '"'];
|
||||
var hex_only = /^[0-9a-f]+$/i;
|
||||
var output = "";
|
||||
for (var i=0; i<string.length; i++) {
|
||||
if(string[i].includes(to_escape)) {
|
||||
output += '\\'+string[i];
|
||||
output += '\\'+string[i];
|
||||
}
|
||||
else {
|
||||
output += string[i];
|
||||
output += string[i];
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function generateWifiString(ssid, password, hidden,encryption){
|
||||
//https://github.com/evgeni/qifi/blob/gh-pages/index.html#L198
|
||||
var qrstring = 'WIFI:S:'+escapeString(ssid)+';T:'+encryption+';P:'+escapeString(password)+';';
|
||||
if (hidden) {
|
||||
qrstring += 'H:true';
|
||||
}
|
||||
return qrstring;
|
||||
//https://github.com/evgeni/qifi/blob/gh-pages/index.html#L198
|
||||
var qrstring = 'WIFI:S:'+escapeString(ssid)+';T:'+encryption+';P:'+escapeString(password)+';';
|
||||
if (hidden) {
|
||||
qrstring += 'H:true';
|
||||
}
|
||||
return qrstring;
|
||||
}
|
||||
|
||||
function generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite){
|
||||
var meCardStringOutput = 'MECARD:';
|
||||
var meCardStringOutput = 'MECARD:';
|
||||
|
||||
//first & Last name part of string, can have one or both
|
||||
if (meNameFirst.trim().length != 0 && meNameLast.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameLast.trim()+','+meNameFirst.trim()+';';
|
||||
}
|
||||
else if (meNameLast.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameLast.trim()+';';
|
||||
}
|
||||
else if (meNameFirst.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameFirst.trim()+';';
|
||||
}
|
||||
//first & Last name part of string, can have one or both
|
||||
if (meNameFirst.trim().length != 0 && meNameLast.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameLast.trim()+','+meNameFirst.trim()+';';
|
||||
}
|
||||
else if (meNameLast.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameLast.trim()+';';
|
||||
}
|
||||
else if (meNameFirst.trim().length != 0) {
|
||||
meCardStringOutput += 'N:'+meNameFirst.trim()+';';
|
||||
}
|
||||
|
||||
if (mePhoneNumber.trim().length != 0) {
|
||||
meCardStringOutput += 'TEL:'+mePhoneNumber.trim()+';';
|
||||
}
|
||||
if (mePhoneNumber.trim().length != 0) {
|
||||
meCardStringOutput += 'TEL:'+mePhoneNumber.trim()+';';
|
||||
}
|
||||
|
||||
if (meEmail.trim().length != 0) {
|
||||
meCardStringOutput += 'EMAIL:'+meEmail.trim()+';';
|
||||
}
|
||||
if (meEmail.trim().length != 0) {
|
||||
meCardStringOutput += 'EMAIL:'+meEmail.trim()+';';
|
||||
}
|
||||
|
||||
if (meWebsite.trim().length != 0) {
|
||||
meCardStringOutput += 'URL:'+meWebsite.trim()+';';
|
||||
}
|
||||
if (meWebsite.trim().length != 0) {
|
||||
meCardStringOutput += 'URL:'+meWebsite.trim()+';';
|
||||
}
|
||||
|
||||
meCardStringOutput += ';';
|
||||
return meCardStringOutput;
|
||||
}
|
||||
meCardStringOutput += ';';
|
||||
return meCardStringOutput;
|
||||
}
|
||||
|
||||
function refreshQRCode(){
|
||||
if (qrcode == null){
|
||||
qrcode = new QRCode("qrcode", {
|
||||
text: document.getElementById("text").value,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff"
|
||||
});
|
||||
}
|
||||
document.getElementById("errors").innerText="";
|
||||
qrcode.clear(); // clear the code.
|
||||
var qrText = "";
|
||||
if(document.getElementById("useWIFI").checked){
|
||||
const ssid = document.getElementById("ssid").value;
|
||||
const password = document.getElementById("password").value;
|
||||
const encryption = document.getElementById("encryption").value;
|
||||
const hidden = document.getElementById("hidden").checked;
|
||||
const wifiString = generateWifiString(ssid, password, hidden, encryption);
|
||||
qrText= wifiString;
|
||||
} else if (document.getElementById("useMECARD").checked) {
|
||||
const meNameFirst = document.getElementById("meNameFirst").value;
|
||||
const meNameLast = document.getElementById("meNameLast").value;
|
||||
const mePhoneNumber = document.getElementById("mePhoneNumber").value;
|
||||
const meEmail = document.getElementById("meEmail").value;
|
||||
const meWebsite = document.getElementById("meWebsite").value;
|
||||
const meCardString = generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite);
|
||||
qrText = meCardString;
|
||||
} else if (document.getElementById("useCAM").checked) {
|
||||
qrText= document.getElementById("camQrResult").innerText;
|
||||
} else if (document.getElementById("useFILE").checked) {
|
||||
qrText= document.getElementById("fileQrResult").innerText;
|
||||
} else {
|
||||
qrText = document.getElementById("text").value;
|
||||
}
|
||||
if (qrcode == null){
|
||||
qrcode = new QRCode("qrcode", {
|
||||
text: document.getElementById("text").value,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff"
|
||||
});
|
||||
}
|
||||
document.getElementById("errors").innerText="";
|
||||
qrcode.clear(); // clear the code.
|
||||
var qrText = "";
|
||||
if(document.getElementById("useWIFI").checked){
|
||||
const ssid = document.getElementById("ssid").value;
|
||||
const password = document.getElementById("password").value;
|
||||
const encryption = document.getElementById("encryption").value;
|
||||
const hidden = document.getElementById("hidden").checked;
|
||||
const wifiString = generateWifiString(ssid, password, hidden, encryption);
|
||||
qrText= wifiString;
|
||||
} else if (document.getElementById("useMECARD").checked) {
|
||||
const meNameFirst = document.getElementById("meNameFirst").value;
|
||||
const meNameLast = document.getElementById("meNameLast").value;
|
||||
const mePhoneNumber = document.getElementById("mePhoneNumber").value;
|
||||
const meEmail = document.getElementById("meEmail").value;
|
||||
const meWebsite = document.getElementById("meWebsite").value;
|
||||
const meCardString = generateMeCardString(meNameFirst, meNameLast, mePhoneNumber, meEmail, meWebsite);
|
||||
qrText = meCardString;
|
||||
} else if (document.getElementById("useCAM").checked) {
|
||||
qrText= document.getElementById("camQrResult").innerText;
|
||||
} else if (document.getElementById("useFILE").checked) {
|
||||
qrText= document.getElementById("fileQrResult").innerText;
|
||||
} else {
|
||||
qrText = document.getElementById("text").value;
|
||||
}
|
||||
|
||||
console.info("Given qrtext was: " + qrText);
|
||||
qrcode._htOption.text = qrText;
|
||||
qrcode._htOption.correctLevel = parseInt(document.getElementById("correction").value);
|
||||
try {
|
||||
qrcode.makeCode(qrText);
|
||||
} catch (error) {
|
||||
document.getElementById("errors").innerText="Error: QR could not be created.";
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
targetSize = Math.min(deviceWidth - border, deviceHeight - border);
|
||||
console.info("Targetsize: " + targetSize);
|
||||
var finalSizeQr=targetSize;
|
||||
var finalSizeCanvas=targetSize;
|
||||
|
||||
console.info("Given qrtext was: " + qrText);
|
||||
qrcode._htOption.text = qrText;
|
||||
qrcode._htOption.correctLevel = parseInt(document.getElementById("correction").value);
|
||||
try {
|
||||
qrcode.makeCode(qrText);
|
||||
} catch (error) {
|
||||
document.getElementById("errors").innerText="Error: QR could not be created.";
|
||||
console.error(error);
|
||||
}
|
||||
var integerScale = Math.max(Math.floor(targetSize / (qrcode._oQRCode.moduleCount + 1)),1);
|
||||
if (integerScale == 1) document.getElementById("errors").innerText = "Warning, QR will probably be too small to properly scan. Try less data or less error correction.";
|
||||
|
||||
targetSize = Math.min(deviceWidth - border, deviceHeight - border);
|
||||
console.info("Targetsize: " + targetSize);
|
||||
var finalSizeQr=targetSize;
|
||||
var finalSizeCanvas=targetSize;
|
||||
console.info("IntegerScale: " + integerScale);
|
||||
|
||||
var integerScale = Math.max(Math.floor(targetSize / (qrcode._oQRCode.moduleCount + 1)),1);
|
||||
if (integerScale == 1) document.getElementById("errors").innerText = "Warning, QR will probably be too small to properly scan. Try less data or less error correction.";
|
||||
if (!document.getElementById("preventIntegerScaling").checked){
|
||||
finalSizeQr = integerScale * (qrcode._oQRCode.moduleCount + 1);
|
||||
finalSizeCanvas = finalSizeQr - 1;
|
||||
}
|
||||
|
||||
console.info("IntegerScale: " + integerScale);
|
||||
console.info("FinalSizeQr: " + finalSizeQr);
|
||||
console.info("FinalSizeCanvas: " + finalSizeCanvas);
|
||||
|
||||
if (!document.getElementById("preventIntegerScaling").checked){
|
||||
finalSizeQr = integerScale * (qrcode._oQRCode.moduleCount + 1);
|
||||
finalSizeCanvas = finalSizeQr - 1;
|
||||
}
|
||||
qrcode._htOption.width = finalSizeQr;
|
||||
qrcode._htOption.height = finalSizeQr;
|
||||
|
||||
console.info("FinalSizeQr: " + finalSizeQr);
|
||||
console.info("FinalSizeCanvas: " + finalSizeCanvas);
|
||||
|
||||
qrcode._htOption.width = finalSizeQr;
|
||||
qrcode._htOption.height = finalSizeQr;
|
||||
|
||||
document.getElementsByTagName("canvas")[0].width = finalSizeCanvas;
|
||||
document.getElementsByTagName("canvas")[0].height = finalSizeCanvas;
|
||||
try {
|
||||
qrcode.makeCode(qrText);
|
||||
} catch (error) {
|
||||
document.getElementById("errors").innerText="Error: QR could not be created.";
|
||||
console.error(error);
|
||||
}
|
||||
document.getElementsByTagName("canvas")[0].width = finalSizeCanvas;
|
||||
document.getElementsByTagName("canvas")[0].height = finalSizeCanvas;
|
||||
try {
|
||||
qrcode.makeCode(qrText);
|
||||
} catch (error) {
|
||||
document.getElementById("errors").innerText="Error: QR could not be created.";
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("useTEXT").addEventListener("change",function(){toggleVis("srcText");});
|
||||
|
|
@ -316,13 +317,13 @@
|
|||
document.getElementById("meWebsite").addEventListener("change",refreshQRCode);
|
||||
|
||||
document.getElementById("useCAM").addEventListener("change",function(){
|
||||
initQrScanner();
|
||||
initQrCam();
|
||||
toggleVis("srcScanCam");
|
||||
initQrScanner();
|
||||
initQrCam();
|
||||
toggleVis("srcScanCam");
|
||||
});
|
||||
document.getElementById("useFILE").addEventListener("change",function(){
|
||||
initQrScanner();
|
||||
toggleVis("srcScanFile");
|
||||
initQrScanner();
|
||||
toggleVis("srcScanFile");
|
||||
});
|
||||
document.getElementById("useWIFI").addEventListener("change",function(){toggleVis("srcWifi");});
|
||||
document.getElementById("ssid").addEventListener("change",refreshQRCode);
|
||||
|
|
@ -336,17 +337,18 @@
|
|||
document.getElementById("useWIFI").addEventListener("change",refreshQRCode);
|
||||
document.getElementById("preventIntegerScaling").addEventListener("change",refreshQRCode);
|
||||
document.getElementById("correction").addEventListener("change",refreshQRCode);
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
var content = document.getElementById("text").value;
|
||||
if(document.getElementById("useWIFI").checked){
|
||||
content = document.getElementById("ssid").value
|
||||
}
|
||||
if(!(document.getElementById("description").value === "")){
|
||||
content = document.getElementById("description").value;
|
||||
}
|
||||
var img = imageconverter.canvastoString(document.getElementsByTagName("canvas")[0],{mode:"1bit",output:"string",compression:true});
|
||||
var app = `var img = ${img};
|
||||
${ document.getElementById("preventBrightnessChangeOnTouch").checked ? '' : `var backlight = 0;
|
||||
var content = document.getElementById("text").value;
|
||||
if(document.getElementById("useWIFI").checked){
|
||||
content = document.getElementById("ssid").value
|
||||
}
|
||||
if(document.getElementById("description").value !== ""){
|
||||
content = document.getElementById("description").value;
|
||||
}
|
||||
var img = imageconverter.canvastoString(document.getElementsByTagName("canvas")[0],{mode:"1bit",output:"string",compression:true});
|
||||
var app = `var img = ${img};
|
||||
${document.getElementById("preventBrightnessChangeOnTouch").checked ? '' : `var backlight = 0;
|
||||
Bangle.on('touch', function(button, xy) {
|
||||
backlight += 0.3;
|
||||
if (backlight > 1) backlight = 0;
|
||||
|
|
@ -354,50 +356,52 @@ Bangle.on('touch', function(button, xy) {
|
|||
});
|
||||
`}
|
||||
|
||||
${document.getElementById("boostBacklight").checked ? 'Bangle.setLCDBrightness(1);' : ''}
|
||||
${document.getElementById("stayOn").checked ? 'Bangle.setLCDTimeout(0);' : ''}
|
||||
${document.getElementById("hideDescription").checked ? '' : `var content = ${JSON.stringify(content)};`}
|
||||
${document.getElementById("boostBacklight").checked ? 'Bangle.setLCDBrightness(1);' : ''}
|
||||
${document.getElementById("stayOn").checked ? 'Bangle.setLCDTimeout(0);' : ''}
|
||||
${document.getElementById("hideDescription").checked ? '' : `var content = ${JSON.stringify(content)};`}
|
||||
g.clear(1).setColor(1,1,1).setBgColor(0,0,0);
|
||||
g.fillRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||
g.drawImage(img,(g.getWidth()-img[0])/2,(g.getHeight()-img[1])/2);
|
||||
${ document.getElementById("hideDescription").checked ? '' : `g.setFontAlign(0,0).setFont("6x8").setColor(0,0,0);
|
||||
${document.getElementById("hideDescription").checked ? '' : `g.setFontAlign(0,0).setFont("6x8").setColor(0,0,0);
|
||||
g.drawString(content,g.getWidth()/2,g.getHeight()-(g.getHeight()-img[1])/4);
|
||||
`}
|
||||
g.setColor(1,1,1);
|
||||
`;
|
||||
sendCustomizedApp({
|
||||
storage:[{name:"qrcode.app.js", url:"app.js", content:app},]
|
||||
});
|
||||
|
||||
var appname = document.getElementById("appname").value.trim() || "qrcode";
|
||||
sendCustomizedApp({
|
||||
storage:[{name:`${appname}.app.js`, url:"app.js", content:app}]
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
document.getElementById('camList').addEventListener('change', event => {
|
||||
scanner.setCamera(event.target.value).then(updateFlashAvailability);
|
||||
scanner.setCamera(event.target.value).then(updateFlashAvailability);
|
||||
});
|
||||
|
||||
document.getElementById('flashToggle').addEventListener('click', () => {
|
||||
scanner.toggleFlash().then(() => document.getElementById('flashState').textContent = scanner.isFlashOn() ? 'on' : 'off');
|
||||
scanner.toggleFlash().then(() => document.getElementById('flashState').textContent = scanner.isFlashOn() ? 'on' : 'off');
|
||||
});
|
||||
|
||||
document.getElementById('startButton').addEventListener('click', () => {
|
||||
scanner.start();
|
||||
scanner.start();
|
||||
});
|
||||
|
||||
document.getElementById('stopButton').addEventListener('click', () => {
|
||||
scanner.stop();
|
||||
scanner.stop();
|
||||
});
|
||||
|
||||
document.getElementById('fileSelector').addEventListener('change', event => {
|
||||
const file = document.getElementById('fileSelector').files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
QrScanner.scanImage(file)
|
||||
.then(result => setResult(document.getElementById('fileQrResult'), result))
|
||||
.catch(e => setResult(document.getElementById('fileQrResult'), e || 'No QR code found.'));
|
||||
const file = document.getElementById('fileSelector').files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
QrScanner.scanImage(file)
|
||||
.then(result => setResult(document.getElementById('fileQrResult'), result))
|
||||
.catch(e => setResult(document.getElementById('fileQrResult'), e || 'No QR code found.'));
|
||||
});
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "qrcode",
|
||||
"name": "Custom QR Code",
|
||||
"shortName": "QR Code",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Use this to upload a customised QR code to Bangle.js",
|
||||
"icon": "app.png",
|
||||
"tags": "qrcode",
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@
|
|||
0.29: Improve clkinfo startup time by 10ms
|
||||
0.30: Fix possible bug in toggling an alarm to on, from clkinfo
|
||||
0.31: Ensure we reschedule alarms after setTimeZone has been called (fix #3791)
|
||||
0.32: clkinfo ensures an alarm won't trigger immediately (copying `alarm`'s behaviour)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ exports.resetTimer = function(alarm, time) {
|
|||
time = time || new Date();
|
||||
var currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000);
|
||||
alarm.t = (currentTime + alarm.timer) % 86400000;
|
||||
alarm.last = "timer" in alarm || alarm.t >= require("time_utils").getCurrentTimeMillis()
|
||||
? 0
|
||||
: new Date().getDate();
|
||||
};
|
||||
/// Get time until the given alarm (object). Return undefined if alarm not enabled, or if 86400000 or more, alarm could be *more* than a day in the future
|
||||
exports.getTimeToAlarm = function(alarm, time) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "sched",
|
||||
"name": "Scheduler",
|
||||
"version": "0.31",
|
||||
"version": "0.32",
|
||||
"description": "Scheduling library for alarms and timers",
|
||||
"icon": "app.png",
|
||||
"type": "scheduler",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Minor code improvements
|
||||
0.03: Bug fixes and some usability and performance improvements
|
||||
0.04: Add squash preset, simplify logic, fix compatibility with BangleJS 2
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ In this mode any score increments will be decrements. To move back a set, reduce
|
|||
| `Sets per page` | How many sets should be shown in the app. Further sets will be available by scrolling (ignored if higher than `Sets to win`) |
|
||||
| `Score to win` | What score ends a given set |
|
||||
| `2-point lead` | Does winning a set require a two-point lead |
|
||||
| `Maximum score?` | Should there be a maximum score, at which point the two-point lead rule falls away |
|
||||
| `Maximum score` | At which score should the two-point lead rule fall away (ignored if lower than Sets to win) |
|
||||
| `Maximum score` | Should there be a maximum score, at which point the two-point lead rule falls away (ignored if lower than Sets to win) |
|
||||
| `Tennis scoring` | If enabled, each point in a set will require a full tennis game |
|
||||
| `TB sets?` | Should sets that have reached `(maxScore-1):(maxScore-1)` be decided with a tiebreak |
|
||||
| All other options starting with TB | Equivalent to option with same name but applied to tiebreaks |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "score",
|
||||
"name": "Score Tracker",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Score Tracker for sports that use plain numbers (e.g. Badminton, Volleyball, Soccer, Table Tennis, ...). Also supports tennis scoring.",
|
||||
"icon": "score.app.png",
|
||||
"screenshots": [{"url":"screenshot_score.png"}],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ require('Font5x9Numeric7Seg').add(Graphics);
|
|||
require('Font7x11Numeric7Seg').add(Graphics);
|
||||
require('FontTeletext5x9Ascii').add(Graphics);
|
||||
|
||||
const KEY_SCORE_L = 0;
|
||||
const KEY_SCORE_R = 1;
|
||||
const KEY_MENU = 2;
|
||||
const KEY_TENNIS_H = 3;
|
||||
const KEY_TENNIS_L = 4;
|
||||
|
||||
let settingsMenu = eval(require('Storage').read('score.settings.js'));
|
||||
let settings = settingsMenu(null, null, true);
|
||||
|
||||
|
|
@ -44,39 +50,49 @@ function setupDisplay() {
|
|||
}
|
||||
|
||||
function setupInputWatchers(init) {
|
||||
Bangle.setUI('updown', v => {
|
||||
if (v) {
|
||||
if (isBangle1) {
|
||||
Bangle.setUI('updown',
|
||||
isBangle1
|
||||
? (v => {
|
||||
if (v) {
|
||||
let i = settings.mirrorScoreButtons ? v : v * -1;
|
||||
handleInput(Math.floor((i+2)/2));
|
||||
} else {
|
||||
}
|
||||
})
|
||||
: (v => {
|
||||
if (v) {
|
||||
// +1 -> 4
|
||||
// -1 -> 3
|
||||
handleInput(Math.floor((v+2)/2)+3);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
if (init) {
|
||||
if (isBangle1) {
|
||||
setWatch(() => handleInput(2), BTN2, { repeat: true });
|
||||
}
|
||||
Bangle.on('touch', (b, e) => {
|
||||
if (isBangle1) {
|
||||
setWatch(
|
||||
() => handleInput(KEY_MENU),
|
||||
isBangle1 ? BTN2 : BTN,
|
||||
{ repeat: true },
|
||||
);
|
||||
Bangle.on('touch',
|
||||
isBangle1
|
||||
? ((b, e) => {
|
||||
if (b === 1) {
|
||||
handleInput(3);
|
||||
handleInput(KEY_TENNIS_H);
|
||||
} else {
|
||||
handleInput(4);
|
||||
handleInput(KEY_TENNIS_L);
|
||||
}
|
||||
} else {
|
||||
})
|
||||
: ((b, e) => {
|
||||
if (e.y > 18) {
|
||||
if (e.x < getXCoord(w => w/2)) {
|
||||
handleInput(0);
|
||||
handleInput(KEY_SCORE_L);
|
||||
} else {
|
||||
handleInput(1);
|
||||
handleInput(KEY_SCORE_R);
|
||||
}
|
||||
} else {
|
||||
// long press except if we have the menu opened or we are in the emulator (that doesn't
|
||||
// seem to support long press events)
|
||||
if (e.type === 2 || settingsMenuOpened || process.env.BOARD === 'EMSCRIPTEN2') {
|
||||
handleInput(2);
|
||||
handleInput(KEY_MENU);
|
||||
} else {
|
||||
let p = null;
|
||||
|
||||
|
|
@ -97,8 +113,8 @@ function setupInputWatchers(init) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,14 +153,13 @@ function showSettingsMenu() {
|
|||
if (reset) {
|
||||
setupMatch();
|
||||
}
|
||||
if (isBangle1 || (!isBangle1 && back)) {
|
||||
settingsMenuOpened = null;
|
||||
|
||||
draw();
|
||||
settingsMenuOpened = null;
|
||||
|
||||
setupDisplay();
|
||||
setupInputWatchers();
|
||||
}
|
||||
draw();
|
||||
|
||||
setupDisplay();
|
||||
setupInputWatchers();
|
||||
}, function (msg) {
|
||||
switch (msg) {
|
||||
case 'end_set':
|
||||
|
|
@ -344,7 +359,7 @@ function handleInput(button) {
|
|||
// console.log('button:', button);
|
||||
if (settingsMenuOpened) {
|
||||
|
||||
if (!isBangle1 && button == 2) {
|
||||
if (!isBangle1 && button == KEY_MENU) { // Bangle2 long press, hide menu
|
||||
E.showMenu();
|
||||
|
||||
settingsMenuOpened = null;
|
||||
|
|
@ -359,15 +374,15 @@ function handleInput(button) {
|
|||
}
|
||||
|
||||
switch (button) {
|
||||
case 0:
|
||||
case 1:
|
||||
case KEY_SCORE_L:
|
||||
case KEY_SCORE_R:
|
||||
score(button);
|
||||
break;
|
||||
case 2:
|
||||
case KEY_MENU:
|
||||
showSettingsMenu();
|
||||
return;
|
||||
case 3:
|
||||
case 4: {
|
||||
case KEY_TENNIS_H:
|
||||
case KEY_TENNIS_L: {
|
||||
let hLimit = currentSet() - setsPerPage() + 1;
|
||||
let lLimit = 0;
|
||||
let val = (button * 2 - 7);
|
||||
|
|
@ -382,8 +397,7 @@ function handleInput(button) {
|
|||
}
|
||||
|
||||
function draw() {
|
||||
g.setFontAlign(0,0);
|
||||
g.clear();
|
||||
g.reset().setFontAlign(0,0).clear();
|
||||
|
||||
for (let p = 0; p < 2; p++) {
|
||||
if (matchWon(p)) {
|
||||
|
|
|
|||
|
|
@ -26,5 +26,10 @@
|
|||
"winScore": 11,
|
||||
"enableTwoAhead": true,
|
||||
"enableMaxScore": false
|
||||
},
|
||||
"Squash": {
|
||||
"winScore": 9,
|
||||
"enableTwoAhead": true,
|
||||
"enableMaxScore": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,220 +1,194 @@
|
|||
(function () {
|
||||
return (function (back, inApp, ret) {
|
||||
const isBangle1 = process.env.BOARD === 'BANGLEJS'
|
||||
(function (back, inApp, ret) {
|
||||
const isBangle1 = process.env.BOARD === 'BANGLEJS'
|
||||
|
||||
function fillSettingsWithDefaults(settings) {
|
||||
if (isBangle1) {
|
||||
if (settings.mirrorScoreButtons == null) {
|
||||
settings.mirrorScoreButtons = false;
|
||||
}
|
||||
if (settings.keepDisplayOn == null) {
|
||||
settings.keepDisplayOn = true;
|
||||
}
|
||||
}
|
||||
if (settings.winSets == null) {
|
||||
settings.winSets = 2;
|
||||
}
|
||||
if (settings.setsPerPage == null) {
|
||||
settings.setsPerPage = 5;
|
||||
}
|
||||
if (settings.winScore == null) {
|
||||
settings.winScore = 21;
|
||||
}
|
||||
if (settings.enableTwoAhead == null) {
|
||||
settings.enableTwoAhead = true;
|
||||
}
|
||||
if (settings.enableMaxScore == null) {
|
||||
settings.enableMaxScore = true;
|
||||
}
|
||||
if (settings.maxScore == null) {
|
||||
settings.maxScore = 30;
|
||||
}
|
||||
if (settings.enableTennisScoring == null) {
|
||||
settings.enableTennisScoring = false;
|
||||
}
|
||||
function fillSettingsWithDefaults(settings) {
|
||||
settings = Object.assign({
|
||||
winSets: 2,
|
||||
setsPerPage: 5,
|
||||
winScore: 21,
|
||||
enableTwoAhead: true,
|
||||
enableMaxScore: true,
|
||||
maxScore: 30,
|
||||
enableTennisScoring: false,
|
||||
|
||||
if (settings.enableMaxScoreTiebreak == null) {
|
||||
settings.enableMaxScoreTiebreak = false;
|
||||
}
|
||||
if (settings.maxScoreTiebreakWinScore == null) {
|
||||
settings.maxScoreTiebreakWinScore = 6;
|
||||
}
|
||||
if (settings.maxScoreTiebreakEnableTwoAhead == null) {
|
||||
settings.maxScoreTiebreakEnableTwoAhead = true;
|
||||
}
|
||||
if (settings.maxScoreTiebreakEnableMaxScore == null) {
|
||||
settings.maxScoreTiebreakEnableMaxScore = false;
|
||||
}
|
||||
if (settings.maxScoreTiebreakMaxScore == null) {
|
||||
settings.maxScoreTiebreakMaxScore = 15;
|
||||
}
|
||||
enableMaxScoreTiebreak: false,
|
||||
maxScoreTiebreakWinScore: 6,
|
||||
maxScoreTiebreakEnableTwoAhead: true,
|
||||
maxScoreTiebreakEnableMaxScore: false,
|
||||
maxScoreTiebreakMaxScore: 15,
|
||||
}, settings);
|
||||
|
||||
return settings;
|
||||
if (isBangle1) {
|
||||
settings = Object.assign({
|
||||
mirrorScoreButtons: false,
|
||||
keepDisplayOn: true,
|
||||
}, settings);
|
||||
}
|
||||
|
||||
const fileName = 'score.json';
|
||||
let settings = require('Storage').readJSON(fileName, 1) || {};
|
||||
const offon = ['No', 'Yes'];
|
||||
return settings;
|
||||
}
|
||||
|
||||
let presetsFileName = 'score.presets.json';
|
||||
let presets = require('Storage').readJSON(presetsFileName);
|
||||
let presetNames = Object.keys(presets);
|
||||
const fileName = 'score.json';
|
||||
let settings = require('Storage').readJSON(fileName, 1) || {};
|
||||
const offon = ['No', 'Yes'];
|
||||
|
||||
let changed = false;
|
||||
let presetsFileName = 'score.presets.json';
|
||||
let presets = require('Storage').readJSON(presetsFileName);
|
||||
let presetNames = Object.keys(presets);
|
||||
|
||||
function save(settings) {
|
||||
require('Storage').writeJSON(fileName, settings);
|
||||
let changed = false;
|
||||
|
||||
function save(settings) {
|
||||
require('Storage').writeJSON(fileName, settings);
|
||||
}
|
||||
|
||||
function setAndSave(key, value, notChanged) {
|
||||
if (!notChanged) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
function setAndSave(key, value, notChanged) {
|
||||
if (!notChanged) {
|
||||
changed = true;
|
||||
}
|
||||
settings[key] = value;
|
||||
if (key === 'winScore' && settings.maxScore < value) {
|
||||
settings.maxScore = value;
|
||||
}
|
||||
save(settings);
|
||||
settings[key] = value;
|
||||
if (key === 'winScore' && settings.maxScore < value) {
|
||||
settings.maxScore = value;
|
||||
}
|
||||
save(settings);
|
||||
}
|
||||
|
||||
settings = fillSettingsWithDefaults(settings);
|
||||
settings = fillSettingsWithDefaults(settings);
|
||||
|
||||
if (ret) {
|
||||
return settings;
|
||||
}
|
||||
if (ret) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
const presetMenu = function (appMenuBack) {
|
||||
let ret = function (changed) { E.showMenu(appMenu(appMenuBack, changed ? 2 : null)); };
|
||||
let m = {
|
||||
'': {'title': 'Score Presets'},
|
||||
'< Back': ret,
|
||||
};
|
||||
for (let i = 0; i < presetNames.length; i++) {
|
||||
m[presetNames[i]] = (function (i) {
|
||||
return function() {
|
||||
changed = true;
|
||||
let mirrorScoreButtons = settings.mirrorScoreButtons;
|
||||
let keepDisplayOn = settings.keepDisplayOn;
|
||||
|
||||
settings = fillSettingsWithDefaults(presets[presetNames[i]]);
|
||||
|
||||
settings.mirrorScoreButtons = mirrorScoreButtons;
|
||||
settings.keepDisplayOn = keepDisplayOn;
|
||||
save(settings);
|
||||
ret(true);
|
||||
};
|
||||
})(i);
|
||||
}
|
||||
|
||||
return m;
|
||||
const presetMenu = function (appMenuBack) {
|
||||
let ret = function (changed) { E.showMenu(appMenu(appMenuBack, changed ? 2 : null)); };
|
||||
let m = {
|
||||
'': {'title': 'Score Presets'},
|
||||
'< Back': ret,
|
||||
};
|
||||
for (let i = 0; i < presetNames.length; i++) {
|
||||
m[presetNames[i]] = (function (i) {
|
||||
return function() {
|
||||
changed = true;
|
||||
let mirrorScoreButtons = settings.mirrorScoreButtons;
|
||||
let keepDisplayOn = settings.keepDisplayOn;
|
||||
|
||||
const appMenu = function (back, selected) {
|
||||
let m = {};
|
||||
settings = fillSettingsWithDefaults(presets[presetNames[i]]);
|
||||
|
||||
m[''] = {'title': 'Score Settings'};
|
||||
if (selected != null) {
|
||||
m[''].selected = selected;
|
||||
}
|
||||
m['< Back'] = function () { back(settings, changed, true); };
|
||||
m['Presets'] = function () { E.showMenu(presetMenu(back)); };
|
||||
if (isBangle1) {
|
||||
m['Mirror Buttons'] = {
|
||||
value: settings.mirrorScoreButtons,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('mirrorScoreButtons', m, true),
|
||||
settings.mirrorScoreButtons = mirrorScoreButtons;
|
||||
settings.keepDisplayOn = keepDisplayOn;
|
||||
save(settings);
|
||||
ret(true);
|
||||
};
|
||||
m['Keep display on'] = {
|
||||
value: settings.keepDisplayOn,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('keepDisplayOn', m, true),
|
||||
}
|
||||
}
|
||||
m['Sets to win'] = {
|
||||
value: settings.winSets,
|
||||
min:1,
|
||||
onchange: m => setAndSave('winSets', m),
|
||||
};
|
||||
m['Sets per page'] = {
|
||||
value: settings.setsPerPage,
|
||||
min:1,
|
||||
max:5,
|
||||
onchange: m => setAndSave('setsPerPage', m),
|
||||
};
|
||||
m['Score to win'] = {
|
||||
value: settings.winScore,
|
||||
min:1,
|
||||
max: 999,
|
||||
onchange: m => setAndSave('winScore', m),
|
||||
};
|
||||
m['2-point lead'] = {
|
||||
value: settings.enableTwoAhead,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableTwoAhead', m),
|
||||
};
|
||||
m['Maximum score?'] = {
|
||||
value: settings.enableMaxScore,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableMaxScore', m),
|
||||
};
|
||||
m['Maximum score'] = {
|
||||
value: settings.maxScore,
|
||||
min: 1,
|
||||
max: 999,
|
||||
onchange: m => setAndSave('maxScore', m),
|
||||
};
|
||||
m['Tennis scoring'] = {
|
||||
value: settings.enableTennisScoring,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableTennisScoring', m),
|
||||
};
|
||||
m['TB sets?'] = {
|
||||
value: settings.enableMaxScoreTiebreak,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableMaxScoreTiebreak', m),
|
||||
};
|
||||
m['TB Score to win'] = {
|
||||
value: settings.maxScoreTiebreakWinScore,
|
||||
onchange: m => setAndSave('maxScoreTiebreakWinScore', m),
|
||||
};
|
||||
m['TB 2-point lead'] = {
|
||||
value: settings.maxScoreTiebreakEnableTwoAhead,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('maxScoreTiebreakEnableTwoAhead', m),
|
||||
};
|
||||
m['TB max score?'] = {
|
||||
value: settings.maxScoreTiebreakEnableMaxScore,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('maxScoreTiebreakEnableMaxScore', m),
|
||||
};
|
||||
m['TB max score'] = {
|
||||
value: settings.maxScoreTiebreakMaxScore,
|
||||
onchange: m => setAndSave('maxScoreTiebreakMaxScore', m),
|
||||
};
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
const inAppMenu = function () {
|
||||
let m = {
|
||||
'': {'title': 'Score Menu'},
|
||||
'< Back': function () { back(settings, changed, false); },
|
||||
'Correct mode': function () { inApp('correct_mode'); back(settings, false, true); },
|
||||
'Reset match': function () { back(settings, true, true); },
|
||||
'End current set': function () { inApp('end_set'); back(settings, changed, true); },
|
||||
'Configuration': function () { E.showMenu(appMenu(function () {
|
||||
E.showMenu(inAppMenu());
|
||||
})); },
|
||||
};
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
if (inApp != null) {
|
||||
E.showMenu(inAppMenu());
|
||||
} else {
|
||||
E.showMenu(appMenu(back));
|
||||
})(i);
|
||||
}
|
||||
|
||||
});
|
||||
})()
|
||||
return m;
|
||||
};
|
||||
|
||||
const appMenu = function (back, selected) {
|
||||
let m = {};
|
||||
|
||||
m[''] = {'title': 'Score Settings'};
|
||||
if (selected != null) {
|
||||
m[''].selected = selected;
|
||||
}
|
||||
m['< Back'] = function () { back(settings, changed, true); };
|
||||
m['Presets'] = function () { E.showMenu(presetMenu(back)); };
|
||||
if (isBangle1) {
|
||||
m['Mirror Buttons'] = {
|
||||
value: settings.mirrorScoreButtons,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('mirrorScoreButtons', m, true),
|
||||
};
|
||||
m['Keep display on'] = {
|
||||
value: settings.keepDisplayOn,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('keepDisplayOn', m, true),
|
||||
}
|
||||
}
|
||||
m['Sets to win'] = {
|
||||
value: settings.winSets,
|
||||
min:1,
|
||||
onchange: m => setAndSave('winSets', m),
|
||||
};
|
||||
m['Sets per page'] = {
|
||||
value: settings.setsPerPage,
|
||||
min:1,
|
||||
max:5,
|
||||
onchange: m => setAndSave('setsPerPage', m),
|
||||
};
|
||||
m['Score to win'] = {
|
||||
value: settings.winScore,
|
||||
min:1,
|
||||
max: 999,
|
||||
onchange: m => setAndSave('winScore', m),
|
||||
};
|
||||
m['2-point lead'] = {
|
||||
value: settings.enableTwoAhead,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableTwoAhead', m),
|
||||
};
|
||||
m['Maximum score?'] = {
|
||||
value: settings.enableMaxScore,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableMaxScore', m),
|
||||
};
|
||||
m['Maximum score'] = {
|
||||
value: settings.maxScore,
|
||||
min: 1,
|
||||
max: 999,
|
||||
onchange: m => setAndSave('maxScore', m),
|
||||
};
|
||||
m['Tennis scoring'] = {
|
||||
value: settings.enableTennisScoring,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableTennisScoring', m),
|
||||
};
|
||||
m['TB sets?'] = {
|
||||
value: settings.enableMaxScoreTiebreak,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('enableMaxScoreTiebreak', m),
|
||||
};
|
||||
m['TB Score to win'] = {
|
||||
value: settings.maxScoreTiebreakWinScore,
|
||||
onchange: m => setAndSave('maxScoreTiebreakWinScore', m),
|
||||
};
|
||||
m['TB 2-point lead'] = {
|
||||
value: settings.maxScoreTiebreakEnableTwoAhead,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('maxScoreTiebreakEnableTwoAhead', m),
|
||||
};
|
||||
m['TB max score?'] = {
|
||||
value: settings.maxScoreTiebreakEnableMaxScore,
|
||||
format: m => offon[~~m],
|
||||
onchange: m => setAndSave('maxScoreTiebreakEnableMaxScore', m),
|
||||
};
|
||||
m['TB max score'] = {
|
||||
value: settings.maxScoreTiebreakMaxScore,
|
||||
onchange: m => setAndSave('maxScoreTiebreakMaxScore', m),
|
||||
};
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
const inAppMenu = function () {
|
||||
let m = {
|
||||
'': {'title': 'Score Menu'},
|
||||
'< Back': function () { back(settings, changed); },
|
||||
'Correct mode': function () { inApp('correct_mode'); back(settings, false); },
|
||||
'Reset match': function () { back(settings, true); },
|
||||
'End current set': function () { inApp('end_set'); back(settings, changed); },
|
||||
'Configuration': function () { E.showMenu(appMenu(function () {
|
||||
E.showMenu(inAppMenu());
|
||||
})); },
|
||||
};
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
if (inApp != null) {
|
||||
E.showMenu(inAppMenu());
|
||||
} else {
|
||||
E.showMenu(appMenu(back));
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -86,3 +86,4 @@ of 'Select Clock'
|
|||
0.75: Restore previous menu's scroll positions
|
||||
0.76: Add altitude calibration menu (and update README after menu changed)
|
||||
0.77: Save altitude calibration when user exits via reset
|
||||
0.78: Fix menu scroll restore on BangleJS1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "setting",
|
||||
"name": "Settings",
|
||||
"version": "0.77",
|
||||
"version": "0.78",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"icon": "settings.png",
|
||||
"tags": "tool,system",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ function pushMenu(menu) {
|
|||
function restoreMenu(menu) {
|
||||
// equivalent to pushMenu(null); popMenu(menu);
|
||||
if(!menu[""]) menu[""] = {};
|
||||
menu[""].scroll = menuScroller.scroll;
|
||||
if(menuScroller) // may be undefined on BangleJS1
|
||||
menu[""].scroll = menuScroller.scroll;
|
||||
menuScroller = E.showMenu(menu).scroller;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New app!
|
||||
0.02: Minor code improvements
|
||||
0.03: Minor code improvements
|
||||
0.04: Remove buffer finding code (which fails if screen rotated) and make dependent on fw 2v21's Bangle.getOptions().lcdBufferPtr (fix #3818)
|
||||
|
|
@ -618,10 +618,7 @@ var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAA
|
|||
const sintable = new Uint8Array(256);
|
||||
let bgColor = 0;
|
||||
const BLACK = g.setColor.bind(g, 0);
|
||||
const WHITE = g.setColor.bind(g, 0xFFFF);
|
||||
let lcdBuffer = 0,
|
||||
start = 0;
|
||||
|
||||
let lcdBuffer = 0;
|
||||
let locked = false;
|
||||
let charging = false;
|
||||
let stopped = true;
|
||||
|
|
@ -641,75 +638,6 @@ function setupInterval(force) {
|
|||
}
|
||||
}
|
||||
|
||||
function test(addr, y) {
|
||||
BLACK().fillRect(0, y, 176, y);
|
||||
if (peek8(addr)) return false;
|
||||
WHITE().fillRect(0, y, 176, y);
|
||||
let b = peek8(addr);
|
||||
BLACK().fillRect(0, y, 176, y);
|
||||
if (!b) return false;
|
||||
return !peek8(addr);
|
||||
}
|
||||
|
||||
function probe() {
|
||||
if (!start) {
|
||||
start = 0x20000000;
|
||||
if (test(Bangle.getOptions().lcdBufferPtr, 0))
|
||||
start = Bangle.getOptions().lcdBufferPtr; // FW=2v21
|
||||
else if (test(0x2002d3fe, 0)) // try to skip loading if possible
|
||||
start = 0x2002d3fe; // FW=2v20
|
||||
}
|
||||
const end = Math.min(start + 0x800, 0x20038000);
|
||||
|
||||
if (start >= end) {
|
||||
print("Could not find framebuffer");
|
||||
return;
|
||||
}
|
||||
|
||||
BLACK().fillRect(0, 0, 176, 0);
|
||||
// sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp
|
||||
for (; start < end; start += 64) {
|
||||
if (peek8(start)) continue;
|
||||
WHITE().fillRect(0, 0, 176, 0);
|
||||
let b = peek8(start);
|
||||
BLACK().fillRect(0, 0, 176, 0);
|
||||
if (!b) continue;
|
||||
if (!peek8(start)) break;
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
setTimeout(probe, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// find the beginning of the row
|
||||
while (test(start - 1, 0))
|
||||
start--;
|
||||
|
||||
/*
|
||||
let stride = (176 * 3 + 7) >> 3,
|
||||
padding = 0;
|
||||
for (let i = 0; i < 20; ++i, ++padding) {
|
||||
if (test(start + stride + padding, 1)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stride += padding;
|
||||
if (padding == 20) {
|
||||
print("Warning: Could not calculate padding");
|
||||
stride = 68;
|
||||
}
|
||||
*/
|
||||
let stride = 68;
|
||||
|
||||
lcdBuffer = start;
|
||||
print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
|
||||
gfx.init(start, stride, E.getAddressOf(sintable, true));
|
||||
gfx.setCamera(0, 0, 0);
|
||||
setupInterval(true);
|
||||
}
|
||||
|
||||
function init() {
|
||||
require("Font5x9Numeric7Seg").add(Graphics);
|
||||
g.setFont("5x9Numeric7Seg");
|
||||
|
|
@ -723,7 +651,23 @@ function init() {
|
|||
// setup sin/cos table
|
||||
for (let i = 0; i < sintable.length; ++i)
|
||||
sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1);
|
||||
setTimeout(probe, 1);
|
||||
|
||||
lcdBuffer = Bangle.getOptions().lcdBufferPtr;
|
||||
if (!lcdBuffer) {
|
||||
E.showMessage("Needs firmwave 2v21 or newer");
|
||||
return;
|
||||
}
|
||||
let stride = 68;
|
||||
//print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
|
||||
var sintablePtr = E.getAddressOf(sintable, true);
|
||||
if (!sintablePtr) {
|
||||
lcdBuffer = 0;
|
||||
E.showMessage("Not enough memory");
|
||||
return;
|
||||
}
|
||||
gfx.init(lcdBuffer, stride, sintablePtr);
|
||||
gfx.setCamera(0, 0, 0);
|
||||
setupInterval(true);
|
||||
}
|
||||
|
||||
function tick(locked) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "synthwave",
|
||||
"name": "synthwave clock",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "A watchface with an animated 3D scene.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Tally
|
||||
|
||||
An app that lets you keep a tally - when events happen, etc.
|
||||
|
||||
Use the clock info to add to the tally. The app can be used to review/edit/delete tallies
|
||||
|
||||
## Usage
|
||||
|
||||
Navigate to the tally clock info, then swipe up/down to select one of the tallies to enter. Tap on it to add to the tally.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwInkgf/AAXAg4FD8EPAofwAowcDAqkD4AFW4fAAqYEBAtp3XTZStFXIsAh/gD4cPBAIFDwAFEbAkOAokMAokIApQAKA=="))
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
var storage = require("Storage");
|
||||
function readTallies() {
|
||||
var tallies = [];
|
||||
var f = storage.open("tallies.csv", "r");
|
||||
var line;
|
||||
while ((line = f.readLine()) !== undefined) {
|
||||
var parts = line.replace("\n", "").split(",");
|
||||
tallies.push({ date: new Date(parts[0]), name: parts[1] });
|
||||
}
|
||||
return tallies;
|
||||
}
|
||||
function saveTallies(tallies) {
|
||||
var f = storage.open("tallies.csv", "w");
|
||||
for (var _i = 0, tallies_1 = tallies; _i < tallies_1.length; _i++) {
|
||||
var tally = tallies_1[_i];
|
||||
f.write([tally.date.toISOString(), tally.name].join(",") + "\n");
|
||||
}
|
||||
}
|
||||
var dayEq = function (a, b) {
|
||||
return a.getFullYear() === b.getFullYear()
|
||||
&& a.getMonth() === b.getMonth()
|
||||
&& a.getDate() === b.getDate();
|
||||
};
|
||||
function showTallies(tallies) {
|
||||
var menu = {
|
||||
"": { title: "Tallies" },
|
||||
"< Back": function () { return load(); },
|
||||
};
|
||||
var today = new Date;
|
||||
var day;
|
||||
tallies.forEach(function (tally, i) {
|
||||
var td = tally.date;
|
||||
if (!dayEq(day !== null && day !== void 0 ? day : today, td)) {
|
||||
var s = "".concat(td.getFullYear(), "-").concat(pad2(td.getMonth() + 1), "-").concat(pad2(td.getDate()));
|
||||
menu[s] = function () { };
|
||||
day = td;
|
||||
}
|
||||
menu["".concat(tfmt(tally), ": ").concat(tally.name)] = function () { return editTally(tallies, i); };
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
function editTally(tallies, i) {
|
||||
var tally = tallies[i];
|
||||
var onback = function () {
|
||||
saveTallies(tallies);
|
||||
E.removeListener("kill", onback);
|
||||
};
|
||||
E.on("kill", onback);
|
||||
var menu = {
|
||||
"": { title: "Edit ".concat(tally.name) },
|
||||
"< Back": function () {
|
||||
onback();
|
||||
showTallies(tallies);
|
||||
},
|
||||
"Name": {
|
||||
value: tally.name,
|
||||
onchange: function () {
|
||||
setTimeout(function () {
|
||||
require("textinput")
|
||||
.input({ text: tally.name })
|
||||
.then(function (text) {
|
||||
if (text) {
|
||||
tally.name = text;
|
||||
}
|
||||
editTally(tallies, i);
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
"Time": {
|
||||
value: tally.date.getTime(),
|
||||
format: function (_tm) { return tfmt(tally); },
|
||||
onchange: function (v) {
|
||||
tally.date = new Date(v);
|
||||
},
|
||||
step: 60000,
|
||||
},
|
||||
"Delete": function () {
|
||||
E.showPrompt("Delete \"".concat(tally.name, "\"?"), {
|
||||
title: "Delete",
|
||||
buttons: { Yes: true, No: false },
|
||||
}).then(function (confirm) {
|
||||
if (confirm) {
|
||||
tallies.splice(i, 1);
|
||||
saveTallies(tallies);
|
||||
showTallies(tallies);
|
||||
return;
|
||||
}
|
||||
editTally(tallies, i);
|
||||
});
|
||||
},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
function tfmt(tally) {
|
||||
var d = tally.date;
|
||||
return "".concat(d.getHours(), ":").concat(pad2(d.getMinutes()));
|
||||
}
|
||||
var pad2 = function (s) { return ("0" + s.toFixed(0)).slice(-2); };
|
||||
showTallies(readTallies());
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 296 B |
|
|
@ -0,0 +1,120 @@
|
|||
const storage = require("Storage");
|
||||
|
||||
type Tally = {
|
||||
date: Date,
|
||||
name: string,
|
||||
};
|
||||
|
||||
function readTallies() {
|
||||
const tallies: Tally[] = [];
|
||||
const f = storage.open("tallies.csv", "r");
|
||||
let line;
|
||||
while ((line = f.readLine()) !== undefined) {
|
||||
const parts = line.replace("\n", "").split(",");
|
||||
tallies.push({ date: new Date(parts[0]), name: parts[1] });
|
||||
}
|
||||
return tallies;
|
||||
}
|
||||
|
||||
function saveTallies(tallies: Tally[]) {
|
||||
const f = storage.open("tallies.csv", "w");
|
||||
for(const tally of tallies){
|
||||
f.write([tally.date.toISOString(), tally.name].join(",") + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
const dayEq = (a: Date, b: Date) =>
|
||||
a.getFullYear() === b.getFullYear()
|
||||
&& a.getMonth() === b.getMonth()
|
||||
&& a.getDate() === b.getDate();
|
||||
|
||||
function showTallies(tallies: Tally[]) {
|
||||
const menu: Menu = {
|
||||
"": { title: "Tallies" },
|
||||
"< Back": () => load(),
|
||||
};
|
||||
|
||||
const today = new Date;
|
||||
let day: undefined | Date;
|
||||
|
||||
tallies.forEach((tally, i) => {
|
||||
const td = tally.date;
|
||||
if(!dayEq(day ?? today, td)){
|
||||
const s = `${td.getFullYear()}-${pad2(td.getMonth() + 1)}-${pad2(td.getDate())}`;
|
||||
menu[s] = () => {};
|
||||
day = td;
|
||||
}
|
||||
|
||||
menu[`${tfmt(tally)}: ${tally.name}`] = () => editTally(tallies, i);
|
||||
});
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editTally(tallies: Tally[], i: number) {
|
||||
const tally = tallies[i]!;
|
||||
|
||||
const onback = () => {
|
||||
saveTallies(tallies);
|
||||
E.removeListener("kill", onback);
|
||||
};
|
||||
E.on("kill", onback);
|
||||
|
||||
const menu: Menu = {
|
||||
"": { title: `Edit ${tally.name}` },
|
||||
"< Back": () => {
|
||||
onback();
|
||||
showTallies(tallies)
|
||||
},
|
||||
"Name": {
|
||||
value: tally.name,
|
||||
onchange: () => {
|
||||
setTimeout(() => {
|
||||
require("textinput")
|
||||
.input({ text: tally.name })
|
||||
.then(text => {
|
||||
if (text) {
|
||||
tally.name = text;
|
||||
}
|
||||
editTally(tallies, i);
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
"Time": {
|
||||
value: tally.date.getTime(),
|
||||
format: (_tm: number) => tfmt(tally),
|
||||
onchange: (v: number) => {
|
||||
tally.date = new Date(v);
|
||||
},
|
||||
step: 60000, // 1 min
|
||||
},
|
||||
"Delete": () => {
|
||||
E.showPrompt(`Delete "${tally.name}"?`, {
|
||||
title: "Delete",
|
||||
buttons: { Yes: true, No: false },
|
||||
}).then(confirm => {
|
||||
if (confirm) {
|
||||
tallies.splice(i, 1);
|
||||
saveTallies(tallies);
|
||||
showTallies(tallies);
|
||||
return;
|
||||
}
|
||||
|
||||
// need to regrab ui, can't `m.draw()`
|
||||
editTally(tallies, i);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function tfmt(tally: Tally) {
|
||||
const d = tally.date;
|
||||
return `${d.getHours()}:${pad2(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
const pad2 = (s: number) => ("0" + s.toFixed(0)).slice(-2);
|
||||
|
||||
showTallies(readTallies());
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
(function () {
|
||||
var storage = require("Storage");
|
||||
var tallyEntries = storage.readJSON("tallycfg.json", 1) || [];
|
||||
var img = atob("GBiBAAAAAAAAAAAAAB//+D///DAADDAADDAYDDAYDDAZjDAZjDGZjDGZjDGZjDGZjDAADDAADD///B//+APAAAMAAAIAAAAAAAAAAA==");
|
||||
return {
|
||||
name: "Tally",
|
||||
img: img,
|
||||
items: tallyEntries.map(function (ent) { return ({
|
||||
name: ent.name,
|
||||
img: img,
|
||||
get: function () {
|
||||
return { text: this.name, img: img };
|
||||
},
|
||||
run: function () {
|
||||
var f = storage.open("tallies.csv", "a");
|
||||
f.write([
|
||||
new Date().toISOString(),
|
||||
this.name,
|
||||
].join(",") + "\n");
|
||||
},
|
||||
show: function () { },
|
||||
hide: function () { },
|
||||
}); }),
|
||||
};
|
||||
})
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
(function() {
|
||||
const storage = require("Storage");
|
||||
const tallyEntries = storage.readJSON("tallycfg.json", 1) as TallySettings || [];
|
||||
|
||||
// transparent
|
||||
const img = atob("GBiBAAAAAAAAAAAAAB//+D///DAADDAADDAYDDAYDDAZjDAZjDGZjDGZjDGZjDGZjDAADDAADD///B//+APAAAMAAAIAAAAAAAAAAA==")
|
||||
// non-transparent
|
||||
//const img = atob("GBgBAAAAAAAAAAAAH//4P//8MAAMMAAMMBgMMBgMMBmMMBmMMZmMMZmMMZmMMZmMMAAMMAAMP//8H//4A4AAAwAAAgAAAAAAAAAA")
|
||||
|
||||
return {
|
||||
name: "Tally",
|
||||
img,
|
||||
items: tallyEntries.map(ent => ({
|
||||
name: ent.name,
|
||||
img,
|
||||
get: function() {
|
||||
return { text: this.name, img };
|
||||
},
|
||||
run: function() {
|
||||
const f = storage.open("tallies.csv", "a");
|
||||
f.write([
|
||||
new Date().toISOString(),
|
||||
this.name,
|
||||
].join(",") + "\n");
|
||||
},
|
||||
show: function(){},
|
||||
hide: function(){},
|
||||
})),
|
||||
};
|
||||
}) satisfies ClockInfoFunc
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 760 B |
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"id": "tally",
|
||||
"name": "Tally Keeper",
|
||||
"shortName": "Tally",
|
||||
"version": "0.01",
|
||||
"description": "Add to a tally from clock info",
|
||||
"icon": "app.png",
|
||||
"tags": "clkinfo",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name": "tally.settings.js", "url": "settings.js" },
|
||||
{ "name": "tally.img", "url": "app-icon.js", "evaluate": true },
|
||||
{ "name": "tally.clkinfo.js", "url": "clkinfo.js" },
|
||||
{ "name": "tally.app.js", "url": "app.js" }
|
||||
],
|
||||
"data": [
|
||||
{ "name": "tallycfg.json" },
|
||||
{ "name": "tallies.csv" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
(function (back) {
|
||||
var storage = require("Storage");
|
||||
var SETTINGS_FILE = "tallycfg.json";
|
||||
var tallycfg = storage.readJSON(SETTINGS_FILE, 1) || [];
|
||||
function saveSettings() {
|
||||
storage.writeJSON(SETTINGS_FILE, tallycfg);
|
||||
}
|
||||
function showMainMenu() {
|
||||
var menu = {
|
||||
"": { "title": "Tally Configs" },
|
||||
"< Back": back,
|
||||
"Add New": function () { return showEditMenu(); },
|
||||
};
|
||||
tallycfg.forEach(function (tally, index) {
|
||||
menu[tally.name] = function () { return showEditMenu(tally, index); };
|
||||
});
|
||||
E.showMenu(menu);
|
||||
}
|
||||
function showEditMenu(tally, index) {
|
||||
var isNew = tally == null;
|
||||
if (tally == null) {
|
||||
tally = { name: "" };
|
||||
index = tallycfg.length;
|
||||
tallycfg.push(tally);
|
||||
}
|
||||
var menu = {
|
||||
"": { "title": isNew ? "New Tally" : "Edit Tally" },
|
||||
"< Back": function () {
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
},
|
||||
};
|
||||
menu[tally.name || "<set name>"] = function () {
|
||||
require("textinput")
|
||||
.input({ text: tally.name })
|
||||
.then(function (text) {
|
||||
tally.name = text;
|
||||
showEditMenu(tally, index);
|
||||
});
|
||||
};
|
||||
if (!isNew) {
|
||||
menu["Delete"] = function () {
|
||||
tallycfg.splice(index, 1);
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
E.showMenu(menu);
|
||||
}
|
||||
showMainMenu();
|
||||
})
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
type TallySettings = TallySetting[];
|
||||
type TallySetting = { name: string };
|
||||
|
||||
(function(back) {
|
||||
const storage = require("Storage");
|
||||
const SETTINGS_FILE = "tallycfg.json";
|
||||
|
||||
const tallycfg = storage.readJSON(SETTINGS_FILE, 1) as TallySettings || [];
|
||||
|
||||
function saveSettings() {
|
||||
storage.writeJSON(SETTINGS_FILE, tallycfg);
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
const menu: Menu = {
|
||||
"": { "title": "Tally Configs" },
|
||||
"< Back": back,
|
||||
"Add New": () => showEditMenu(),
|
||||
};
|
||||
|
||||
tallycfg.forEach((tally, index) => {
|
||||
menu[tally.name] = () => showEditMenu(tally, index);
|
||||
});
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showEditMenu(tally?: TallySetting, index?: number) {
|
||||
const isNew = tally == null;
|
||||
if (tally == null) {
|
||||
tally = { name: "" };
|
||||
index = tallycfg.length;
|
||||
tallycfg.push(tally);
|
||||
}
|
||||
|
||||
const menu: Menu = {
|
||||
"": { "title": isNew ? "New Tally" : "Edit Tally" },
|
||||
"< Back": () => {
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
},
|
||||
};
|
||||
|
||||
menu[tally.name || "<set name>"] = () => {
|
||||
require("textinput")
|
||||
.input({ text: tally.name })
|
||||
.then(text => {
|
||||
tally.name = text;
|
||||
|
||||
showEditMenu(tally, index);
|
||||
});
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
menu["Delete"] = () => {
|
||||
tallycfg.splice(index!, 1);
|
||||
saveSettings();
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
}) satisfies SettingsFunc;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New app!
|
||||
0.02: Minor code improvements
|
||||
0.03: Minor code improvements
|
||||
0.04: Remove buffer finding code (which fails if screen rotated) and make dependent on fw 2v21's Bangle.getOptions().lcdBufferPtr (fix #3818)
|
||||
|
|
@ -480,7 +480,6 @@ void render(int* n, const unsigned char* m){
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
`);
|
||||
|
||||
const nodeCount = 4;
|
||||
|
|
@ -489,10 +488,7 @@ const sintable = new Uint8Array(256);
|
|||
const translation = new Uint32Array(10);
|
||||
let bgColor = 0;
|
||||
const BLACK = g.setColor.bind(g, 0);
|
||||
const WHITE = g.setColor.bind(g, 0xFFFF);
|
||||
let lcdBuffer = 0,
|
||||
start = 0;
|
||||
|
||||
let lcdBuffer = 0;
|
||||
let locked = false;
|
||||
let charging = false;
|
||||
let stopped = true;
|
||||
|
|
@ -512,75 +508,6 @@ function setupInterval(force) {
|
|||
}
|
||||
}
|
||||
|
||||
function test(addr, y) {
|
||||
BLACK().fillRect(0, y, 176, y);
|
||||
if (peek8(addr)) return false;
|
||||
WHITE().fillRect(0, y, 176, y);
|
||||
let b = peek8(addr);
|
||||
BLACK().fillRect(0, y, 176, y);
|
||||
if (!b) return false;
|
||||
return !peek8(addr);
|
||||
}
|
||||
|
||||
function probe() {
|
||||
if (!start) {
|
||||
start = 0x20000000;
|
||||
if (test(Bangle.getOptions().lcdBufferPtr, 0))
|
||||
start = Bangle.getOptions().lcdBufferPtr; // FW=2v21
|
||||
else if (test(0x2002d3fe, 0)) // try to skip loading if possible
|
||||
start = 0x2002d3fe; // FW=2v20
|
||||
}
|
||||
const end = Math.min(start + 0x800, 0x20038000);
|
||||
|
||||
if (start >= end) {
|
||||
print("Could not find framebuffer");
|
||||
return;
|
||||
}
|
||||
|
||||
BLACK().fillRect(0, 0, 176, 0);
|
||||
// sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp
|
||||
for (; start < end; start += 64) {
|
||||
if (peek8(start)) continue;
|
||||
WHITE().fillRect(0, 0, 176, 0);
|
||||
let b = peek8(start);
|
||||
BLACK().fillRect(0, 0, 176, 0);
|
||||
if (!b) continue;
|
||||
if (!peek8(start)) break;
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
setTimeout(probe, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// find the beginning of the row
|
||||
while (test(start - 1, 0))
|
||||
start--;
|
||||
|
||||
/*
|
||||
let stride = (176 * 3 + 7) >> 3,
|
||||
padding = 0;
|
||||
for (let i = 0; i < 20; ++i, ++padding) {
|
||||
if (test(start + stride + padding, 1)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stride += padding;
|
||||
if (padding == 20) {
|
||||
print("Warning: Could not calculate padding");
|
||||
stride = 68;
|
||||
}
|
||||
*/
|
||||
let stride = 68;
|
||||
|
||||
lcdBuffer = start;
|
||||
print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
|
||||
gfx.init(start, stride, E.getAddressOf(sintable, true));
|
||||
gfx.setCamera(0, 0, -300 << 8);
|
||||
setupInterval(true);
|
||||
}
|
||||
|
||||
function init() {
|
||||
bgColor = g.theme.bg & 0x8410;
|
||||
bgColor = ((bgColor >> 15) | (bgColor >> 9) | (bgColor >> 2));
|
||||
|
|
@ -612,7 +539,22 @@ function init() {
|
|||
c: i
|
||||
};
|
||||
}
|
||||
setTimeout(probe, 1);
|
||||
lcdBuffer = Bangle.getOptions().lcdBufferPtr;
|
||||
if (!lcdBuffer) {
|
||||
E.showMessage("Needs firmwave 2v21 or newer");
|
||||
return;
|
||||
}
|
||||
let stride = 68;
|
||||
//print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride);
|
||||
var sintablePtr = E.getAddressOf(sintable, true);
|
||||
if (!sintablePtr) {
|
||||
lcdBuffer = 0;
|
||||
E.showMessage("Not enough memory");
|
||||
return;
|
||||
}
|
||||
gfx.init(lcdBuffer, stride, sintablePtr);
|
||||
gfx.setCamera(0, 0, -300 << 8);
|
||||
setupInterval(true);
|
||||
}
|
||||
|
||||
function updateNode(index) {
|
||||
|
|
@ -657,12 +599,17 @@ function drawNode(index) {
|
|||
translation[i++] = o.y * 256;
|
||||
translation[i++] = o.z * 256;
|
||||
translation[i++] = o.c;
|
||||
gfx.render(E.getAddressOf(translation, true));
|
||||
let translationPtr = E.getAddressOf(translation, true);
|
||||
if (!translationPtr) {
|
||||
lcdBuffer = 0;
|
||||
E.showMessage("Not enough memory");
|
||||
return;
|
||||
}
|
||||
gfx.render(translationPtr);
|
||||
}
|
||||
|
||||
function tick(locked) {
|
||||
g.reset();
|
||||
|
||||
if (lcdBuffer && !locked) {
|
||||
BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows
|
||||
gfx.clear(bgColor);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "warpdrive",
|
||||
"name": "warpdrive clock",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "A watchface with an animated 3D scene.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Remove library stub
|
||||
0.05: Don't turn on LCD
|
||||
0.06: Don't draw outside of widget field
|
||||
0.07: Don't leave clipRect modified, as per [this comment](https://github.com/espruino/BangleApps/pull/3813#issuecomment-2826952155).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widmsggrid",
|
||||
"name": "Messages Grid Widget",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Widget that displays notification icons in a grid",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
r++;
|
||||
}
|
||||
});
|
||||
g.reset(); // Make sure we don't leave clipRect set to some smaller rectangle.
|
||||
if (w.total > 1) {
|
||||
// show total number of messages in bottom-right corner
|
||||
g.reset();
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA/4AB31H//A/hL/ABfABRMD+ALJh5kKn//BRCBCP4ILT/WrBZOq1W/BY/+BYOvBY0DBYZhGh/6BYOrMI0/BYf5qpGGBYWVqtQC4+qqtVqgvF1NVAIIABI4ogBAAdAL4gKEBZg8E/oLiBQpUEgILHJAUFBY4kCioLHQoQKHGAYL4I4RTLNZaDLGA78EAH4AR"))
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* https://web.archive.org/web/20110610213848/http://www.meteormetrics.com/zambretti.htm
|
||||
*/
|
||||
|
||||
const storage = require('Storage');
|
||||
const Layout = require("Layout");
|
||||
|
||||
let height;
|
||||
let mainScreen = false;
|
||||
|
||||
const ZAMBRETTI_FORECAST = {
|
||||
A: /*LANG*/'Settled Fine',
|
||||
B: /*LANG*/'Fine Weather',
|
||||
C: /*LANG*/'Becoming Fine',
|
||||
D: /*LANG*/'Fine Becoming Less Settled',
|
||||
E: /*LANG*/'Fine, Possibly showers',
|
||||
F: /*LANG*/'Fairly Fine, Improving',
|
||||
G: /*LANG*/'Fairly Fine, Possibly showers, early',
|
||||
H: /*LANG*/'Fairly Fine Showery Later',
|
||||
I: /*LANG*/'Showery Early, Improving',
|
||||
J: /*LANG*/'Changeable Mending',
|
||||
K: /*LANG*/'Fairly Fine, Showers likely',
|
||||
L: /*LANG*/'Rather Unsettled Clearing Later',
|
||||
M: /*LANG*/'Unsettled, Probably Improving',
|
||||
N: /*LANG*/'Showery Bright Intervals',
|
||||
O: /*LANG*/'Showery Becoming more unsettled',
|
||||
P: /*LANG*/'Changeable some rain',
|
||||
Q: /*LANG*/'Unsettled, short fine Intervals',
|
||||
R: /*LANG*/'Unsettled, Rain later',
|
||||
S: /*LANG*/'Unsettled, rain at times',
|
||||
T: /*LANG*/'Very Unsettled, Finer at times',
|
||||
U: /*LANG*/'Rain at times, worse later.',
|
||||
V: /*LANG*/'Rain at times, becoming very unsettled',
|
||||
W: /*LANG*/'Rain at Frequent Intervals',
|
||||
X: /*LANG*/'Very Unsettled, Rain',
|
||||
Y: /*LANG*/'Stormy, possibly improving',
|
||||
Z: /*LANG*/'Stormy, much rain',
|
||||
};
|
||||
|
||||
const ZAMBRETTI_FALLING = {
|
||||
1050: 'A',
|
||||
1040: 'B',
|
||||
1024: 'C',
|
||||
1018: 'H',
|
||||
1010: 'O',
|
||||
1004: 'R',
|
||||
998: 'U',
|
||||
991: 'V',
|
||||
985: 'X',
|
||||
};
|
||||
|
||||
const ZAMBRETTI_STEADY = {
|
||||
1033: 'A',
|
||||
1023: 'B',
|
||||
1014: 'E',
|
||||
1008: 'K',
|
||||
1000: 'N',
|
||||
994: 'P',
|
||||
989: 'S',
|
||||
981: 'W',
|
||||
974: 'X',
|
||||
960: 'Z',
|
||||
};
|
||||
|
||||
const ZAMBRETTI_RISING = {
|
||||
1030: 'A',
|
||||
1022: 'B',
|
||||
1012: 'C',
|
||||
1007: 'F',
|
||||
1000: 'G',
|
||||
995: 'I',
|
||||
990: 'J',
|
||||
984: 'L',
|
||||
978: 'M',
|
||||
970: 'Q',
|
||||
965: 'T',
|
||||
959: 'Y',
|
||||
947: 'Z',
|
||||
};
|
||||
|
||||
function correct_season(letter, dir) {
|
||||
const month = new Date().getMonth() + 1;
|
||||
const location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
|
||||
const northern_hemisphere = location.lat > 0;
|
||||
const summer = northern_hemisphere ? (month >= 4 && month <= 9) : (month >= 10 || month <= 3);
|
||||
|
||||
let corr = 0;
|
||||
if (dir < 0 && !summer) { // Winter falling
|
||||
corr = +1;
|
||||
} else if (dir > 0 && summer) { // Summer rising
|
||||
corr = -1;
|
||||
}
|
||||
return letter == 'A' || letter == 'Z' ? letter : String.fromCharCode(letter.charCodeAt(0)+corr);
|
||||
}
|
||||
|
||||
function get_zambretti_letter(pressure, dir) {
|
||||
let table = (() => {
|
||||
if (dir < 0) { // Barometer Falling
|
||||
return ZAMBRETTI_FALLING;
|
||||
} else if (dir == 0) { // Barometer Steady
|
||||
return ZAMBRETTI_STEADY;
|
||||
} else { // Barometer Rising
|
||||
return ZAMBRETTI_RISING;
|
||||
}
|
||||
})();
|
||||
|
||||
const closest = Object.keys(table).reduce(function(prev, curr) {
|
||||
return (Math.abs(curr - pressure) < Math.abs(prev - pressure) ? curr : prev);
|
||||
});
|
||||
|
||||
return correct_season(table[closest], dir);
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
const settings = require('Storage').readJSON("zambretti.json", true) || {};
|
||||
height = settings.height;
|
||||
}
|
||||
|
||||
function showMenu() {
|
||||
const menu = {
|
||||
"" : {
|
||||
title : "Zambretti Forecast",
|
||||
},
|
||||
"< Back": () => {
|
||||
E.showMenu();
|
||||
layout.forgetLazyState();
|
||||
show();
|
||||
},
|
||||
/*LANG*/"Exit": () => load(),
|
||||
/*LANG*/"Settings": () =>
|
||||
eval(require('Storage').read('zambretti.settings.js'))(() => {
|
||||
loadSettings();
|
||||
showMenu();
|
||||
}),
|
||||
'Plot history': () => {E.showMenu(); plot();},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
const layout = new Layout({
|
||||
type:"v", c: [
|
||||
{type:"txt", font:"9%", label:/*LANG*/"Zambretti Forecast", bgCol:g.theme.bgH, fillx: true, pad: 1},
|
||||
{type:"txt", font:"12%", id:"forecast", filly: 1, wrap: 1, width: Bangle.appRect.w, pad: 1},
|
||||
{type:"h", c:[
|
||||
{type: 'v', c:[
|
||||
{type:"txt", font:"9%", label:/*LANG*/"Pressure", pad: 3, halign: -1},
|
||||
{type:"txt", font:"9%", label:/*LANG*/"Difference", pad: 3, halign: -1},
|
||||
{type:"txt", font:"9%", label:/*LANG*/"Temperature", pad: 3, halign: -1},
|
||||
]},
|
||||
{type: 'v', c:[
|
||||
{type:"txt", font:"9%", id:"pressure", pad: 3, halign: -1},
|
||||
{type:"txt", font:"9%", id:"diff", pad: 3, halign: -1},
|
||||
{type:"txt", font:"9%", id:"temp", pad: 3, halign: -1},
|
||||
]}
|
||||
]},
|
||||
]
|
||||
}, {lazy:true});
|
||||
|
||||
function draw(temperature) {
|
||||
const history3 = storage.readJSON("zambretti.log.json", true) || []; // history of recent 3 hours
|
||||
const pressure_cur = history3[history3.length-1].p;
|
||||
const pressure_last = history3[0].p;
|
||||
const diff = pressure_cur - pressure_last;
|
||||
const pressure_sea = pressure_cur * Math.pow(1 - (0.0065 * height) / (temperature + (0.0065 * height) + 273.15),-5.257);
|
||||
|
||||
layout.forecast.label = ZAMBRETTI_FORECAST[get_zambretti_letter(pressure_sea, diff)];
|
||||
layout.pressure.label = Math.round(pressure_sea);
|
||||
layout.diff.label = diff;
|
||||
layout.temp.label = require("locale").number(temperature,1);
|
||||
layout.render();
|
||||
//layout.debug();
|
||||
|
||||
mainScreen = true;
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
btn: (n) => {mainScreen = false; showMenu();},
|
||||
});
|
||||
}
|
||||
|
||||
function show() {
|
||||
Bangle.getPressure().then(p =>{if (p) draw(p.temperature);});
|
||||
}
|
||||
|
||||
function plot() {
|
||||
const interval = 15; // minutes
|
||||
const history3 = require('Storage').readJSON("zambretti.log.json", true) || []; // history of recent 3 hours
|
||||
|
||||
const now = new Date()/(1000);
|
||||
let curtime = now-3*60*60; // 3h ago
|
||||
const data = [];
|
||||
while (curtime <= now) {
|
||||
// find closest value in history for this timestamp
|
||||
const closest = history3.reduce((prev, curr) => {
|
||||
return (Math.abs(curr.ts - curtime) < Math.abs(prev.ts - curtime) ? curr : prev);
|
||||
});
|
||||
data.push(closest.p);
|
||||
curtime += interval*60;
|
||||
}
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: () => showMenu(),
|
||||
});
|
||||
|
||||
g.reset().setFont("6x8",1);
|
||||
require("graph").drawLine(g, data, {
|
||||
axes: true,
|
||||
x: 4,
|
||||
y: Bangle.appRect.y+8,
|
||||
height: Bangle.appRect.h-20,
|
||||
gridx: 1,
|
||||
gridy: 1,
|
||||
miny: Math.min.apply(null, data)-1,
|
||||
maxy: Math.max.apply(null, data)+1,
|
||||
title: /*LANG*/"Barometer history (mBar)",
|
||||
ylabel: y => y,
|
||||
xlabel: i => {
|
||||
const t = -3*60 + interval*i;
|
||||
if (t % 60 === 0) {
|
||||
return "-" + t/60 + "h";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
g.reset().clear();
|
||||
loadSettings();
|
||||
Bangle.loadWidgets();
|
||||
|
||||
if (height === undefined) {
|
||||
// setting of height required
|
||||
eval(require('Storage').read('zambretti.settings.js'))(() => {
|
||||
loadSettings();
|
||||
show();
|
||||
});
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// Update every 15 minutes
|
||||
setInterval(() => {
|
||||
if (mainScreen) {
|
||||
show();
|
||||
}
|
||||
}, 15*60*1000);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 716 B |
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
// Copied from widbaroalarm
|
||||
const LOG_FILE = "zambretti.log.json";
|
||||
const history3 = require('Storage').readJSON(LOG_FILE, true) || []; // history of recent 3 hours
|
||||
let currentPressures = [];
|
||||
|
||||
const isValidPressureValue = (pressure) => {
|
||||
return !(pressure == undefined || pressure <= 0);
|
||||
};
|
||||
|
||||
const handlePressureValue = (pressure) => {
|
||||
if (pressure == undefined || pressure <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ts = Math.round(Date.now() / 1000); // seconds
|
||||
const d = {"ts" : ts, "p" : pressure};
|
||||
|
||||
history3.push(d);
|
||||
|
||||
// delete oldest entries until we have max 50
|
||||
while (history3.length > 50) {
|
||||
history3.shift();
|
||||
}
|
||||
|
||||
// delete entries older than 3h
|
||||
for (let i = 0; i < history3.length; i++) {
|
||||
if (history3[i].ts < ts - (3 * 60 * 60)) {
|
||||
history3.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// write data to storage
|
||||
require('Storage').writeJSON(LOG_FILE, history3);
|
||||
};
|
||||
|
||||
const barometerPressureHandler = (e) => {
|
||||
const MEDIANLENGTH = 20;
|
||||
while (currentPressures.length > MEDIANLENGTH)
|
||||
currentPressures.pop();
|
||||
|
||||
const pressure = e.pressure;
|
||||
if (isValidPressureValue(pressure)) {
|
||||
currentPressures.unshift(pressure);
|
||||
let median = currentPressures.slice().sort();
|
||||
|
||||
if (median.length > 10) {
|
||||
var mid = median.length >> 1;
|
||||
let medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9);
|
||||
if (medianPressure > 0) {
|
||||
turnOff();
|
||||
handlePressureValue(medianPressure);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
turn on barometer power
|
||||
take multiple measurements
|
||||
sort the results
|
||||
take the middle one (median)
|
||||
turn off barometer power
|
||||
*/
|
||||
const getPressureValue = () => {
|
||||
Bangle.setBarometerPower(true, "zambretti");
|
||||
Bangle.on('pressure', barometerPressureHandler);
|
||||
setTimeout(turnOff, 30000);
|
||||
};
|
||||
|
||||
const turnOff = () => {
|
||||
Bangle.removeListener('pressure', barometerPressureHandler);
|
||||
Bangle.setBarometerPower(false, "zambretti");
|
||||
};
|
||||
|
||||
// delay pressure measurement by interval-lastrun
|
||||
const interval = 15; // minutes
|
||||
const lastRun = history3.length > 0 ? history3[history3.length-1].ts : 0;
|
||||
const lastRunAgo = Math.round(Date.now() / 1000) - lastRun;
|
||||
let diffNextRun = interval*60-lastRunAgo;
|
||||
if (diffNextRun < 0) {
|
||||
diffNextRun = 0; // run asap
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (interval > 0) {
|
||||
setInterval(getPressureValue, interval * 60000);
|
||||
}
|
||||
getPressureValue();
|
||||
}, diffNextRun*1000);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "zambretti",
|
||||
"name": "Zambretti Weather Forecaster",
|
||||
"shortName": "Zn. Weather",
|
||||
"version": "0.01",
|
||||
"description": "Zambretti Forecaster by Negretti and Zambra, uses the Barometer for empirical weather forecast in the Northern Hemisphere (see https://web.archive.org/web/20110610213848/http://www.meteormetrics.com/zambretti.htm), similar to weather stations. Watch must be stationary and its height above sea level set.",
|
||||
"icon": "app.png",
|
||||
"tags": "outdoors,weather",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies": {"mylocation":"app"},
|
||||
"screenshots": [ {"url":"screenshot.png"} ],
|
||||
"storage": [
|
||||
{"name":"zambretti.app.js","url":"app.js"},
|
||||
{"name":"zambretti.settings.js","url":"settings.js"},
|
||||
{"name":"zambretti.boot.js","url":"boot.js"},
|
||||
{"name":"zambretti.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"zambretti.json"}, {"name":"zambretti.log.json"}]
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,25 @@
|
|||
(function(back) {
|
||||
const FILE = "zambretti.json";
|
||||
// Load settings
|
||||
const settings = Object.assign({
|
||||
height: 0,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "Zambretti Forecast" },
|
||||
"< Back" : () => back(),
|
||||
'Height above sea level (m)': {
|
||||
value: settings.height,
|
||||
min: 0, max: 1000,
|
||||
onchange: v => {
|
||||
settings.height = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
2
core
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit 76b887dd6fc5693786fc1e63b97f3e4ab4306ed7
|
||||
Subproject commit ef99424a9fbd01be504841a6c759ba4292a542f7
|
||||
|
|
@ -85,6 +85,7 @@
|
|||
<label class="chip tooltip" filterid="ram" data-tooltip="Apps that don't save anything to flash memory">Online</label>
|
||||
<label class="chip tooltip" filterid="clkinfo" data-tooltip="Info displayed on clocks, or clocks with info">Clock Info</label>
|
||||
<label class="chip tooltip" filterid="health" data-tooltip="Apps for your health">Health</label>
|
||||
<label class="chip tooltip" filterid="fonts" data-tooltip="Extra fonts for non-latin languages">Fonts</label>
|
||||
<label class="chip tooltip" filterid="favourites" data-tooltip="Apps that you've liked ❤️">Favourites</label>
|
||||
</div>
|
||||
<div class="sort-nav hidden">
|
||||
|
|
|
|||
|
|
@ -228,5 +228,34 @@
|
|||
"quarter to *$2": "viertel vor *$2",
|
||||
"ten to *$2": "zehn vor *$2",
|
||||
"five to *$2": "fünf vor *$2"
|
||||
},
|
||||
"zambretti": {
|
||||
"//": "App-specific overrides",
|
||||
"Settled Fine": "Beständig sonnig",
|
||||
"Fine Weather": "Sonniges Wetter",
|
||||
"Becoming Fine": "Es wird schöner",
|
||||
"Fine Becoming Less Settled": "Sonnig, Tendenz unbeständiger",
|
||||
"Fine, Possibly showers": "Sonnig, eventuell Schauer",
|
||||
"Fairly Fine, Improving": "Heiter bis wolkig, Besserung zu erwarten",
|
||||
"Fairly Fine, Possibly showers, early": "Heiter bis wolkig, anfangs evtl. Schauer",
|
||||
"Fairly Fine Showery Later": "Heiter bis wolkig, später Regen",
|
||||
"Showery Early, Improving": "Anfangs noch Schauer, dann Besserung",
|
||||
"Changeable Mending": "Wechselhaft mit Schauern",
|
||||
"Fairly Fine, Showers likely": "Heiter bis wolkig, vereinzelt Regen",
|
||||
"Rather Unsettled Clearing Later": "Unbeständig, spaeter Aufklarung",
|
||||
"Unsettled, Probably Improving": "Unbeständig, evtl. Besserung.",
|
||||
"Showery Bright Intervals": "Regnerisch mit heiteren Phasen",
|
||||
"Showery Becoming more unsettled": "Regnerisch, wird unbeständiger",
|
||||
"Changeable some rain": "Wechselhaft mit etwas Regen",
|
||||
"Unsettled, short fine Intervals": "Unbeständig mit heiteren Phasen",
|
||||
"Unsettled, Rain later": "Unbeständig, später Regen",
|
||||
"Unsettled, rain at times": "Unbeständig mit etwas Regen",
|
||||
"Very Unsettled, Finer at times": "Wechselhaft und regnerisch",
|
||||
"Rain at times, worse later.": "Gelegentlich Regen, Verschlechterung",
|
||||
"Rain at times, becoming very unsettled": "Zuweilen Regen, sehr unbeständig",
|
||||
"Rain at Frequent Intervals": "Häufiger Regen",
|
||||
"Very Unsettled, Rain": "Regen, sehr unbeständig",
|
||||
"Stormy, possibly improving": "Stürmisch, evtl. Besserung",
|
||||
"Stormy, much rain": "Stürmisch mit viel Regen"
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ exports.show = function() {
|
|||
/// Remove anything not needed if the overlay was removed
|
||||
exports.cleanupOverlay = function() {
|
||||
exports.offset = -24;
|
||||
Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
|
||||
Bangle.setLCDOverlay && Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
|
||||
delete exports.autohide;
|
||||
delete Bangle.appRect;
|
||||
if (exports.animInterval) {
|
||||
|
|
@ -92,15 +92,17 @@ exports.swipeOn = function(autohide) {
|
|||
const o = exports.offset;
|
||||
Bangle.appRect.y = o+24;
|
||||
Bangle.appRect.h = 1 + Bangle.appRect.y2 - Bangle.appRect.y;
|
||||
if (o>-24) {
|
||||
Bangle.setLCDOverlay(og, 0, o, {
|
||||
id:"widget_utils",
|
||||
remove:()=>{
|
||||
require("widget_utils").cleanupOverlay();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
|
||||
if (Bangle.setLCDOverlay) {
|
||||
if (o>-24) {
|
||||
Bangle.setLCDOverlay(og, 0, o, {
|
||||
id:"widget_utils",
|
||||
remove:()=>{
|
||||
require("widget_utils").cleanupOverlay();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Bangle.setLCDOverlay(undefined, {id: "widget_utils"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,3 +8,4 @@ declare function require(moduleName: "Layout"): typeof Layout.Layout;
|
|||
declare function require(moduleName: "power_usage"): PowerUsageModule;
|
||||
declare function require(moduleName: "exstats"): typeof ExStats;
|
||||
declare function require(moduleName: "time_utils"): typeof TimeUtils;
|
||||
declare function require(moduleName: "textinput"): typeof TextInput;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
declare module TextInput {
|
||||
function input(options: { text: string }): Promise<string>;
|
||||
}
|
||||
2
webtools
2
webtools
|
|
@ -1 +1 @@
|
|||
Subproject commit 4dc22df5fd9af2f8fc41a626cc7d8d5456b8e614
|
||||
Subproject commit 8b02a5932f88c652a9d59a947b7a2e14afe5d8f2
|
||||
Loading…
Reference in New Issue