1
0
Fork 0

Updated infoclk with multiple new features and bugfixes

master
Bruce Blore 2023-05-12 15:49:22 -07:00
parent 01d4cea940
commit 333b957c74
8 changed files with 977 additions and 550 deletions

View File

@ -1,3 +1,13 @@
0.01: New app!
0.02-0.07: Bug fixes
0.08: Submitted to the app loader
0.09: Added weather dependency
Up and down swipes can now be configured separately
The settings menu can now handle having shortcuts configured to apps that were removed
Default notification app is now messageui rather than messages
Support for dual stage unlock
Support for a calendar bar
The clock face is redrawn less often, hoping to save some battery
Option to show the seconds when unlocked, even when otherwise hidden by other settings
Broke out config loading into separate file to avoid duplicating a whole bunch of code
Added support for fast loading

View File

@ -16,6 +16,8 @@ There are generally a few apps that the user uses far more frequently than the o
## Configurability
Dual stage unlock allows for unlocking to be split into two stages: lighting the screen upon the actual unlock, and displaying the extra information and shortcuts after a user-configurable number of taps. This may be useful if you want to quickly glance at the clock with a wrist flick in the dark, or if you want to show the time to other people. Swipe shortcuts are active even after the first stage.
Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics:
* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds.

View File

@ -1,280 +1,302 @@
const SETTINGS_FILE = "infoclk.json";
const FONT = require('infoclk-font.js');
{
const FONT = require('infoclk-font.js');
const storage = require("Storage");
const locale = require("locale");
const weather = require('weather');
const storage = require("Storage");
const locale = require("locale");
const weather = require('weather');
let config = Object.assign({
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
},
let config = require('infoclk-config.js').getConfig();
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
let timeInRange = function (start, time, end) {
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
// Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes();
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
left: '#LAUNCHER',
right: '#LAUNCHER',
},
dayProgress: {
// A progress bar representing how far through the day you are
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}, storage.readJSON(SETTINGS_FILE));
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
function timeInRange(start, time, end) {
// Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes();
// Normalize to prevent the numbers from wrapping around at midnight
if (end <= start) {
end += 2400;
if (timeNumber < start) timeNumber += 2400;
}
return start <= timeNumber && timeNumber <= end;
}
// Return whether settings should be displayed based on the user's configuration
function shouldDisplaySeconds(now) {
return !(
(config.seconds.hideAlways) ||
(config.seconds.hideLocked && Bangle.isLocked()) ||
(E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
);
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
function getFontSize(length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// Get the current day of the week according to user settings
function getDayString(now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
}
// Pad a number with zeros to be the given number of digits
function pad(number, digits) {
let result = '' + number;
while (result.length < digits) result = '0' + result;
return result;
}
// Get the current date formatted according to the user settings
function getDateString(now) {
let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
}
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
function getDayProgress(now) {
let start = config.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes();
let end = config.dayProgress.end;
let reset = config.dayProgress.reset;
// Normalize
if (end <= start) end += 2400;
if (current < start) current += 2400;
if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours
function toDecimalHours(time) {
let hours = Math.floor(time / 100);
let minutes = time % 100;
return hours + (minutes / 60);
}
start = toDecimalHours(start);
current = toDecimalHours(current);
end = toDecimalHours(end);
reset = toDecimalHours(reset);
let progress = (current - start) / (end - start);
if (progress < 0 || progress > 1) {
if (current < reset) return 1;
else return 0;
} else {
return progress;
}
}
// Get a Gadgetbridge weather string
function getWeatherString() {
let current = weather.get();
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
else return 'Weather unknown!';
}
// Get a second weather row showing humidity, wind speed, and wind direction
function getWeatherRow2() {
let current = weather.get();
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
else return 'Check Gadgetbridge';
}
// Get a step string
function getStepsString() {
return '' + Bangle.getHealthStatus('day').steps + ' steps';
}
// Get a health string including daily steps and recent bpm
function getHealthString() {
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
}
// Set the next timeout to draw the screen
let drawTimeout;
function setNextDrawTimeout() {
if (drawTimeout) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let time;
let now = new Date();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
else time = 60000 - (now.getTime() % 60000);
drawTimeout = setTimeout(draw, time);
}
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
const DIGIT_HEIGHT = 64; // How tall the digits are
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// Draw the clock
function draw() {
//Prepare to draw
g.reset()
.setFontAlign(0, 0);
if (E.getBattery() <= config.lowBattColor.level) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
now = new Date();
if (Bangle.isLocked()) { //When the watch is locked
g.clearRect(0, 24, g.getWidth(), g.getHeight());
//Draw the hours and minutes
let x = 0;
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
if (digit == ':') x += COLON_WIDTH;
else x += DIGIT_WIDTH;
}
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
//Draw the seconds if necessary
if (shouldDisplaySeconds(now)) {
let tens = Math.floor(now.getSeconds() / 10);
let ones = now.getSeconds() % 10;
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
// Draw the day of week and date assuming the seconds are displayed
g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
} else {
//Draw the day of week and date without the seconds
let string = getDayString(now) + ' ' + getDateString(now);
g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
// Normalize to prevent the numbers from wrapping around at midnight
if (end <= start) {
end += 2400;
if (timeNumber < start) timeNumber += 2400;
}
// Draw the bottom area
if (config.bottomLocked.display == 'progress') {
let color = config.dayProgress.color;
return start <= timeNumber && timeNumber <= end;
}
// Return whether settings should be displayed based on the user's configuration
let shouldDisplaySeconds = function (now) {
return (config.seconds.forceWhenUnlocked > 0 && getUnlockStage() >= config.seconds.forceWhenUnlocked) || !(
(config.seconds.hideAlways) ||
(config.seconds.hideLocked && getUnlockStage() < 2) ||
(E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
);
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
let getFontSize = function (length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if (size < minSize) return minSize;
else if (size > maxSize) return maxSize;
else return Math.floor(size);
}
// Get the current day of the week according to user settings
let getDayString = function (now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
}
// Pad a number with zeros to be the given number of digits
let pad = function (number, digits) {
let result = '' + number;
while (result.length < digits) result = '0' + result;
return result;
}
// Get the current date formatted according to the user settings
let getDateString = function (now) {
let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
}
// Get a Gadgetbridge weather string
let getWeatherString = function () {
let current = weather.get();
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
else return 'Weather unknown!';
}
// Get a second weather row showing humidity, wind speed, and wind direction
let getWeatherRow2 = function () {
let current = weather.get();
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
else return 'Check Gadgetbridge';
}
// Get a step string
let getStepsString = function () {
return '' + Bangle.getHealthStatus('day').steps + ' steps';
}
// Get a health string including daily steps and recent bpm
let getHealthString = function () {
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
}
// Set the next timeout to draw the screen
let drawTimeout;
let setNextDrawTimeout = function () {
if (drawTimeout !== undefined) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let time;
let now = new Date();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
else time = 60000 - (now.getTime() % 60000);
drawTimeout = setTimeout(drawLockedSeconds, time);
}
/** Return one of the following values:
* 0: Watch is locked
* 1: Watch is unlocked, but should still be displaying the large clock (first stage unlock)
* 2: Watch is unlocked and should be displaying the extra info and icons (second stage unlock)
*/
let getUnlockStage = function () {
if (Bangle.isLocked()) return 0;
else if (dualStageTaps < config.dualStageUnlock) return 1;
else return 2;
}
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
const DIGIT_HEIGHT = 64; // How tall the digits are
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// Draw a bar with the given top and bottom position
let drawBar = function (x1, y1, x2, y2) {
// Draw a day progress bar at the given position with given width and height
let drawDayProgress = function (x1, y1, x2, y2) {
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
let getDayProgress = function (now) {
let start = config.bar.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes();
let end = config.bar.dayProgress.end;
let reset = config.bar.dayProgress.reset;
// Normalize
if (end <= start) end += 2400;
if (current < start) current += 2400;
if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours
let toDecimalHours = function (time) {
let hours = Math.floor(time / 100);
let minutes = time % 100;
return hours + (minutes / 60);
}
start = toDecimalHours(start);
current = toDecimalHours(current);
end = toDecimalHours(end);
reset = toDecimalHours(reset);
let progress = (current - start) / (end - start);
if (progress < 0 || progress > 1) {
if (current < reset) return 1;
else return 0;
} else {
return progress;
}
}
let color = config.bar.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
} else {
.fillRect(x1, y1, x1 + (x2 - x1) * getDayProgress(now), y2);
}
// Draw a calendar bar at the given position with given width and height
let drawCalendar = function (x1, y1, x2, y2) {
let calendar = storage.readJSON('android.calendar.json', true) || [];
let now = (new Date()).getTime();
let endTime = now + config.bar.calendar.duration * 1000;
// Events must end in the future. Requirement to end in the future rather than start is so ongoing events display partially at the left
// Events must start before the end of the lookahead window
// Sort longer events first, so shorter events get placed on top. Tries to prevent the situation where an event entirely within the timespan of another gets completely covered
calendar = calendar.filter(event => ((now < 1000 * (event.timestamp + event.durationInSeconds)) && (event.timestamp * 1000 < endTime)))
.sort((a, b) => { return b.durationInSeconds - a.durationInSeconds; });
pipes = []; // Cache the pipes and draw them all at once, on top of the bar
for (let event of calendar) {
// left = boundary + how far event is in the future mapped from our allowed duration to a distance in pixels, clamped to x1
let leftUnclamped = x1 + (event.timestamp * 1000 - now) * (x2 - x1) / (config.bar.calendar.duration * 1000);
let left = Math.max(leftUnclamped, x1);
// right = unclamped left + how long the event is mapped from seconds to a distance in pixels, clamped to x2
let rightUnclamped = leftUnclamped + event.durationInSeconds * (x2 - x1) / (config.bar.calendar.duration)
let right = Math.min(rightUnclamped, x2);
//Draw the actual bar
if (event.color) g.setColor("#" + (0x1000000 + Number(event.color)).toString(16).padStart(6, "0")); // Line plagiarized from the agenda app
else {
let color = config.bar.calendar.defaultColor;
g.setColor(color[0], color[1], color[2]);
}
g.fillRect(left, y1, right, y2);
// Cache the pipes if necessary
if (leftUnclamped == left) pipes.push(left);
if (rightUnclamped == right) pipes.push(right);
}
// Draw the pipes
let color = config.bar.calendar.pipeColor;
g.setColor(color[0], color[1], color[2]);
for (let pipe of pipes) {
g.fillRect(pipe - 1, y1, pipe + 1, y2);
}
}
if (config.bar.type == 'dayProgress') {
drawDayProgress(x1, y1, x2, y2);
} else if (config.bar.type == 'calendar') {
drawCalendar(x1, y1, x2, y2);
} else if (config.bar.type == 'split') {
let xavg = (x1 + x2) / 2;
drawDayProgress(x1, y1, xavg, y2);
drawCalendar(xavg, y1, x2, y2);
g.setColor(g.theme.fg).fillRect(xavg - 1, y1, xavg + 1, y2);
}
}
// Return whether low battery behavior should be used.
// - If the watch isn't charging and the battery is low, mark it low. Once the battery is marked low, it stays marked low for subsequent calls.
// - When the watch sees external power, unmark the low battery.
// This allows us to redraw the full time in the low battery color to avoid only the seconds changing, but still do it once. And it avoids alternating.
let lowBattery = false;
let checkLowBattery = function () {
if (!Bangle.isCharging() && E.getBattery() <= config.lowBattColor.level) lowBattery = true;
else if (Bangle.isCharging()) lowBattery = false;
return lowBattery;
}
let onCharging = charging => {
checkLowBattery();
drawLockedSeconds(true);
}
Bangle.on('charging', onCharging);
// Draw the big seconds that are displayed when the screen is locked. Call drawClock if anything else needs to be updated
let drawLockedSeconds = function (forceDrawClock) {
// If the watch is in the second stage of unlock, call drawClock()
if (getUnlockStage() == 2) {
drawClock();
setNextDrawTimeout();
return
}
now = new Date();
// If we should not be displaying the seconds right now, call drawClock()
if (!shouldDisplaySeconds(now)) {
drawClock();
setNextDrawTimeout();
return;
}
// If the seconds are zero, or we are forced to raw the clock, call drawClock() but also display the seconds
else if (now.getSeconds() == 0 || forceDrawClock) {
drawClock();
}
// If none of the prior conditions are met, draw the seconds only and do not call drawClock()
g.reset()
.setFontAlign(0, 0)
.clearRect(SECONDS_LEFT, SECONDS_TOP, g.getWidth(), SECONDS_TOP + DIGIT_HEIGHT);
// If the battery is low, redraw the clock so it can change color
if (checkLowBattery()) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
let tens = Math.floor(now.getSeconds() / 10);
let ones = now.getSeconds() % 10;
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
setNextDrawTimeout();
}
// Draw the bottom text area
let drawBottomText = function () {
g.clearRect(0, SECONDS_TOP + DIGIT_HEIGHT, g.getWidth(), g.getHeight());
if (config.bottomLocked.display == 'progress') drawBar(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth(), g.getHeight());
else {
let bottomString;
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
@ -282,124 +304,214 @@ function draw() {
else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
else bottomString = ' ';
g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
g.reset()
.setFontAlign(0, 0)
.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
.drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
}
}
// Draw the day progress bar between the rows if necessary
if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP);
// Draw the clock
let drawClock = function (now) {
//Prepare to draw
g.reset()
.setFontAlign(0, 0);
if (checkLowBattery()) {
let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]);
}
} else {
if (now == undefined) now = new Date();
//If the watch is unlocked
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
getHealthString(),
getWeatherString(),
getWeatherRow2()
];
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
//When the watch is locked or in first stage
if (getUnlockStage() < 2) {
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length);
//Draw the hours and minutes
g.clearRect(0, 24, g.getWidth(), SECONDS_TOP);
let x = 0;
let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) {
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
g.setFont('Vector', size)
.drawString(row, g.getWidth() / 2, y);
y += maxHeight;
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
if (digit == ':') x += COLON_WIDTH;
else x += DIGIT_WIDTH;
}
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
// If the seconds should be displayed, don't use the area when drawing the date
if (shouldDisplaySeconds(now)) {
g.clearRect(0, SECONDS_TOP, SECONDS_LEFT, SECONDS_TOP + DIGIT_HEIGHT)
.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
}
// Otherwise, use the seconds area
else {
let string = getDayString(now) + ' ' + getDateString(now);
g.clearRect(0, SECONDS_TOP, g.getWidth(), SECONDS_TOP + DIGIT_HEIGHT)
.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
}
drawBottomText();
// Draw the bar between the rows if necessary
if (config.bar.enabledLocked && config.bottomLocked.display != 'progress') drawBar(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth(), SECONDS_TOP);
}
// When watch in second stage
else {
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
getHealthString(),
getWeatherString(),
getWeatherRow2()
];
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
if (config.dayProgress.enabledUnlocked) {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2);
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.bar.enabledUnlocked ? (rows.length + 1) : rows.length);
let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) {
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
g.setFont('Vector', size)
.drawString(row, g.getWidth() / 2, y);
y += maxHeight;
}
if (config.bar.enabledUnlocked) drawBar(0, y - maxHeight / 2, g.getWidth(), y + maxHeight / 2);
}
}
setNextDrawTimeout();
}
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
function drawIcons() {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
let appId = config.shortcuts[i];
let appInfo = storage.readJSON(appId + '.info', 1);
if (!appInfo) continue;
icon = storage.read(appInfo.icon);
g.drawImage(icon, x, y, {
scale: 0.916666666667
});
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
let drawIcons = function () {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
let appId = config.shortcuts[i];
let appInfo = storage.readJSON(appId + '.info', 1);
if (!appInfo) continue;
icon = storage.read(appInfo.icon);
g.drawImage(icon, x, y, {
scale: 0.916666666667
});
}
}
}
weather.on("update", draw);
Bangle.on("step", draw);
Bangle.on('lock', locked => {
//If the watch is unlocked, draw the icons
if (!locked) drawIcons();
draw();
});
// Draw only the bottom row if we are in first or second stage unlock, otherwise call drawClock()
let drawBottomRowOrClock = function () {
if (getUnlockStage() < 2) drawBottomText();
else drawClock();
}
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
weather.on("update", drawBottomRowOrClock);
Bangle.on("step", drawBottomRowOrClock);
let onLock = locked => {
//If the watch is unlocked and the necessary number of dual stage taps have been performed, draw the shortcuts
if (!locked && dualStageTaps >= config.dualStageUnlock) drawIcons();
// Launch an app given the current ID. Handles special cases:
// false: Do nothing
// '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing
function launch(appId) {
if (appId == false) return;
else if (appId == '#LAUNCHER') {
Bangle.buzz();
Bangle.showLauncher();
} else {
let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) {
// If locked, reset dual stage taps to zero
else if (locked) dualStageTaps = 0;
drawLockedSeconds(true);
};
Bangle.on('lock', onLock);
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();
// Launch an app given the current ID. Handles special cases:
// false: Do nothing
// '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing
let launch = function (appId, fast) {
if (appId == false) return;
else if (appId == '#LAUNCHER') {
Bangle.buzz();
load(appInfo.src);
Bangle.showLauncher();
} else {
let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) {
Bangle.buzz();
if (fast) Bangle.load(appInfo.src);
else load(appInfo.src);
}
}
}
}
//Set up touch to launch the selected app
Bangle.on('touch', function (button, xy) {
let x = Math.floor(xy.x / 44);
if (x < 0) x = 0;
else if (x > 3) x = 3;
//Set up touch to launch the selected app, and to handle dual stage unlock
let dualStageTaps = 0;
let y = Math.floor(xy.y / 44);
if (y < 0) y = -1;
else if (y > 3) y = 1;
else y -= 2;
let onTouch = function (button, xy) {
// If only the first stage has been unlocked, increase the counter
if (dualStageTaps < config.dualStageUnlock) {
dualStageTaps++;
Bangle.buzz();
if (y < 0) {
Bangle.buzz();
Bangle.showLauncher();
} else {
let i = 4 * y + x;
launch(config.shortcuts[i]);
// If we reach the unlock threshold, redraw the screen because we have now done the second unlock stage
if (dualStageTaps == config.dualStageUnlock) {
drawIcons();
drawClock();
setNextDrawTimeout(); // In case we need to replace an every minute timeout with an every second timeout
}
// If we have unlocked both stages, handle a shortcut tap
} else {
let x = Math.floor(xy.x / 44);
if (x < 0) x = 0;
else if (x > 3) x = 3;
let y = Math.floor(xy.y / 44);
if (y < 0) y = -1;
else if (y > 3) y = 1;
else y -= 2;
if (y < 0) {
Bangle.buzz();
Bangle.showLauncher();
} else {
let i = 4 * y + x;
launch(config.shortcuts[i], config.fastLoad.shortcuts[i]);
}
}
};
Bangle.on('touch', onTouch);
//Set up swipe handler
let onSwipe = function (lr, ud) {
if (lr == -1) launch(config.swipe.left, config.fastLoad.swipe.left);
else if (lr == 1) launch(config.swipe.right, config.fastLoad.swipe.right);
else if (ud == -1) launch(config.swipe.up, config.fastLoad.swipe.up);
else if (ud == 1) launch(config.swipe.down, config.fastLoad.swipe.down);
};
Bangle.on('swipe', onSwipe);
// If the clock starts with the watch unlocked, the first stage of unlocking is skipped
if (!Bangle.isLocked()) {
dualStageTaps = config.dualStageUnlock;
drawIcons();
}
});
//Set up swipe handler
Bangle.on('swipe', function (direction) {
if (direction == -1) launch(config.swipe.left);
else if (direction == 0) launch(config.swipe.up);
else launch(config.swipe.right);
});
// Show launcher when middle button pressed, and enable fast loading
Bangle.setUI({
mode: "clock", remove: () => {
if (drawTimeout !== undefined) {
clearTimeout(drawTimeout);
drawTimeout = undefined;
}
Bangle.removeListener('charging', onCharging);
weather.removeListener('update', drawBottomRowOrClock);
Bangle.removeListener('step', drawBottomRowOrClock);
Bangle.removeListener('lock', onLock);
Bangle.removeListener('touch', onTouch);
Bangle.removeListener('swipe', onSwipe);
g.reset();
}
});
if (!Bangle.isLocked()) drawIcons();
drawLockedSeconds(true);
draw();
}

124
apps/infoclk/configLoad.js Normal file
View File

@ -0,0 +1,124 @@
const storage = require("Storage");
const SETTINGS_FILE = "infoclk.json";
let defaultConfig = {
dualStageUnlock: 0,
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
forceWhenUnlocked: 1, // Force the seconds to be displayed when the watch is unlocked, no matter the other settings. 0 = never, 1 = first or second stage unlock, 2 = second stage unlock only
},
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 4 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messageui', // Swipe up or swipe down, due to limitation of event handler
down: 'messageui',
left: '#LAUNCHER',
right: '#LAUNCHER',
},
fastLoad: {
shortcuts: [
false, false, false, false,
false, false, false, false
],
swipe: {
up: false,
down: false,
left: false,
right: false
}
},
bar: {
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
type: 'split', // off = no bar, dayProgress = day progress bar, calendar = calendar bar, split = both
dayProgress: { // A progress bar representing how far through the day you are
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
calendar: {
duration: 10800,
pipeColor: [1, 1, 1],
defaultColor: [0, 0, 1]
},
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}
let storedConfig = storage.readJSON(SETTINGS_FILE, true) || {};
// Ugly slow workaround because object.constructor doesn't exist on Bangle
function isDictionary(object) {
return JSON.stringify(object)[0] == '{';
}
/** Merge two objects recursively. (Object.assign() cannot be used here because it is NOT recursive.)
* Any key that is in one object but not the other will be included as is.
* Any key that is in both objects, but whose value is not a dictionary in both objects, will have the version in overlay included.
* Any key that whose value is a dictionary in both properties will have its result be set to a recursive call to merge.
*/
function merge(overlay, base) {
let result = base;
for (objectKey in overlay) {
if (!Object.keys(base).includes(objectKey)) result[objectKey] = overlay[objectKey]; // If the key isn't there, add it
else if (isDictionary(base[objectKey]) && isDictionary(overlay[objectKey])) // If the key is a dictionary in both, do recursive call
result[objectKey] = merge(overlay[objectKey], base[objectKey]);
else result[objectKey] = overlay[objectKey]; // Otherwise, override
}
return result;
}
exports.getConfig = () => {
return merge(storedConfig, defaultConfig);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,8 +1,7 @@
{
"id": "infoclk",
"name": "Informational clock",
"version": "0.08",
"dependencies": {"weather":"app"},
"version": "0.09",
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
"readme": "README.md",
"icon": "icon.png",
@ -24,6 +23,10 @@
"name": "infoclk-font.js",
"url": "font.js"
},
{
"name": "infoclk-config.js",
"url": "configLoad.js"
},
{
"name": "infoclk.img",
"url": "icon.js",

View File

@ -2,71 +2,7 @@
const SETTINGS_FILE = "infoclk.json";
const storage = require('Storage');
let config = Object.assign({
seconds: {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked: false, // Hide the seconds when the display is locked.
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd: 700, // The time when the seconds are shown again
hideAlways: false, // Always hide (never show) the seconds
},
date: {
// Settings related to the display of the date
mmdd: true, // If true, display the month first. If false, display the date first.
separator: '-', // The character that goes between the month and date
monthName: false, // If false, display the month as a number. If true, display the name.
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
},
bottomLocked: {
display: 'weather' // What to display in the bottom row when locked:
// 'weather': The current temperature and weather description
// 'steps': Step count
// 'health': Step count and bpm
// 'progress': Day progress bar
// false: Nothing
},
shortcuts: [
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
'stlap', 'keytimer', 'pomoplus', 'alarm',
'rpnsci', 'calendar', 'torch', 'weather'
],
swipe: {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
left: '#LAUNCHER',
right: '#LAUNCHER',
},
dayProgress: {
// A progress bar representing how far through the day you are
enabledLocked: true, // Whether this bar is enabled when the watch is locked
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
color: [0, 0, 1], // The color of the bar
start: 700, // The time of day that the bar starts filling
end: 2200, // The time of day that the bar becomes full
reset: 300 // The time of day when the progress bar resets from full to empty
},
lowBattColor: {
// The text can change color to indicate that the battery is low
level: 20, // The percentage where this happens
color: [1, 0, 0] // The color that the text changes to
}
}, storage.readJSON(SETTINGS_FILE));
let config = require('infoclk-config.js').getConfig();
function saveSettings() {
storage.writeJSON(SETTINGS_FILE, config);
@ -172,6 +108,18 @@
}
}
});
},
'...unconditionally when unlocked': {
value: config.seconds.forceWhenUnlocked,
format: value => ['No', 'First or second stage', 'Second stage only'][value],
onchange: value => {
config.seconds.forceWhenUnlocked = value;
saveSettings();
},
min: 0,
max: 2,
step: 1,
wrap: false
}
});
}
@ -190,7 +138,7 @@
{ name: 'Weather', val: 'weather' },
{ name: 'Step count', val: 'steps' },
{ name: 'Steps + BPM', val: 'health' },
{ name: 'Day progresss bar', val: 'progress' },
{ name: 'Bar', val: 'progress' },
{ name: 'Nothing', val: false }
];
@ -213,7 +161,7 @@
name: appInfo.name,
val: appInfo.id
});
}
};
E.showMenu({
'': {
@ -222,128 +170,260 @@
},
'Top first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[0] = shortcutOptions[value].val;
config.fastLoad.shortcuts[0] = false;
saveSettings();
}
},
'Top second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[1] = shortcutOptions[value].val;
config.fastLoad.shortcuts[1] = false;
saveSettings();
}
},
'Top third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[2] = shortcutOptions[value].val;
config.fastLoad.shortcuts[2] = false;
saveSettings();
}
},
'Top fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[3] = shortcutOptions[value].val;
config.fastLoad.shortcuts[3] = false;
saveSettings();
}
},
'Bottom first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[4] = shortcutOptions[value].val;
config.fastLoad.shortcuts[4] = false;
saveSettings();
}
},
'Bottom second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[5] = shortcutOptions[value].val;
config.fastLoad.shortcuts[5] = false;
saveSettings();
}
},
'Bottom third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[6] = shortcutOptions[value].val;
config.fastLoad.shortcuts[6] = false;
saveSettings();
}
},
'Bottom fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.shortcuts[7] = shortcutOptions[value].val;
config.fastLoad.shortcuts[7] = false;
saveSettings();
}
},
'Swipe up': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.up = shortcutOptions[value].val;
config.fastLoad.swipe.up = false;
saveSettings();
}
},
'Swipe down': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.down),
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.down = shortcutOptions[value].val;
config.fastLoad.swipe.down = false;
saveSettings();
}
},
'Swipe left': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.left = shortcutOptions[value].val;
config.fastLoad.swipe.left = false;
saveSettings();
}
},
'Swipe right': {
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right),
format: value => shortcutOptions[value].name,
format: value => (value == -1) ? 'Unknown app!' : shortcutOptions[value].name,
min: 0,
max: shortcutOptions.length - 1,
wrap: false,
onchange: value => {
config.swipe.right = shortcutOptions[value].val;
config.fastLoad.swipe.right = false;
saveSettings();
}
},
}
});
}
// The menu for configuring which apps can be fast loaded
function showFastLoadMenu() {
E.showMenu();
E.showAlert(/*LANG*/"WARNING! Only enable fast loading for apps that use widgets.").then(() => {
E.showMenu({
'': {
'title': 'Shortcuts',
'back': showMainMenu
},
'Top first': {
value: config.fastLoad.shortcuts[0],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[0] = value;
saveSettings();
}
},
'Top second': {
value: config.fastLoad.shortcuts[1],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[1] = value;
saveSettings();
}
},
'Top third': {
value: config.fastLoad.shortcuts[2],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[2] = value;
saveSettings();
}
},
'Top fourth': {
value: config.fastLoad.shortcuts[3],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[3] = value;
saveSettings();
}
},
'Bottom first': {
value: config.fastLoad.shortcuts[4],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[4] = value;
saveSettings();
}
},
'Bottom second': {
value: config.fastLoad.shortcuts[5],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[5] = value;
saveSettings();
}
},
'Bottom third': {
value: config.fastLoad.shortcuts[6],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[6] = value;
saveSettings();
}
},
'Bottom fourth': {
value: config.fastLoad.shortcuts[7],
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.shortcuts[7] = value;
saveSettings();
}
},
'Swipe up': {
value: config.fastLoad.swipe.up,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.up = value;
saveSettings();
}
},
'Swipe down': {
value: config.fastLoad.swipe.down,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.down = value;
saveSettings();
}
},
'Swipe left': {
value: config.fastLoad.swipe.left,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.left = value;
saveSettings();
}
},
'Swipe right': {
value: config.fastLoad.swipe.right,
format: value => value ? 'Fast' : 'Slow',
onchange: value => {
config.fastLoad.swipe.right = value;
saveSettings();
}
}
});
})
}
const COLOR_OPTIONS = [
{ name: 'Black', val: [0, 0, 0] },
{ name: 'Blue', val: [0, 0, 1] },
@ -355,11 +435,197 @@
{ name: 'White', val: [1, 1, 1] }
];
const BAR_MODE_OPTIONS = [
{ name: 'None', val: 'off' },
{ name: 'Day progress only', val: 'dayProgress' },
{ name: 'Calendar only', val: 'calendar' },
{ name: 'Split', val: 'split' }
];
// Workaround for being unable to use == on arrays: convert them into strings
function colorString(color) {
return `${color[0]} ${color[1]} ${color[2]}`;
}
//Menu to configure the bar
function showBarMenu() {
E.showMenu({
'': {
'title': 'Bar',
'back': showMainMenu
},
'Enable while locked': {
value: config.bar.enabledLocked,
onchange: value => {
config.bar.enableLocked = value;
saveSettings();
}
},
'Enable while unlocked': {
value: config.bar.enabledUnlocked,
onchange: value => {
config.bar.enabledUnlocked = value;
saveSettings();
}
},
'Mode': {
value: BAR_MODE_OPTIONS.map(item => item.val).indexOf(config.bar.type),
format: value => BAR_MODE_OPTIONS[value].name,
onchange: value => {
config.bar.type = BAR_MODE_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: BAR_MODE_OPTIONS.length - 1,
wrap: true
},
'Day progress': () => {
E.showMenu({
'': {
'title': 'Day progress',
'back': showBarMenu
},
'Color': {
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.bar.dayProgress.color)),
format: value => COLOR_OPTIONS[value].name,
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: false,
onchange: value => {
config.bar.dayProgress.color = COLOR_OPTIONS[value].val;
saveSettings();
}
},
'Start hour': {
value: Math.floor(config.bar.dayProgress.start / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.start % 100;
config.bar.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'Start minute': {
value: config.bar.dayProgress.start % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.start / 100);
config.bar.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'End hour': {
value: Math.floor(config.bar.dayProgress.end / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.end % 100;
config.bar.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'End minute': {
value: config.bar.dayProgress.end % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.end / 100);
config.bar.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'Reset hour': {
value: Math.floor(config.bar.dayProgress.reset / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.bar.dayProgress.reset % 100;
config.bar.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
},
'Reset minute': {
value: config.bar.dayProgress.reset % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.bar.dayProgress.reset / 100);
config.bar.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
}
});
},
'Calendar bar': () => {
E.showMenu({
'': {
'title': 'Calendar bar',
'back': showBarMenu
},
'Look ahead duration': {
value: config.bar.calendar.duration,
format: value => {
let hours = value / 3600;
let minutes = (value % 3600) / 60;
let seconds = value % 60;
let result = (hours == 0) ? '' : `${hours} hr`;
if (minutes != 0) {
if (result == '') result = `${minutes} min`;
else result += `, ${minutes} min`;
}
if (seconds != 0) {
if (result == '') result = `${seconds} sec`;
else result += `, ${seconds} sec`;
}
return result;
},
onchange: value => {
config.bar.calendar.duration = value;
saveSettings();
},
min: 900,
max: 86400,
step: 900
},
'Pipe color': {
value: COLOR_OPTIONS.map(color => colorString(color.val)).indexOf(colorString(config.bar.calendar.pipeColor)),
format: value => COLOR_OPTIONS[value].name,
onchange: value => {
config.bar.calendar.pipeColor = COLOR_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: true
},
'Default color': {
value: COLOR_OPTIONS.map(color => colorString(color.val)).indexOf(colorString(config.bar.calendar.defaultColor)),
format: value => COLOR_OPTIONS[value].name,
onchange: value => {
config.bar.calendar.defaultColor = COLOR_OPTIONS[value].val;
saveSettings();
},
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: true
}
});
}
});
}
//Shows the top level menu
function showMainMenu() {
E.showMenu({
@ -367,6 +633,16 @@
'title': 'Informational Clock',
'back': back
},
'Dual stage unlock': {
value: config.dualStageUnlock,
format: value => (value == 0) ? "Off" : `${value} taps`,
min: 0,
step: 1,
onchange: value => {
config.dualStageUnlock = value;
saveSettings();
}
},
'Seconds display': showSecondsMenu,
'Day of week format': {
value: config.date.dayFullName,
@ -433,108 +709,8 @@
}
},
'Shortcuts': showShortcutMenu,
'Day progress': () => {
E.showMenu({
'': {
'title': 'Day progress',
'back': showMainMenu
},
'Enable while locked': {
value: config.dayProgress.enabledLocked,
onchange: value => {
config.dayProgress.enableLocked = value;
saveSettings();
}
},
'Enable while unlocked': {
value: config.dayProgress.enabledUnlocked,
onchange: value => {
config.dayProgress.enabledUnlocked = value;
saveSettings();
}
},
'Color': {
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.dayProgress.color)),
format: value => COLOR_OPTIONS[value].name,
min: 0,
max: COLOR_OPTIONS.length - 1,
wrap: false,
onchange: value => {
config.dayProgress.color = COLOR_OPTIONS[value].val;
saveSettings();
}
},
'Start hour': {
value: Math.floor(config.dayProgress.start / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.start % 100;
config.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'Start minute': {
value: config.dayProgress.start % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.start / 100);
config.dayProgress.start = (100 * hour) + minute;
saveSettings();
}
},
'End hour': {
value: Math.floor(config.dayProgress.end / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.end % 100;
config.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'End minute': {
value: config.dayProgress.end % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.end / 100);
config.dayProgress.end = (100 * hour) + minute;
saveSettings();
}
},
'Reset hour': {
value: Math.floor(config.dayProgress.reset / 100),
format: hourToString,
min: 0,
max: 23,
wrap: true,
onchange: hour => {
minute = config.dayProgress.reset % 100;
config.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
},
'Reset minute': {
value: config.dayProgress.reset % 100,
min: 0,
max: 59,
wrap: true,
onchange: minute => {
hour = Math.floor(config.dayProgress.reset / 100);
config.dayProgress.reset = (100 * hour) + minute;
saveSettings();
}
}
});
},
'Fast load shortcuts': showFastLoadMenu,
'Bar': showBarMenu,
'Low battery color': () => {
E.showMenu({
'': {