add edgeclk

pull/2964/head
Tim Kuhlmann 2023-08-11 12:46:34 +02:00
parent 6107e9474c
commit bcc77f5f25
10 changed files with 464 additions and 0 deletions

1
apps/edgeclk/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial release.

24
apps/edgeclk/README.md Normal file
View File

@ -0,0 +1,24 @@
# Edge Clock
![Screenshot](screenshot.png)
![Screenshot](screenshot2.png)
![Screenshot](screenshot3.png)
Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability.
It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging.
There are three progress bars that indicate day of the week, time of the day, and daily step goal.
The watch face is monochrome and allows for applying your favorite color scheme.
The appearance is highly configurable. In the settings menu you can:
- De-/activate a buzz when the charger is connected while the watch face is active.
- Decide if month or day should be displayed first.
- Switch between 24h and 12h clock.
- Hide or display seconds.*
- Show AM/PM in place of the seconds.
- Set the daily step goal.
- En- or disable the individual progress bars.
- Set if your week should start with Monday or Sunday (for week progress bar).
*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well.
The clock implements Fast Loading for faster switching to and fro.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIJGtgEDkEAn0b3P8uF8kUK1P+sFclQFCuFMkULCIMgvk+GRH/ABQRF5feGwMH7/KjcAmUGqfLzYLBg1fBYU+s1f/QRDofbDog7Xj8P/EH4f+Ao/5w4FBg8MgOB4eOAo8AgIFCDoIFJhk4Aok5DogFEGoIFBGoIdDAozgLOKNwAovv33wg0f/AFFsGGmEGwf4seOmCnBuALCg/YqFv30wgGf7AFEA"))

306
apps/edgeclk/app.js Normal file
View File

@ -0,0 +1,306 @@
{
/* Configuration
------------------------------------------------------------------------------*/
const settings = Object.assign({
buzzOnCharge: true,
monthFirst: true,
twentyFourH: true,
showAmPm: false,
showSeconds: true,
stepGoal: 10000,
stepBar: true,
weekBar: true,
mondayFirst: true,
dayBar: true,
}, require('Storage').readJSON('edgeclk.settings.json', true) || {});
/* Runtime Variables
------------------------------------------------------------------------------*/
let startTimeout;
let drawInterval;
let lcdPower = true;
let charging = Bangle.isCharging();
const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA');
const iconSize = [19, 26];
const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA=');
const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
/* Draw Functions
------------------------------------------------------------------------------*/
const drawAll = function () {
const date = new Date();
drawDate(date);
if (settings.showSeconds) drawSecs(date);
drawTime(date);
drawLower();
};
const drawLower = function (stepsOnlyCount) {
if (charging) {
drawCharge();
} else {
drawSteps(stepsOnlyCount);
}
};
const drawDate = function (date) {
const top = 30;
g.reset();
// weekday
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
g.setFontAlign(-1, -1); // left top
g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true);
// date
g.setFontAlign(1, -1); // right top
// Note: to save space first and last two lines of ASCII are left out.
// That is why '-' is assigned to '\' and ' ' (space) to '_'.
if (settings.monthFirst) {
g.drawString((date.getMonth()+1).toString().padStart(2, '_')
+ '\\'
+ date.getDate().toString().padStart(2, 0),
g.getWidth(), top + 12, true);
} else {
g.drawString('_'
+ date.getDate().toString().padStart(2, 0)
+ '\\'
+ (date.getMonth()+1).toString(),
g.getWidth(), top + 12, true);
}
// line/progress bar
if (settings.weekBar) {
let weekday = date.getDay();
if (settings.mondayFirst) {
if (weekday === 0) { weekday = 7; }
} else {
weekday += 1;
}
drawBar(top, weekday/7);
} else {
drawLine(top);
}
};
const drawTime = function (date) {
const top = 72;
g.reset();
const h = date.getHours();
g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9)
g.setFontAlign(-1, -1); // left top
g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0),
0, top+12, true);
g.setFontAlign(0, -1); // center top
g.drawString(':', g.getWidth()/2, top+12, false);
const m = date.getMinutes();
g.setFontAlign(1, -1); // right top
g.drawString(m.toString().padStart(2, 0),
g.getWidth(), top+12, true);
if (settings.showAmPm) {
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
g.setFontAlign(1, 1); // right bottom
g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true);
}
if (settings.dayBar) {
drawBar(top, (h*60+m)/1440);
} else {
drawLine(top);
}
};
const drawSecs = function (date) {
g.reset();
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
g.setFontAlign(1, 1); // right bottom
g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true);
};
const drawSteps = function (onlyCount) {
g.reset();
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
g.setFontAlign(-1, 1); // left bottom
const steps = Bangle.getHealthStatus('day').steps;
g.drawString(steps.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true);
if (onlyCount === true) {
return;
}
const progress = steps / settings.stepGoal;
if (settings.stepBar) {
drawBar(g.getHeight() - 38, progress);
} else {
drawLine(g.getHeight() - 38);
}
// icon
if (progress < 1) {
g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]);
} else {
g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]);
}
};
const drawCharge = function () {
g.reset();
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
g.setFontAlign(-1, 1); // left bottom
const charge = E.getBattery();
g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true);
drawBar(g.getHeight() - 38, charge / 100);
g.drawImage(plugIcon, 0, g.getHeight() - 26);
};
const drawBar = function (top, progress) {
g.drawRect(0, top, g.getWidth()-1, top + 5);
g.drawRect(1, top+1, g.getWidth()-2, top + 4);
const barLen = progress > 1 ? g.getWidth() : g.getWidth() * progress;
g.drawLine(2, top+2, barLen, top + 2);
g.drawLine(2, top+3, barLen, top + 3);
};
const drawLine = function (top) {
const width = g.getWidth();
g.drawLine(0, top+2, width, top + 2);
g.drawLine(0, top+3, width, top + 3);
};
/* Event Handlers
------------------------------------------------------------------------------*/
const onSecondInterval = function () {
const date = new Date();
drawSecs(date);
if (date.getSeconds() === 0) {
onMinuteInterval();
}
};
const onMinuteInterval = function () {
const date = new Date();
drawTime(date);
drawLower(true);
};
const onMinuteIntervalStarter = function () {
drawInterval = setInterval(onMinuteInterval, 60000);
startTimeout = null;
onMinuteInterval();
};
const onLcdPower = function (on) {
lcdPower = on;
if (on) {
drawAll();
startTimers();
} else {
stopTimers();
}
};
const onMidnight = function () {
if (!lcdPower) return;
drawDate(new Date());
// Lower part (steps/charge) will be updated every minute.
// However, to save power while on battery only step count will get updated.
// This will update icon and progress bar as well:
if (!charging) drawSteps();
};
const onHealth = function () {
if (!lcdPower || charging) return;
// This will update progress bar and icon:
drawSteps();
};
const onLock = function (locked) {
if (locked) return;
drawLower();
};
const onCharging = function (isCharging) {
charging = isCharging;
if (isCharging && settings.buzzOnCharge) Bangle.buzz();
if (!lcdPower) return;
drawLower();
};
/* Lifecycle Functions
------------------------------------------------------------------------------*/
const registerEvents = function () {
// This is for original Bangle.js; version two has always-on display:
Bangle.on('lcdPower', onLcdPower);
// Midnight event is triggered qhen health data is reset and a new day begins:
Bangle.on('midnight', onMidnight);
// Health data is published via 10 mins interval:
Bangle.on('health', onHealth);
// Lock event signals screen (un)lock:
Bangle.on('lock', onLock);
// Charging event signals when charging status changes:
Bangle.on('charging', onCharging);
};
const deregisterEvents = function () {
Bangle.removeListener('lcdPower', onLcdPower);
Bangle.removeListener('midnight', onMidnight);
Bangle.removeListener('health', onHealth);
Bangle.removeListener('lock', onLock);
Bangle.removeListener('charging', onCharging);
};
const startTimers = function () {
if (drawInterval) return;
if (settings.showSeconds) {
drawInterval = setInterval( onSecondInterval, 1000);
} else {
startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000);
}
};
const stopTimers = function () {
if (startTimeout) clearTimeout(startTimeout);
if (!drawInterval) return;
clearInterval(drawInterval);
drawInterval = null;
};
/* Startup Process
------------------------------------------------------------------------------*/
g.clear();
drawAll();
startTimers();
registerEvents();
Bangle.setUI({mode: 'clock', remove: function() {
stopTimers();
deregisterEvents();
}});
Bangle.loadWidgets();
Bangle.drawWidgets();
}

BIN
apps/edgeclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,20 @@
{
"id": "edgeclk",
"name": "Edge Clock",
"shortName": "Edge Clock",
"version": "0.01",
"description": "Crisp clock with perfect readability.",
"readme": "README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"edgeclk.app.js", "url": "app.js"},
{"name":"edgeclk.settings.js", "url": "settings.js"},
{"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true}
],
"data": [{"name":"edgeclk.settings.json"}]
}

BIN
apps/edgeclk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

112
apps/edgeclk/settings.js Normal file
View File

@ -0,0 +1,112 @@
(function(back) {
const SETTINGS_FILE = 'edgeclk.settings.json';
const storage = require('Storage');
const settings = {
buzzOnCharge: true,
monthFirst: true,
twentyFourH: true,
showAmPm: false,
showSeconds: true,
stepGoal: 10000,
stepBar: true,
weekBar: true,
mondayFirst: true,
dayBar: true,
};
const saved_settings = storage.readJSON(SETTINGS_FILE, true);
if (saved_settings) {
for (const key in saved_settings) {
if (!settings.hasOwnProperty(key)) continue;
settings[key] = saved_settings[key];
}
}
let save = function() {
storage.write(SETTINGS_FILE, settings);
}
E.showMenu({
'': { 'title': 'Edge Clock' },
'< Back': back,
'Charge Buzz': {
value: settings.buzzOnCharge,
onchange: () => {
settings.buzzOnCharge = !settings.buzzOnCharge;
save();
},
},
'Month First': {
value: settings.monthFirst,
onchange: () => {
settings.monthFirst = !settings.monthFirst;
save();
},
},
'24h Clock': {
value: settings.twentyFourH,
onchange: () => {
settings.twentyFourH = !settings.twentyFourH;
save();
},
},
'Show AM/PM': {
value: settings.showAmPm,
onchange: () => {
settings.showAmPm = !settings.showAmPm;
// TODO can this be visually changed?
if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false;
save();
},
},
'Show Seconds': {
value: settings.showSeconds,
onchange: () => {
settings.showSeconds = !settings.showSeconds;
// TODO can this be visually changed?
if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false;
save();
},
},
'Step Goal': {
value: settings.stepGoal,
min: 250,
max: 50000,
step: 250,
onchange: v => {
settings.stepGoal = v;
save();
}
},
'Step Progress': {
value: settings.stepBar,
onchange: () => {
settings.stepBar = !settings.stepBar;
save();
}
},
'Week Progress': {
value: settings.weekBar,
onchange: () => {
settings.weekBar = !settings.weekBar;
save();
},
},
'Week Start': {
value: settings.mondayFirst,
format: () => settings.mondayFirst ? 'Monday' : 'Sunday',
onchange: () => {
settings.mondayFirst = !settings.mondayFirst;
save();
},
},
'Day Progress': {
value: settings.dayBar,
onchange: () => {
settings.dayBar = !settings.dayBar;
save();
},
},
});
})