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.01: New app!
0.02-0.07: Bug fixes 0.02-0.07: Bug fixes
0.08: Submitted to the app loader 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 ## 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: 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. * 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,78 +1,14 @@
const SETTINGS_FILE = "infoclk.json"; {
const FONT = require('infoclk-font.js'); const FONT = require('infoclk-font.js');
const storage = require("Storage"); const storage = require("Storage");
const locale = require("locale"); const locale = require("locale");
const weather = require('weather'); const weather = require('weather');
let config = Object.assign({ let config = require('infoclk-config.js').getConfig();
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));
// 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 // 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) { let timeInRange = function (start, time, end) {
// Convert the given date object to a time number // Convert the given date object to a time number
let timeNumber = time.getHours() * 100 + time.getMinutes(); let timeNumber = time.getHours() * 100 + time.getMinutes();
@ -87,17 +23,17 @@ function timeInRange(start, time, end) {
} }
// Return whether settings should be displayed based on the user's configuration // Return whether settings should be displayed based on the user's configuration
function shouldDisplaySeconds(now) { let shouldDisplaySeconds = function (now) {
return !( return (config.seconds.forceWhenUnlocked > 0 && getUnlockStage() >= config.seconds.forceWhenUnlocked) || !(
(config.seconds.hideAlways) || (config.seconds.hideAlways) ||
(config.seconds.hideLocked && Bangle.isLocked()) || (config.seconds.hideLocked && getUnlockStage() < 2) ||
(E.getBattery() <= config.seconds.hideBattery) || (E.getBattery() <= config.seconds.hideBattery) ||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd)) (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 // 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 getFontSize = function (length, maxWidth, minSize, maxSize) {
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character 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 size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
@ -108,20 +44,20 @@ function getFontSize(length, maxWidth, minSize, maxSize) {
} }
// Get the current day of the week according to user settings // Get the current day of the week according to user settings
function getDayString(now) { let getDayString = function (now) {
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()]; 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()]; else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
} }
// Pad a number with zeros to be the given number of digits // Pad a number with zeros to be the given number of digits
function pad(number, digits) { let pad = function (number, digits) {
let result = '' + number; let result = '' + number;
while (result.length < digits) result = '0' + result; while (result.length < digits) result = '0' + result;
return result; return result;
} }
// Get the current date formatted according to the user settings // Get the current date formatted according to the user settings
function getDateString(now) { let getDateString = function (now) {
let month; let month;
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2); 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 if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
@ -131,12 +67,83 @@ function getDateString(now) {
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`; 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 // 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 getDayProgress = function (now) {
let start = config.dayProgress.start; let start = config.bar.dayProgress.start;
let current = now.getHours() * 100 + now.getMinutes(); let current = now.getHours() * 100 + now.getMinutes();
let end = config.dayProgress.end; let end = config.bar.dayProgress.end;
let reset = config.dayProgress.reset; let reset = config.bar.dayProgress.reset;
// Normalize // Normalize
if (end <= start) end += 2400; if (end <= start) end += 2400;
@ -144,7 +151,7 @@ function getDayProgress(now) {
if (reset < start) reset += 2400; if (reset < start) reset += 2400;
// Convert an hhmm number into a floating-point hours // Convert an hhmm number into a floating-point hours
function toDecimalHours(time) { let toDecimalHours = function (time) {
let hours = Math.floor(time / 100); let hours = Math.floor(time / 100);
let minutes = time % 100; let minutes = time % 100;
@ -166,78 +173,161 @@ function getDayProgress(now) {
} }
} }
// Get a Gadgetbridge weather string let color = config.bar.dayProgress.color;
function getWeatherString() { g.setColor(color[0], color[1], color[2])
let current = weather.get(); .fillRect(x1, y1, x1 + (x2 - x1) * getDayProgress(now), y2);
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 // Draw a calendar bar at the given position with given width and height
function getWeatherRow2() { let drawCalendar = function (x1, y1, x2, y2) {
let current = weather.get(); let calendar = storage.readJSON('android.calendar.json', true) || [];
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`; let now = (new Date()).getTime();
else return 'Check Gadgetbridge'; 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);
} }
// Get a step string // Draw the pipes
function getStepsString() { let color = config.bar.calendar.pipeColor;
return '' + Bangle.getHealthStatus('day').steps + ' steps'; g.setColor(color[0], color[1], color[2]);
for (let pipe of pipes) {
g.fillRect(pipe - 1, y1, pipe + 1, y2);
}
} }
// Get a health string including daily steps and recent bpm if (config.bar.type == 'dayProgress') {
function getHealthString() { drawDayProgress(x1, y1, x2, y2);
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`; } 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);
}
} }
// Set the next timeout to draw the screen // Return whether low battery behavior should be used.
let drawTimeout; // - 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.
function setNextDrawTimeout() { // - When the watch sees external power, unmark the low battery.
if (drawTimeout) { // 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.
clearTimeout(drawTimeout); let lowBattery = false;
drawTimeout = undefined; let checkLowBattery = function () {
if (!Bangle.isCharging() && E.getBattery() <= config.lowBattColor.level) lowBattery = true;
else if (Bangle.isCharging()) lowBattery = false;
return lowBattery;
} }
let time; let onCharging = charging => {
let now = new Date(); checkLowBattery();
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000); drawLockedSeconds(true);
else time = 60000 - (now.getTime() % 60000); }
Bangle.on('charging', onCharging);
drawTimeout = setTimeout(draw, time); // 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();
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) // If we should not be displaying the seconds right now, call drawClock()
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space if (!shouldDisplaySeconds(now)) {
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space drawClock();
const DIGIT_HEIGHT = 64; // How tall the digits are setNextDrawTimeout();
return;
}
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space // If the seconds are zero, or we are forced to raw the clock, call drawClock() but also display the seconds
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon else if (now.getSeconds() == 0 || forceDrawClock) {
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 drawClock();
}
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start // If none of the prior conditions are met, draw the seconds only and do not call drawClock()
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row g.reset()
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row .setFontAlign(0, 0)
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it .clearRect(SECONDS_LEFT, SECONDS_TOP, g.getWidth(), SECONDS_TOP + DIGIT_HEIGHT);
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
// 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();
else if (config.bottomLocked.display == 'steps') bottomString = getStepsString();
else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
else bottomString = ' ';
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 clock // Draw the clock
function draw() { let drawClock = function (now) {
//Prepare to draw //Prepare to draw
g.reset() g.reset()
.setFontAlign(0, 0); .setFontAlign(0, 0);
if (E.getBattery() <= config.lowBattColor.level) { if (checkLowBattery()) {
let color = config.lowBattColor.color; let color = config.lowBattColor.color;
g.setColor(color[0], color[1], color[2]); g.setColor(color[0], color[1], color[2]);
} }
now = new Date(); if (now == undefined) now = new Date();
if (Bangle.isLocked()) { //When the watch is locked //When the watch is locked or in first stage
g.clearRect(0, 24, g.getWidth(), g.getHeight()); if (getUnlockStage() < 2) {
//Draw the hours and minutes //Draw the hours and minutes
g.clearRect(0, 24, g.getWidth(), SECONDS_TOP);
let x = 0; 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 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
@ -247,54 +337,29 @@ function draw() {
} }
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP); if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
//Draw the seconds if necessary // If the seconds should be displayed, don't use the area when drawing the date
if (shouldDisplaySeconds(now)) { if (shouldDisplaySeconds(now)) {
let tens = Math.floor(now.getSeconds() / 10); g.clearRect(0, SECONDS_TOP, SECONDS_LEFT, SECONDS_TOP + DIGIT_HEIGHT)
let ones = now.getSeconds() % 10; .setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
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) .drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT)) .setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y); .drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
}
} else { // Otherwise, use the seconds area
//Draw the day of week and date without the seconds else {
let string = getDayString(now) + ' ' + getDateString(now); let string = getDayString(now) + ' ' + getDateString(now);
g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT)) 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); .drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
} }
// Draw the bottom area drawBottomText();
if (config.bottomLocked.display == 'progress') {
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
} else {
let bottomString;
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString(); // Draw the bar between the rows if necessary
else if (config.bottomLocked.display == 'steps') bottomString = getStepsString(); if (config.bar.enabledLocked && config.bottomLocked.display != 'progress') drawBar(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth(), SECONDS_TOP);
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)))
.drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
} }
// When watch in second stage
// Draw the day progress bar between the rows if necessary else {
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);
}
} else {
//If the watch is unlocked
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2); g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
rows = [ rows = [
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`, `${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
@ -305,7 +370,7 @@ function draw() {
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2); if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM'); if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length); let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.bar.enabledUnlocked ? (rows.length + 1) : rows.length);
let y = HHMM_TOP + maxHeight / 2; let y = HHMM_TOP + maxHeight / 2;
for (let row of rows) { for (let row of rows) {
@ -315,18 +380,12 @@ function draw() {
y += maxHeight; y += maxHeight;
} }
if (config.dayProgress.enabledUnlocked) { if (config.bar.enabledUnlocked) drawBar(0, y - maxHeight / 2, g.getWidth(), y + maxHeight / 2);
let color = config.dayProgress.color;
g.setColor(color[0], color[1], color[2])
.fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), 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. // 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() { let drawIcons = function () {
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()); g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
let x = [0, 44, 88, 132, 0, 44, 88, 132][i]; let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
@ -341,16 +400,25 @@ function drawIcons() {
} }
} }
weather.on("update", draw); // Draw only the bottom row if we are in first or second stage unlock, otherwise call drawClock()
Bangle.on("step", draw); let drawBottomRowOrClock = function () {
Bangle.on('lock', locked => { if (getUnlockStage() < 2) drawBottomText();
//If the watch is unlocked, draw the icons else drawClock();
if (!locked) drawIcons(); }
draw();
}); 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();
// If locked, reset dual stage taps to zero
else if (locked) dualStageTaps = 0;
drawLockedSeconds(true);
};
Bangle.on('lock', onLock);
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets // Load widgets
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -359,7 +427,7 @@ Bangle.drawWidgets();
// false: Do nothing // false: Do nothing
// '#LAUNCHER': Open the launcher // '#LAUNCHER': Open the launcher
// nonexistent app: Do nothing // nonexistent app: Do nothing
function launch(appId) { let launch = function (appId, fast) {
if (appId == false) return; if (appId == false) return;
else if (appId == '#LAUNCHER') { else if (appId == '#LAUNCHER') {
Bangle.buzz(); Bangle.buzz();
@ -368,13 +436,30 @@ function launch(appId) {
let appInfo = storage.readJSON(appId + '.info', 1); let appInfo = storage.readJSON(appId + '.info', 1);
if (appInfo) { if (appInfo) {
Bangle.buzz(); Bangle.buzz();
load(appInfo.src); if (fast) Bangle.load(appInfo.src);
else load(appInfo.src);
} }
} }
} }
//Set up touch to launch the selected app //Set up touch to launch the selected app, and to handle dual stage unlock
Bangle.on('touch', function (button, xy) { let dualStageTaps = 0;
let onTouch = function (button, xy) {
// If only the first stage has been unlocked, increase the counter
if (dualStageTaps < config.dualStageUnlock) {
dualStageTaps++;
Bangle.buzz();
// 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); let x = Math.floor(xy.x / 44);
if (x < 0) x = 0; if (x < 0) x = 0;
else if (x > 3) x = 3; else if (x > 3) x = 3;
@ -389,17 +474,44 @@ Bangle.on('touch', function (button, xy) {
Bangle.showLauncher(); Bangle.showLauncher();
} else { } else {
let i = 4 * y + x; let i = 4 * y + x;
launch(config.shortcuts[i]); 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();
}
// 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();
} }
}); });
//Set up swipe handler drawLockedSeconds(true);
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);
});
if (!Bangle.isLocked()) drawIcons(); }
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", "id": "infoclk",
"name": "Informational clock", "name": "Informational clock",
"version": "0.08", "version": "0.09",
"dependencies": {"weather":"app"},
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked", "description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
"readme": "README.md", "readme": "README.md",
"icon": "icon.png", "icon": "icon.png",
@ -24,6 +23,10 @@
"name": "infoclk-font.js", "name": "infoclk-font.js",
"url": "font.js" "url": "font.js"
}, },
{
"name": "infoclk-config.js",
"url": "configLoad.js"
},
{ {
"name": "infoclk.img", "name": "infoclk.img",
"url": "icon.js", "url": "icon.js",

View File

@ -2,71 +2,7 @@
const SETTINGS_FILE = "infoclk.json"; const SETTINGS_FILE = "infoclk.json";
const storage = require('Storage'); const storage = require('Storage');
let config = Object.assign({ let config = require('infoclk-config.js').getConfig();
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));
function saveSettings() { function saveSettings() {
storage.writeJSON(SETTINGS_FILE, config); 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: 'Weather', val: 'weather' },
{ name: 'Step count', val: 'steps' }, { name: 'Step count', val: 'steps' },
{ name: 'Steps + BPM', val: 'health' }, { name: 'Steps + BPM', val: 'health' },
{ name: 'Day progresss bar', val: 'progress' }, { name: 'Bar', val: 'progress' },
{ name: 'Nothing', val: false } { name: 'Nothing', val: false }
]; ];
@ -213,7 +161,7 @@
name: appInfo.name, name: appInfo.name,
val: appInfo.id val: appInfo.id
}); });
} };
E.showMenu({ E.showMenu({
'': { '': {
@ -222,126 +170,258 @@
}, },
'Top first': { 'Top first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[0] = shortcutOptions[value].val; config.shortcuts[0] = shortcutOptions[value].val;
config.fastLoad.shortcuts[0] = false;
saveSettings(); saveSettings();
} }
}, },
'Top second': { 'Top second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[1] = shortcutOptions[value].val; config.shortcuts[1] = shortcutOptions[value].val;
config.fastLoad.shortcuts[1] = false;
saveSettings(); saveSettings();
} }
}, },
'Top third': { 'Top third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[2] = shortcutOptions[value].val; config.shortcuts[2] = shortcutOptions[value].val;
config.fastLoad.shortcuts[2] = false;
saveSettings(); saveSettings();
} }
}, },
'Top fourth': { 'Top fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[3] = shortcutOptions[value].val; config.shortcuts[3] = shortcutOptions[value].val;
config.fastLoad.shortcuts[3] = false;
saveSettings(); saveSettings();
} }
}, },
'Bottom first': { 'Bottom first': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[4] = shortcutOptions[value].val; config.shortcuts[4] = shortcutOptions[value].val;
config.fastLoad.shortcuts[4] = false;
saveSettings(); saveSettings();
} }
}, },
'Bottom second': { 'Bottom second': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[5] = shortcutOptions[value].val; config.shortcuts[5] = shortcutOptions[value].val;
config.fastLoad.shortcuts[5] = false;
saveSettings(); saveSettings();
} }
}, },
'Bottom third': { 'Bottom third': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[6] = shortcutOptions[value].val; config.shortcuts[6] = shortcutOptions[value].val;
config.fastLoad.shortcuts[6] = false;
saveSettings(); saveSettings();
} }
}, },
'Bottom fourth': { 'Bottom fourth': {
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]), 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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.shortcuts[7] = shortcutOptions[value].val; config.shortcuts[7] = shortcutOptions[value].val;
config.fastLoad.shortcuts[7] = false;
saveSettings(); saveSettings();
} }
}, },
'Swipe up': { 'Swipe up': {
value: shortcutOptions.map(item => item.val).indexOf(config.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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.swipe.up = shortcutOptions[value].val; 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(); saveSettings();
} }
}, },
'Swipe left': { 'Swipe left': {
value: shortcutOptions.map(item => item.val).indexOf(config.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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.swipe.left = shortcutOptions[value].val; config.swipe.left = shortcutOptions[value].val;
config.fastLoad.swipe.left = false;
saveSettings(); saveSettings();
} }
}, },
'Swipe right': { 'Swipe right': {
value: shortcutOptions.map(item => item.val).indexOf(config.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, min: 0,
max: shortcutOptions.length - 1, max: shortcutOptions.length - 1,
wrap: false, wrap: false,
onchange: value => { onchange: value => {
config.swipe.right = shortcutOptions[value].val; 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(); 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 = [ const COLOR_OPTIONS = [
@ -355,11 +435,197 @@
{ name: 'White', val: [1, 1, 1] } { 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 // Workaround for being unable to use == on arrays: convert them into strings
function colorString(color) { function colorString(color) {
return `${color[0]} ${color[1]} ${color[2]}`; 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 //Shows the top level menu
function showMainMenu() { function showMainMenu() {
E.showMenu({ E.showMenu({
@ -367,6 +633,16 @@
'title': 'Informational Clock', 'title': 'Informational Clock',
'back': back '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, 'Seconds display': showSecondsMenu,
'Day of week format': { 'Day of week format': {
value: config.date.dayFullName, value: config.date.dayFullName,
@ -433,108 +709,8 @@
} }
}, },
'Shortcuts': showShortcutMenu, 'Shortcuts': showShortcutMenu,
'Day progress': () => { 'Fast load shortcuts': showFastLoadMenu,
E.showMenu({ 'Bar': showBarMenu,
'': {
'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();
}
}
});
},
'Low battery color': () => { 'Low battery color': () => {
E.showMenu({ E.showMenu({
'': { '': {