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

master
Gordon Williams 2023-02-06 11:20:57 +00:00
commit c3827f9c24
50 changed files with 1062 additions and 106 deletions

View File

@ -38,3 +38,6 @@
0.35: Add automatic translation of more strings
0.36: alarm widget moved out of app
0.37: add message input and dated Events
0.38: Display date in locale
When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection
Display alarm label in delete prompt

View File

@ -40,6 +40,14 @@ function handleFirstDayOfWeek(dow) {
// Check the first day of week and update the dow field accordingly (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
function getLabel(e) {
const dateStr = e.date && require("locale").date(new Date(e.date), 1);
return (e.timer
? require("time_utils").formatDuration(e.timer)
: (dateStr ? `${dateStr} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""))
) + (e.msg ? " " + e.msg : "");
}
function showMainMenu() {
const menu = {
"": { "title": /*LANG*/"Alarms & Timers" },
@ -48,11 +56,7 @@ function showMainMenu() {
};
alarms.forEach((e, index) => {
var label = (e.timer
? require("time_utils").formatDuration(e.timer)
: (e.date ? `${e.date.substring(5,10)} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""))
) + (e.msg ? " " + e.msg : "");
menu[label] = {
menu[getLabel(e)] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
};
@ -184,7 +188,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
E.showPrompt(getLabel(alarm) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
@ -264,7 +268,7 @@ function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
},
/*LANG*/"Custom": {
value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
}
};
@ -372,7 +376,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
if (!keyboard) delete menu[/*LANG*/"Message"];
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
if (confirm) {
alarms.splice(timerIndex, 1);
saveAndReload();

View File

@ -2,7 +2,7 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.37",
"version": "0.38",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm",

View File

@ -11,6 +11,7 @@ sub-items simply swipe up/down. To run an action (e.g. trigger home assistant),
![](screenshot_3.png)
Note: Check out the settings to change different themes.
## Settings
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).

View File

@ -1,2 +1,3 @@
0.01: First version
0.02: Support BangleJS2
0.03: Added threshold

View File

@ -6,6 +6,8 @@ The first stage of charging Li-ion ends at ~80% capacity when the charge voltage
This app has no UI and no configuration. To disable the app, you have to uninstall it.
New in v0.03: before the very first buzz, the average value after the peak is written to chargent.json and used as threshold for future charges. This reduces the time spent in the second charge stage.
Side notes
- Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100.
- This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise.

View File

@ -6,20 +6,28 @@
if (charging) {
if (!id) {
var max = 0;
var count = 0;
var cnt = 0;
var sum = 0;
var lim = (require('Storage').readJSON('chargent.json', true) || {}).limit || 0;
id = setInterval(() => {
var battlvl = analogRead(pin);
if (max < battlvl) {
max = battlvl;
count = 0;
var val = analogRead(pin);
if (max < val) {
max = val;
cnt = 1;
sum = val;
} else {
count++;
if (10 <= count) { // 10 * 30s == 5 min // TODO ? customizable
cnt++;
sum += val;
}
if (10 < cnt || (lim && lim <= max)) { // 10 * 30s == 5 min // TODO ? customizable
if (!lim) {
lim = sum / cnt;
require('Storage').writeJSON('chargent.json', {limit: lim});
}
// TODO ? customizable
Bangle.buzz(500);
setTimeout(() => Bangle.buzz(500), 1000);
}
}
}, 30*1000);
}
} else {

View File

@ -1,6 +1,6 @@
{ "id": "chargent",
"name": "Charge Gently",
"version": "0.02",
"version": "0.03",
"description": "When charging, reminds you to disconnect the watch to prolong battery life.",
"icon": "icon.png",
"type": "bootloader",
@ -9,5 +9,8 @@
"readme": "README.md",
"storage": [
{"name": "chargent.boot.js", "url": "boot.js"}
],
"data": [
{"name": "chargent.json"}
]
}

View File

@ -1,3 +1,4 @@
0.01: First release
0.02: Move translations to locale module (removed watch settings, now pick language in Bangle App Loader, More..., Settings)
0.03: Change for fast loading, use widget_utils to hide widgets
0.04: Add animation when display changes

View File

@ -16,7 +16,6 @@ Most translations are taken from the original Fuzzy Text International code.
## TODO
* Bold hour word (as the pebble version has)
* Animation when changing time?
## References
Based on Pebble app Fuzzy Text International: https://github.com/hallettj/Fuzzy-Text-International

View File

@ -33,13 +33,17 @@
]
};
let text_scale = 3.5;
let text_scale = 4;
let timeout = 2.5*60;
let drawTimeout;
let animInterval;
let time_string = "";
let time_string_old = "";
let time_string_old_wrapped = "";
let loadSettings = function() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'showWidgets': false};
}
};
let queueDraw = function(seconds) {
let millisecs = seconds * 1000;
@ -48,10 +52,7 @@ let queueDraw = function(seconds) {
drawTimeout = undefined;
draw();
}, millisecs - (Date.now() % millisecs));
}
const h = g.getHeight();
const w = g.getWidth();
};
let getTimeString = function(date) {
let segment = Math.round((date.getMinutes()*60 + date.getSeconds() + 1)/300);
@ -63,18 +64,47 @@ let getTimeString = function(date) {
f_string = f_string.replace('$2', fuzzy_string.hours[(hour + 1) % 12]);
}
return f_string;
}
};
let draw = function() {
let time_string = getTimeString(new Date()).replace('*', '');
// print(time_string);
g.setFont('Vector', (h-24*2)/text_scale);
g.setFontAlign(0, 0);
g.clearRect(0, 24, w, h-24);
g.setColor(g.theme.fg);
g.drawString(g.wrapString(time_string, w).join("\n"), w/2, h/2);
time_string = getTimeString(new Date()).replace('*', '');
//print(time_string);
if (time_string != time_string_old) {
g.setFont('Vector', R.h/text_scale).setFontAlign(0, 0);
animate(3);
}
queueDraw(timeout);
}
};
let animate = function(step) {
if (animInterval) clearInterval(animInterval);
let time_string_new_wrapped = g.wrapString(time_string, R.w).join("\n");
slideX = 0;
animInterval = setInterval(function() {
let time_start = getTime()
//blank old time
g.setColor(g.theme.bg);
g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2);
g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2);
g.setColor(g.theme.fg);
slideX += step;
let stop = false;
if (slideX>=R.w) {
slideX=R.w;
stop = true;
}
//draw shifted new time
g.drawString(time_string_old_wrapped, R.x + R.w/2 + slideX, R.y + R.h/2);
g.drawString(time_string_new_wrapped, R.x - R.w/2 + slideX, R.y + R.h/2);
if (stop) {
time_string_old = time_string;
clearInterval(animInterval);
animInterval=undefined;
time_string_old_wrapped = time_string_new_wrapped;
}
print(Math.round((getTime() - time_start)*1000))
}, 30);
};
g.clear();
loadSettings();
@ -95,6 +125,8 @@ Bangle.setUI({
// Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if (animInterval) clearInterval(animInterval);
animInterval = undefined;
require('widget_utils').show(); // re-show widgets
}
});
@ -106,5 +138,6 @@ if (settings.showWidgets) {
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
}
R = Bangle.appRect;
draw();
}

View File

@ -2,7 +2,7 @@
"id":"fuzzyw",
"name":"Fuzzy Text Clock",
"shortName": "Fuzzy Text",
"version": "0.03",
"version": "0.04",
"description": "An imprecise clock for when you're not in a rush",
"readme": "README.md",
"icon":"fuzzyw.png",

View File

@ -1 +1,2 @@
0.01: New app!
0.02: Added settings to show/hide widgets and settings for different styles.

View File

@ -14,6 +14,10 @@ Here you can see an example of a locked bangle with a low battery:
![](screenshot_3.png)
## Settings
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
- Theme: Select your custom theme, independent of system settings.
## Creator
- [David Peer](https://github.com/peerdavid).

View File

@ -1,6 +1,82 @@
/************************************************
* Happy Clock
*/
const storage = require('Storage');
const widget_utils = require("widget_utils");
/************************************************
* Settings
*/
const SETTINGS_FILE = "happyclk.setting.json";
let settings = {
color: "Dark",
screen: "Dynamic"
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key];
}
var color_map = {
"Dark":{
fg: "#fff",
bg: "#000",
eye: "#fff",
eyePupils: "#000"
},
"Black":{
fg: "#fff",
bg: "#000",
eye: "#000",
eyePupils: "#fff"
},
"White":{
fg: "#000",
bg: "#fff",
eye: "#fff",
eyePupils: "#000"
},
"Blue":{
fg: "#fff",
bg: "#00f",
eye: "#fff",
eyePupils: "#000"
},
"Green":{
fg: "#000",
bg: "#0f0",
eye: "#fff",
eyePupils: "#000"
},
"Red":{
fg: "#fff",
bg: "#f00",
eye: "#fff",
eyePupils: "#000"
},
"Purple":{
fg: "#fff",
bg: "#f0f",
eye: "#fff",
eyePupils: "#000"
},
"Yellow":{
fg: "#000",
bg: "#ff0",
eye: "#fff",
eyePupils: "#000"
}
};
var colors = color_map[settings.color];
/************************************************
* Globals
*/
var W = g.getWidth(),R=W/2;
var H = g.getHeight();
var drawTimeout;
@ -10,6 +86,16 @@ var drawTimeout;
* HELPER
*/
let isFullscreen = function() {
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked();
} else {
return s == "full";
}
};
// Based on the great multi clock from https://github.com/jeffmer/BangleApps/
Graphics.prototype.drawPupils = function(cx, cy, r1, dx, dy, angle) {
angle = angle % 360;
@ -19,11 +105,12 @@ Graphics.prototype.drawPupils = function(cx, cy, r1, dx, dy, angle) {
g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 32);
g.setColor(g.theme.bg);
g.setColor(colors.eye);
g.fillCircle(cx, cy, 27);
g.fillCircle(cx+dx, cy+dy, 28);
g.setColor(g.theme.fg);
g.setColor(colors.eyePupils);
g.fillCircle(x, y, 8);
g.fillCircle(x+1, y, 8);
};
@ -85,6 +172,7 @@ let drawEyes = function(){
let drawSmile = function(isLocked){
g.setColor(colors.fg);
var y = 120;
var o = parseInt(E.getBattery()*0.8);
@ -100,6 +188,9 @@ let drawSmile = function(isLocked){
}
let drawEyeBrow = function(){
if(!isFullscreen()) return;
g.setColor(colors.fg);
var w = 6;
for(var i = 0; i < w; i++){
g.drawLine(25, 25+i, 70, 15+i%3);
@ -108,6 +199,15 @@ let drawEyeBrow = function(){
}
let drawWidgets = function(){
if (isFullscreen()) {
widget_utils.hide();
} else {
Bangle.drawWidgets();
}
}
let draw = function(){
// Queue draw in one minute
@ -118,12 +218,16 @@ let draw = function(){
}
let drawHelper = function(isLocked){
g.setColor(g.theme.bg);
g.fillRect(0, isFullscreen() ? 0 : 24, W, H);
g.setColor(g.theme.fg);
g.reset().clear();
drawEyes();
drawEyeBrow();
drawSmile(isLocked);
drawWidgets();
}
@ -140,6 +244,15 @@ Bangle.on('lcdPower',on=>{
});
Bangle.on('lock', function(isLocked) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if(!isLocked && settings.screen.toLowerCase() == "dynamic"){
// If we have to show the widgets again, we load it from our
// cache and not through Bangle.loadWidgets as its much faster!
widget_utils.show();
}
draw(isLocked);
});
@ -162,15 +275,9 @@ let queueDraw = function() {
// Show launcher when middle button pressed
Bangle.setUI("clock");
Bangle.loadWidgets();
/*
* we are not drawing the widgets as we are taking over the whole screen
* so we will blank out the draw() functions of each widget and change the
* area to the top bar doesn't get cleared.
*/
require('widget_utils').hide();
// Clear the screen once, at startup and draw clock
// g.setTheme({bg:"#fff",fg:"#000",dark:false});
g.setTheme({bg:colors.bg,fg:colors.fg,dark:false});
draw();
// After drawing the watch face, we can draw the widgets

View File

@ -0,0 +1,43 @@
(function(back) {
const SETTINGS_FILE = "happyclk.setting.json";
// initialize with default settings...
const storage = require('Storage')
let settings = {
color: "Dark",
screen: "Dynamic"
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
function save() {
storage.write(SETTINGS_FILE, settings)
}
var colorOptions = ["Dark", "Black", "White", "Blue", "Green", "Red", "Purple", "Yellow"];
var screenOptions = ["Normal", "Dynamic", "Full"];
E.showMenu({
'': { 'title': 'Happy Clock' },
'< Back': back,
'Screen': {
value: 0 | screenOptions.indexOf(settings.screen),
min: 0, max: screenOptions.length-1,
format: v => screenOptions[v],
onchange: v => {
settings.screen = screenOptions[v];
save();
},
},
'Theme': {
value: 0 | colorOptions.indexOf(settings.color),
min: 0, max: colorOptions.length-1,
format: v => colorOptions[v],
onchange: v => {
settings.color = colorOptions[v];
save();
},
},
});
})

View File

@ -3,7 +3,7 @@
"name": "Happy Clock",
"shortName":"Happy Clock",
"icon": "happyclk.png",
"version":"0.01",
"version":"0.02",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "A happy clock :)",
@ -12,10 +12,13 @@
"screenshots": [
{"url":"screenshot_1.png"},
{"url":"screenshot_2.png"},
{"url":"screenshot_3.png"}
{"url":"screenshot_3.png"},
{"url":"screenshot_4.png"},
{"url":"screenshot_5.png"}
],
"storage": [
{"name":"happyclk.app.js","url":"happyclk.app.js"},
{"name":"happyclk.img","url":"happyclk.icon.js","evaluate":true}
{"name":"happyclk.img","url":"happyclk.icon.js","evaluate":true},
{"name":"happyclk.settings.js","url":"happyclk.settings.js"}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -20,3 +20,4 @@
still be loaded when they weren't supposed to.
0.15: Ensure that we hide widgets if in fullscreen mode
(So that widgets are still hidden if launcher is fast-loaded)
0.16: Use firmware provided E.showScroller method

View File

@ -2,7 +2,7 @@
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
"version": "0.15",
"version": "0.16",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",

View File

@ -1 +1,2 @@
0.01: First public version
0.02: Disable screen lock when breathing

View File

@ -2,7 +2,7 @@
A minimalistic app that will help you practive breathing.
Author: Written by pancake in 2022, powered by insomnia
Author: Written by pancake in 2022, updated in 2023, powered by insomnia
## Features
@ -10,6 +10,7 @@ Author: Written by pancake in 2022, powered by insomnia
* [x] Tap to start
* [x] Subtle vibrations
* [x] Drag to pause breathing
* [x] Dont lock screen while breathing
* [ ] Automatic buzz every hour during day
## Screenshots

View File

@ -14,6 +14,7 @@ var mode = 0;
var sin = 0;
var dragged = 0;
var lastTime = Date.now();
function breath(t) {
var r = Math.abs(Math.sin(t / 100)) * w2;
g.fillCircle(w/2,h/2, r);
@ -26,7 +27,7 @@ setTimeout(()=>{Bangle.buzz(60);}, 500);
function showTouchScreen() {
g.setColor(1,1,1);
g.fillCircle (w2, h2, h2-5);
g.fillCircle(w2, h2, h2-5);
g.setColor(0,0,0);
g.setFont("Vector", 32);
g.drawString("Tap to", w/6, h2-fs);
@ -40,7 +41,7 @@ g.clear();
function animateCircle() {
g.clear();
g.setColor(1,1,1);
g.fillCircle (w2, h2, radius);
g.fillCircle(w2, h2, radius);
radius-=2;
if (radius < 40) {
breathing = true;
@ -68,6 +69,9 @@ function main() {
return;
}
started = true;
Bangle.setLCDPower(1);
Bangle.setLocked(0);
Bangle.setLCDTimeout(0);
animateCircle();
Bangle.buzz(40);
}
@ -78,13 +82,13 @@ function main() {
main();
function startBreathing() {
var cicles = 3;
g.setFont("Vector", fs);
var cicles = 3;
g.setFont("Vector", fs);
var interval = setInterval(function() {
if (lastTime + 10 > Date.now()) {
function breathTime() {
if (lastTime + 10 > Date.now()) {
return;
}
}
lastTime = Date.now();
g.setColor(0, 0, 0);
g.clear();
@ -120,6 +124,6 @@ if (lastTime + 10 > Date.now()) {
Bangle.showClock();
}
dragged = 0;
}, 4);
}
var interval = setInterval(breathTime, 4);
}

View File

@ -2,7 +2,7 @@
"id": "inspire",
"name": "Inspire Breathing",
"shortName": "Inspire",
"version": "0.01",
"version": "0.02",
"description": "exercise breathing every now and then",
"icon": "app-icon.png",
"tags": "tools,health",

View File

@ -4,7 +4,7 @@ Based on the Pebble watchface Weather Land.
Mountain Pass Clock changes depending on time (day/night) and weather conditions.
This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification). To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather.
This clock requires Gadgetbridge and an app that Gadgetbridge can use to get the current weather from OpenWeatherMap (e.g. Weather Notification), or a Bangle app that will update weather.json such as OWM Weather. To set up Gadgetbridge and weather, see https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather.
The scene will change according to the following OpenWeatherMap conditions: clear, cloudy, overcast, lightning, drizzle, rain, fog and snow. Each weather condition has night/day scenes.

View File

@ -323,11 +323,28 @@ function setWeather() {
draw(a);
}
function readWeather() {
var weatherJson = require("Storage").readJSON('weather.json', 1);
// save updated weather data if available and it has been an hour since last updated
if (weatherJson !== undefined && (data.time === undefined || (data.time + 3600000) < weatherJson.weather.time)) {
data = {
time: weatherJson.weather.time,
temp: weatherJson.weather.temp,
code: weatherJson.weather.code
};
require("Storage").writeJSON('mtnclock.json', data);
}
}
const _GB = global.GB;
global.GB = (event) => {
if (event.t==="weather") {
data = event;
require("Storage").write('mtnclock.json', event);
data = {
temp: event.temp,
code: event.code,
time: Date.now()
};
require("Storage").writeJSON('mtnclock.json', data);
setWeather();
}
if (_GB) setTimeout(_GB, 0, event);
@ -340,11 +357,13 @@ function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
readWeather();
setWeather();
queueDraw();
}, 60000 - (Date.now() % 60000));
}
queueDraw();
readWeather();
setWeather();
Bangle.setUI("clock");

View File

@ -2,7 +2,7 @@
"id": "mtnclock",
"name": "Mountain Pass Clock",
"shortName": "Mtn Clock",
"version": "0.01",
"version": "0.02",
"description": "A clock that changes scenery based on time and weather.",
"readme":"README.md",
"icon": "app.png",

View File

@ -3,3 +3,4 @@
0.03: Use default Bangle formatter for booleans
0.04: Remove calibration with current voltage (Calibrate->Auto) as it is now handled by settings app
Allow automatic calibration on every charge longer than 3 hours
0.05: Add back button to settings menu.

View File

@ -2,7 +2,7 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
"version": "0.04",
"version": "0.05",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png",
"type": "bootloader",

View File

@ -23,6 +23,7 @@
'': {
'title': 'Power Manager'
},
"< Back" : back,
'Monotonic percentage': {
value: !!settings.forceMonoPercentage,
onchange: v => {

View File

@ -1,2 +1,4 @@
0.01: New App!
0.02: Fix fast loading on swipe to clock
0.03: Adds a setting for going back to clock on a timeout
0.04: Fix timeouts closing fast loaded apps

View File

@ -110,6 +110,8 @@ let layout = new Layout({
}, {
remove: ()=>{
Bangle.removeListener("swipe", onSwipe);
Bangle.removeListener("touch", updateTimeout);
if (timeout) clearTimeout(timeout);
delete Graphics.prototype.setFont8x12;
}
});
@ -117,6 +119,16 @@ g.clear();
layout.render();
Bangle.drawWidgets();
let timeout;
const updateTimeout = function(){
if (settings.timeout){
if (timeout) clearTimeout(timeout);
timeout = setTimeout(Bangle.showClock,settings.timeout*1000);
}
};
updateTimeout();
// swipe event listener for exit gesture
let onSwipe = function (lr, ud) {
if(exitGesture == "swipeup" && ud == -1) Bangle.showClock();
@ -126,4 +138,5 @@ let onSwipe = function (lr, ud) {
}
Bangle.on("swipe", onSwipe);
Bangle.on("touch", updateTimeout);
}

View File

@ -2,7 +2,7 @@
"id": "qcenter",
"name": "Quick Center",
"shortName": "QCenter",
"version": "0.02",
"version": "0.04",
"description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.",
"icon": "app.png",
"tags": "",

View File

@ -49,6 +49,11 @@
E.showMenu(exitGestureMenu);
};
// Set Timeout
mainmenu["Timeout: " + (settings.timeout ? (settings.timeout+"s") : "Off")] = function () {
E.showMenu(timeoutMenu);
};
//List all pinned apps, redirecting to menu with options to unpin and reorder
pinnedApps.forEach((app, i) => {
mainmenu[app.name] = function () {
@ -129,5 +134,22 @@
showMainMenu();
};
// menu for setting timeout
var timeoutMenu = {
"": { title: "Timeout", back: showMainMenu }
};
timeoutMenu["Off"] = function () {
save("timeout", 0);
showMainMenu();
};
let timeoutvalues = [10,20,30,60];
for (c in timeoutvalues){
let v = timeoutvalues[c];
timeoutMenu[v+"s"] = function () {
save("timeout", v);
showMainMenu();
};
}
showMainMenu();
});

223
apps/sched/interface.html Normal file
View File

@ -0,0 +1,223 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<script src="../../core/lib/interface.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ical.js/0.0.3/ical.min.js"></script>
<script>
let dataElement = document.getElementById("data");
let alarms;
let schedSettings;
function readFile(input) {
document.getElementById('upload').disabled = true;
const offsetMinutes = document.getElementById("offsetMinutes").value;
for(let i=0; i<input.files.length; i++) {
const reader = new FileReader();
reader.addEventListener("load", () => {
const jCalData = ICAL.parse(reader.result);
const comp = new ICAL.Component(jCalData[1]);
// Fetch the VEVENT part
comp.getAllSubcomponents('vevent').forEach(vevent => {
event = new ICAL.Event(vevent);
const exists = alarms.some(alarm => alarm.id === event.uid);
const alarm = eventToAlarm(event, offsetMinutes*60*1000);
renderAlarm(alarm, exists);
if (exists) {
alarms = alarms.filter(alarm => alarm.id !== event.uid); // remove if already exists
const tr = document.querySelector(`.event-row[data-uid='${event.uid}']`);
document.getElementById('events').removeChild(tr);
}
alarms.push(alarm);
});
}, false);
reader.readAsText(input.files[i], "UTF-8");
}
}
function dateToMsSinceMidnight(date) {
const dateMidnight = new Date(date);
dateMidnight.setHours(0,0,0,0);
return date - dateMidnight;
}
function dateFromAlarm(alarm) {
const date = new Date(alarm.date);
return new Date(date.getTime() + alarm.t);
}
function getAlarmDefaults() {
const date = new Date();
return {
on: true,
t: dateToMsSinceMidnight(date),
dow: 127,
date: date.toISOString().substring(0,10),
last: 0,
rp: "defaultRepeat" in schedSettings ? schedSettings.defaultRepeat : false,
vibrate: "defaultAlarmPattern" in schedSettings ? schedSettings.defaultAlarmPattern : "::",
as: false,
};
}
function eventToAlarm(event, offsetMs) {
const dateOrig = event.startDate.toJSDate();
const date = offsetMs ? new Date(dateOrig - offsetMs) : dateOrig;
const alarm = {...getAlarmDefaults(), ...{
id: event.uid,
msg: event.summary,
t: dateToMsSinceMidnight(date),
date: date.toISOString().substring(0,10),
data: {end: event.endDate.toJSDate().toISOString()}
}};
if (offsetMs) { // Alarm time is not real event time, so do a backup
alarm.data.time = dateOrig.toISOString();
}
return alarm;
}
function upload() {
Util.showModal("Saving...");
Util.writeStorage("sched.json", JSON.stringify(alarms), () => {
location.reload(); // reload so we see current data
});
}
function renderAlarm(alarm, exists) {
const localDate = dateFromAlarm(alarm);
const tr = document.createElement('tr');
tr.classList.add('event-row');
tr.dataset.uid = alarm.id;
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
const inputTime = document.createElement('input');
inputTime.type = "datetime-local";
inputTime.classList.add('event-date');
inputTime.classList.add('form-input');
inputTime.dataset.uid = alarm.id;
inputTime.value = localDate.toISOString().slice(0,16);
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(date);
alarm.date = date.toISOString().substring(0,10);
});
tdTime.appendChild(inputTime);
const tdSummary = document.createElement('td');
tr.appendChild(tdSummary);
const inputSummary = document.createElement('input');
inputSummary.type = "text";
inputSummary.classList.add('event-summary');
inputSummary.classList.add('form-input');
inputSummary.dataset.uid = alarm.id;
inputSummary.maxLength=40;
const realHumanStartTime = alarm.data?.time ? ' ' + (new Date(alarm.data.time)).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : '';
const summary = (alarm.msg?.substring(0, inputSummary.maxLength) || "");
inputSummary.value = summary.endsWith(realHumanStartTime) ? summary : summary + realHumanStartTime;
inputSummary.onchange = (e => {
alarm.msg = inputSummary.value;
});
tdSummary.appendChild(inputSummary);
inputSummary.onchange();
const tdInfo = document.createElement('td');
tr.appendChild(tdInfo);
const buttonDelete = document.createElement('button');
buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-action');
tdInfo.prepend(buttonDelete);
const iconDelete = document.createElement('i');
iconDelete.classList.add('icon');
iconDelete.classList.add('icon-delete');
buttonDelete.appendChild(iconDelete);
buttonDelete.onclick = (e => {
alarms = alarms.filter(a => a !== alarm);
document.getElementById('events').removeChild(tr);
});
document.getElementById('events').appendChild(tr);
document.getElementById('upload').disabled = false;
}
function addAlarm() {
const alarm = getAlarmDefaults();
renderAlarm(alarm);
alarms.push(alarm);
}
function getData() {
Util.showModal("Loading...");
Util.readStorage('sched.json',data=>{
alarms = JSON.parse(data || "[]") || [];
Util.readStorage('sched.settings.json',data=>{
schedSettings = JSON.parse(data || "{}") || {};
Util.hideModal();
alarms.forEach(alarm => {
if (alarm.date) {
renderAlarm(alarm, true);
}
});
});
});
}
// Called when app starts
function onInit() {
getData();
}
</script>
</head>
<body>
<h4>Manage dated events</h4>
<div class="float-right">
<button class="btn" onclick="addAlarm()">
<i class="icon icon-plus"></i>
</button>
</div>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Summary</th>
<th></th>
</tr>
</thead>
<tbody id="events">
</tbody>
</table>
<div class="divider"></div>
<div class="form-horizontal">
<div class="form-group">
<div class="col-5 col-xs-12">
<label class="form-label" for="fileinput">Add from iCalendar file</label>
</div>
<div class="col-7 col-xs-12">
<input id="fileinput" class="form-input" type="file" onchange="readFile(this)" accept=".ics,.ifb,.ical,.ifbf" multiple/>
</div>
</div>
<div class="form-group">
<div class="col-5 col-xs-12">
<label class="form-label" for="fileinput">Minutes to alarm in advance</label>
</div>
<div class="col-7 col-xs-12">
<input id="offsetMinutes" class="form-input" type="number" value="0" min="0" step="5"/>
</div>
</div>
</div>
<div class="divider"></div>
<button id="upload" class="btn btn-primary" onClick="upload()" disabled>Upload</button>
<button id="reload" class="btn" onClick="location.reload()">Reload</button>
</body>
</html>

View File

@ -10,6 +10,7 @@
"provides_modules" : ["sched"],
"default" : true,
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"sched.boot.js","url":"boot.js"},
{"name":"sched.js","url":"sched.js"},

2
apps/tempgraph/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: 3/Feb/2023 Added 'Temperature Graph' app to depository.

36
apps/tempgraph/README.md Normal file
View File

@ -0,0 +1,36 @@
# Temperature Graph
**Temperature Graph** (tempgraph) is a Bangle.js 2 app for recording graphs of the temperature for various time periods from 10 minutes to 7 days long. It samples the watch's temperature sensor 150 times while creating a graph, regardless of the time period selected.
### Menu Options
* **Widgets** Toggles the watch's widgets on and off. With them off gives you a bigger graph when viewing it.
* **Duration** Select the time period for drawing the graph, from 10 minutes to 7 days long.
* **Draw Graph** Draws the graph.
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
* Pressing the watch button takes you back to the menu. **Note:** While the graph can still be viewed after returning to the menu, you can't continue recording it if you had returned to the menu before the time period was up. The graph is saved in the watch though so it's still there the next time you start the app.
* **Show Graph** Shows the last drawn graph.
* Tapping the screen toggles the graph between Celsius (red) and Fahrenheit (blue).
* Pressing the watch button takes you back to the menu.
* **Save Graph** Sends a screengrab of the graph to the Espruino Web IDE from where you can save it as you would any image on a webpage.
* **Save Data** Sends a CSV file of the graph's temperature data to the Espruino Web IDE where you can save it for further use. I suggest you use the Espruino Web IDE's Terminal Logger (selected in the IDE's Settings/General) to record the data as it's sent. This is the easiest way to save it as a text file.
* **Show Temp** Shows the current temperature.
### Note
Using the watch in a normal fashion can raise the temperature it's sensing to quite a few degrees above the surrounding temperature and it may take half an hour or so to drop to close to the surrounding temperature. After that it seems to give quite accurate readings, assuming the thermometer I've been comparing it to is itself reasonably accurate. So best to load the app then not touch the watch for half an hour before starting a recording. This is assuming you're not wearing the app and are just using it to record the temperature where you've put the watch. You could of course wear it and it'll still draw a graph, which might also be useful.
### Screenshots
![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
### Creator
Carl Read ([mail](mailto:cread98@orcon.net.nz), [github](https://github.com/CarlR9))
#### License
[MIT License](LICENSE)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+goA/AH4AgrHXABFYF0XXkYAK64utGENYFxoABSTxeHXYJglF+UAAIQvEBApfhE4UAF4IDBFwZf/X7hfsR4K/tL96/vRwpf/X/5fJGYK/tL9u02i/tF4KOFL/6/XF4ZftR4K/tL96/vRwpf/X/5fJGYK/tL96/vRwpf/X7gADF8ouBGA4v/F/6/urAmGABFYF7pgIL0owPF0KSC64AIRj4A/AH4ACA="))

395
apps/tempgraph/app.js Normal file
View File

@ -0,0 +1,395 @@
// Temperature Graph
// BangleJS Script
Bangle.setBarometerPower(true,"tempgraph");
Bangle.loadWidgets();
var wids=WIDGETS;
var widsOn=true;
var rm=null;
var gt=null;
var dg=null;
var Layout=require("Layout");
var C=true;
var temp,tempMode,readErrCnt,watchButton2;
var graph=require("Storage").readJSON("tempgraph.json",true);
if(graph==undefined) {
graph=[];
}
var timesData=[
// dur=duration, u=time units, d=divisions on graph, s=seconds per unit.
{dur:10,u:"Mins",d:5,s:60},
{dur:20,u:"Mins",d:4,s:60},
{dur:30,u:"Mins",d:3,s:60},
{dur:40,u:"Mins",d:4,s:60},
{dur:1,u:"Hr",d:4,s:3600},
{dur:2,u:"Hrs",d:4,s:3600},
{dur:3,u:"Hrs",d:3,s:3600},
{dur:4,u:"Hrs",d:4,s:3600},
{dur:6,u:"Hrs",d:6,s:3600},
{dur:8,u:"Hrs",d:4,s:3600},
{dur:12,u:"Hrs",d:6,s:3600},
{dur:16,u:"Hrs",d:4,s:3600},
{dur:20,u:"Hrs",d:5,s:3600},
{dur:1,u:"Day",d:4,s:3600},
{dur:2,u:"Days",d:4,s:86400},
{dur:3,u:"Days",d:3,s:86400},
{dur:4,u:"Days",d:4,s:86400},
{dur:5,u:"Days",d:5,s:86400},
{dur:6,u:"Days",d:6,s:86400},
{dur:7,u:"Days",d:7,s:86400}
];
var times=[];
for(n=0;n<timesData.length;n++){
times.push(timesData[n].dur+" "+timesData[n].u);
}
var durInd=0;
var duration=times[durInd];
function drawWids(){
g.clear();
if(widsOn){
WIDGETS=wids;
Bangle.drawWidgets();
} else {
WIDGETS={};
}
}
function openMenu(){
drawWids();
E.showMenu(menu);
}
function redoMenu(){
clearInterval(rm);
E.showMenu();
openMenu();
}
function refreshMenu(){
rm = setInterval(redoMenu,100);
}
function getF(c){
// Get Fahrenheit temperature from Celsius.
return c*1.8+32;
}
function getT(){
Bangle.getPressure().then(p=>{
temp=p.temperature;
if(tempMode=="drawGraph"&&graph.length>0&&Math.abs(graph[graph.length-1].temp-temp)>10&&readErrCnt<2){
// A large change in temperature may be a reading error. ie. A 0C or less reading after
// a 20C reading. So if this happens, the reading is repeated up to 2 times to hopefully
// skip such errors.
readErrCnt++;
print("readErrCnt "+readErrCnt);
return;
}
clearInterval(gt);
readErrCnt=0;
switch (tempMode){
case "showTemp":
showT();
break;
case "drawGraph":
var date=new Date();
var dateStr=require("locale").date(date).trim();
var hrs=date.getHours();
var mins=date.getMinutes();
var secs=date.getSeconds();
graph.push({
temp:temp,
date:dateStr,
hrs:hrs,
mins:mins,
secs:secs
});
if(graph.length==1){
graph[0].dur=durInd;
}
require("Storage").writeJSON("tempgraph.json", graph);
if(graph.length==150){
clearInterval(dg);
}
drawG();
}
});
}
function getTemp(){
readErrCnt=0;
gt = setInterval(getT,800);
}
function setButton(){
var watchButton=setWatch(function(){
clearInterval(gt);
clearInterval(dg);
clearWatch(watchButton);
Bangle.removeListener("touch",screenTouch);
openMenu();
},BTN);
Bangle.on('touch',screenTouch);
}
function setButton2(){
watchButton2=setWatch(function(){
clearWatch(watchButton2);
openMenu();
},BTN);
}
function zPad(n){
return n.toString().padStart(2,0);
}
function screenTouch(n,ev){
if(ev.y>23&&ev.y<152){
C=C==false;
drawG(false);
}
}
function drawG(){
function cf(t){
if(C){
return t;
}
return getF(t);
}
drawWids();
var top=1;
var bar=21;
var barBot=175-22;
if(widsOn){
top=25;
bar=bar+24;
barBot=barBot-24;
}
var low=graph[0].temp;
var hi=low;
for(n=0;n<graph.length;n++){
var t=graph[n].temp;
if(low>t){
low=t;
}
if(hi<t){
hi=t;
}
}
var tempHi=Math.ceil((cf(hi)+2)/10)*10;
var tempLow=Math.floor((cf(low)-2)/10)*10;
var div=2;
if(tempHi-tempLow>10){
div=5;
}
if(C){
g.setColor(1,0,0);
}else{
g.setColor(0,0,1);
}
var step=(barBot-bar)/((tempHi-tempLow)/div);
for(n=0;n<graph.length;n++){
var pos=tempLow-cf(graph[n].temp);
g.drawLine(n+3,pos*(step/div)+barBot,n+3,barBot+3);
}
g.fillRect(161,barBot+5,174,barBot+20);
g.setColor(1,1,1);
g.setFont("6x8:2");
if(C){
g.drawString("C",163,barBot+5);
}else{
g.drawString("F",163,barBot+5);
}
g.setColor(0,0,0);
g.setFont6x15();
g.drawString("Temperature Graph - "+times[graph[0].dur],1,top);
g.drawRect(2,bar-4,153,barBot+4);
g.setFont("6x8:1");
var num=tempHi;
for(n=bar;n<=barBot;n=n+step){
g.drawLine(3,n,152,n);
g.drawString(num.toString().padStart(3," "),155,n-4);
num=num-div;
}
step=151/timesData[graph[0].dur].d;
for(n=step+2;n<152;n=n+step){
g.drawLine(n,bar-4,n,barBot+4);
}
grSt=graph[0];
g.drawString("Start: "+grSt.date+" "+grSt.hrs+":"+zPad(grSt.mins),1,barBot+6);
var lastT=graph[graph.length-1].temp;
g.drawString("Last Reading:",1,barBot+14);
g.setColor(1,0,0);
g.drawString(lastT.toFixed(1)+"C",85,barBot+14);
g.setColor(0,0,1);
g.drawString(getF(lastT).toFixed(1)+"F",121,barBot+14);
process.memory(true);
}
function drawGraph(){
setButton();
tempMode="drawGraph";
durInd=times.indexOf(duration);
graph=[];
getTemp();
dg=setInterval(getTemp,1000*timesData[durInd].dur*timesData[durInd].s/150);
}
function showGraph(){
setButton();
drawG();
}
function noBluetooth(){
if(NRF.getSecurityStatus().connected){
return false;
}else{
message("Error! Your\nBangle Watch\ncurrently has\nno Bluetooth\nconnection.");
return true;
}
}
function saveGraph(){
if(noBluetooth()){
return;
}
drawG();
g.flip();
g.dump();
message("Graph has\nbeen sent\nto Web IDE\nfor saving.\n");
}
function saveData(){
if(noBluetooth()){
return;
}
drawG();
g.flip();
print("Temperature Graph - "+times[graph[0].dur]+"\n");
print("\"Date\",\"Time\",\"Celsius\",\"Fahrenheit\"");
for(n=0;n<graph.length;n++){
var gr=graph[n];
print("\""+gr.date+"\",\""+gr.hrs+":"+zPad(gr.mins)+":"+zPad(gr.secs)+"\","+gr.temp+","+getF(gr.temp));
}
message("Data has\nbeen sent\nto Web IDE\nfor saving.\n");
}
function message(mes){
setButton2();
var messageLO=new Layout({
type:"v",c:[
{type:"txt",font:"6x8:2",width:171,label:mes,id:"label"},
{type:"btn",font:"6x8:2",pad:3,label:"OK",cb:l=>exit()},
],lazy:true
});
drawWids();
messageLO.render();
}
function showT(){
tempLO.lab1.label=tempLO.lab3.label;
tempLO.lab2.label=tempLO.lab4.label;
tempLO.lab3.label=tempLO.lab5.label;
tempLO.lab4.label=tempLO.lab6.label;
tempLO.lab5.label=temp.toFixed(2)+"C";
tempLO.lab6.label=getF(temp).toFixed(2)+"F";
tempLO.render();
}
function exit(){
clearWatch(watchButton2);
openMenu();
}
function showTemp(){
tempMode="showTemp";
setButton2();
tempLO=new Layout({
type:"v",c:[
{type:"h",c:[
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab1"},
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab2"}
]},
{type:"h",c:[
{type:"txt",pad:5,col:"#f77",font:"6x8:2",label:" ",id:"lab3"},
{type:"txt",pad:5,col:"#77f",font:"6x8:2",label:" ",id:"lab4"}
]},
{type:"h",c:[
{type:"txt",pad:5,col:"#f00",font:"6x8:2",label:" ",id:"lab5"},
{type:"txt",pad:5,col:"#00f",font:"6x8:2",label:" ",id:"lab6"}
]},
{type:"h",c:[
{type:"btn",pad:2,font:"6x8:2",label:"Temp",cb:l=>getTemp()},
{type:"btn",pad:2,font:"6x8:2",label:"Exit",cb:l=>exit()}
]}
]
},{lazy:true});
tempLO.render();
getTemp();
}
var menu={
"":{
"title":" Temp. Graph"
},
"Widgets":{
value:widsOn,
format:vis=>vis?"Hide":"Show",
onchange:vis=>{
widsOn=vis;
refreshMenu();
}
},
"Duration":{
value:times.indexOf(duration),
min:0,max:times.length-1,step:1,wrap:true,
format:tim=>times[tim],
onchange:(dur)=>{
duration=times[dur];
}
},
"Draw Graph":function(){
E.showMenu();
drawGraph();
},
"Show Graph" : function(){
E.showMenu();
if(graph.length>0){
showGraph();
}else{
message("No graph to\nshow as no\ngraph has been\ndrawn yet.");
}
},
"Save Graph" : function(){
E.showMenu();
if(graph.length>0){
saveGraph();
}else{
message("No graph to\nsave as no\ngraph has been\ndrawn yet.");
}
},
"Save Data" : function(){
E.showMenu();
if(graph.length>0){
saveData();
}else{
message("No data to\nsave as no\ngraph has been\ndrawn yet.");
}
},
"Show Temp":function(){
E.showMenu();
showTemp();
}
};
openMenu();

BIN
apps/tempgraph/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,19 @@
{ "id": "tempgraph",
"name": "Temperature Graph",
"shortName":"Temp Graph",
"version":"0.01",
"description": "An app for recording the temperature for time periods ranging from 10 minutes to 7 days.",
"icon": "app.png",
"type": "app",
"tags": "temperature,tempgraph,graph",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"tempgraph.app.js","url":"app.js"},
{"name":"tempgraph.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"tempgraph.json"}
],
"screenshots": [{"url":"screenshot_1.png"},{"url":"screenshot_2.png"},{"url":"screenshot_3.png"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -4,3 +4,4 @@
Add option to show seconds
0.03: Fix Bell not appearing on alarms > 24h and redrawing interval
Update to match the default alarm widget, and not show itself when an alarm is hidden.
0.04: Fix check for active alarm

View File

@ -2,7 +2,7 @@
"id": "widalarmeta",
"name": "Alarm & Timer ETA",
"shortName": "Alarm ETA",
"version": "0.03",
"version": "0.04",
"description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).",
"icon": "widget.png",
"type": "widget",

View File

@ -9,10 +9,10 @@
function draw() {
const times = alarms
.map(alarm => {
.map(alarm =>
alarm.hidden !== true
&& require("sched").getTimeToAlarm(alarm)
})
)
.filter(a => a !== undefined);
const next = times.length > 0 ? Math.min.apply(null, times) : 0;
let calcWidth = 0;