Updated infoclk with multiple new features and bugfixes
parent
01d4cea940
commit
333b957c74
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
// 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
|
||||||
// Settings related to the display of the date
|
let timeInRange = function (start, time, end) {
|
||||||
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
|
|
||||||
function timeInRange(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();
|
||||||
|
|
@ -84,20 +20,20 @@ function timeInRange(start, time, end) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return start <= timeNumber && timeNumber <= end;
|
return start <= timeNumber && timeNumber <= 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
|
||||||
|
|
||||||
|
|
@ -105,23 +41,23 @@ function getFontSize(length, maxWidth, minSize, maxSize) {
|
||||||
if (size < minSize) return minSize;
|
if (size < minSize) return minSize;
|
||||||
else if (size > maxSize) return maxSize;
|
else if (size > maxSize) return maxSize;
|
||||||
else return Math.floor(size);
|
else return Math.floor(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()];
|
||||||
|
|
@ -129,14 +65,85 @@ function getDateString(now) {
|
||||||
|
|
||||||
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
|
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
|
||||||
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
|
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
|
// Get a Gadgetbridge weather string
|
||||||
function getDayProgress(now) {
|
let getWeatherString = function () {
|
||||||
let start = config.dayProgress.start;
|
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 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;
|
||||||
|
|
||||||
|
|
@ -164,80 +171,163 @@ function getDayProgress(now) {
|
||||||
} else {
|
} else {
|
||||||
return progress;
|
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 color = config.bar.dayProgress.color;
|
||||||
let now = new Date();
|
g.setColor(color[0], color[1], color[2])
|
||||||
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
|
.fillRect(x1, y1, x1 + (x2 - x1) * getDayProgress(now), y2);
|
||||||
else time = 60000 - (now.getTime() % 60000);
|
}
|
||||||
|
|
||||||
drawTimeout = setTimeout(draw, time);
|
// 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
|
||||||
|
|
||||||
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)
|
for (let event of calendar) {
|
||||||
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
|
// left = boundary + how far event is in the future mapped from our allowed duration to a distance in pixels, clamped to x1
|
||||||
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
|
let leftUnclamped = x1 + (event.timestamp * 1000 - now) * (x2 - x1) / (config.bar.calendar.duration * 1000);
|
||||||
const DIGIT_HEIGHT = 64; // How tall the digits are
|
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);
|
||||||
|
|
||||||
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
|
//Draw the actual bar
|
||||||
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
|
if (event.color) g.setColor("#" + (0x1000000 + Number(event.color)).toString(16).padStart(6, "0")); // Line plagiarized from the agenda app
|
||||||
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
|
else {
|
||||||
|
let color = config.bar.calendar.defaultColor;
|
||||||
|
g.setColor(color[0], color[1], color[2]);
|
||||||
|
}
|
||||||
|
g.fillRect(left, y1, right, y2);
|
||||||
|
|
||||||
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
|
// Cache the pipes if necessary
|
||||||
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
|
if (leftUnclamped == left) pipes.push(left);
|
||||||
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
|
if (rightUnclamped == right) pipes.push(right);
|
||||||
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
|
// Draw the pipes
|
||||||
function draw() {
|
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();
|
||||||
|
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
|
||||||
|
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.
|
||||||
}
|
let drawIcons = function () {
|
||||||
|
|
||||||
// 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());
|
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];
|
||||||
|
|
@ -339,27 +398,36 @@ function drawIcons() {
|
||||||
scale: 0.916666666667
|
scale: 0.916666666667
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show launcher when middle button pressed
|
weather.on("update", drawBottomRowOrClock);
|
||||||
Bangle.setUI("clock");
|
Bangle.on("step", drawBottomRowOrClock);
|
||||||
// Load widgets
|
let onLock = locked => {
|
||||||
Bangle.loadWidgets();
|
//If the watch is unlocked and the necessary number of dual stage taps have been performed, draw the shortcuts
|
||||||
Bangle.drawWidgets();
|
if (!locked && dualStageTaps >= config.dualStageUnlock) drawIcons();
|
||||||
|
|
||||||
// Launch an app given the current ID. Handles special cases:
|
// If locked, reset dual stage taps to zero
|
||||||
// false: Do nothing
|
else if (locked) dualStageTaps = 0;
|
||||||
// '#LAUNCHER': Open the launcher
|
|
||||||
// nonexistent app: Do nothing
|
drawLockedSeconds(true);
|
||||||
function launch(appId) {
|
};
|
||||||
|
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;
|
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
|
//Set up swipe handler
|
||||||
Bangle.on('swipe', function (direction) {
|
let onSwipe = function (lr, ud) {
|
||||||
if (direction == -1) launch(config.swipe.left);
|
if (lr == -1) launch(config.swipe.left, config.fastLoad.swipe.left);
|
||||||
else if (direction == 0) launch(config.swipe.up);
|
else if (lr == 1) launch(config.swipe.right, config.fastLoad.swipe.right);
|
||||||
else launch(config.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 (!Bangle.isLocked()) drawIcons();
|
// If the clock starts with the watch unlocked, the first stage of unlocking is skipped
|
||||||
|
if (!Bangle.isLocked()) {
|
||||||
|
dualStageTaps = config.dualStageUnlock;
|
||||||
|
drawIcons();
|
||||||
|
}
|
||||||
|
|
||||||
draw();
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
drawLockedSeconds(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 |
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
'': {
|
'': {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue