Merge branch 'espruino:master' into Smart-Battery

master
RKBoss6 2025-07-24 16:17:05 -04:00 committed by GitHub
commit 925c71a207
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 596 additions and 304 deletions

1
apps/backlite/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New app! (settings, boot.js).

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;
if (!("lcdTimeout" in s)) s.lcdTimeout = 5; // fallback so logic doesn't break
require("Storage").writeJSON("setting.json", 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.01",
"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";
// Load settings
var settings = Object.assign({
brightness: 0.3,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Show the menu
E.showMenu({
"" : { "title" : "BackLite" },
'Brightness': {
value: 0.3|settings.brightness,
min: 0.1, max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v;
writeSettings();
}
},
});
})

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

@ -17,3 +17,5 @@
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;
@ -156,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.18",
"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

@ -39,3 +39,4 @@
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

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

@ -2,7 +2,7 @@
"id": "health",
"name": "Health Tracking",
"shortName": "Health",
"version": "0.35",
"version": "0.36",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"screenshots" : [ { "url":"screenshot.png" } ],

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})

View File

@ -88,3 +88,4 @@ 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, default brightness is now 0 as well.

View File

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

View File

@ -1,3 +1,4 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
@ -474,11 +475,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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -3,66 +3,151 @@
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../core/lib/customize.js"></script>
<div id="storageInfo"></div>
<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>
// Called when we know what device we're using
let globalApps = [];
let storageStats = null;
function onInit(device) {
Util.showModal("Reading Storage...");
Puck.eval(`require("Storage").list(/\\.info$/).map(appInfoName => {
let appInfo = require("Storage").readJSON(appInfoName,1)||{};
//print(appInfoName, appInfo);
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("?",".*")+"$");
}
// normal files
if (data[0]) data[0].split(",").forEach(wc => {
require("Storage").list(wildcardToRegexp(wc), {sf:false}).forEach(f => {
dataSize += require("Storage").read(f).length
});
});
// storage files
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];
})`, function(apps) {
apps.sort((a,b) => (b[1]+b[2]) - (a[1]+a[2]));
Util.hideModal();
console.log(apps);
document.getElementById("storageInfo").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>
${apps.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>`;
if (apps.length === 0) {
document.getElementById("storageInfo").innerHTML = "<p>No apps found</p>";
}
});
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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,9 +1,9 @@
{
"id": "storageanalyzer",
"name": "Storage Analyzer",
"version": "0.01",
"description": "Analyses Bangle.js storage and shows which apps are using it",
"icon": "app.png",
"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"],

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,15 +109,25 @@ let showTagMenu = (tag) => {
}
},
select : i => {
let app = appsByTag[tag][i];
if (!app) return;
if (!app.src || require("Storage").read(app.src)===undefined) {
Bangle.setUI();
E.showMessage(/*LANG*/"App Source\nNot found");
setTimeout(showMainMenu, 2000);
} else {
load(app.src);
const loadApp = () => {
let app = appsByTag[tag][i];
if (!app) return;
if (!app.src || require("Storage").read(app.src)===undefined) {
Bangle.setUI();
E.showMessage(/*LANG*/"App Source\nNot found");
setTimeout(showMainMenu, 2000);
} 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,237 +1,217 @@
// Tea Timer
// Button press stops timer, next press restarts timer
let drag;
var counter = 0;
var counterStart = 150; // 150 seconds
var counterInterval;
const states = {
init: 1, // unused
help: 2, // show help text
start: 4, // show/change initial counter
count: 8, // count down
countUp: 16, // count up after timer finished
stop: 32 // timer stopped
const FILE = "teatimer.json";
const DEFAULTS = {
timerDuration: 150,
bigJump: 60,
smallJump: 15,
finishBuzzDuration: 1500,
overtimeBuzzDuration: 100,
overtimeBuzzLimit: 60,
overtimeBuzzSeconds: 15
};
var state = states.start;
let setting = require("Storage").readJSON("setting.json",1);
E.setTimeZone(setting.timezone);
// Title showing current time
function appTitle() {
return "Tea Timer\n" + currentTime();
// Enum for states
const STATES = {
INIT: "init",
RUNNING: "running",
PAUSED: "paused",
FINISHED: "finished",
OVERTIME: "overtime"
};
let savedSettings = require("Storage").readJSON(FILE, 1) || {};
let settings = Object.assign({}, DEFAULTS, savedSettings);
let state = STATES.INIT;
let showHelp = false;
let startTime = 0;
let remaining = settings.timerDuration;
let target = 0;
let drag = null;
let dragAdjusted = false;
let lastTapTime = 0;
// === Helpers ===
function formatTime(s) {
let m = Math.floor(s / 60);
let sec = (s % 60).toString().padStart(2, '0');
return `${m}:${sec}`;
}
function currentTime() {
let min = Date().getMinutes();
if (min < 10) min = "0" + min;
return Date().getHours() + ":" + min;
function getTimeStr() {
let d = new Date();
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
function timeFormated(sec) {
let min = Math.floor(sec / 60);
sec = sec % 60;
if (sec < 10) sec = "0" + sec;
return min + ":" + sec;
function isState(s) {
return state === s;
}
// initialize timer and show timer value => state: start
function initTimer() {
counter = counterStart;
setState(states.start);
showCounter(true);
function setState(s) {
state = s;
}
// timer value (counter) can be changed in state start
function changeCounter(diff) {
if (state == states.start) {
if (counter + diff > 0) {
counter = counter + diff;
showCounter(true);
}
// === UI Drawing ===
function drawUI() {
g.reset();
g.setBgColor(g.theme.bg).clear();
g.setColor(g.theme.fg);
let cx = g.getWidth() / 2;
// Time (top right)
g.setFont("6x8", 2);
g.setFontAlign(1, 0);
g.drawString(getTimeStr(), g.getWidth() - 4, 10);
// Help text
if (showHelp) {
g.setFontAlign(0, 0);
g.setFont("Vector", 15);
g.drawString(
`Swipe up/down: ±${settings.bigJump}s\nSwipe left/right: ±${settings.smallJump}s\n\nBTN1: Start/Pause\nDouble Tap: Hide Help`,
cx, 80
);
return;
}
// Title
g.setFont("Vector", 20);
g.setFontAlign(0, 0);
let label = (isState(STATES.OVERTIME)) ? "Time's Up!" : "Tea Timer";
g.drawString(label, cx, 40);
// Time remaining / overtime
g.setFont("Vector", 60);
g.setColor(isState(STATES.OVERTIME) ? "#f00" : g.theme.fg);
g.drawString(formatTime(remaining), cx, 100);
// Bottom state text
g.setFontAlign(0, 0);
if (isState(STATES.PAUSED)) {
g.setFont("6x8", 2);
g.drawString("paused", cx, g.getHeight() - 20);
} else if (!isState(STATES.RUNNING) && !isState(STATES.OVERTIME)) {
g.setFont("Vector", 13);
g.drawString("double tap for help", cx, g.getHeight() - 20);
}
}
// start or restart timer => state: count
// === Timer Logic ===
function startTimer() {
counterStart = counter;
setState(states.count);
countDown();
if (!counterInterval)
counterInterval = setInterval(countDown, 1000);
setState(STATES.RUNNING);
startTime = Date.now();
target = startTime + remaining * 1000;
}
/* show current counter value at start and while count down
Show
- Title with current time
- initial timer value
- remaining time
- hint for help in state start
*/
function showCounter(withHint) {
g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier
E.showMessage("", appTitle());
g.reset().setFontAlign(0,0); // center font
// draw the current counter value
g.setBgColor(-1).setColor(0,0,1); // blue
g.setFont("Vector",20); // vector font, 20px
g.drawString("Timer: " + timeFormated(counterStart),80,55);
g.setFont("Vector",60); // vector font, 60px
g.drawString(timeFormated(counter),83,100);
if (withHint) {
g.setFont("Vector",20); // vector font, 80px
g.drawString("Tap for help",80,150);
function pauseTimer() {
if (isState(STATES.RUNNING)) {
remaining = Math.max(0, Math.ceil((target - Date.now()) / 1000));
setState(STATES.PAUSED);
}
}
// count down and update every second
// when time is up, start counting up
function countDown() {
counter--;
// Out of time
if (counter<=0) {
outOfTime();
countUp();
counterInterval = setInterval(countUp, 1000);
return;
function resumeTimer() {
if (isState(STATES.PAUSED)) {
startTime = Date.now();
target = startTime + remaining * 1000;
setState(STATES.RUNNING);
}
showCounter(false);
}
//
function outOfTime() {
E.showMessage("Time is up!",appTitle());
setState(states.countUp);
resetTimer();
Bangle.buzz();
Bangle.buzz();
}
/* this counts up (one minute), after time is up
Show
- Title with current time
- initial timer value
- "Time is up!"
- time since timer finished
*/
function countUp() {
// buzz for 15 seconds
counter++;
if (counter <=15) {
Bangle.buzz();
}
// stop counting up after 60 seconds
if (counter > 60) {
outOfTime();
return;
}
g.reset(); // workaround for E.showMessage bg color in 2v14 and earlier
E.showMessage("", appTitle());
g.reset().setFontAlign(0,0); // center font
g.setBgColor(-1).setColor(0,0,1); // blue
g.setFont("Vector",20); // vector font, 20px
g.drawString("Timer: " + timeFormated(counterStart),80,55);
g.setFont("Vector",30); // vector font, 80px
g.setBgColor(-1).setColor(1,0,0); // red
g.drawString("Time is up!",85,85);
g.setFont("Vector",40); // vector font, 80px
// draw the current counter value
g.drawString(timeFormated(counter),80,130);
}
// reset when interupted by user oder 60 seconds after timer finished
function resetTimer() {
clearInterval();
counterInterval = undefined;
setState(STATES.INIT);
remaining = settings.timerDuration;
}
// timer is stopped by user => state: stop
function stopTimer() {
resetTimer();
E.showMessage("Timer stopped!", appTitle());
setState(states.stop);
}
// timer is stopped by user while counting up => state: start
function stopTimer2() {
resetTimer();
initTimer();
}
function setState(st) {
state = st;
}
function buttonPressed() {
switch(state) {
case states.init:
initTimer();
break;
case states.help:
initTimer();
break;
case states.start:
startTimer();
break;
case states.count:
stopTimer();
break;
case states.countUp:
stopTimer2();
break;
case states.stop:
initTimer();
break;
default:
initTimer();
break;
}
}
/* Change initial counter value by swiping
swipe up: +1 minute
swipe down: -1 minute
swipe right: +15 seconds
swipe left: -15 seconds */
function initDragEvents() {
Bangle.on("drag", e => {
if (state == states.start) {
if (!drag) { // start dragging
drag = {x: e.x, y: e.y};
} else if (!e.b) { // released
const dx = e.x-drag.x, dy = e.y-drag.y;
drag = null;
if (Math.abs(dx)>Math.abs(dy)+10) {
// horizontal
changeCounter(dx>0 ? 15 : -15);
} else if (Math.abs(dy)>Math.abs(dx)+10) {
// vertical
changeCounter(dy>0 ? -60 : 60);
}
function tick() {
if (isState(STATES.RUNNING)) {
remaining -= 1;
if (remaining <= 0) {
remaining = 0;
setState(STATES.OVERTIME);
startTime = Date.now();
remaining = 0; // Start overtime count-up from 0
Bangle.buzz(settings.finishBuzzDuration);
}
} else if (isState(STATES.OVERTIME)) {
remaining += 1;
if (remaining <= settings.overtimeBuzzSeconds) {
Bangle.buzz(settings.overtimeBuzzDuration, 0.3);
}
if (remaining >= settings.overtimeBuzzLimit) {
resetTimer(); // Stop overtime after max duration
}
}
});
drawUI();
}
// show help text while in start state (see initDragEvents())
function showHelp() {
if (state == states.start) {
state = states.help;
g.setBgColor(g.theme.bg);
g.setColor(g.theme.fg);
E.showMessage("Swipe up/down\n+/- one minute\n\nSwipe left/right\n+/- 15 seconds\n\nPress Btn1 to start","Tea timer help");
// === UI Controls ===
function toggleTimer() {
if (showHelp) {
showHelp = false;
} else if (isState(STATES.OVERTIME)) {
resetTimer();
} else if (isState(STATES.INIT)) {
startTimer();
} else if (isState(STATES.PAUSED)) {
resumeTimer();
} else if (isState(STATES.RUNNING)) {
pauseTimer();
}
// return to start
else if (state == states.help) {
counterStart = counter;
initTimer();
drawUI();
}
function handleDoubleTap() {
if (isState(STATES.INIT)) {
let now = Date.now();
if (now - lastTapTime < 400) {
showHelp = !showHelp;
drawUI();
}
lastTapTime = now;
}
}
// drag events in start state (to change counter value)
initDragEvents();
// Show help test in start state
Bangle.on('touch', function(button, xy) { showHelp(); });
// event handling for button1
setWatch(buttonPressed, BTN1, {repeat: true});
initTimer();
function adjustTimer(diff) {
if (isState(STATES.INIT)) {
remaining = Math.max(5, remaining + diff);
settings.timerDuration = remaining;
drawUI();
}
}
function handleDrag(e) {
if (isState(STATES.INIT) && !showHelp) {
if (e.b) {
if (!drag) {
drag = { x: e.x, y: e.y };
dragAdjusted = false;
} else if (!dragAdjusted) {
let dx = e.x - drag.x;
let dy = e.y - drag.y;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > settings.smallJump) {
adjustTimer(dx > 0 ? settings.smallJump : -settings.smallJump);
dragAdjusted = true;
} else if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > settings.bigJump) {
adjustTimer(dy > 0 ? -settings.bigJump : settings.bigJump);
dragAdjusted = true;
}
}
} else {
drag = null;
dragAdjusted = false;
}
}
}
// === Init App ===
setWatch(toggleTimer, BTN1, { repeat: true });
Bangle.on("drag", handleDrag);
Bangle.on("touch", handleDoubleTap);
resetTimer();
drawUI();
setInterval(tick, 1000);

View File

@ -1,21 +1,26 @@
{
"id": "teatimer",
"name": "Tea Timer",
"version": "0.06",
"version": "0.07",
"description": "A simple timer. You can easily set up the time.",
"icon": "teatimer.png",
"type": "app",
"tags": "tool",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"data": [
{ "name": "teatimer.json" }
],
"storage": [
{"name":"teatimer.app.js","url":"app.js"},
{"name": "teatimer.settings.js", "url": "settings.js" },
{"name":"teatimer.img","url":"app-icon.js","evaluate":true}
],
"screenshots": [
{"url":"TeatimerStart.jpg"},
{"url":"TeatimerHelp.jpg"},
{"url":"TeatimerRun.jpg"},
{"url":"TeatimerUp.jpg"}
{"url":"TeatimerStart.png"},
{"url":"TeatimerHelp.png"},
{"url":"TeatimerRun.png"},
{"url":"TeatimerPause.png"},
{"url":"TeatimerUp.png"}
]
}

47
apps/teatimer/settings.js Normal file
View File

@ -0,0 +1,47 @@
(function(back) {
const FILE = "teatimer.json";
const DEFAULTS = {
timerDuration: 150, // Initial timer duration in seconds
bigJump: 60, // Jump for vertical swipes
smallJump: 15 // Jump for horizontal swipes
};
let settings = require("Storage").readJSON(FILE, 1) || DEFAULTS;
function saveSettings() {
require("Storage").writeJSON(FILE, settings);
}
function showSettingsMenu() {
E.showMenu({
'': { title: 'Tea Timer Settings' },
'< Back': back,
'Default Duration (sec)': {
value: settings.timerDuration,
min: 5, max: 900, step: 5,
onchange: v => {
settings.timerDuration = v;
saveSettings();
}
},
'Swipe Up/Down (sec)': {
value: settings.bigJump,
min: 5, max: 300, step: 5,
onchange: v => {
settings.bigJump = v;
saveSettings();
}
},
'Swipe Left/Right (sec)': {
value: settings.smallJump,
min: 5, max: 60, step: 5,
onchange: v => {
settings.smallJump = v;
saveSettings();
}
}
});
}
showSettingsMenu();
})