2022-11-02 19:06:17 +00:00
const SETTINGS _FILE = "infoclk.json" ;
const FONT = require ( 'infoclk-font.js' ) ;
const storage = require ( "Storage" ) ;
const locale = require ( "locale" ) ;
const weather = require ( 'weather' ) ;
let config = Object . assign ( {
seconds : {
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
hideLocked : false , // Hide the seconds when the display is locked.
hideBattery : 20 , // Hide the seconds when the battery is at or below a defined percentage.
hideTime : true , // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
hideStart : 2200 , // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
hideEnd : 700 , // The time when the seconds are shown again
hideAlways : false , // Always hide (never show) the seconds
} ,
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
2022-11-06 04:05:47 +00:00
'stlap' , 'keytimer' , 'pomoplus' , 'alarm' ,
2022-11-02 19:06:17 +00:00
'rpnsci' , 'calendar' , 'torch' , 'weather'
] ,
swipe : {
// 3 shortcuts to launch upon swiping:
// false = no shortcut
// '#LAUNCHER' = open the launcher
// any other string = name of app to open
up : 'messages' , // Swipe up or swipe down, due to limitation of event handler
left : '#LAUNCHER' ,
right : '#LAUNCHER' ,
} ,
dayProgress : {
// A progress bar representing how far through the day you are
enabledLocked : true , // Whether this bar is enabled when the watch is locked
enabledUnlocked : false , // Whether the bar is enabled when the watch is unlocked
color : [ 0 , 0 , 1 ] , // The color of the bar
start : 700 , // The time of day that the bar starts filling
end : 2200 , // The time of day that the bar becomes full
reset : 300 // The time of day when the progress bar resets from full to empty
} ,
lowBattColor : {
// The text can change color to indicate that the battery is low
level : 20 , // The percentage where this happens
color : [ 1 , 0 , 0 ] // The color that the text changes to
}
} , storage . readJSON ( SETTINGS _FILE ) ) ;
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
function timeInRange ( start , time , end ) {
// Convert the given date object to a time number
let timeNumber = time . getHours ( ) * 100 + time . getMinutes ( ) ;
// Normalize to prevent the numbers from wrapping around at midnight
if ( end <= start ) {
end += 2400 ;
if ( timeNumber < start ) timeNumber += 2400 ;
}
return start <= timeNumber && timeNumber <= end ;
}
// Return whether settings should be displayed based on the user's configuration
function shouldDisplaySeconds ( now ) {
return ! (
( config . seconds . hideAlways ) ||
( config . seconds . hideLocked && Bangle . isLocked ( ) ) ||
( E . getBattery ( ) <= config . seconds . hideBattery ) ||
( config . seconds . hideTime && timeInRange ( config . seconds . hideStart , now , config . seconds . hideEnd ) )
) ;
}
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
function getFontSize ( length , maxWidth , minSize , maxSize ) {
let size = Math . floor ( maxWidth / length ) ; //Number of pixels of width available to character
size *= ( 20 / 12 ) ; //Convert to height, assuming 20 pixels of height for every 12 of width
// Clamp to within range
if ( size < minSize ) return minSize ;
else if ( size > maxSize ) return maxSize ;
else return Math . floor ( size ) ;
}
// Get the current day of the week according to user settings
function getDayString ( now ) {
if ( config . date . dayFullName ) return [ 'Sunday' , 'Monday' , 'Tuesday' , 'Wednesday' , 'Thursday' , 'Friday' , 'Saturday' ] [ now . getDay ( ) ] ;
else return [ 'Sun' , 'Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' ] [ now . getDay ( ) ] ;
}
// Pad a number with zeros to be the given number of digits
function pad ( number , digits ) {
let result = '' + number ;
while ( result . length < digits ) result = '0' + result ;
return result ;
}
// Get the current date formatted according to the user settings
function getDateString ( now ) {
let month ;
if ( ! config . date . monthName ) month = pad ( now . getMonth ( ) + 1 , 2 ) ;
else if ( config . date . monthFullName ) month = [ 'January' , 'February' , 'March' , 'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' ] [ now . getMonth ( ) ] ;
else month = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] [ now . getMonth ( ) ] ;
if ( config . date . mmdd ) return ` ${ month } ${ config . date . separator } ${ pad ( now . getDate ( ) , 2 ) } ` ;
else return ` ${ pad ( now . getDate ( ) , 2 ) } ${ config . date . separator } ${ month } ` ;
}
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
function getDayProgress ( now ) {
let start = config . dayProgress . start ;
let current = now . getHours ( ) * 100 + now . getMinutes ( ) ;
let end = config . dayProgress . end ;
let reset = config . dayProgress . reset ;
// Normalize
if ( end <= start ) end += 2400 ;
if ( current < start ) current += 2400 ;
if ( reset < start ) reset += 2400 ;
// Convert an hhmm number into a floating-point hours
function toDecimalHours ( time ) {
let hours = Math . floor ( time / 100 ) ;
let minutes = time % 100 ;
return hours + ( minutes / 60 ) ;
}
start = toDecimalHours ( start ) ;
current = toDecimalHours ( current ) ;
end = toDecimalHours ( end ) ;
reset = toDecimalHours ( reset ) ;
let progress = ( current - start ) / ( end - start ) ;
if ( progress < 0 || progress > 1 ) {
if ( current < reset ) return 1 ;
else return 0 ;
} else {
return progress ;
}
}
// Get a Gadgetbridge weather string
function getWeatherString ( ) {
let current = weather . get ( ) ;
if ( current ) return locale . temp ( current . temp - 273.15 ) + ', ' + current . txt ;
else return 'Weather unknown!' ;
}
// Get a second weather row showing humidity, wind speed, and wind direction
function getWeatherRow2 ( ) {
let current = weather . get ( ) ;
if ( current ) return ` ${ current . hum } %, ${ locale . speed ( current . wind ) } ${ current . wrose } ` ;
else return 'Check Gadgetbridge' ;
}
// Get a step string
function getStepsString ( ) {
return '' + Bangle . getHealthStatus ( 'day' ) . steps + ' steps' ;
}
// Get a health string including daily steps and recent bpm
function getHealthString ( ) {
return ` ${ Bangle . getHealthStatus ( 'day' ) . steps } steps ${ Bangle . getHealthStatus ( 'last' ) . bpm } bpm ` ;
}
// Set the next timeout to draw the screen
let drawTimeout ;
function setNextDrawTimeout ( ) {
if ( drawTimeout ) {
clearTimeout ( drawTimeout ) ;
drawTimeout = undefined ;
}
let time ;
let now = new Date ( ) ;
if ( shouldDisplaySeconds ( now ) ) time = 1000 - ( now . getTime ( ) % 1000 ) ;
else time = 60000 - ( now . getTime ( ) % 60000 ) ;
drawTimeout = setTimeout ( draw , time ) ;
}
const DIGIT _WIDTH = 40 ; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
const COLON _WIDTH = 19 ; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
const HHMM _TOP = 27 ; // 24 pixels for widgets + 3 pixels of space
const DIGIT _HEIGHT = 64 ; // How tall the digits are
const SECONDS _TOP = HHMM _TOP + DIGIT _HEIGHT + 3 ; // The top edge of the seconds, top of hours and minutes + digit height + space
const SECONDS _LEFT = 2 * DIGIT _WIDTH + COLON _WIDTH ; // The left edge of the seconds: displayed after 2 digits and the colon
const DATE _LETTER _HEIGHT = DIGIT _HEIGHT / 2 ; // Each letter of the day of week and date will be half the height of the time digits
const DATE _CENTER _X = SECONDS _LEFT / 2 ; // Day of week and date will be centered between left edge of screen and where seconds start
const DOW _CENTER _Y = SECONDS _TOP + ( DATE _LETTER _HEIGHT / 2 ) ; // Day of week will be the top row
const DATE _CENTER _Y = DOW _CENTER _Y + DATE _LETTER _HEIGHT ; // Date will be the bottom row
const DOW _DATE _CENTER _Y = SECONDS _TOP + ( DIGIT _HEIGHT / 2 ) ; // When displaying both on one row, center it
const BOTTOM _CENTER _Y = ( ( SECONDS _TOP + DIGIT _HEIGHT + 3 ) + g . getHeight ( ) ) / 2 ;
// Draw the clock
function draw ( ) {
//Prepare to draw
g . reset ( )
. setFontAlign ( 0 , 0 ) ;
if ( E . getBattery ( ) <= config . lowBattColor . level ) {
let color = config . lowBattColor . color ;
g . setColor ( color [ 0 ] , color [ 1 ] , color [ 2 ] ) ;
}
now = new Date ( ) ;
if ( Bangle . isLocked ( ) ) { //When the watch is locked
g . clearRect ( 0 , 24 , g . getWidth ( ) , g . getHeight ( ) ) ;
//Draw the hours and minutes
let x = 0 ;
for ( let digit of locale . time ( now , 1 ) ) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
if ( digit != ' ' ) g . drawImage ( FONT [ digit ] , x , HHMM _TOP ) ;
if ( digit == ':' ) x += COLON _WIDTH ;
else x += DIGIT _WIDTH ;
}
if ( storage . readJSON ( 'setting.json' ) [ '12hour' ] ) g . drawImage ( FONT [ ( now . getHours ( ) < 12 ) ? 'am' : 'pm' ] , 0 , HHMM _TOP ) ;
//Draw the seconds if necessary
if ( shouldDisplaySeconds ( now ) ) {
let tens = Math . floor ( now . getSeconds ( ) / 10 ) ;
let ones = now . getSeconds ( ) % 10 ;
g . drawImage ( FONT [ tens ] , SECONDS _LEFT , SECONDS _TOP )
. drawImage ( FONT [ ones ] , SECONDS _LEFT + DIGIT _WIDTH , SECONDS _TOP ) ;
// Draw the day of week and date assuming the seconds are displayed
g . setFont ( 'Vector' , getFontSize ( getDayString ( now ) . length , SECONDS _LEFT , 6 , DATE _LETTER _HEIGHT ) )
. drawString ( getDayString ( now ) , DATE _CENTER _X , DOW _CENTER _Y )
. setFont ( 'Vector' , getFontSize ( getDateString ( now ) . length , SECONDS _LEFT , 6 , DATE _LETTER _HEIGHT ) )
. drawString ( getDateString ( now ) , DATE _CENTER _X , DATE _CENTER _Y ) ;
} else {
//Draw the day of week and date without the seconds
let string = getDayString ( now ) + ' ' + getDateString ( now ) ;
g . setFont ( 'Vector' , getFontSize ( string . length , g . getWidth ( ) , 6 , DATE _LETTER _HEIGHT ) )
. drawString ( string , g . getWidth ( ) / 2 , DOW _DATE _CENTER _Y ) ;
}
// Draw the bottom area
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 ( ) ;
else if ( config . bottomLocked . display == 'steps' ) bottomString = getStepsString ( ) ;
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 ) ;
}
// Draw the day progress bar between the rows if necessary
if ( config . dayProgress . enabledLocked && config . bottomLocked . display != 'progress' ) {
let color = config . dayProgress . color ;
g . setColor ( color [ 0 ] , color [ 1 ] , color [ 2 ] )
. fillRect ( 0 , HHMM _TOP + DIGIT _HEIGHT , g . getWidth ( ) * getDayProgress ( now ) , SECONDS _TOP ) ;
}
} else {
//If the watch is unlocked
g . clearRect ( 0 , 24 , g . getWidth ( ) , g . getHeight ( ) / 2 ) ;
rows = [
` ${ getDayString ( now ) } ${ getDateString ( now ) } ${ locale . time ( now , 1 ) } ` ,
getHealthString ( ) ,
getWeatherString ( ) ,
getWeatherRow2 ( )
] ;
if ( shouldDisplaySeconds ( now ) ) rows [ 0 ] += ':' + pad ( now . getSeconds ( ) , 2 ) ;
if ( storage . readJSON ( 'setting.json' ) [ '12hour' ] ) rows [ 0 ] += ( ( now . getHours ( ) < 12 ) ? ' AM' : ' PM' ) ;
let maxHeight = ( ( g . getHeight ( ) / 2 ) - HHMM _TOP ) / ( config . dayProgress . enabledUnlocked ? ( rows . length + 1 ) : rows . length ) ;
let y = HHMM _TOP + maxHeight / 2 ;
for ( let row of rows ) {
let size = getFontSize ( row . length , g . getWidth ( ) , 6 , maxHeight ) ;
g . setFont ( 'Vector' , size )
. drawString ( row , g . getWidth ( ) / 2 , y ) ;
y += maxHeight ;
}
if ( config . dayProgress . enabledUnlocked ) {
let color = config . dayProgress . color ;
g . setColor ( color [ 0 ] , color [ 1 ] , color [ 2 ] )
. fillRect ( 0 , y - maxHeight / 2 , 176 * getDayProgress ( now ) , y + maxHeight / 2 ) ;
}
}
setNextDrawTimeout ( ) ;
}
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
function drawIcons ( ) {
g . reset ( ) . clearRect ( 0 , 24 , g . getWidth ( ) , g . getHeight ( ) ) ;
for ( let i = 0 ; i < 8 ; i ++ ) {
let x = [ 0 , 44 , 88 , 132 , 0 , 44 , 88 , 132 ] [ i ] ;
let y = [ 88 , 88 , 88 , 88 , 132 , 132 , 132 , 132 ] [ i ] ;
let appId = config . shortcuts [ i ] ;
let appInfo = storage . readJSON ( appId + '.info' , 1 ) ;
if ( ! appInfo ) continue ;
icon = storage . read ( appInfo . icon ) ;
g . drawImage ( icon , x , y , {
scale : 0.916666666667
} ) ;
}
}
weather . on ( "update" , draw ) ;
Bangle . on ( "step" , draw ) ;
Bangle . on ( 'lock' , locked => {
//If the watch is unlocked, draw the icons
if ( ! locked ) drawIcons ( ) ;
draw ( ) ;
} ) ;
// Show launcher when middle button pressed
Bangle . setUI ( "clock" ) ;
// 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
function launch ( appId ) {
if ( appId == false ) return ;
else if ( appId == '#LAUNCHER' ) {
Bangle . buzz ( ) ;
Bangle . showLauncher ( ) ;
} else {
let appInfo = storage . readJSON ( appId + '.info' , 1 ) ;
if ( appInfo ) {
Bangle . buzz ( ) ;
load ( appInfo . src ) ;
}
}
}
//Set up touch to launch the selected app
Bangle . on ( 'touch' , function ( button , xy ) {
let x = Math . floor ( xy . x / 44 ) ;
if ( x < 0 ) x = 0 ;
else if ( x > 3 ) x = 3 ;
let y = Math . floor ( xy . y / 44 ) ;
if ( y < 0 ) y = - 1 ;
else if ( y > 3 ) y = 1 ;
else y -= 2 ;
if ( y < 0 ) {
Bangle . buzz ( ) ;
Bangle . showLauncher ( ) ;
} else {
let i = 4 * y + x ;
launch ( config . shortcuts [ i ] ) ;
}
} ) ;
//Set up swipe handler
Bangle . on ( 'swipe' , function ( direction ) {
if ( direction == - 1 ) launch ( config . swipe . left ) ;
else if ( direction == 0 ) launch ( config . swipe . up ) ;
else launch ( config . swipe . right ) ;
} ) ;
if ( ! Bangle . isLocked ( ) ) drawIcons ( ) ;
draw ( ) ;