diff --git a/apps/elapsed_t/ChangeLog b/apps/elapsed_t/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/elapsed_t/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/elapsed_t/README.md b/apps/elapsed_t/README.md new file mode 100644 index 000000000..b085cd70f --- /dev/null +++ b/apps/elapsed_t/README.md @@ -0,0 +1,22 @@ +# Elapsed Time Clock +A clock that calculates the time difference between now (in blue) and any given target date (in red). + +The results is show in years, months, days, hours, minutes, seconds. To save battery life, the seconds are shown only when the watch is unlocked, or can be disabled entirely. + +The time difference is positive if the target date is in the past and negative if it is in the future. + +![Screenshot 1](screenshot1.png) +![Screenshot 2](screenshot2.png) +![Screenshot 3](screenshot3.png) + +# Settings +## Time and date formats: +- time can be shown in 24h or in AM/PM format +- date can be shown in DD/MM/YYYY, MM/DD/YYYY or YYYY-MM-DD format + +## Display years and months +You can select if the difference is shown with years, months and days, or just days. + +# TODO +- add the option to set an alarm to the target date +- add an offset to said alarm (e.g. x hours/days... before/after) diff --git a/apps/elapsed_t/app-icon.js b/apps/elapsed_t/app-icon.js new file mode 100644 index 000000000..0e9a434fc --- /dev/null +++ b/apps/elapsed_t/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwcA/4A/AH8kyVJARAQE/YRLn4RD/IRT5cs2QCEEbQgFAQYjIrMlAQwjR5JHIsv2pNkz3JsgjKl/yEAO/I5l/+REBz/7I5f/EYf/I5Vf//2rNlz//8gjJAgIjE/hHIy7xEAAQjIDoIAG+RHHCA///wjHCJIjHMoI1HEY+zCI6zJv4dCFIX9R5PPR4vsEZNJCILXC/77JyXLn4jD/b7KpMnI4fZBARHHpcsEYW2AQIjKARBHIDoICECJIjRpZKCAQYjbCMH/CJVLCAgA/AHYA==")) diff --git a/apps/elapsed_t/app.js b/apps/elapsed_t/app.js new file mode 100644 index 000000000..8237ef32c --- /dev/null +++ b/apps/elapsed_t/app.js @@ -0,0 +1,438 @@ +const APP_NAME = "elapsed_t"; + +const COLOUR_BLACK = 0x0; +const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25) +const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5) +const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75) +const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0) +const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1) +const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0) +const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1) +const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0) +const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5) +const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0) + +const SCREEN_WIDTH = g.getWidth(); +const SCREEN_HEIGHT = g.getHeight(); +const BIG_FONT_SIZE = 38; +const SMALL_FONT_SIZE = 22; + +var arrowFont = atob("BwA4AcAOAHADgBwA4McfOf3e/+P+D+A+AOA="); // contains only the > character + +var now = new Date(); + +var settings = Object.assign({ + // default values + displaySeconds: true, + displayMonthsYears: true, + dateFormat: 0, + time24: true +}, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + +var temp_displaySeconds = settings.displaySeconds; + +var data = Object.assign({ + // default values + target: { + isSet: false, + Y: now.getFullYear(), + M: now.getMonth() + 1, // Month is zero-based, so add 1 + D: now.getDate(), + h: now.getHours(), + m: now.getMinutes(), + s: now.getSeconds() + } +}, require('Storage').readJSON(APP_NAME + ".data.json", true) || {}); + +function writeData() { + require('Storage').writeJSON(APP_NAME + ".data.json", data); +} + +function writeSettings() { + require('Storage').writeJSON(APP_NAME + ".settings.json", settings); + temp_displaySeconds = settings.temp_displaySeconds; +} + +let inMenu = false; + +Bangle.on('touch', function (zone, e) { + if (!inMenu) { + if (drawTimeout) clearTimeout(drawTimeout); + E.showMenu(menu); + inMenu = true; + } +}); + +function pad2(number) { + return (String(number).padStart(2, '0')); +} + +function formatDateTime(date, dateFormat, time24, showSeconds) { + var formattedDateTime = { + date: "", + time: "" + }; + + var DD = pad2(date.getDate()); + var MM = pad2(date.getMonth() + 1); // Month is zero-based + var YYYY = date.getFullYear(); + var h = date.getHours(); + var hh = pad2(date.getHours()); + var mm = pad2(date.getMinutes()); + var ss = pad2(date.getSeconds()); + + switch (dateFormat) { + case 0: + formattedDateTime.date = `${DD}/${MM}/${YYYY}`; + break; + + case 1: + formattedDateTime.date = `${MM}/${DD}/${YYYY}`; + break; + + case 2: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + + default: + formattedDateTime.date = `${YYYY}-${MM}-${DD}`; + break; + } + + if (time24) { + formattedDateTime.time = `${hh}:${mm}${showSeconds ? `:${ss}` : ''}`; + } else { + var ampm = (h >= 12 ? 'PM' : 'AM'); + var h_ampm = h % 12; + h_ampm = (h_ampm == 0 ? 12 : h_ampm); + formattedDateTime.time = `${h_ampm}:${mm}${showSeconds ? `:${ss}` : ''}${ampm}`; + } + + return formattedDateTime; +} + +function howManyDaysInMonth(month, year) { + return new Date(year, month, 0).getDate(); +} + +function handleExceedingDay() { + var maxDays = howManyDaysInMonth(data.target.M, data.target.Y); + menu.Day.max = maxDays; + if (data.target.D > maxDays) { + menu.Day.value = maxDays; + data.target.D = maxDays; + } +} + +function setTarget(set) { + if (set) { + target = new Date( + data.target.Y, + data.target.M - 1, + data.target.D, + data.target.h, + data.target.m, + data.target.s + ); + data.target.isSet = true; + } else { + target = new Date(); + Object.assign( + data, + { + target: { + isSet: false, + Y: now.getFullYear(), + M: now.getMonth() + 1, // Month is zero-based, so add 1 + D: now.getDate(), + h: now.getHours(), + m: now.getMinutes(), + s: now.getSeconds() + } + } + ); + } + + writeData(); +} + +var target; +setTarget(data.target.isSet); + +var drawTimeout; +var queueMillis = 1000; + +var menu = { + "": { + "title": "Set target", + back: function () { + E.showMenu(); + Bangle.setUI("clock"); + inMenu = false; + draw(); + } + }, + 'Day': { + value: data.target.D, + min: 1, max: 31, wrap: true, + onchange: v => { + data.target.D = v; + } + }, + 'Month': { + value: data.target.M, + min: 1, max: 12, noList: true, wrap: true, + onchange: v => { + data.target.M = v; + handleExceedingDay(); + } + }, + 'Year': { + value: data.target.Y, + min: 1900, max: 2100, + onchange: v => { + data.target.Y = v; + handleExceedingDay(); + } + }, + 'Hours': { + value: data.target.h, + min: 0, max: 23, wrap: true, + onchange: v => { + data.target.h = v; + }, + format: function (v) { return pad2(v); } + }, + 'Minutes': { + value: data.target.m, + min: 0, max: 59, wrap: true, + onchange: v => { + data.target.m = v; + }, + format: function (v) { return pad2(v); } + }, + 'Seconds': { + value: 0, + min: 0, max: 59, wrap: true, + onchange: v => { + data.target.s = v; + }, + format: function (v) { return pad2(v); } + }, + 'Save': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(true); + writeSettings(); + temp_displaySeconds = settings.displaySeconds; + updateQueueMillis(settings.displaySeconds); + draw(); + }, + 'Reset': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(false); + updateQueueMillis(settings.displaySeconds); + draw(); + } +}; + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + var delay = queueMillis - (Date.now() % queueMillis); + if (queueMillis == 60000 && signIsNegative()) { + delay += 1000; + } + + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw(); + }, delay); +} + +function updateQueueMillis(displaySeconds) { + if (displaySeconds) { + queueMillis = 1000; + } else { + queueMillis = 60000; + } +} + +Bangle.on('lock', function (on, reason) { + if (on) { // screen is locked + temp_displaySeconds = false; + updateQueueMillis(false); + draw(); + } else { // screen is unlocked + temp_displaySeconds = settings.displaySeconds; + updateQueueMillis(temp_displaySeconds); + draw(); + } +}); + +function signIsNegative() { + var now = new Date(); + return (now < target); +} + +function diffToTarget() { + var diff = { + sign: "+", + Y: "0", + M: "0", + D: "0", + hh: "00", + mm: "00", + ss: "00" + }; + + if (!data.target.isSet) { + return (diff); + } + + var now = new Date(); + diff.sign = now < target ? '-' : '+'; + + if (settings.displayMonthsYears) { + var start; + var end; + + if (now > target) { + start = target; + end = now; + } else { + start = now; + end = target; + } + + diff.Y = end.getFullYear() - start.getFullYear(); + diff.M = end.getMonth() - start.getMonth(); + diff.D = end.getDate() - start.getDate(); + diff.hh = end.getHours() - start.getHours(); + diff.mm = end.getMinutes() - start.getMinutes(); + diff.ss = end.getSeconds() - start.getSeconds(); + + // Adjust negative differences + if (diff.ss < 0) { + diff.ss += 60; + diff.mm--; + } + if (diff.mm < 0) { + diff.mm += 60; + diff.hh--; + } + if (diff.hh < 0) { + diff.hh += 24; + diff.D--; + } + if (diff.D < 0) { + var lastMonthDays = new Date(end.getFullYear(), end.getMonth(), 0).getDate(); + diff.D += lastMonthDays; + diff.M--; + } + if (diff.M < 0) { + diff.M += 12; + diff.Y--; + } + + + } else { + var timeDifference = target - now; + timeDifference = Math.abs(timeDifference); + + // Calculate days, hours, minutes, and seconds + diff.D = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + diff.hh = Math.floor((timeDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + diff.mm = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60)); + diff.ss = Math.floor((timeDifference % (1000 * 60)) / 1000); + } + + // add zero padding + diff.hh = pad2(diff.hh); + diff.mm = pad2(diff.mm); + diff.ss = pad2(diff.ss); + + return diff; +} + +function draw() { + + var nowFormatted = formatDateTime(new Date(), settings.dateFormat, settings.time24, temp_displaySeconds); + var targetFormatted = formatDateTime(target, settings.dateFormat, settings.time24, true); + var diff = diffToTarget(); + + var diffYMD; + if (settings.displayMonthsYears) + diffYMD = `${diff.sign}${diff.Y}Y ${diff.M}M ${diff.D}D`; + else + diffYMD = `${diff.sign}${diff.D}D`; + + var diff_hhmm = `${diff.hh}:${diff.mm}`; + + g.clearRect(0, 24, SCREEN_WIDTH, SCREEN_HEIGHT); + //console.log("drawing"); + + let y = 24; //Bangle.getAppRect().y; + + // draw current date + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_BLUE); + g.drawString(nowFormatted.date, 4, y); + y += SMALL_FONT_SIZE; + + // draw current time + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_BLUE); + g.drawString(nowFormatted.time, 4, y); + y += SMALL_FONT_SIZE; + + // draw arrow + g.setFontCustom(arrowFont, 62, 16, 13).setFontAlign(-1, -1).setColor(COLOUR_RED); + g.drawString(">", 4, y + 3); + + if (data.target.isSet) { + // draw target date + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_RED); + g.drawString(targetFormatted.date, 4 + 16 + 6, y); + } else { + // draw NOT SET + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_RED); + g.drawString("NOT SET", 4 + 16 + 6, y); + } + + y += SMALL_FONT_SIZE; + + // draw target time + if (data.target.isSet) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_RED); + g.drawString(targetFormatted.time, 4, y); + } + y += SMALL_FONT_SIZE + 4; + + // draw separator + g.setColor(COLOUR_BLACK); + g.drawLine(0, y - 4, SCREEN_WIDTH, y - 4); + + // draw diffYMD + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(0, -1).setColor(COLOUR_BLACK); + g.drawString(diffYMD, SCREEN_WIDTH / 2, y); + y += SMALL_FONT_SIZE; + + // draw diff_hhmm + g.setFont("Vector", BIG_FONT_SIZE).setFontAlign(0, -1).setColor(COLOUR_BLACK); + g.drawString(diff_hhmm, SCREEN_WIDTH / 2, y); + + // draw diff_ss + if (temp_displaySeconds) { + g.setFont("Vector", SMALL_FONT_SIZE).setFontAlign(-1, -1).setColor(COLOUR_GREY); + g.drawString(diff.ss, SCREEN_WIDTH / 2 + 52, y + 13); + } + + queueDraw(); +} + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +Bangle.setUI("clock"); + +draw(); diff --git a/apps/elapsed_t/app.png b/apps/elapsed_t/app.png new file mode 100644 index 000000000..c2cac4fa1 Binary files /dev/null and b/apps/elapsed_t/app.png differ diff --git a/apps/elapsed_t/metadata.json b/apps/elapsed_t/metadata.json new file mode 100644 index 000000000..832128955 --- /dev/null +++ b/apps/elapsed_t/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "elapsed_t", + "name": "Elapsed Time Clock", + "shortName": "Elapsed Time", + "type": "clock", + "version":"0.01", + "description": "A clock that calculates the time difference between now and any given target date.", + "tags": ["clock", "tool"], + "src": "elapsed_t.app.js", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"elapsed_t.app.js","url":"app.js"}, + {"name":"elapsed_t.settings.js","url":"settings.js"}, + {"name":"elapsed_t.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"elapsed_t.data.json"}], + "icon": "app.png", + "readme": "README.md", + "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }, { "url": "screenshot3.png" }], + "allow_emulator":true +} diff --git a/apps/elapsed_t/screenshot1.png b/apps/elapsed_t/screenshot1.png new file mode 100644 index 000000000..d15a5a9ae Binary files /dev/null and b/apps/elapsed_t/screenshot1.png differ diff --git a/apps/elapsed_t/screenshot2.png b/apps/elapsed_t/screenshot2.png new file mode 100644 index 000000000..e2a10ab62 Binary files /dev/null and b/apps/elapsed_t/screenshot2.png differ diff --git a/apps/elapsed_t/screenshot3.png b/apps/elapsed_t/screenshot3.png new file mode 100644 index 000000000..8ca6212f6 Binary files /dev/null and b/apps/elapsed_t/screenshot3.png differ diff --git a/apps/elapsed_t/settings.js b/apps/elapsed_t/settings.js new file mode 100644 index 000000000..d3a7cb357 --- /dev/null +++ b/apps/elapsed_t/settings.js @@ -0,0 +1,55 @@ +(function(back) { + var APP_NAME = "elapsed_t"; + var FILE = APP_NAME + ".settings.json"; + // Load settings + var settings = Object.assign({ + // default values + displaySeconds: true, + displayMonthsYears: true, + dateFormat: 0, + time24: true + }, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]; + + // Show the menu + E.showMenu({ + "" : { "title" : "Elapsed Time" }, + "< Back" : () => back(), + 'Show\nseconds': { + value: !!settings.displaySeconds, + onchange: v => { + settings.displaySeconds = v; + writeSettings(); + } + }, + 'Show months/\nyears': { + value: !!settings.displayMonthsYears, + onchange: v => { + settings.displayMonthsYears = v; + writeSettings(); + } + }, + 'Time format': { + value: !!settings.time24, + onchange: v => { + settings.time24 = v; + writeSettings(); + }, + format: function (v) {return v ? "24h" : "AM/PM";} + }, + 'Date format': { + value: settings.dateFormat, + min: 0, max: 2, wrap: true, + onchange: v => { + settings.dateFormat = v; + writeSettings(); + }, + format: function (v) {return dateFormats[v];} + } + }); +})