Merge branch 'espruino:master' into Weather-Feels-Like-Updates

master
RKBoss6 2025-08-01 18:47:52 -04:00 committed by GitHub
commit 76c3801f89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 1698 additions and 449 deletions

2
apps/backlite/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app! (settings, boot.js).
0.02: Fix settings defaulting brightness to 0

20
apps/backlite/README.md Normal file
View File

@ -0,0 +1,20 @@
# BackLite
### This app needs the latest settings app update (v 0.80), to ensure that setting the brightness to `0` does not default to `1`.
BackLite is an app which greatly conserves battery life by only turning the backlight on when you long press the button from a locked state.
Modern watches have a dedicated button to turn the backlight on, so as not to waste battery in an already light environment. This app recreates that functionality for the Bangle.js, which only has one button.
#### Warning: This app overwrites the LCD brightness setting in `Bangle.js LCD settings`. If it is changed, the app will basically lose functionality. It auto-fixes itself every boot, so if you change the brightness, just reboot :)
# Usage
When you unlock with a press of the button, or any other way you unlock the watch, the backlight will not turn on, as most of the time you are able to read it, due to the transreflective display on the Bangle.js 2.
If you press and hold the button to unlock the watch (for around half a second), the backlight will turn on for 5 seconds - just enough to see what you need to see. After that, it will turn off again.
Some apps like `Light Switch Widget` will prevent this app from working properly.
# Settings
`Brightness` - The LCD brightness when unlocked with a long press.
# Creator
RKBoss6
TODO: Add a setting for long press time, or light duration

36
apps/backlite/boot.js Normal file
View File

@ -0,0 +1,36 @@
{
let getSettings = function(){
return Object.assign({
// default values
brightness: 0.3,
}, require('Storage').readJSON("BackLite.settings.json", true) || {});
};
//Set LCD to zero every reboot
let s = require("Storage").readJSON("setting.json", 1) || {};
s.brightness = 0;
require("Storage").writeJSON("setting.json", s);
//remove large settings object from memory
delete s;
const longPressTime=400; //(ms)
Bangle.on('lock', function(isLocked) {
Bangle.setLCDBrightness(0);
if (!isLocked) {
// Just unlocked — give a short delay and check if BTN1 is still pressed
setTimeout(() => {
if (digitalRead(BTN1)) {
//set brightness until. locked.
Bangle.setLCDBrightness(getSettings().brightness);
} else {
Bangle.setLCDBrightness(0);
}
}, longPressTime); // Slight delay to allow unlock to settle
}
});
}

BIN
apps/backlite/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,17 @@
{
"id": "backlite",
"name": "BackLite",
"version": "0.02",
"description": "Conserves battery life by turning the backlight on only on a long press of the button from a locked state. **Requires the latest settings update (v0.80)**",
"icon": "icon.png",
"type": "bootloader",
"tags": "system",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"backlite.boot.js","url":"boot.js"},
{"name":"backlite.settings.js","url":"settings.js"}
],
"data": [{"name":"BackLite.settings.json"}]
}

25
apps/backlite/settings.js Normal file
View File

@ -0,0 +1,25 @@
(function(back) {
var FILE = "BackLite.settings.json";
var settings = require("Storage").readJSON(FILE, 1) || {};
if (!isFinite(settings.brightness)) settings.brightness = 0.3;
function writeSettings() {
require("Storage").writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "BackLite" },
"< Back": back, // fallback if run standalone
"Brightness": {
value: settings.brightness,
min: 0.1, max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v;
writeSettings();
}
},
});
})

4
apps/bbreaker/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: It works somehow, early version for testers and feedback :)
0.02: Changed almost all code with Frederic version of Pong and adjusted to be a BrickBreaker!, still Alpha
0.03: Rewrote the whole thing to have less code and better graphics, now it works.
0.04: Rewrote part of the code to coupe with the flickering and added better logic to handle the graphics.

28
apps/bbreaker/README.md Normal file
View File

@ -0,0 +1,28 @@
# BrickBreaker
A simple BreakOut clone for the Banglejs
![Screenshot](bbreaker.png)
## Usage
![Screenshot](playing.png)
Buttons 1 and 3 to move the BrickBreaker!
Button 2 to pause and start the game again.
## Disclaimer
This game was created to learn JS and how to interact with Banglejs, meaning that it may not be perfect :).
Built with love with base on the tutorial: 2D breakout game using pure JavaScript
https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
Started on 2020 but rewrote all in 2025 and this is the version I got without having issues with Memory Exhaustion.
And yes, for Bangle 1, old school!
## Creator
Israel Ochoa <isuraeru at gmail.com>

View File

@ -0,0 +1 @@
atob("MDCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQAAAAAAAAAAAAAAVQAAAAAAAAAAAAAFWUAAAAAAAAAAAABVuUAAAAAAAAAAAAVa/lAAAAAAAAAAABWv/lQAAAAAAAAAAVb/+VAAAAAAAAAABW//lUAAAAAAAAAABX/6VAAAAAAAAAAAAW/lUAAAAAAAAAAAAFpVAAAAAAAAAAAAAFVQAAAAAAAAAAAAABUAAAFVVVVQVVQAAAQAAAFVVVVQVVQAAAAAAAFv//5QW5QAAAAAAAG///9QW9QAAAAAAAG///9QW9QAAAAAAAFqqqpQWpQAAAAAAAFVVVVQVVQAAAAAAAFVVVVQVVQAAAAAAAAAAAAAAAAAAAABVVVVQFVVVVQAAAAFVVVVUVVVVVQAAAAFqqqqUWqqqpQAAAAFv//+UW///9QAAAAFv//+UW///9QAAAAFv//+UWv//5QAAAAFVVVVUVVVVVQAAAABVVVVUFVVVVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVVVUFVVVVQVVQABaqqqUFqqqpQWpQABv//+UF///9QW5QABv//+UG///9QW5QABv//+UG///9QW5QABaqqqUFqqqpQWlQABVVVVUFVVVVQVVQAAVVVVABVVVVABUAAAAAAAAAAAAAAAAAA==")

293
apps/bbreaker/app.js Normal file
View File

@ -0,0 +1,293 @@
(function () {
var BALL_RADIUS = 3;
var PADDLE_WIDTH = 26;
var PADDLE_HEIGHT = 6;
var BRICK_ROWS = 2;
var BRICK_HEIGHT = 8;
var BRICK_PADDING = 4;
var BRICK_OFFSET_TOP = 60;
var BRICK_OFFSET_LEFT = 2;
var SPEED_MULTIPLIER = 1.1;
var PADDLE_SPEED = 12;
var ball, paddle, interval;
var bricks = [];
var BRICK_WIDTH, BRICK_COLS;
var score = 0;
var level = 1;
var highScore = 0;
var paused = false;
var gameOver = false;
var lives = 3;
var paddleMove = 0;
var storage = require("Storage");
function loadHighScore() {
var saved = storage.readJSON("breakout_highscore.json", 1);
highScore = saved && saved.highScore ? saved.highScore : 0;
}
function saveHighScore() {
if (score > highScore) {
highScore = score;
storage.writeJSON("breakout_highscore.json", { highScore });
}
}
function initBricks() {
bricks = [];
for (var i = 0; i < BRICK_ROWS * BRICK_COLS; i++) {
bricks.push(1);
}
}
function initGame() {
var screenWidth = g.getWidth();
BRICK_COLS = Math.min(5, Math.floor((screenWidth - BRICK_OFFSET_LEFT + BRICK_PADDING) / (15 + BRICK_PADDING)));
BRICK_WIDTH = Math.floor((screenWidth - BRICK_OFFSET_LEFT - (BRICK_COLS - 1) * BRICK_PADDING) / BRICK_COLS);
ball = {
x: screenWidth / 2,
y: g.getHeight() - 40,
dx: (Math.random() > 0.5 ? 1 : -1) * 3,
dy: -3,
radius: BALL_RADIUS,
prevPos: null
};
paddle = {
x: screenWidth / 2 - PADDLE_WIDTH / 2,
y: g.getHeight() - 20,
width: PADDLE_WIDTH,
height: PADDLE_HEIGHT,
prevPos: null
};
lives = 3;
score = 0;
level = 1;
gameOver = false;
paused = false;
loadHighScore();
initBricks();
}
function drawLives() {
var heartSize = 6;
var spacing = 2;
var startX = g.getWidth() - (lives * (heartSize + spacing)) - 2;
var y = 32;
g.setColor(1, 0, 0);
for (var i = 0; i < lives; i++) {
var x = startX + i * (heartSize + spacing);
g.fillPoly([x + 3, y, x + 6, y + 3, x + 3, y + 6, x, y + 3], true);
}
}
function drawBricks() {
g.setColor(0, 1, 0);
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) {
var c = i % BRICK_COLS;
var r = Math.floor(i / BRICK_COLS);
var brickX = BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_PADDING);
var brickY = BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_PADDING);
g.fillRect(brickX, brickY, brickX + BRICK_WIDTH - 1, brickY + BRICK_HEIGHT - 1);
}
}
}
function drawBall() {
g.setColor(1, 1, 1);
g.fillCircle(ball.x, ball.y, ball.radius);
g.setColor(0.7, 0.7, 0.7);
g.fillCircle(ball.x - 0.5, ball.y - 0.5, ball.radius - 1);
}
function drawPaddle() {
g.setColor(0, 1, 1);
g.fillRect(paddle.x, paddle.y, paddle.x + paddle.width - 1, paddle.y + paddle.height - 1);
}
function drawHUD() {
g.setColor(0, 0, 0).fillRect(0, 0, g.getWidth(), BRICK_OFFSET_TOP - 1);
g.setColor(1, 1, 1);
g.setFont("6x8", 1);
g.setFontAlign(-1, -1);
g.drawString("Score: " + score, 2, 22);
g.setFontAlign(0, -1);
g.drawString("High: " + highScore, g.getWidth() / 2, 22);
g.setFontAlign(1, -1);
g.drawString("Lvl: " + level, g.getWidth() - 2, 22);
drawLives();
if (paused) {
g.setFontAlign(0, 0);
g.drawString("PAUSED", g.getWidth() / 2, g.getHeight() / 2);
}
}
function draw() {
if (paddle.prevPos) {
g.setColor(0, 0, 0).fillRect(paddle.prevPos.x - 1, paddle.prevPos.y - 1, paddle.prevPos.x + paddle.width + 1, paddle.prevPos.y + paddle.height + 1);
}
if (ball.prevPos) {
g.setColor(0, 0, 0).fillCircle(ball.prevPos.x, ball.prevPos.y, ball.radius + 1);
}
drawHUD();
drawBall();
drawPaddle();
g.flip();
ball.prevPos = { x: ball.x, y: ball.y };
paddle.prevPos = { x: paddle.x, y: paddle.y, width: paddle.width, height: paddle.height };
}
function showGameOver() {
g.clear();
g.setFont("6x8", 2);
g.setFontAlign(0, 0);
g.setColor(1, 0, 0);
g.drawString("GAME OVER", g.getWidth() / 2, g.getHeight() / 2 - 20);
g.setFont("6x8", 1);
g.setColor(1, 1, 1);
g.drawString("Score: " + score, g.getWidth() / 2, g.getHeight() / 2);
g.drawString("High: " + highScore, g.getWidth() / 2, g.getHeight() / 2 + 12);
g.drawString("BTN2 = Restart", g.getWidth() / 2, g.getHeight() / 2 + 28);
g.flip();
}
function collisionDetection() {
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) {
var c = i % BRICK_COLS;
var r = Math.floor(i / BRICK_COLS);
var brickX = BRICK_OFFSET_LEFT + c * (BRICK_WIDTH + BRICK_PADDING);
var brickY = BRICK_OFFSET_TOP + r * (BRICK_HEIGHT + BRICK_PADDING);
if (ball.x + ball.radius > brickX && ball.x - ball.radius < brickX + BRICK_WIDTH && ball.y + ball.radius > brickY && ball.y - ball.radius < brickY + BRICK_HEIGHT) {
ball.dy = -ball.dy;
bricks[i] = 0;
score += 10;
g.setColor(0, 0, 0).fillRect(brickX, brickY, brickX + BRICK_WIDTH - 1, brickY + BRICK_HEIGHT - 1);
break;
}
}
}
}
function allBricksCleared() {
for (var i = 0; i < bricks.length; i++) {
if (bricks[i]) return false;
}
return true;
}
function resetAfterLifeLost() {
clearInterval(interval);
interval = undefined;
ball.x = g.getWidth() / 2;
ball.y = g.getHeight() - 40;
ball.dx = (Math.random() > 0.5 ? 1 : -1) * 3;
ball.dy = -3;
paddle.x = g.getWidth() / 2 - PADDLE_WIDTH / 2;
ball.prevPos = null;
paddle.prevPos = null;
g.clear();
drawBricks();
draw();
setTimeout(() => { interval = setInterval(update, 50); }, 1000);
}
function update() {
if (paused || gameOver) return;
if (paddleMove) {
paddle.x += paddleMove * PADDLE_SPEED;
if (paddle.x < 0) paddle.x = 0;
if (paddle.x + paddle.width > g.getWidth()) {
paddle.x = g.getWidth() - paddle.width;
}
}
ball.x += ball.dx;
ball.y += ball.dy;
if (ball.x + ball.radius > g.getWidth() || ball.x - ball.radius < 0) {
ball.dx = -ball.dx;
}
if (ball.y - ball.radius < 0) {
ball.dy = -ball.dy;
}
if (ball.y + ball.radius >= paddle.y && ball.x >= paddle.x && ball.x <= paddle.x + paddle.width) {
ball.dy = -ball.dy;
ball.y = paddle.y - ball.radius;
}
if (ball.y + ball.radius > g.getHeight()) {
lives--;
if (lives > 0) {
resetAfterLifeLost();
return;
} else {
clearInterval(interval);
interval = undefined;
saveHighScore();
gameOver = true;
setTimeout(showGameOver, 50);
return;
}
}
collisionDetection();
if (allBricksCleared()) {
level++;
ball.dx = (Math.random() > 0.5 ? 1 : -1) * Math.abs(ball.dx) * SPEED_MULTIPLIER;
ball.dy *= SPEED_MULTIPLIER;
initBricks();
ball.prevPos = null;
paddle.prevPos = null;
g.clear();
drawBricks();
}
draw();
}
function showGetReady(callback) {
g.clear();
g.setFont("6x8", 2);
g.setFontAlign(0, 0);
g.setColor(1, 1, 0);
g.drawString("GET READY!", g.getWidth() / 2, g.getHeight() / 2);
g.flip();
setTimeout(() => {
g.clear();
drawBricks();
draw();
callback();
}, 1500);
}
function startGame() {
initGame();
showGetReady(() => {
interval = setInterval(update, 50);
});
}
setWatch(() => {
if (gameOver) {
startGame();
return;
}
paused = !paused;
if (paused) {
drawHUD();
g.flip();
} else {
g.clear();
drawBricks();
draw();
}
}, BTN2, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = -1; }, BTN1, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = 0; }, BTN1, { repeat: true, edge: "falling" });
setWatch(() => { paddleMove = 1; }, BTN3, { repeat: true, edge: "rising" });
setWatch(() => { paddleMove = 0; }, BTN3, { repeat: true, edge: "falling" });
startGame();
})();

BIN
apps/bbreaker/bbreaker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,15 @@
{ "id": "bbreaker",
"name": "BrickBreaker",
"shortName":"BrickBreaker",
"icon": "bbreaker.png",
"version":"0.04",
"description": "A simple BreakOut clone for the Banglejs",
"tags": "game",
"readme": "README.md",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"bbreaker.app.js","url":"app.js"},
{"name":"bbreaker.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/bbreaker/playing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -5,7 +5,7 @@
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo",
"tags": "clkinfo,clock",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfoclk.clkinfo.js","url":"clkinfo.js"}

View File

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

BIN
apps/clkinfodist/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,22 @@
(function() {
let strideLength = (require("Storage").readJSON("myprofile.json",1)||{}).strideLength ?? 0.79,
lastSteps = 0;
function stepUpdateHandler() { distance.emit("redraw"); }
var distance = {
name : "Distance",
get : () => { let v = (Bangle.getHealthStatus("day").steps - lastSteps)*strideLength; return {
text : require("locale").distance(v,1),
img : atob("GBiBAAMAAAeAAA/AAA/AAA/gAA/gwAfh4AfD4APD4AOH4AAH4ADj4AHjwAHhwADgAAACAAAHgAAPAAAHAAgCEBgAGD///BgAGAgAEA==")
};},
run : function() {
lastSteps = (lastSteps>=Bangle.getHealthStatus("day").steps) ? 0 : Bangle.getHealthStatus("day").steps;
this.emit("redraw");
},
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
hide : function() { Bangle.removeListener("step", stepUpdateHandler); }
};
return {
name: "Bangle",
items: [ distance ]
};
})

BIN
apps/clkinfodist/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,14 @@
{ "id": "clkinfodist",
"name": "Clockinfo Distance",
"version":"0.01",
"description": "Uses the 'My Profile' app's Stride Length to calculate distance travelled based on step count. Tap to reset for measuring distances.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clkinfo",
"tags": "clkinfo,distance,steps,outdoors,tool",
"dependencies": {"myprofile":"app"},
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"clkinfodist.clkinfo.js","url":"clkinfo.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -16,3 +16,6 @@
0.15: Fix error when displaying a category with only one clockinfo (fix #3728)
0.16: Add BLE clkinfo entry
0.17: Fix BLE icon alignment and border on some clocks
0.18: Tweak BLE icon to add gap and ensure middle of B isn't filled
0.19: Fix Altitude ClockInfo after BLE added
Tapping Altitude now updates the reading

View File

@ -39,22 +39,23 @@ exports.load = function() {
var hrm = 0;
var alt = "--";
// callbacks (needed for easy removal of listeners)
function batteryUpdateHandler() { bangleItems[0].emit("redraw"); }
function stepUpdateHandler() { bangleItems[1].emit("redraw"); }
function batteryUpdateHandler() { bangleItems.find(i=>i.name=="Battery").emit("redraw"); }
function stepUpdateHandler() { bangleItems.find(i=>i.name=="Steps").emit("redraw"); }
function hrmUpdateHandler(e) {
if (e && e.confidence>60) hrm = Math.round(e.bpm);
bangleItems[2].emit("redraw");
bangleItems.find(i=>i.name=="HRM").emit("redraw");
}
function altUpdateHandler() {
try {
Bangle.getPressure().then(data=>{
if (!data) return;
alt = Math.round(data.altitude) + "m";
bangleItems[3].emit("redraw");
bangleItems.find(i=>i.name=="Altitude").emit("redraw");
});
} catch (e) {
print("Caught "+e+"\n in function altUpdateHandler in module clock_info");
bangleItems[3].emit('redraw');}
bangleItems.find(i=>i.name=="Altitude").emit('redraw');
}
}
// actual menu
var menu = [{
@ -120,7 +121,6 @@ exports.load = function() {
},
},
{ name: "BLE",
hasRange: false,
isOn: () => {
const s = NRF.getSecurityStatus();
return s.advertising || s.connected;
@ -128,7 +128,8 @@ exports.load = function() {
get: function() {
return {
text: this.isOn() ? "On" : "Off",
img: atob("GBiBAAAAAAAAAAAYAAAcAAAWAAATAAARgAMRgAGTAADWAAB8AAA4AAA4AAB8AADWAAGTAAMRgAARgAATAAAWAAAcAAAYAAAAAAAAAA==")
img: atob("GBiBAAAAAAAAAAAYAAAcAAAWAAATAAARgAMRgAGTAADGAAB8AAA4AAA4AAB8AADGAAGTAAMRgAARgAATAAAWAAAcAAAYAAAAAAAAAA==")
// small gaps added to BLE icon to ensure middle of B isn't filled
};
},
run: function() {
@ -155,6 +156,7 @@ exports.load = function() {
min : 0, max : settings.maxAltitude,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==")
}),
run : function() { alt = "--"; this.emit("redraw"); altUpdateHandler(); },
show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; },
});

View File

@ -1,7 +1,7 @@
{ "id": "clock_info",
"name": "Clock Info Module",
"shortName": "Clock Info",
"version":"0.17",
"version":"0.19",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png",
"type": "module",

View File

@ -4,3 +4,4 @@
0.04: Fix lint warnings
0.05: Fix on not reading counter defaults in Settings
0.06: Added ability to display only one counter and fast-scrolling
0.07: Add option to keep unlocked

View File

@ -7,12 +7,19 @@ var s = Object.assign({
fullscreen:true,
buzz: true,
colortext: true,
keepunlocked: false,
}, require('Storage').readJSON("counter2.json", true) || {});
var sGlob = Object.assign({
timeout: 10,
}, require('Storage').readJSON("setting.json", true) || {});
const lockTimeout = s.keepunlocked ? 0 : 1000;
if (s.keepunlocked) {
Bangle.setOptions({lockTimeout});
Bangle.setLocked(false);
}
const f1 = (s.colortext) ? "#f00" : "#fff";
const f2 = (s.colortext) ? "#00f" : "#fff";
const b1 = (s.colortext) ? g.theme.bg : "#f00";
@ -80,7 +87,7 @@ function updateScreen() {
Bangle.on('lock', e => {
drag = undefined;
var timeOutTimer = sGlob.timeout * 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer});
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
if (dragtimeout) clearTimeout(dragtimeout);
fastupdateoccurring = false;
});
@ -102,7 +109,7 @@ Bangle.on("drag", e => {
drag = undefined;
if (dragtimeout) {
let timeOutTimer = 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer});
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
clearTimeout(dragtimeout);
}
fastupdateoccurring = false;
@ -134,7 +141,7 @@ function resetcounter(which) {
fastupdateoccurring = false;
if (dragtimeout) {
let timeOutTimer = 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer});
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
clearTimeout(dragtimeout);
}
if (which == null) {
@ -152,7 +159,6 @@ function resetcounter(which) {
ignoreonce = true;
}
updateScreen();
setWatch(function() {
@ -163,6 +169,6 @@ setWatch(function() {
}
}
var timeOutTimer = sGlob.timeout * 1000;
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout: timeOutTimer});
Bangle.setOptions({backlightTimeout: timeOutTimer, lockTimeout});
load();
}, BTN1, {repeat:true, edge:"falling"});

View File

@ -1,7 +1,7 @@
{
"id": "counter2",
"name": "Counter2",
"version": "0.06",
"version": "0.07",
"description": "Dual Counter",
"readme":"README.md",
"icon": "counter2-icon.png",

View File

@ -72,6 +72,13 @@
writeSettings();
},
};
appMenu['Keep unlocked'] = {
value: settings.keepunlocked,
onchange: v => {
settings.keepunlocked = v;
writeSettings();
},
};
E.showMenu(appMenu);
}

View File

@ -7,6 +7,8 @@
"icon": "icon.png",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"type": "clock",
"tags": "clock",
"storage": [
{"name":"drained.boot.js","url":"boot.js"},
{"name":"drained.app.js","url":"app.js"},

View File

@ -0,0 +1 @@
0.01: initial import

View File

@ -0,0 +1,5 @@
# Elite clock ![](app.png)
Simple binary clock for leet haxorz.
Written by: [Pavel Machek](https://github.com/pavelmachek)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIspiEPgEeAoU/4F/wGAiEAsEA4AFImAHBAolj/gRD4YFEC4UPwEfgAFC4EfF5IpHAp4dC4EQv/A/+AHYJlDnjY/AH4AJ"))

BIN
apps/eliteclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,86 @@
// Bangle.js 2 - Binary Leet ClockZ
/*
bangle.js2: create binary 'leet clock' where the time is shown as text
"leet clockz" with binary 0 being normal character and binary one
being leet translation. Be careful to only update time on minute
boundaries.
So yeah 1337 c10ckZ = certified leetspeak 😎
ChatGPT said:
Alright, heres a hardcore hacker variant of elite clock in full-on aggressive leetspeak:
£|173 ¢|_0¢|<Z
Breakdown:
E £ or 3
L | or £
I 1 or !
T 7 or +
C ¢ or (
O 0
K |< or X
Other extreme variants:
3|173 (|_0(<Z
|!73 ©|0xX
3L1+3 (L0XXZ
*/
const TEXT = "leet\nclockz";
const LEET = {
"l": "1",
"e": "3",
"t": "7",
" ": " ",
"c": "(",
"o": "0",
"k": "X",
"z": "2"
};
// Convert hour (12h) and minute to binary mask
function getBinaryFromTime(d) {
let h = d.getHours() % 12;
if (h === 0) h = 12; // 12-hour format: 0 becomes 12
const m = d.getMinutes();
const bin = ((h << 7) | m).toString(2).padStart(11, '0');
return bin;
}
// Map normal characters to leet if binary mask says so
function getDisplayText(binMask) {
return TEXT.split("").map((ch, i) =>
binMask[i] === '1' ? (LEET[ch] || ch) : ch
).join("");
}
function draw() {
g.reset().clear();
const now = new Date();
const bin = getBinaryFromTime(now);
const txt = getDisplayText(bin);
const w = 0;
g.setFont("Vector", 47).setFontAlign(0,0);
g.drawString(txt, (g.getWidth() - w) / 2, (g.getHeight() - 0) / 2);
}
function scheduleNextDraw() {
const now = new Date();
const msToNextMin = 60000 - (now.getSeconds() * 1000 + now.getMilliseconds());
setTimeout(() => {
draw();
scheduleNextDraw();
}, msToNextMin);
}
// Init
draw();
scheduleNextDraw();
//Bangle.loadWidgets();
//Bangle.drawWidgets();

View File

@ -0,0 +1,14 @@
{ "id": "eliteclock",
"name": "Elite clock",
"version": "0.01",
"description": "Simple binary clock for leet haxorz",
"icon": "app.png",
"readme": "README.md",
"supports" : ["BANGLEJS2"],
"type": "clock",
"tags": "clock",
"storage": [
{"name":"eliteclock.app.js","url":"eliteclock.app.js"},
{"name":"eliteclock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -35,3 +35,10 @@
0.31: Add support for new health format (storing more data)
Added graphs for Temperature and Battery
0.32: If getting HRM every 3/10 minutes, don't turn it on if the Bangle is charging or hasn't moved and is face down/up
0.33: Ensure readAllRecordsSince always includes the current day
Speed improvements (put temporary functions in RAM where possible)
0.34: Fix readFullDatabase (was skipping first month of data)
0.35: Update boot/lib.min.js
0.36: Fix Distance graphs that used '1*' to remove the suffix
0.37: Reduce movement limit for HRM off from 400 to 100
Fix daily summary for movement (was not scaling down correctly)

View File

@ -32,7 +32,7 @@ function menuStepCount() {
}
function menuDistance() {
const distMult = 1*require("locale").distance(myprofile.strideLength, 2); // hackish: this removes the distance suffix, e.g. 'm'
const distMult = parseFloat(require("locale").distance(myprofile.strideLength, 2)); // this removes the distance suffix, e.g. 'm'
E.showMenu({
"": { title:/*LANG*/"Distance" },
/*LANG*/"< Back": () => menuStepCount(),

View File

@ -6,7 +6,7 @@
function startMeasurement() {
// if is charging, or hardly moved and face up/down, don't start HRM
if (Bangle.isCharging() ||
(Bangle.getHealthStatus("last").movement<400 && Math.abs(Bangle.getAccel().z)>0.99)) return;
(Bangle.getHealthStatus("last").movement<100 && Math.abs(Bangle.getAccel().z)>0.99)) return;
// otherwise turn HRM on
Bangle.setHRMPower(1, "health");
setTimeout(() => {
@ -81,12 +81,6 @@ Bangle.on("health", health => {
require("Storage").write(fn, "HEALTH2\0", 0, DB_HEADER_LEN + DB_RECORDS_PER_MONTH*inf.r); // header (and allocate full new file)
}
var recordPos = DB_HEADER_LEN+(rec*inf.r);
// scale down reported movement value in order to fit it within a
// uint8 DB field
health = Object.assign({}, health);
health.movement /= 8;
require("Storage").write(fn, inf.encode(health), recordPos);
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
// we're at the end of the day. Read in all of the data for the day and sum it up

View File

@ -1,5 +1,5 @@
(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function c(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",c);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
c()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(c=>{Object.assign(a,c);c=new Date(Date.now()-59E4);if(a&&0<a.steps){var b=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;b.stepGoalNotification&&0<b.stepGoal&&d>=b.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:b.stepGoal+
" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),b.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",b))}var g=function(f){return 145*(f.getDate()-1)+6*f.getHours()+(0|6*f.getMinutes()/60)}(c);c=function(f){return"health-"+f.getFullYear()+"-"+(f.getMonth()+1)+".raw"}(c);d=require("Storage").read(c);if(void 0!==d){b=require("health").getDecoder(d);var e=d.substr(8+g*b.r,b.r);if(e!=b.clr){print("HEALTH ERR: Already written!");return}}else b=
require("health").getDecoder("HEALTH2"),require("Storage").write(c,"HEALTH2\x00",0,8+4495*b.r);var h=8+g*b.r;a=Object.assign({},a);a.movement/=8;require("Storage").write(c,b.encode(a),h);if(143==g%145)if(g=h+b.r,d.substr(g,b.r)!=b.clr)print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)e=d.substr(h,b.r),e!=b.clr&&(e=b.decode(e),a.steps+=e.steps,a.bpm+=e.bpm,a.movement+=e.movement,a.movCnt++,e.bpm&&a.bpmCnt++),h-=b.r;a.bpmCnt&&
(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(c,b.encode(a),g)}})})
{let a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){let d=function(b){function c(){Bangle.isCharging()||100>Bangle.getHealthStatus("last").movement&&.99<Math.abs(Bangle.getAccel().z)||(Bangle.setHRMPower(1,"health"),setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4*a))}c();1==a&&(setTimeout(c,2E5),setTimeout(c,4E5))};Bangle.on("health",d);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,
"health")});90>Bangle.getHealthStatus().bpmConfidence&&d()}else Bangle.setHRMPower(!!a,"health")}Bangle.on("health",a=>{(Bangle.getPressure?Bangle.getPressure():Promise.resolve({})).then(d=>{Object.assign(a,d);d=new Date(Date.now()-59E4);if(a&&0<a.steps){var b=require("Storage").readJSON("health.json",1)||{},c=Bangle.getHealthStatus("day").steps;b.stepGoalNotification&&0<b.stepGoal&&c>=b.stepGoal&&(c=(new Date(Date.now())).toISOString().split("T")[0],!b.stepGoalNotificationDate||b.stepGoalNotificationDate<
c)&&(Bangle.buzz(200,.5),require("notify").show({title:b.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),b.stepGoalNotificationDate=c,require("Storage").writeJSON("health.json",b))}var g=function(f){return 145*(f.getDate()-1)+6*f.getHours()+(0|6*f.getMinutes()/60)}(d);d=function(f){return"health-"+f.getFullYear()+"-"+(f.getMonth()+1)+".raw"}(d);c=require("Storage").read(d);if(void 0!==c){b=require("health").getDecoder(c);var e=c.substr(8+g*b.r,
b.r);if(e!=b.clr){print("HEALTH ERR: Already written!");return}}else b=require("health").getDecoder("HEALTH2"),require("Storage").write(d,"HEALTH2\x00",0,8+4495*b.r);var h=8+g*b.r;require("Storage").write(d,b.encode(a),h);if(143==g%145)if(g=h+b.r,c.substr(g,b.r)!=b.clr)print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)e=c.substr(h,b.r),e!=b.clr&&(e=b.decode(e),a.steps+=e.steps,a.bpm+=e.bpm,a.movement+=e.movement,a.movCnt++,
e.bpm&&a.bpmCnt++),h-=b.r;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(d,b.encode(a),g)}})})

View File

@ -41,48 +41,49 @@ exports.getDecoder = function(fileContents) {
return {
r : 10, // record length
clr : "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",
decode : h => { var v = {
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpmMin : h.charCodeAt(2),
bpmMax : h.charCodeAt(3),
movement : h.charCodeAt(4)*8,
battery : h.charCodeAt(5)&127,
isCharging : !!(h.charCodeAt(5)&128),
temperature : h.charCodeAt(6)/2, // signed?
altitude : ((h.charCodeAt(7)&31)<<8)|h.charCodeAt(8), // signed?
activity : exports.ACTIVITY[h.charCodeAt(7)>>5]
decode : h => { "ram"; var d = h.charCodeAt.bind(h), v = {
steps : (d(0)<<8) | d(1),
bpmMin : d(2),
bpmMax : d(3),
movement : d(4)*8,
battery : d(5)&127,
isCharging : !!(d(5)&128),
temperature : d(6)/2, // signed?
altitude : ((d(7)&31)<<8)|d(8), // signed?
activity : exports.ACTIVITY[d(7)>>5]
};
if (v.temperature>80) v.temperature-=128;
v.bpm = (v.bpmMin+v.bpmMax)/2;
if (v.altitude > 7500) v.altitude-=8192;
return v;
},
encode : health => {var alt=health.altitude&8191;return String.fromCharCode(
encode : health => { "ram"; var alt=health.altitude&8191;return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps
health.bpmMin || health.bpm, // 8 bit bpm
health.bpmMax || health.bpm, // 8 bit bpm
Math.min(health.movement, 255),
Math.min(health.movement >> 3, 255),
E.getBattery()|(Bangle.isCharging()&&128),
0|Math.round(health.temperature*2),
(alt>>8)|(Math.max(0,exports.ACTIVITY.indexOf(health.activity))<<5),alt&255,
0 // tbd
)}
);}
};
} else { // HEALTH1
return {
r : 4, // record length
clr : "\xFF\xFF\xFF\xFF",
decode : h => ({
decode : h => { "ram"; return {
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2),
bpmMin : h.charCodeAt(2),
bpmMax : h.charCodeAt(2),
movement : h.charCodeAt(3)*8
}),
encode : health => String.fromCharCode(
};},
encode : health => { "ram"; return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps
health.bpm, // 8 bit bpm
Math.min(health.movement, 255))
Math.min(health.movement >> 3, 255));
}
};
}
};
@ -111,12 +112,10 @@ exports.readAllRecords = function(d, cb) {
// Read the entire database. There is no guarantee that the months are read in order.
exports.readFullDatabase = function(cb) {
require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(val => {
console.log(val);
var parts = val.split('-');
var y = parseInt(parts[1],10);
var mo = parseInt(parts[2].replace('.raw', ''),10);
exports.readAllRecords(new Date(y, mo, 1), (r) => {
var mo = parseInt(parts[2].replace('.raw', ''),10) - 1;
exports.readAllRecords(new Date(y, mo, 1), (r) => {"ram";
r.date = new Date(y, mo, r.day, r.hr, r.min);
cb(r);
});
@ -127,9 +126,9 @@ exports.readFullDatabase = function(cb) {
// There may be some records for the day of the timestamp previous to the timestamp
exports.readAllRecordsSince = function(d, cb) {
var currentDate = new Date().getTime();
var di = d;
var di = new Date(d.toISOString().substr(0,10)); // copy date (ignore time)
while (di.getTime() <= currentDate) {
exports.readDay(di, (r) => {
exports.readDay(di, (r) => {"ram";
r.date = new Date(di.getFullYear(), di.getMonth(), di.getDate(), r.hr, r.min);
cb(r);
});
@ -149,7 +148,7 @@ exports.readDailySummaries = function(d, cb) {
if (h!=inf.clr) cb(Object.assign(inf.decode(h), {day:day+1}));
idx += DB_RECORDS_PER_DAY*inf.r;
}
}
};
// Read all records from the given day
exports.readDay = function(d, cb) {
@ -167,4 +166,4 @@ exports.readDay = function(d, cb) {
idx += inf.r;
}
}
}
};

View File

@ -1,6 +1,6 @@
function k(b){return"health-"+b.getFullYear()+"-"+(b.getMonth()+1)+".raw"}function l(b){return 145*(b.getDate()-1)+6*b.getHours()+(0|6*b.getMinutes()/60)}exports.ACTIVITY="UNKNOWN NOT_WORN WALKING EXERCISE LIGHT_SLEEP DEEP_SLEEP".split(" ");exports.getDecoder=function(b){return"HEALTH2"==b.substr(0,7)?{r:10,clr:"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",decode:a=>{a={steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(3),
movement:8*a.charCodeAt(4),battery:a.charCodeAt(5)&127,isCharging:!!(a.charCodeAt(5)&128),temperature:a.charCodeAt(6)/2,altitude:(a.charCodeAt(7)&31)<<8|a.charCodeAt(8),activity:exports.ACTIVITY[a.charCodeAt(7)>>5]};80<a.temperature&&(a.temperature-=128);a.bpm=(a.bpmMin+a.bpmMax)/2;7500<a.altitude&&(a.altitude-=8192);return a},encode:a=>{var c=a.altitude&8191;return String.fromCharCode(a.steps>>8,a.steps&255,a.bpmMin||a.bpm,a.bpmMax||a.bpm,Math.min(a.movement,255),E.getBattery()|(Bangle.isCharging()&&
128),0|Math.round(2*a.temperature),c>>8|Math.max(0,exports.ACTIVITY.indexOf(a.activity))<<5,c&255,0)}}:{r:4,clr:"\xff\xff\xff\xff",decode:a=>({steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpm:a.charCodeAt(2),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(2),movement:8*a.charCodeAt(3)}),encode:a=>String.fromCharCode(a.steps>>8,a.steps&255,a.bpm,Math.min(a.movement,255))}};exports.readAllRecords=function(b,a){b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d={},e=8,
f=0;31>f;f++){d.day=f+1;for(var g=0;24>g;g++){d.hr=g;for(var h=0;6>h;h++){d.min=10*h;var m=b.substr(e,c.r);m!=c.clr&&a(Object.assign(c.decode(m),d));e+=c.r}}e+=c.r}};exports.readFullDatabase=function(b){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(a=>{console.log(a);a=a.split("-");var c=parseInt(a[1],10),d=parseInt(a[2].replace(".raw",""),10);exports.readAllRecords(new Date(c,d,1),e=>{e.date=new Date(c,d,e.day,e.hr,e.min);b(e)})})};exports.readAllRecordsSince=function(b,a){for(var c=
(new Date).getTime();b.getTime()<=c;)exports.readDay(b,d=>{d.date=new Date(b.getFullYear(),b.getMonth(),b.getDate(),d.hr,d.min);a(d)}),b.setDate(b.getDate()+1)};exports.readDailySummaries=function(b,a){l(b);b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d=8+144*c.r,e=0;31>e;e++){var f=b.substr(d,c.r);f!=c.clr&&a(Object.assign(c.decode(f),{day:e+1}));d+=145*c.r}};exports.readDay=function(b,a){l(b);var c=k(b);c=require("Storage").read(c);if(void 0!==c){var d=exports.getDecoder(c),
e={};b=8+145*d.r*(b.getDate()-1);for(var f=0;24>f;f++){e.hr=f;for(var g=0;6>g;g++){e.min=10*g;var h=c.substr(b,d.r);h!=d.clr&&a(Object.assign(d.decode(h),e));b+=d.r}}}}
function k(b){return"health-"+b.getFullYear()+"-"+(b.getMonth()+1)+".raw"}function l(b){return 145*(b.getDate()-1)+6*b.getHours()+(0|6*b.getMinutes()/60)}exports.ACTIVITY="UNKNOWN NOT_WORN WALKING EXERCISE LIGHT_SLEEP DEEP_SLEEP".split(" ");exports.getDecoder=function(b){return"HEALTH2"==b.substr(0,7)?{r:10,clr:"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",decode:a=>{"ram";a=a.charCodeAt.bind(a);a={steps:a(0)<<8|a(1),bpmMin:a(2),bpmMax:a(3),movement:8*a(4),
battery:a(5)&127,isCharging:!!(a(5)&128),temperature:a(6)/2,altitude:(a(7)&31)<<8|a(8),activity:exports.ACTIVITY[a(7)>>5]};80<a.temperature&&(a.temperature-=128);a.bpm=(a.bpmMin+a.bpmMax)/2;7500<a.altitude&&(a.altitude-=8192);return a},encode:a=>{"ram";var c=a.altitude&8191;return String.fromCharCode(a.steps>>8,a.steps&255,a.bpmMin||a.bpm,a.bpmMax||a.bpm,Math.min(a.movement>>3,255),E.getBattery()|(Bangle.isCharging()&&128),0|Math.round(2*a.temperature),c>>8|Math.max(0,exports.ACTIVITY.indexOf(a.activity))<<
5,c&255,0)}}:{r:4,clr:"\xff\xff\xff\xff",decode:a=>{"ram";return{steps:a.charCodeAt(0)<<8|a.charCodeAt(1),bpm:a.charCodeAt(2),bpmMin:a.charCodeAt(2),bpmMax:a.charCodeAt(2),movement:8*a.charCodeAt(3)}},encode:a=>{"ram";return String.fromCharCode(a.steps>>8,a.steps&255,a.bpm,Math.min(a.movement>>3,255))}}};exports.readAllRecords=function(b,a){b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d={},e=8,f=0;31>f;f++){d.day=f+1;for(var g=0;24>g;g++){d.hr=g;for(var h=
0;6>h;h++){d.min=10*h;var m=b.substr(e,c.r);m!=c.clr&&a(Object.assign(c.decode(m),d));e+=c.r}}e+=c.r}};exports.readFullDatabase=function(b){require("Storage").list(/health-[0-9]+-[0-9]+.raw/).forEach(a=>{a=a.split("-");var c=parseInt(a[1],10),d=parseInt(a[2].replace(".raw",""),10)-1;exports.readAllRecords(new Date(c,d,1),e=>{"ram";e.date=new Date(c,d,e.day,e.hr,e.min);b(e)})})};exports.readAllRecordsSince=function(b,a){for(var c=(new Date).getTime(),d=new Date(b.toISOString().substr(0,10));d.getTime()<=
c;)exports.readDay(d,e=>{"ram";e.date=new Date(d.getFullYear(),d.getMonth(),d.getDate(),e.hr,e.min);a(e)}),d.setDate(d.getDate()+1)};exports.readDailySummaries=function(b,a){l(b);b=k(b);b=require("Storage").read(b);if(void 0!==b)for(var c=exports.getDecoder(b),d=8+144*c.r,e=0;31>e;e++){var f=b.substr(d,c.r);f!=c.clr&&a(Object.assign(c.decode(f),{day:e+1}));d+=145*c.r}};exports.readDay=function(b,a){l(b);var c=k(b);c=require("Storage").read(c);if(void 0!==c){var d=exports.getDecoder(c),e={};b=8+145*
d.r*(b.getDate()-1);for(var f=0;24>f;f++){e.hr=f;for(var g=0;6>g;g++){e.min=10*g;var h=c.substr(b,d.r);h!=d.clr&&a(Object.assign(d.decode(h),e));b+=d.r}}}}

View File

@ -2,9 +2,10 @@
"id": "health",
"name": "Health Tracking",
"shortName": "Health",
"version": "0.32",
"version": "0.37",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"tags": "tool,system,health",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",

BIN
apps/health/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -4,6 +4,7 @@
"version": "0.13",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"screenshots" : [ { "url":"screenshot.png" } ],
"tags": "health",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [

BIN
apps/hrm/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -16,5 +16,4 @@ It is also possible to load existing icon into editor, using
"load_icon("");" command. At the end of iconbits.app.js file there are
more utility functions.
Create 48x48 icon in gimp.

BIN
apps/iconbits/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -18,3 +18,4 @@
0.17: Default to passing full UTF8 strings into messages app (which can now process them with an international font)
0.18: Fix UTF8 conversion (check for `font` library, not `fonts`)
0.19: Convert numeric weather values to int from BangleDumpWeather shortcut
0.20: Add feels-like temperature data field to weather parsing from BangleDumpWeather shortcut.

View File

@ -180,6 +180,7 @@ E.on('notify',msg=>{
let weatherEvent = {
t: "weather",
temp: d.temp,
feels: d.feels,
hi: d.hi,
lo: d.lo,
hum: d.hum,
@ -192,7 +193,7 @@ E.on('notify',msg=>{
loc: d.loc
}
// Convert string fields to numbers for iOS weather shortcut
const numFields = ['code', 'wdir', 'temp', 'hi', 'lo', 'hum', 'wind', 'uv', 'rain'];
const numFields = ['code', 'wdir', 'temp','feels', 'hi', 'lo', 'hum', 'wind', 'uv', 'rain'];
numFields.forEach(field => {
if (weatherEvent[field] != null) weatherEvent[field] = +weatherEvent[field];
});

View File

@ -1,8 +1,8 @@
{
"id": "ios",
"name": "iOS Integration",
"version": "0.19",
"description": "Display notifications/music/etc from iOS devices",
"version": "0.20",
"description": "Display/pull notifications, music, weather, and agenda from iOS devices",
"icon": "app.png",
"tags": "tool,system,ios,apple,messages,notifications",
"dependencies": {"messages":"module"},

21
apps/jwalk/README.md Normal file
View File

@ -0,0 +1,21 @@
# Japanese Walking Timer
A simple timer designed to help you manage your walking intervals, whether you're in a relaxed mode or an intense workout!
![](screenshot.png)
## Usage
- The timer starts with a default total duration and interval duration, which can be adjusted in the settings.
- Tap the screen to pause or resume the timer.
- The timer will switch modes between "Relax" and "Intense" at the end of each interval.
- The display shows the current time, the remaining interval time, and the total time left.
## Creator
[Fabian Köll] ([Koell](https://github.com/Koell))
## Icon
[Icon](https://www.koreanwikiproject.com/wiki/images/2/2f/%E8%A1%8C.png)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///A4IDBvvv11zw0xlljjnnJ3USoARP0uICJ+hnOACJ8mkARO9Mn0AGDhP2FQ8FhM9L4nyyc4CI0OpJZBgVN//lkmSsARGnlMPoMH2mSpMkzPQCAsBoViAgMC/WTt2T2giGhUTiBWDm3SU5FQ7yNOgeHum7Ypu+3sB5rFMgP3tEB5MxBg2X//+yAFBOIKhBngcFn8pkmTO4ShFAAUT+cSSQOSpgKDlihCPoN/mIOBCIVvUIsBk//zWStOz////u27QRCheTzEOtVJnV+6070BgGj2a4EL5V39MAgkm2ARGvGbNwMkOgUHknwCAsC43DvAIEg8mGo0Um+yCI0nkARF0O8nQjHCIsFh1gCJ08WwM6rARLgftNAMzCIsDI4te4gDBuYRM/pxCCJoADCI6PHdINDCI0kYo8BqYRHYowRByZ9GCJEDCLXACLVQAoUL+mXCJBrBiARD7clCJNzBIl8pIRIgEuwBGExMmUI4qH9MnYo4AH3MxCB0Ai/oCJ4AY"))

178
apps/jwalk/app.js Normal file
View File

@ -0,0 +1,178 @@
// === Utility Functions ===
function formatTime(seconds) {
let mins = Math.floor(seconds / 60);
let secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
function getTimeStr() {
let d = new Date();
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
function updateCachedLeftTime() {
cachedLeftTime = "Left: " + formatTime(state.remainingTotal);
}
// === Constants ===
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
// === Settings and State ===
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
let state = {
remainingTotal: settings.totalDuration * 60,
intervalDuration: settings.intervalDuration * 60,
remainingInterval: 0,
intervalEnd: 0,
paused: false,
currentMode: settings.startMode === 1 ? "Intense" : "Relax",
finished: false,
forceDraw: false,
};
let cachedLeftTime = "";
let lastMinuteStr = getTimeStr();
let drawTimerInterval;
// === UI Rendering ===
function drawUI() {
let y = Bangle.appRect.y + 8;
g.reset().setBgColor(g.theme.bg).clearRect(Bangle.appRect);
g.setColor(g.theme.fg);
let displayInterval = state.paused
? state.remainingInterval
: Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
g.setFont("Vector", 40);
g.setFontAlign(0, 0);
g.drawString(formatTime(displayInterval), g.getWidth() / 2, y + 70);
let cy = y + 100;
if (state.paused) {
g.setFont("Vector", 15);
g.drawString("PAUSED", g.getWidth() / 2, cy);
} else {
let cx = g.getWidth() / 2;
g.setColor(g.theme.accent || g.theme.fg2 || g.theme.fg);
if (state.currentMode === "Relax") {
g.fillCircle(cx, cy, 5);
} else {
g.fillPoly([
cx, cy - 6,
cx - 6, cy + 6,
cx + 6, cy + 6
]);
}
g.setColor(g.theme.fg);
}
g.setFont("6x8", 2);
g.setFontAlign(0, -1);
g.drawString(state.currentMode, g.getWidth() / 2, y + 15);
g.drawString(cachedLeftTime, g.getWidth() / 2, cy + 15);
if (settings.showClock) {
g.setFontAlign(1, 0);
g.drawString(lastMinuteStr, g.getWidth() - 4, y);
}
g.flip();
}
// === Workout Logic ===
function toggleMode() {
state.currentMode = state.currentMode === "Relax" ? "Intense" : "Relax";
Bangle.buzz(settings.modeBuzzerDuration);
state.forceDraw = true;
}
function startNextInterval() {
if (state.remainingTotal <= 0) {
finishWorkout();
return;
}
state.remainingInterval = Math.min(state.intervalDuration, state.remainingTotal);
state.remainingTotal -= state.remainingInterval;
updateCachedLeftTime();
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.forceDraw = true;
}
function togglePause() {
if (state.finished) return;
if (!state.paused) {
state.remainingInterval = Math.max(0, Math.floor((state.intervalEnd - Date.now()) / 1000));
state.paused = true;
} else {
state.intervalEnd = Date.now() + state.remainingInterval * 1000;
state.paused = false;
}
drawUI();
}
function finishWorkout() {
clearInterval(drawTimerInterval);
Bangle.buzz(settings.finishBuzzerDuration);
state.finished = true;
setTimeout(() => {
g.clear();
g.setFont("Vector", 30);
g.setFontAlign(0, 0);
g.drawString("Well done!", g.getWidth() / 2, g.getHeight() / 2);
g.flip();
const exitHandler = () => {
Bangle.removeListener("touch", exitHandler);
Bangle.removeListener("btn1", exitHandler);
load(); // Exit app
};
Bangle.on("touch", exitHandler);
setWatch(exitHandler, BTN1, { repeat: false });
}, 500);
}
// === Timer Tick ===
function tick() {
if (state.finished) return;
const currentMinuteStr = getTimeStr();
if (currentMinuteStr !== lastMinuteStr) {
lastMinuteStr = currentMinuteStr;
state.forceDraw = true;
}
if (!state.paused && (state.intervalEnd - Date.now()) / 1000 <= 0) {
toggleMode();
startNextInterval();
return;
}
if (state.forceDraw || settings.updateWhileLocked || !Bangle.isLocked()) {
drawUI();
state.forceDraw = false;
}
}
// === Initialization ===
Bangle.on("touch", togglePause);
Bangle.loadWidgets();
Bangle.drawWidgets();
updateCachedLeftTime();
startNextInterval();
drawUI();
drawTimerInterval = setInterval(tick, 1000);

BIN
apps/jwalk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

19
apps/jwalk/metadata.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "jwalk",
"name": "Japanese Walking",
"shortName": "J-Walk",
"icon": "app.png",
"version": "0.01",
"description": "Alternating walk timer: 3 min Relax / 3 min Intense for a set time. Tap to pause/resume. Start mode, interval and total time configurable via Settings.",
"tags": "walk,timer,fitness",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"data": [
{ "name": "jwalk.json" }
],
"storage": [
{ "name": "jwalk.app.js", "url": "app.js" },
{ "name": "jwalk.settings.js", "url": "settings.js" },
{ "name": "jwalk.img", "url": "app-icon.js", "evaluate": true }
]
}

BIN
apps/jwalk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

65
apps/jwalk/settings.js Normal file
View File

@ -0,0 +1,65 @@
(function (back) {
const FILE = "jwalk.json";
const DEFAULTS = {
totalDuration: 30,
intervalDuration: 3,
startMode: 0,
modeBuzzerDuration: 1000,
finishBuzzerDuration: 1500,
showClock: 1,
updateWhileLocked: 0
};
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
function saveSettings() {
require("Storage").writeJSON(FILE, settings);
}
function showSettingsMenu() {
E.showMenu({
'': { title: 'Japanese Walking' },
'< Back': back,
'Total Time (min)': {
value: settings.totalDuration,
min: 10, max: 60, step: 1,
onchange: v => { settings.totalDuration = v; saveSettings(); }
},
'Interval (min)': {
value: settings.intervalDuration,
min: 1, max: 10, step: 1,
onchange: v => { settings.intervalDuration = v; saveSettings(); }
},
'Start Mode': {
value: settings.startMode,
min: 0, max: 1,
format: v => v ? "Intense" : "Relax",
onchange: v => { settings.startMode = v; saveSettings(); }
},
'Display Clock': {
value: settings.showClock,
min: 0, max: 1,
format: v => v ? "Show" : "Hide" ,
onchange: v => { settings.showClock = v; saveSettings(); }
},
'Update UI While Locked': {
value: settings.updateWhileLocked,
min: 0, max: 1,
format: v => v ? "Always" : "On Change",
onchange: v => { settings.updateWhileLocked = v; saveSettings(); }
},
'Mode Buzz (ms)': {
value: settings.modeBuzzerDuration,
min: 0, max: 2000, step: 50,
onchange: v => { settings.modeBuzzerDuration = v; saveSettings(); }
},
'Finish Buzz (ms)': {
value: settings.finishBuzzerDuration,
min: 0, max: 5000, step: 100,
onchange: v => { settings.finishBuzzerDuration = v; saveSettings(); }
},
});
}
showSettingsMenu();
})

View File

@ -117,3 +117,4 @@
Remove workaround for 2v10 (>3 years ago) - assume everyone is on never firmware now
0.86: Default to showing message scroller (with title, bigger icon)
0.87: Make choosing of font size more repeatable
0.88: Adjust padding calculation so messages are spaced out properly even when using international fonts

View File

@ -14,7 +14,7 @@
// a message
require("messages").pushMessage({"t":"add","id":1575479849,"src":"WhatsApp","title":"My Friend","body":"Hey! How's everything going?",reply:1,negative:1})
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going? This is a really really long message that is really so super long you'll have to scroll it lots and lots",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":1575479850,"src":"Skype","title":"My Friend","body":"Hey! How's everything going? This is a really really long message that is really so super long you'll have to scroll it lots and lots",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23232,"src":"Skype","title":"Mr. Bobby McBobFace","body":"Boopedy-boop",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23233,"src":"Skype","title":"Thyttan test","body":"Nummerplåtsbelysning trodo",positive:1,negative:1})
require("messages").pushMessage({"t":"add","id":23234,"src":"Skype","title":"Thyttan test 2","body":"Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo Nummerplåtsbelysning trodo",positive:1,negative:1})
@ -391,8 +391,6 @@ function showMessage(msgid, persist) {
}
}
lines = g.setFont(bodyFont).wrapString(body, w);
if (lines.length<3)
lines.unshift(""); // if less lines, pad them out a bit at the top!
}
let negHandler,posHandler,rowLeftDraw,rowRightDraw;
if (msg.negative) {
@ -432,12 +430,14 @@ function showMessage(msgid, persist) {
}
let fontHeight = g.setFont(bodyFont).getFontHeight();
let lineHeight = (fontHeight>25)?fontHeight:25;
if (title.includes("\n")) lineHeight=25; // ensure enough room for 2 lines of title in header
if (title.includes("\n") && lineHeight<25) lineHeight=25; // ensure enough room for 2 lines of title in header
let linesPerRow = 2;
if (fontHeight<17) {
lineHeight = 16;
linesPerRow = 3;
}
if ((lines.length+4.5)*lineHeight < Bangle.appRect.h)
lines.unshift(""); // if less lines, pad them out a bit at the top!
let rowHeight = lineHeight*linesPerRow;
let textLineOffset = -(linesPerRow + ((rowLeftDraw||rowRightDraw)?1:0));
let msgIcon = require("messageicons").getImage(msg);

View File

@ -2,7 +2,7 @@
"id": "messagegui",
"name": "Message UI",
"shortName": "Messages",
"version": "0.87",
"version": "0.88",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -1 +1,4 @@
0.01: App Created w/ clockInfos, bold font, date and time.
0.01: App created w/ clockInfos, bold font, date and time.
0.02: Added text truncation for long ClockInfo strings.
0.03: Added small inline Clock Info next to date
0.04: Fix date not clearing

View File

@ -2,18 +2,20 @@
A beautifully simple, modern clock with two Clock Infos, and a clean UI. Fast-Loads.
A beautifully simple, modern clock with three Clock Infos, and a clean UI. Fast-Loads.
![](Screenshot1.png)
![](Scr1.png)
## Features
* Has 2 Clock Infos, that are individually changeable.
* Has 3 Clock Infos, that are individually changeable.
* Has an inline Clock Info, next to the date, for quick, glanceable data.
* Low battery consumption.
* Uses locale for time and date.
* Bold time font, for quicker readability.
* Uses rounded rectangles and a bold font for a simple, clean, modern look.
* Has Fast Loading, for quicker access to launcher.

BIN
apps/modclock/Scr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
apps/modclock/Scr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -9,6 +9,20 @@ Graphics.prototype.setFontBold = function(scale) {
{ // must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let drawTimeout;
let showInlineClkInfo=true;
let calcStrLength=function(str,maxLength){
//too long
// Example maximum length
var truncatedText = str;
if (str.length > maxLength) {
truncatedText = str.substring(0, maxLength - 3) + "...";
}
return truncatedText;
};
//ROUNDED RECT FUNCTION
let bRoundedRectangle= function(x1,y1,x2,y2,r) {
@ -48,7 +62,6 @@ let bRoundedRectangle= function(x1,y1,x2,y2,r) {
};
//CLOCK INFO
let clockInfoItems = require("clock_info").load();
@ -58,7 +71,7 @@ let clockInfoItems = require("clock_info").load();
let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : 10, y: 100, w: 72, h:70,
x : 7, y: 100, w: 76, h:70,
// You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info
draw : (itm, info, options) => {
@ -70,7 +83,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
// indicate focus - we're using a border, but you could change color?
if (options.focus){
// show if focused
g.setColor(0,15,255);
g.setColor(0,255,15);
bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8);
}else{
g.setColor(g.theme.fg);
@ -82,7 +95,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
if (info.img){
g.drawImage(info.img, midx-12,midy-21);
}// draw the image
g.setFont("Vector",16).setFontAlign(0,1).drawString(info.text, midx,midy+23); // draw the text
g.setFont("Vector",16).setFontAlign(0,1).drawString(calcStrLength(info.text,8), midx,midy+23); // draw the text
}
});
@ -91,7 +104,7 @@ let clockInfoMenuLeft = require("clock_info").addInteractive(clockInfoItems, {
//CLOCK INFO RIGHT DIMENSIONS: 97,113, w:66, h: 55
let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : 94, y: 100, w: 72, h:70,
x : 91, y: 100, w: 76, h:70,
// You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info
draw : (itm, info, options) => {
@ -103,7 +116,7 @@ let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
// indicate focus - we're using a border, but you could change color?
if (options.focus){
// show if focused
g.setColor(0,15,255);
g.setColor(0,255,15);
bRoundedRectangle(options.x,options.y,options.x+options.w,options.y+options.h,8);
}else{
g.setColor(g.theme.fg);
@ -115,13 +128,40 @@ let clockInfoMenuRight = require("clock_info").addInteractive(clockInfoItems, {
if (info.img){
g.drawImage(info.img, midx-12,midy-21);
}// draw the image
g.setFont("Vector",16).setFontAlign(0,1).drawString(info.text, midx,midy+23); // draw the text
g.setFont("Vector",16).setFontAlign(0,1).drawString(calcStrLength(info.text,8), midx,midy+23); // draw the text
}
});
let clockInfoMenuInline;
if(showInlineClkInfo){
clockInfoMenuInline = require("clock_info").addInteractive(clockInfoItems, {
// Add the dimensions we're rendering to here - these are used to detect taps on the clock info area
x : g.getWidth()/2+6, y: 69, w: 95, h:25,
// You can add other information here you want to be passed into 'options' in 'draw'
// This function draws the info
draw : (itm, info, options) => {
// itm: the item containing name/hasRange/etc
// info: data returned from itm.get() containing text/img/etc
// options: options passed into addInteractive
// Clear the background
g.reset().clearRect(options.x, options.y, options.x+options.w, options.y+options.h);
// indicate focus - we're using a border, but you could change color?
if (options.focus){
// show if focused
g.setColor(0,255,15);
}
// we're drawing center-aligned here
//var midx = options.x+options.w/2;
var midy=options.y+options.h/2;
if (info.img){
g.drawImage(info.img, options.x+4,midy-7.2,{scale: 0.63});
}// draw the image
g.setFont("Vector",15).setFontAlign(-1,0).drawString(calcStrLength(info.text,6), options.x+22,midy+1); // draw the text
}
});
}
@ -149,13 +189,13 @@ let draw = function() {
var meridianStrWidth=g.stringWidth(meridian);
var totalStrWidth=meridianStrWidth+padding+clkStrWidth;
var startX = ((g.getWidth() - totalStrWidth) / 2)+6;
g.clearRect(0,0,g.getWidth(),90);
g.clearRect(0,0,g.getWidth(),64);
g.setFontBold();
g.setFontAlign(-1,1);
g.drawString(clock, startX, Y+2,true);
g.drawString(clock, startX, Y-2);
g.setFont("Vector",20);
g.setFontAlign(-1,1);
g.drawString(meridian, startX + clkStrWidth + padding, Y-5, true);
g.drawString(meridian, startX + clkStrWidth + padding, Y-9, true);
}else{
@ -165,15 +205,17 @@ let draw = function() {
}
// draw the date, in a normal font
g.setFont("Vector",18);
g.setFontAlign(0,0); // align center bottom
g.setFont("Vector",16);
g.setFontAlign(1,0); // align center bottom
// pad the date - this clears the background if the date were to change length
var dateStr = require("locale").dow(new Date(), 1)+", "+ require("locale").month(new Date(), true)+" "+new Date().getDate();
g.drawString(" "+dateStr+" ", g.getWidth()/2, Y+9, true /*clear background*/);
var dateStr = " "+require("locale").dow(new Date(), 1)+", " +new Date().getDate();
//print(g.stringHeight(dateStr));
g.drawString(" "+dateStr+" ", g.getWidth()/2+6, Y+9, true /*clear background*/);
Bangle.drawWidgets();
g.setColor("#ffffff");
// queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
@ -196,6 +238,7 @@ Bangle.setUI({
delete Graphics.prototype.setFontBold;
clockInfoMenuRight.remove();
clockInfoMenuLeft.remove();
if(showInlineClkInfo) clockInfoMenuInline.remove();
}});
@ -203,4 +246,6 @@ g.clear();
// Load widgets
Bangle.loadWidgets();
draw();
}

View File

@ -3,13 +3,13 @@
"name": "Modern Clock",
"shortName":"Modern Clk",
"icon": "icon.png",
"version":"0.01",
"description": "A modern, simple clock, with two Clock Infos and Fast Loading",
"version":"0.04",
"description": "A modern, simple clock, with three ClockInfos and Fast Loading",
"type":"clock",
"tags": "clock,clkinfo",
"supports": ["BANGLEJS2"],
"screenshots" : [ { "url":"Screenshot1.png" },
{ "url":"Screenshot2.png" } ],
"screenshots" : [ { "url":"Scr1.png" },
{ "url":"Scr2.png" } ],
"dependencies" : { "clock_info":"module"},
"allow_emulator":true,
"readme":"README.md",

View File

@ -13,3 +13,4 @@
0.10: Use charging state on boot for auto calibration
Log additional timestamp for trace log
0.11: Minor code improvements
0.12: Round monotonic percentage, rename to stable percentage/voltage

View File

@ -29,6 +29,8 @@
}
}
setInterval(save, saveEvery);
E.on("kill", ()=>{
@ -75,7 +77,7 @@
})(Bangle[functionName]);
}
let functions = {};
let wrapDeferred = ((o,t) => (a) => {
if (a == eval || typeof a == "string") {
return o.apply(this, arguments);
@ -131,19 +133,25 @@
handleCharging(Bangle.isCharging());
}
var savedBatPercent=E.getBattery();
if (settings.forceMonoPercentage){
var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4;
var op = E.getBattery;
var newPercent =Math.round((E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/6);
E.getBattery = function() {
var current = Math.round((op()+op()+op()+op())/4);
if (Bangle.isCharging() && current > p) p = current;
if (!Bangle.isCharging() && current < p) p = current;
return p;
if(Bangle.isCharging()){
if(newPercent > savedBatPercent)
savedBatPercent = newPercent;
}else{
if(newPercent < savedBatPercent)
savedBatPercent = newPercent;
}
return savedBatPercent;
};
}
if (settings.forceMonoVoltage){
var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4;
var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/6;
var ov = NRF.getBattery;
NRF.getBattery = function() {
var current = (ov()+ov()+ov()+ov())/4;

View File

@ -2,8 +2,8 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
"version": "0.11",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"version": "0.12",
"description": "Allow configuration of warnings for battery charging, stabilization of voltage, stabilization of battery percentage, and battery logging.",
"icon": "app.png",
"type": "bootloader",
"tags": "tool",

View File

@ -27,13 +27,13 @@
'Widget': function() {
E.showMenu(submenu_widget);
},
'Monotonic percentage': {
'Stable Percentage': {
value: !!settings.forceMonoPercentage,
onchange: v => {
writeSettings("forceMonoPercentage", v);
}
},
'Monotonic voltage': {
'Stable Voltage': {
value: !!settings.forceMonoVoltage,
onchange: v => {
writeSettings("forceMonoVoltage", v);

View File

@ -3,3 +3,4 @@
0.03: Add settings to configure prompt
0.04: Minor code improvements
0.05: Comment out unused function in settings.js
0.06: Changed to use E.showMessage() instead of g.drawString() to display power off message. Looks better and is more coherent.

View File

@ -19,20 +19,15 @@ if (showPrompt) {
setTimeout(load, 100);
return;
}
g.setFont("6x8",2).setFontAlign(0,0);
var x = g.getWidth()/2;
var y = g.getHeight()/2 + 10;
g.drawString("Powering off...", x, y);
E.showMessage("Powering off...");
setTimeout(function() {
if (Bangle.softOff) Bangle.softOff(); else Bangle.off();
}, 1000);
});
} else {
g.setFont("6x8",2).setFontAlign(0,0);
var x = g.getWidth()/2;
var y = g.getHeight()/2 + 10;
g.drawString("Powering off...", x, y);
E.showMessage("Powering off...");
setTimeout(function() {
if (Bangle.softOff) Bangle.softOff(); else Bangle.off();

View File

@ -1,7 +1,7 @@
{ "id": "poweroff",
"name": "Poweroff",
"shortName":"Poweroff",
"version": "0.05",
"version": "0.06",
"description": "Simple app to power off your Bangle.js",
"icon": "app.png",
"tags": "tool,poweroff,shutdown",

View File

@ -16,3 +16,4 @@
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`
0.15: Workaround bug in 2v26/2v27 firmware

View File

@ -2,7 +2,11 @@ var _a, _b;
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) {
E.showMenu = (function (items) {
if (items == null) {
g.clearRect(Bangle.appRect);
return Bangle.setUI();
}
var RectRnd = function (x1, y1, x2, y2, r) {
var pp = [];
pp.push.apply(pp, g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r]));
@ -122,7 +126,6 @@ E.showMenu = function (items) {
nameScroll_1 = 0;
}, 300, name, v, item, idx, x, iy);
}
g.setColor(g.theme.fg);
iy += fontHeight;
idx++;
};
@ -204,7 +207,10 @@ E.showMenu = function (items) {
else
l.select(evt);
};
Bangle.setUI({
var touchcb = (function (_button, xy) {
cb(void 0, xy);
});
var uiopts = {
mode: "updown",
back: back,
remove: function () {
@ -212,11 +218,20 @@ E.showMenu = function (items) {
if (nameScroller)
clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe);
if (setUITouch)
Bangle.removeListener("touch", touchcb);
(_a = options.remove) === null || _a === void 0 ? void 0 : _a.call(options);
},
touch: (function (_button, xy) {
cb(void 0, xy);
}),
}, cb);
};
var setUITouch = process.env.VERSION >= "2v26";
if (!setUITouch) {
uiopts.touch = touchcb;
}
Bangle.setUI(uiopts, cb);
if (setUITouch) {
Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler;
Bangle.on("touch", touchcb);
}
return l;
};
});

View File

@ -13,7 +13,12 @@ const prosettings = (require("Storage").readJSON("promenu.settings.json", true)
prosettings.naturalScroll ??= false;
prosettings.wrapAround ??= true;
E.showMenu = (items?: Menu): MenuInstance => {
E.showMenu = ((items?: Menu): MenuInstance | void => {
if(items == null){
g.clearRect(Bangle.appRect);
return Bangle.setUI();
}
const RectRnd = (x1: number, y1: number, x2: number, y2: number, r: number) => {
const pp = [];
pp.push(...g.quadraticBezier([x2 - r, y1, x2, y1, x2, y1 + r]));
@ -167,7 +172,6 @@ E.showMenu = (items?: Menu): MenuInstance => {
}, 300, name, v, item, idx, x, iy);
}
g.setColor(g.theme.fg);
iy += fontHeight;
idx++;
}
@ -252,21 +256,47 @@ E.showMenu = (items?: Menu): MenuInstance => {
else l.select(evt);
};
Bangle.setUI({
mode: "updown",
back,
remove: () => {
if (nameScroller) clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe);
options.remove?.();
},
touch: ((_button, xy) => {
const touchcb = ((_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);
}) satisfies TouchCallback;
const uiopts = {
mode: "updown",
back: back as () => void,
remove: () => {
if (nameScroller) clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe);
if(setUITouch)
Bangle.removeListener("touch", touchcb);
options.remove?.();
},
} satisfies SetUIArg<"updown">;
// does setUI install its own touch handler?
const setUITouch = process.env.VERSION >= "2v26";
if (!setUITouch) {
// old firmware, we can use its touch handler - no need for workaround
(uiopts as any).touch = touchcb;
}
Bangle.setUI(uiopts, cb);
if(setUITouch){
// new firmware, remove setUI's touch handler and use just our own to
// avoid `cb` drawing the menu (as part of setUI's touch handler)
// followed by us drawing the menu (as part of our touch handler)
//
// work around details:
// - https://github.com/espruino/Espruino/issues/2648
// - https://github.com/orgs/espruino/discussions/7697#discussioncomment-13782299
Bangle.removeListener("touch", (Bangle as any).touchHandler);
delete (Bangle as any).touchHandler;
Bangle.on("touch", touchcb);
}
return l;
};
}) as typeof E.showMenu;

View File

@ -1,7 +1,7 @@
{
"id": "promenu",
"name": "Pro Menu",
"version": "0.14",
"version": "0.15",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png",
"type": "bootloader",

View File

@ -88,3 +88,6 @@ of 'Select Clock'
0.77: Save altitude calibration when user exits via reset
0.78: Fix menu scroll restore on BangleJS1
0.79: Ensure that tapping on pressure/altitude doesn't cause a menu to display temporarily
0.80: Add option to set LCD brightness to 0, keep default brightness at 1.
0.81: Ensure 'Privacy' (address randomnisation) is only added for Bangle.js 2,
and add warnings in documentation that it can cause connection issues

View File

@ -45,6 +45,8 @@ This is where you adjust settings for an individual app. (eg. Health app: Adjust
* **Passkey** allows you to set a passkey that is required to connect and pair to Bangle.js.
* **Whitelist** allows you to specify only specific devices that you will let connect to your Bangle.js. Simply choose the menu item, then `Add Device`, and then connect to Bangle.js with the device you want to add. If you are already connected you will have to disconnect first. Changes will take effect when you exit the `Settings` app.
* **NOTE:** iOS devices and newer Android devices often implement Address Randomisation and change their Bluetooth address every so often. If you device's address changes, you will be unable to connect until you update the whitelist again.
* **Privacy** - (Bangle.js 2 only) enables BLE privacy mode (see [NRF.setSecurity](https://www.espruino.com/Reference#l_NRF_setSecurity)). This randomises the Bangle's MAC address and can also
remove advertising of its name. **This can cause connection issues with apps that expect to keep a permanent connection like iOS/Gadgetbridge**
## LCD

View File

@ -1,7 +1,7 @@
{
"id": "setting",
"name": "Settings",
"version": "0.79",
"version": "0.81",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",

View File

@ -1,3 +1,4 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
@ -191,7 +192,7 @@ function BLEMenu() {
var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"];
var privacy = [/*LANG*/"Off", /*LANG*/"Show name", /*LANG*/"Hide name"];
return {
var menu = {
'': { 'title': /*LANG*/'Bluetooth' },
'< Back': ()=>popMenu(mainMenu()),
/*LANG*/'Make Connectable': ()=>makeConnectable(),
@ -209,7 +210,32 @@ function BLEMenu() {
updateSettings();
}
},
/*LANG*/'Privacy': {
/*LANG*/'HID': {
value: Math.max(0,0 | hidV.indexOf(settings.HID)),
min: 0, max: hidN.length-1,
format: v => hidN[v],
onchange: v => {
settings.HID = hidV[v];
updateSettings();
}
},
/*LANG*/'Passkey': {
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(() => pushMenu(passkeyMenu())) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value:
(
(settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"Off" : /*LANG*/"On"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
: ""
),
onchange: () => setTimeout(() => pushMenu(whitelistMenu())) // graphical_menu redraws after the call
}
};
if (BANGLEJS2) menu[/*LANG*/'Privacy'] = {
min: 0, max: privacy.length-1,
format: v => privacy[v],
value: (() => {
@ -234,32 +260,8 @@ function BLEMenu() {
}
updateSettings();
}
},
/*LANG*/'HID': {
value: Math.max(0,0 | hidV.indexOf(settings.HID)),
min: 0, max: hidN.length-1,
format: v => hidN[v],
onchange: v => {
settings.HID = hidV[v];
updateSettings();
}
},
/*LANG*/'Passkey': {
value: settings.passkey?settings.passkey:/*LANG*/"none",
onchange: () => setTimeout(() => pushMenu(passkeyMenu())) // graphical_menu redraws after the call
},
/*LANG*/'Whitelist': {
value:
(
(settings.whitelist_disabled || !settings.whitelist) ? /*LANG*/"off" : /*LANG*/"on"
) + (
settings.whitelist
? " (" + settings.whitelist.length + ")"
: ""
),
onchange: () => setTimeout(() => pushMenu(whitelistMenu())) // graphical_menu redraws after the call
}
};
return menu;
}
function showThemeMenu(pop) {
@ -474,11 +476,11 @@ function LCDMenu() {
Object.assign(lcdMenu, {
/*LANG*/'LCD Brightness': {
value: settings.brightness,
min: 0.1,
min : BANGLEJS2 ? 0 : 0.1,
max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v || 1;
settings.brightness = v ?? 1;
updateSettings();
Bangle.setLCDBrightness(settings.brightness);
}

View File

@ -15,3 +15,4 @@
0.17: Minor code improvements
0.18: Add back as a function to prevent translation making it a menu entry
0.19: Write sleep state into health event's .activity field
0.20: Increase default sleep thresholds, tweak settings to allow higher ones to be chosen

View File

@ -11,8 +11,8 @@ global.sleeplog = {
// threshold settings
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
deepTh: 100, // threshold for deep sleep
lightTh: 200, // threshold for light sleep
deepTh: 150, // threshold for deep sleep
lightTh: 300, // threshold for light sleep
wearTemp: 19.5, // temperature threshold to count as worn
}, require("Storage").readJSON("sleeplog.json", true) || {})
};

View File

@ -2,7 +2,7 @@
"id":"sleeplog",
"name":"Sleep Log",
"shortName": "SleepLog",
"version": "0.19",
"version": "0.20",
"description": "Log and view your sleeping habits. This app uses built in movement calculations. View data from Bangle, or from the web app.",
"icon": "app.png",
"type": "app",

View File

@ -11,8 +11,8 @@
// threshold settings
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
deepTh: 100, // threshold for deep sleep
lightTh: 200, // threshold for light sleep
deepTh: 150, // threshold for deep sleep
lightTh: 300, // threshold for light sleep
wearTemp: 19.5, // temperature threshold to count as worn
// app settings
breakToD: 12, // [h] time of day when to start/end graphs
@ -324,9 +324,9 @@
},
/*LANG*/"Deep Sleep": {
value: settings.deepTh,
step: 1,
step: 10,
min: 30,
max: 200,
max: 500,
wrap: true,
noList: true,
onchange: v => {
@ -338,7 +338,7 @@
value: settings.lightTh,
step: 10,
min: 100,
max: 400,
max: 800,
wrap: true,
noList: true,
onchange: v => {

View File

@ -0,0 +1,3 @@
0.01: New App!
0.02: Added pie chart for visualization, tweaked UI.
0.03: Fixed bug with total storage pie chart.

View File

@ -0,0 +1,154 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/customize.js"></script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<!-- Toggle Buttons -->
<div class="btn-group" style="margin: 1em; display: flex; justify-content: center;">
<button id="tableButton" class="btn btn-primary">View Table</button>
<button id="pieChartButton" class="btn">View Pie Chart</button>
</div>
<!-- Table View -->
<div id="storageTable"></div>
<!-- Chart View -->
<div id="storagePieChart" style="display: none; flex-direction: column; align-items: center;">
<div id="piechart" style="width: 100%; max-width: 600px; height: 400px;"></div>
<div id="totalStoragePie" style="width: 100%; max-width: 600px; height: 300px;"></div>
</div>
<script>
let globalApps = [];
let storageStats = null;
function onInit(device) {
Util.showModal("Reading Storage...");
Puck.eval(`(()=>{
let getApps = () => require("Storage").list(/\\.info$/).map(appInfoName => {
let appInfo = require("Storage").readJSON(appInfoName,1)||{};
var fileSize = 0, dataSize = 0;
appInfo.files.split(",").forEach(f => fileSize += require("Storage").read(f).length);
var data = (appInfo.data||"").split(";");
function wildcardToRegexp(wc) {
return new RegExp("^"+wc.replaceAll(".","\\\\.").replaceAll("?",".*")+"$");
}
if (data[0]) data[0].split(",").forEach(wc => {
require("Storage").list(wildcardToRegexp(wc), {sf:false}).forEach(f => {
dataSize += require("Storage").read(f).length
});
});
if (data[1]) data[1].split(",").forEach(wc => {
require("Storage").list(wildcardToRegexp(wc), {sf:true}).forEach(f => {
dataSize += require("Storage").open(f,"r").getLength();
});
});
return [appInfo.id, fileSize, dataSize];
});
return [getApps(), require(\"Storage\").getStats()]; })()`, function(result) {
Util.hideModal();
globalApps = result[0].sort((a,b) => (b[1]+b[2]) - (a[1]+a[2]));
storageStats = result[1];
if (globalApps.length === 0) {
document.getElementById("storageTable").innerHTML = "<p>No apps found</p>";
return;
}
drawTable();
});
}
function roundDecimal(num){
return Math.round(num * 10) / 10;
}
function drawTable() {
document.getElementById("storageTable").innerHTML = `
<table class="table table-striped">
<thead>
<tr>
<th>App</th>
<th>Code (kb)</th>
<th>Data (kb)</th>
<th>Total (kb)</th>
</tr>
</thead>
<tbody>
${globalApps.map(app => `
<tr>
<td>${app[0]}</td>
<td>${(app[1]/1000).toFixed(1)}</td>
<td>${(app[2]/1000).toFixed(1)}</td>
<td>${((app[1]+app[2])/1000).toFixed(1)}</td>
</tr>`).join("")}
</tbody>
</table>`;
}
function drawChart() {
if (globalApps.length === 0) return;
// App-specific chart
const chartData = [
['App', 'Total Size (KB)']
].concat(globalApps.map(app => [app[0], roundDecimal((app[1] + app[2])/1000)]));
const data = google.visualization.arrayToDataTable(chartData);
const options = {
title: 'App Storage Breakdown (KBs)',
chartArea: { width: '90%', height: '80%' },
legend: { position: 'bottom' }
};
const chart = new google.visualization.PieChart(document.getElementById('piechart'));
chart.draw(data, options);
// Total storage chart
if (storageStats) {
const usedKB = roundDecimal(storageStats.fileBytes / 1000);
const freeKB = roundDecimal(storageStats.freeBytes / 1000);
const trashKB = roundDecimal(storageStats.trashBytes / 1000);
const totalData = google.visualization.arrayToDataTable([
['Type', 'KB'],
['Used', usedKB],
['Free', freeKB],
['Trash', trashKB],
]);
const totalOptions = {
title: 'Total Storage Usage (KBs)',
chartArea: { width: '90%', height: '80%' },
legend: { position: 'bottom' }
};
const totalChart = new google.visualization.PieChart(document.getElementById('totalStoragePie'));
totalChart.draw(totalData, totalOptions);
}
}
google.charts.load('current', {'packages':['corechart']});
document.getElementById("pieChartButton").addEventListener("click", function () {
document.getElementById("storageTable").style.display = "none";
document.getElementById("storagePieChart").style.display = "flex";
drawChart();
this.classList.add("btn-primary");
document.getElementById("tableButton").classList.remove("btn-primary");
});
document.getElementById("tableButton").addEventListener("click", function () {
document.getElementById("storageTable").style.display = "block";
document.getElementById("storagePieChart").style.display = "none";
drawTable();
this.classList.add("btn-primary");
document.getElementById("pieChartButton").classList.remove("btn-primary");
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,13 @@
{
"id": "storageanalyzer",
"name": "Storage Analyzer",
"version": "0.03",
"description": "Analyzes Bangle.js storage and shows which apps are using storage space",
"icon": "icon.png",
"type": "RAM",
"tags": "tool,storage,flash,memory",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
}

View File

@ -6,3 +6,4 @@
0.05: Make the "App source not found" warning less buggy
0.06: Fixed a crash if an app has no tags (app.tags is undefined)
0.07: Clear cached app list when updating showClocks setting
0.08: Add haptic feedback option when selecting app or category in menu, increase vector size limit in settings

View File

@ -11,6 +11,7 @@ Settings
- `Font` - The font used (`4x6`, `6x8`, `12x20`, `6x15` or `Vector`). Default `12x20`.
- `Vector Font Size` - The size of the font if `Font` is set to `Vector`. Default `10`.
- `Haptic Feedback` - Whether or not to vibrate slightly when selecting an app or category in the launcher. Default `No`.
- `Show Clocks` - If set to `No` then clocks won't appear in the app list. Default `Yes`.
- `Fullscreen` - If set to `Yes` then widgets won't be loaded. Default `No`.
@ -28,3 +29,4 @@ Contributors
- [atjn](https://github.com/atjn)
- [BlueFox4](https://github.com/BlueFox4)
- [RKBoss6](https://github.com/RKBoss6)

View File

@ -17,7 +17,8 @@ let vectorval = 20;
let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
let settings = Object.assign({
showClocks: true,
fullscreen: false
fullscreen: false,
buzz:false
}, s.readJSON("taglaunch.json", true) || {});
if ("vectorsize" in settings)
vectorval = parseInt(settings.vectorsize);
@ -108,6 +109,7 @@ let showTagMenu = (tag) => {
}
},
select : i => {
const loadApp = () => {
let app = appsByTag[tag][i];
if (!app) return;
if (!app.src || require("Storage").read(app.src)===undefined) {
@ -117,6 +119,15 @@ let showTagMenu = (tag) => {
} else {
load(app.src);
}
};
if(settings.buzz){
Bangle.buzz(25);
//let the buzz have effect
setTimeout(loadApp,27);
}else{
loadApp();
}
},
back : showMainMenu,
remove: unload
@ -138,6 +149,7 @@ let showMainMenu = () => {
}
},
select : i => {
if(settings.buzz)Bangle.buzz(25);
let tag = tagKeys[i];
showTagMenu(tag);
},

View File

@ -2,8 +2,8 @@
"id": "taglaunch",
"name": "Tag Launcher",
"shortName": "Taglauncher",
"version": "0.07",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.",
"version": "0.08",
"description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access from the default launcher.",
"readme": "README.md",
"icon": "app.png",
"type": "launch",

View File

@ -2,7 +2,8 @@
(function(back) {
let settings = Object.assign({
showClocks: true,
fullscreen: false
fullscreen: false,
buzz:false
}, require("Storage").readJSON("taglaunch.json", true) || {});
let fonts = g.getFonts();
@ -21,9 +22,16 @@
},
/*LANG*/"Vector Font Size": {
value: settings.vectorsize || 10,
min:10, max: 20,step:1,wrap:true,
min:10, max: 25,step:1,wrap:true,
onchange: (m) => {save("vectorsize", m)}
},
/*LANG*/"Haptic Feedback": {
value: settings.buzz == true,
onchange: (m) => {
save("buzz", m);
}
},
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
onchange: (m) => {

View File

@ -4,3 +4,4 @@
0.04: Get time zone from settings for showing the clock
0.05: Minor code improvements
0.06: Adjust format of title, save counter before leaving help screen
0.07: Refactor code, fix stuttering timer, add settings menu

View File

@ -3,7 +3,7 @@
A simple timer. You can easily set up the time. The initial time is 2:30
On the first screen, you can
- tap to get help
- double tap to get help
- swipe up/down to change the timer by +/- one minute
- swipe left/right to change the time by +/- 15 seconds
- press Btn1 to start
@ -12,24 +12,31 @@ Press Btn1 again to stop the timer
- when time is up, your Bangle will buzz for 15 seconds
- and it will count up to 60 seconds and stop after that
## Images
_1. Startscreen_
The time changes can be adjusted in the settings menu.
![](TeatimerStart.jpg)
## Images
_1. Start screen_
![](TeatimerStart.png)
Current time is displayed below the Title. Initial time is 2:30.
_2. Help Screen_
![](TeatimerHelp.jpg)
![](TeatimerHelp.png)
_3. Tea Timer running_
![](TeatimerRun.jpg)
Remainig time is shown in big font size. Above the initial time is shown.
![](TeatimerRun.png)
Remainig time is shown in big font size.
_4. When time is up_
_4. Pause Timer
![](TeatimerUp.jpg)
![](TeatimerPause.png)
While the timer is running, you can pause and unpause it by pressing BTN1.
_5. When time is up_
![](TeatimerUp.png)
When time is up, the watch will buzz for 15 seconds. It will count up to 60 seconds.
## Requests

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show More