diff --git a/apps.json b/apps.json index 89be44fcf..93d82c5b9 100644 --- a/apps.json +++ b/apps.json @@ -3258,5 +3258,20 @@ {"name":"gbtwist.app.js","url":"app.js"}, {"name":"gbtwist.img","url":"app-icon.js","evaluate":true} ] +}, +{ "id": "mysticclock", + "name": "Mystic Clock", + "icon": "mystic-clock.png", + "version":"1.00", + "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", + "tags": "clock", + "type":"clock", + "readme": "README.md", + "allow_emulator":true, + "storage": [ + {"name":"mysticclock.app.js","url":"mystic-clock-app.js"}, + {"name":"mysticclock.settings.js","url":"mystic-clock-settings.js"}, + {"name":"mysticclock.img","url":"mystic-clock-icon.js","evaluate":true} + ] } ] diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog new file mode 100644 index 000000000..34fe53627 --- /dev/null +++ b/apps/mysticclock/ChangeLog @@ -0,0 +1 @@ +1.00: First published version. diff --git a/apps/mysticclock/README.md b/apps/mysticclock/README.md new file mode 100644 index 000000000..fd5bbb431 --- /dev/null +++ b/apps/mysticclock/README.md @@ -0,0 +1,40 @@ +# Mystic Clock for Bangle.js + +A retro-inspired watchface featuring time, date, and an interactive data display line. + +## Features + +- 24 or 12-hour time (adjustable via the Settings menu) +- Variable colors (also in the Settings) +- Interactive data display line (use upper and lower watch-buttons to rotate between values) +- Cover watch screen with your hand to put it to sleep (the watch, not your hand) +- International localization of date (which can be disabled via the Settings if memory becomes an issue) + +The interactive line rotates between the following items: + +- Current time zone +- Battery charge level +- Device ID (derived from the last 4 of the MAC) +- Memory usage +- Firmware version + + +## Inspirations + +- [CLI Clock](https://github.com/espruino/BangleApps/tree/master/apps/cliock) +- [Dev Clock](https://github.com/espruino/BangleApps/tree/master/apps/dclock) +- [Digital Clock](https://github.com/espruino/BangleApps/tree/master/apps/digiclock) +- [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock) +- [Simplest Clock](https://github.com/espruino/BangleApps/tree/master/apps/simplest) + +Icon adapted from [Public Domain Vectors](https://publicdomainvectors.org/en/free-clipart/Digital-clock-display-vector-image/10845.html). + + +## Changelog + +- 1.00: First published version. (June 2021) + + +## Author + +Eric Wooodward https://itsericwoodward.com/ diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js new file mode 100644 index 000000000..22c2c8982 --- /dev/null +++ b/apps/mysticclock/mystic-clock-app.js @@ -0,0 +1,215 @@ +/** + * Mystic Clock for Bangle.js + * + * + Original Author: Eric Wooodward https://itsericwoodward.com/ + * + see README.md for details + */ + +/* jshint esversion: 6 */ + +const timeFontSize = 6; +const dataFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; + +const yposTime = 75; +const yposDate = 125; +const yposSymbol = 160; +const yposInfo = 220; + +const settings = require('Storage').readJSON('mysticclock.json', 1) || {}; +const colors = ['white', 'blue', 'green', 'purple', 'red', 'teal', 'other']; +const color = settings.color ? colors[settings.color] : 0; + +const infoData = { + '*GMT_MODE': { + calc: () => (new Date()).toString().split(" ")[5], + }, + BATT_MODE: { + calc: () => `BATT: ${E.getBattery()}%`, + }, + ID_MODE: { + calc: () => { + const val = NRF.getAddress().split(":"); + return `ID: ${val[4]}${val[5]}`; + }, + }, + MEM_MODE: { + calc: () => { + const val = process.memory(); + return `MEM: ${Math.round(val.usage * 100 / val.total)}%`; + }, + }, + VER_MODE: { + calc: () => `FW: ${process.env.VERSION}`, + }, +}; +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function setColor() { + const colorCommands = { + white: () => g.setColor(1, 1, 1), + blue: () => g.setColor(0, 0, 1), + green: () => g.setColor(0, 1, 0), + purple: () => g.setColor(1, 0, 1), + red: () => g.setColor(1, 0, 0), + teal: () => g.setColor(0, 1, 1), + other: () => g.setColor(1, 1, 0) + }; + + // default if value unknown + if (!color || !colorCommands[color]) return colorCommands.white(); + return colorCommands[color](); +} + +function getLocale() { + return require('locale'); +} + +function drawClock() { + + // default draw styles + g.reset(); + + // drawSting centered + g.setFontAlign(0, 0); + + // setup color + setColor(); + + // get date + const d = new Date(); + const dLocal = d.toString().split(" "); + + const useLocale = !settings.useLocale; + + const minutes = (`0${d.getMinutes()}`).substr(-2); + const seconds = (`0${d.getSeconds()}`).substr(-2); + + let hours = (`0${d.getHours()}`).substr(-2); + let meridian = ""; + + if (settings.use12Hour) { + hours = parseInt(hours, 10); + meridian = 'AM'; + if (hours === 0) { + hours = 12; + } + else if (hours >= 12) { + meridian = 'PM'; + if (hours > 12) hours -= 12; + } + hours = (' ' + hours).substr(-2); + } + + g.setFont(font, timeFontSize); + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - 15, yposTime, true); + g.setFont(font, dataFontSize); + + if (settings.use12Hour) { + g.drawString(seconds, xyCenter + 97, yposTime - 10, true); + g.drawString(meridian, xyCenter + 97, yposTime + 10, true); + } + else { + g.drawString(seconds, xyCenter + 97, yposTime + 10, true); + } + + // draw DoW, name of month, date, year + g.setFont(font, dataFontSize); + g.drawString([ + useLocale ? getLocale().dow(d, 1) : dLocal[0], + useLocale ? getLocale().month(d, 1) : dLocal[1], + d.getDate(), + d.getFullYear() + ].join(" "), xyCenter, yposDate, true); + +} + +function drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + // clear info + g.setColor(0, 0, 0); + g.fillRect(0, yposInfo - 8, 239, yposInfo + 25); + + // draw info + g.setFont(font, dataFontSize); + setColor(); + g.drawString((infoData[infoMode].calc()), xyCenter, yposInfo, true); + } +} + +function drawImage() { + setColor(); + g.drawPoly([xyCenter - 100, yposSymbol, xyCenter + 100, yposSymbol, xyCenter, yposSymbol + 30], true); +} + +function drawAll() { + drawClock(); + drawInfo(); + drawImage(); +} + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + + +let secondInterval; + +// handle LCD power state change +Bangle.on('lcdPower', on => { + + // stop running when screen turns off + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + + // start running + if (on) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately + } +}); + +// cover screen to put it to sleep +Bangle.on('touch', (button) => { + if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false); +}); + +// clean app screen +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// if screen already active, draw now and start interval +if (Bangle.isLCDOn()) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately +} + +// show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +// rotate through info when the buttons are pressed +setWatch(() => { + nextInfo(); + drawAll(); +}, BTN3, { repeat: true }); + +setWatch(() => { + prevInfo(); + drawAll(); +}, BTN1, { repeat: true }); diff --git a/apps/mysticclock/mystic-clock-icon.js b/apps/mysticclock/mystic-clock-icon.js new file mode 100644 index 000000000..7415fccd5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A/AH4A/AH4A/AH4ALs1msADCA4MGAgQDBBYIAGg93u92s4DBuEAAYN3swDCC5AhBuwMBg4XBuwEBs4dCC49nHgNwCQREBCYNnEYYXHHQQvBAAJZBAgRPEC5IOCu0GM4YLCuCGDAAREBHwtnJ41gDQIXEOAQvBDoZ7CuwjCWwimTJgLCFZojWEbwbWIAH4A/AH4A/AH4A/AH4AFA")) diff --git a/apps/mysticclock/mystic-clock-settings.js b/apps/mysticclock/mystic-clock-settings.js new file mode 100644 index 000000000..2fa0c49c5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-settings.js @@ -0,0 +1,41 @@ +// make sure to enclose the function in parentheses +(function (back) { + + const settings = require('Storage').readJSON('mysticclock.json',1)||{}; + const colors = ['White', 'Blue', 'Green', 'Purple', 'Red', 'Teal', 'Yellow']; + const offon = ['Off','On']; + const onoff = ['On','Off']; + + function save(key, value) { + settings[key] = value; + require('Storage').writeJSON('mysticclock.json',settings); + } + + const appMenu = { + '': {'title': 'Clock Settings'}, + '< Back': back, + 'Color': { + value: 0|settings['color'], + min:0, + max:6, + format: m => colors[m], + onchange: m => {save('color', m)} + }, + '12 Hour Clock': { + value: 0|settings['use12Hour'], + min:0, + max:1, + format: m => offon[m], + onchange: m => {save('use12Hour', m)} + }, + 'Use Locale': { + value: 0|settings['useLocale'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('useLocale', m)} + } + }; + E.showMenu(appMenu) + +}) diff --git a/apps/mysticclock/mystic-clock.png b/apps/mysticclock/mystic-clock.png new file mode 100644 index 000000000..915e2ee32 Binary files /dev/null and b/apps/mysticclock/mystic-clock.png differ