diff --git a/apps.json b/apps.json index 89be44fcf..01fa1c5e2 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": "mysticdock", + "name": "Mystic Dock", + "icon": "mystic-dock.png", + "version":"1.0", + "description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.", + "tags": "dock", + "type":"dock", + "readme": "README.md", + "storage": [ + {"name":"mysticdock.app.js","url":"mystic-dock-app.js"}, + {"name":"mysticdock.boot.js","url":"mystic-dock-boot.js"}, + {"name":"mysticdock.settings.js","url":"mystic-dock-settings.js"}, + {"name":"mysticdock.img","url":"mystic-dock-icon.js","evaluate":true} + ] } ] diff --git a/apps/mysticdock/ChangeLog b/apps/mysticdock/ChangeLog new file mode 100644 index 000000000..34fe53627 --- /dev/null +++ b/apps/mysticdock/ChangeLog @@ -0,0 +1 @@ +1.00: First published version. diff --git a/apps/mysticdock/README.md b/apps/mysticdock/README.md new file mode 100644 index 000000000..09e81ba09 --- /dev/null +++ b/apps/mysticdock/README.md @@ -0,0 +1,43 @@ +# Mystic Dock for Bangle.js + +A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line. + +## Features + +- Screensaver-like dock mode while charging (displays the current time for 8 seconds and a blank screen for 2, changing text placement with each draw) +- 24 or 12-hour time (adjustable via the Settings menu) +- Variable colors (also in the Settings) +- Interactive watchface display (use upper and lower watch-buttons to activate it and rotate between values at the bottom) +- International localization of watchface date (which can be disabled via the Settings if memory becomes an issue) +- Automatic watchface reload when unplugged (toggleable via the Settings menu) +- Rotates display 90 degrees if it detects it is sideways (for use in a charging dock) + +When in interactive display mode, the bottom 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 + +- [Bluetooth Dock](https://github.com/espruino/BangleApps/tree/master/apps/bluetoothdock) +- [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 [this one](https://publicdomainvectors.org/en/free-clipart/Digital-clock-display-vector-image/10845.html) and [this one](https://publicdomainvectors.org/en/free-clipart/Vector-image-of-power-manager-icon/20141.html) from [Public Domain Vectors](https://publicdomainvectors.org). + + +## Changelog + +- 1.00: First published version. (June 2021) + + +## Author + +Eric Wooodward https://itsericwoodward.com/ diff --git a/apps/mysticdock/mystic-dock-app.js b/apps/mysticdock/mystic-dock-app.js new file mode 100644 index 000000000..2e6fdafc5 --- /dev/null +++ b/apps/mysticdock/mystic-dock-app.js @@ -0,0 +1,247 @@ +/** + * Mystic Dock 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 ypos = [ + 45, // Time + 105, // Date + 145, // Symbol + 210 // Info +]; + +const settings = require('Storage').readJSON('mysticdock.json', 1) || + require('Storage').readJSON('mysticclock.json', 1) || {}; +const colors = ['white', 'blue', 'green', 'purple', 'red', 'teal', 'other']; +const color = settings.color ? colors[settings.color] : 0; + +const yposMax = 190; +const yposMin = 60; +let y = yposMax; + +let lastButtonPressTime; +let wasInActiveMode = false; + + +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 drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + // clear info + g.setColor(0, 0, 0); + g.fillRect(0, ypos[3] - 8, 239, ypos[3] + 25); + + // draw info + g.setFont(font, dataFontSize); + setColor(); + g.drawString((infoData[infoMode].calc()), xyCenter, ypos[3], true); + } +} + +function drawImage() { + setColor(); + g.drawPoly([xyCenter - 100, ypos[2], xyCenter + 100, ypos[2], xyCenter, ypos[2] + 30], true); +} + +function drawClock() { + + // default draw styles + g.reset(); + + // get date + const d = new Date(); + const dLocal = d.toString().split(" "); + + const minutes = (`0${d.getMinutes()}`).substr(-2); + const seconds = (`0${d.getSeconds()}`).substr(-2); + + const useLocale = !settings.useLocale; + + let hours = (`0${d.getHours()}`).substr(-2); + let meridian = ""; + + if (d.getSeconds() % 10 === 0) { + y = Math.floor(Math.random() * (yposMax - yposMin)) + yposMin; + } + + // drawSting centered + g.setFontAlign(0, 0); + + // setup color + setColor(); + + 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); + + if (lastButtonPressTime && ((d.getTime() - lastButtonPressTime) / 1000) < 5) { + + // clear screen when switching modes + if (!wasInActiveMode) { + g.clear(); + wasInActiveMode = true; + } + + // draw clock in center w/ seconds + // show date (locale'd, based on settings) + // show info line below it + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - 15, ypos[0], true); + g.setFont(font, dataFontSize); + + if (settings.use12Hour) { + g.drawString(seconds, xyCenter + 97, ypos[0] - 10, true); + g.drawString(meridian, xyCenter + 97, ypos[0] + 10, true); + } + else { + g.drawString(seconds, xyCenter + 97, ypos[0] + 10, true); + } + + // draw DoW, name of month, date, year + g.setFont(font, dataFontSize); + g.drawString([ + useLocale ? require('locale').dow(d, 1) : dLocal[0], + useLocale ? require('locale').month(d, 1) : dLocal[1], + d.getDate(), + d.getFullYear() + ].join(' '), xyCenter, ypos[1], true); + + drawInfo(); + drawImage(); + } + else if (d.getSeconds() % 10 === 8) { + g.clear(); + wasInActiveMode = false; + } + else if (d.getSeconds() % 10 !== 9) { + // clear screen when switching modes + if (wasInActiveMode) { + g.clear(); + wasInActiveMode = false; + } + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - (settings.use12Hour ? 15 : 0), y, true); + g.setFont(font, dataFontSize); + if (settings.use12Hour) { + g.drawString(meridian, xyCenter + 97, y + 10, true); + } + g.drawString(`BATT: ${E.getBattery() === 100 ? '100' : ('0' + E.getBattery()).substr(-2)}%`, xyCenter, y + 35, true); + } + + g.flip(); +} + + +function nextInfo() { + lastButtonPressTime = Date.now(); + let idx = infoList.indexOf(infoMode); + + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + + +function prevInfo() { + lastButtonPressTime = Date.now(); + let idx = infoList.indexOf(infoMode); + + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + + +if (Bangle.getAccel().x < -0.7) { + g.setRotation(3); // assume watch in charge cradle +} + +g.clear(); + +setInterval(drawClock, 1000); +drawClock(); + +if (Bangle.isCharging()) { + Bangle.on("charging", isCharging => { + const reloadOnUplug = !settings.reloadOnUplug; + + if (!isCharging && reloadOnUplug) load(); + }); +} + +// show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +// change to "active mode" and rotate through info when the buttons are pressed +setWatch(() => { + nextInfo(); + drawClock(); +}, BTN3, { repeat: true }); + +setWatch(() => { + prevInfo(); + drawClock(); +}, BTN1, { repeat: true }); diff --git a/apps/mysticdock/mystic-dock-boot.js b/apps/mysticdock/mystic-dock-boot.js new file mode 100644 index 000000000..7cb7fa8a4 --- /dev/null +++ b/apps/mysticdock/mystic-dock-boot.js @@ -0,0 +1 @@ +Bangle.on("charging", isCharging => { if (isCharging) load("mysticdock.app.js"); }); diff --git a/apps/mysticdock/mystic-dock-icon.js b/apps/mysticdock/mystic-dock-icon.js new file mode 100644 index 000000000..527825dd7 --- /dev/null +++ b/apps/mysticdock/mystic-dock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A6g93u9gs4DCBBIAFu9ms9wAYYIJAAt2FAN2BYMHEwIIIAAkGBQV3AYNns1mBAwXGg4KCIgYTEBAZ2JCYQABBBIXJQoRcCBA0GDQpPCBAUGuwTBBAwfCUwgTDMoVmBA8GQIIXGWoJ9DBA4vHAAIOBcoYIHC4xqCCQR2BBBEGJAKSGAH4Adb4SIBDCYXCUwQwVDCjJCXYS/CDh4SCAAoxPDA72CPaQdCTB57CLgQYCGCIdFJJ4QFTIQXUGwpHQJAapQI4qQPCIqtDVCQECMCR5BJgN2bSArCuACCbSIRCIobZQOgZMCgx4OJIjvCCyAYCCYJMBYB4zHC6oA/AE4=")) diff --git a/apps/mysticdock/mystic-dock-settings.js b/apps/mysticdock/mystic-dock-settings.js new file mode 100644 index 000000000..7bfda1c0f --- /dev/null +++ b/apps/mysticdock/mystic-dock-settings.js @@ -0,0 +1,48 @@ +// make sure to enclose the function in parentheses +(function (back) { + + const settings = require('Storage').readJSON('mysticdock.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('mysticdock.json',settings); + } + + const appMenu = { + '': {'title': 'Dock 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)} + }, + 'Reload on Unplug': { + value: 0|settings['reloadOnUplug'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('reloadOnUplug', 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/mysticdock/mystic-dock.png b/apps/mysticdock/mystic-dock.png new file mode 100644 index 000000000..4c0dce770 Binary files /dev/null and b/apps/mysticdock/mystic-dock.png differ