diff --git a/apps/schedclock/ChangeLog b/apps/schedclock/ChangeLog new file mode 100644 index 0000000000..5560f00bce --- /dev/null +++ b/apps/schedclock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/schedclock/README.md b/apps/schedclock/README.md new file mode 100644 index 0000000000..aef023ec55 --- /dev/null +++ b/apps/schedclock/README.md @@ -0,0 +1,36 @@ +# Schedule Clock Faces + +Change clock faces on a schedule. + +For example: a fun clock face for weekends and after work; a detailed clock face for work days. + +![Screenshot](screenshot1.png) + +## Usage + +* Open the `Schedule Clock` app or find it in the `Settings` > `Apps` menu. +* Set `Enabled` to checked +* Select `Add New` to add a new scheduled face change +* Select the `Day`, `Hour`, `Minute`, and what `Clock` to change to +* Select `Save` to save the new (or changed) schedule + +![SaveButton](screenshot2.png) + +An entry in `Scheduler` will be created for each scheduled clock change. + +If the clockface you selected has been uninstalled, the schedule will still exist but won't do anything. + +## To Uninstall +Before uninstalling this app, clean up any scheduled alarms by setting the `Enabled` toggle to unchecked. + +If you skip this step, orphaned alarms may cause error logs but won't affect functionality. + +You can also remove the extra `schedclock` alarms manually with the [Scheduler](/?id=sched) app. + +## Creator + +[kidneyhex](https://github.com/kidneyhex) + +## Attribution + +App icon: [Schedule](https://icons8.com/icon/E7VlDozxin8k/schedule) by [Icons8](https://icons8.com/) \ No newline at end of file diff --git a/apps/schedclock/app-icon.js b/apps/schedclock/app-icon.js new file mode 100644 index 0000000000..eaa3de0ce8 --- /dev/null +++ b/apps/schedclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///221nnnplr6+9jnn5Hhtlz6Wc2Wcy218vgllrimls/h0vgMf4A/ACcL3YAFC7dEC5UGswAFBQUEolACxG7sslqoADktr2AvM3dikQAFt+3g8NonQu4vJCwslt/nu84omDu9wC5Unu4XhDA4XFqQXJMIwXFu9yC7d5xGXC6l3mYWCC5SnGmYAFC5/pxAAFC5/kogAFC5/hC5m/sQXFkQXO91lktVAAclt1holBiMUC5FE01mAAukCYNIu+EC5NO/4AE/wRCpEzC5UW9wAE8xCCF5hUBAAoRCL5gAQC5AmCARAXLKwQCIC5iGBARAvjL66P/R+kAghfNoAWGC4QAMC8AA/AH4ALA==")) diff --git a/apps/schedclock/app.js b/apps/schedclock/app.js new file mode 100644 index 0000000000..f5ae19852c --- /dev/null +++ b/apps/schedclock/app.js @@ -0,0 +1,6 @@ +(function () { + + // Load the settings page + eval(require("Storage").read("schedclock.settings.js"))(()=>load()); + +})(); diff --git a/apps/schedclock/app.png b/apps/schedclock/app.png new file mode 100644 index 0000000000..597732990b Binary files /dev/null and b/apps/schedclock/app.png differ diff --git a/apps/schedclock/lib.js b/apps/schedclock/lib.js new file mode 100644 index 0000000000..34e967aeb3 --- /dev/null +++ b/apps/schedclock/lib.js @@ -0,0 +1,84 @@ +const SETTINGS_FILE = "schedclock.settings.json"; +const APP_ID = "schedclock"; + +/** + * Called directly by an alarm to load a specific clock face + * @param {string} faceSrc - Source file of the clock face to load (e.g. "myclock.js") + **/ +const setClock = function(faceSrc) { + const settings = require("Storage").readJSON("setting.json", 1) || {}; + // Only change the clock if it's different + if (faceSrc && settings.clock !== faceSrc) { + const face = require("Storage").read(faceSrc); + // If the face doesn't exist, do nothing (but log it) + if (!face) { + console.log("schedclock: Invalid clock face", faceSrc); + return; + } + settings.clock = faceSrc; + settings.clockHasWidgets = face.includes("Bangle.loadWidgets"); + require("Storage").writeJSON("setting.json", settings); + if(Bangle.CLOCK) load(); // Reload clock if we're on it + } +}; + +/** + * Handle alarms and resetting them + * @param {number} index Index of the alarm that went off + * @param {string} clock Clockface + */ +exports.onAlarm = function(index, clock) { + const date = new Date(); + const Sched = require("sched"); + const alarm = Sched.getAlarm(`${APP_ID}.${index}`); + alarm.last = date.getDate(); // prevent second run on the same day + Sched.setAlarm(alarm.id, alarm); + setClock(clock); +}; + +/** + * Function to sync all alarms in the scheduler with the settings file. + * Called every time settings are changed; maybe a bit excessive, but keeps things simple. + **/ +exports.syncAlarms = function() { + const Sched = require("sched"); + const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || []; + + // Remove all existing alarms from the scheduler library + Sched + .getAlarms() + .filter(a => a.appid && a.appid === APP_ID) + .forEach(a => Sched.setAlarm(a.id, undefined)); + + // If the app is disabled, we're done. + if (!settings.enabled) return; + + // Alarms need "last" set to let sched know they've already ran for the day + // So if an alarm is for before "now", set last to yesterday so it still triggers today + // else set last to today. + const currentDate = new Date(); + const currentTime = (currentDate.getHours()*3600000)+(currentDate.getMinutes()*60000)+(currentDate.getSeconds()*1000); + const dayOfMonthToday = currentDate.getDate(); + const dayOfMonthYesterday = dayOfMonthToday - 1; + + // Add a new alarm for each setting item + settings.sched.forEach((item, index) => { + + // Skip invalid records + if (item.hour === undefined || item.minute === undefined) return; + + const scheduledTime = (item.hour * 3600000) + (item.minute * 60000); + + // Create the new alarm object and save it using a unique ID. + Sched.setAlarm(`${APP_ID}.${index}`, { + t: scheduledTime, // time in milliseconds since midnight + on: true, + rp: true, + last: (scheduledTime > currentTime) ? dayOfMonthYesterday : dayOfMonthToday, + dow: item.dow, + hidden: true, + appid: APP_ID, + js: `require('${APP_ID}.lib.js').onAlarm(${index},'${item.face}')`, + }); + }); +}; diff --git a/apps/schedclock/metadata.json b/apps/schedclock/metadata.json new file mode 100644 index 0000000000..284483bf7c --- /dev/null +++ b/apps/schedclock/metadata.json @@ -0,0 +1,23 @@ +{ "id": "schedclock", + "name": "Schedule Clock Faces", + "shortName":"Sched Clock", + "version":"0.01", + "author": "kidneyhex", + "description": "Change clock faces on a schedule.", + "icon": "app.png", + "screenshots": [ + {"url":"screenshot1.png"}, + {"url":"screenshot2.png"} + ], + "tags": "tool", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "dependencies": {"scheduler":"type"}, + "storage": [ + {"name":"schedclock.app.js","url":"app.js"}, + {"name":"schedclock.settings.js","url":"settings.js"}, + {"name":"schedclock.lib.js","url":"lib.js"}, + {"name":"schedclock.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"schedclock.settings.json"}] +} diff --git a/apps/schedclock/screenshot1.png b/apps/schedclock/screenshot1.png new file mode 100644 index 0000000000..e7a267151e Binary files /dev/null and b/apps/schedclock/screenshot1.png differ diff --git a/apps/schedclock/screenshot2.png b/apps/schedclock/screenshot2.png new file mode 100644 index 0000000000..d27e944120 Binary files /dev/null and b/apps/schedclock/screenshot2.png differ diff --git a/apps/schedclock/settings.js b/apps/schedclock/settings.js new file mode 100644 index 0000000000..7ec6c2e6fb --- /dev/null +++ b/apps/schedclock/settings.js @@ -0,0 +1,252 @@ +(function(back) { + /** + * @typedef {Object} ScheduleItemType - Individual Schedule Item + * @property {number} hour - Hour (0-23) + * @property {number} minute - Minute (0-59) + * @property {string} face - Clock face source file (e.g. "myclock.js") + * @property {number} dow - Bitmask for days of week [see Sched documentation] + * + * @typedef {Object} SettingsType - Overall Settings File/Object + * @property {boolean} enabled - Whether this app is enabled + * @property {Array} sched - Array of schedule items + */ + + const SETTINGS_FILE = "schedclock.settings.json"; + // Bitmasks for special day selection for sched.json + const BIN_WORKDAYS = 0b0111110; // 62 - MTWTF + const BIN_WEEKEND = 0b1000001; // 65 - SuSa + const BIN_EVERY_DAY = 0b1111111; // 127 - SuMTWTFSa + // Indexes in daysOfWeek for special day selection + const IND_EVERY_DAY = 7; + const IND_WORKDAYS = 8; + const IND_WEEKEND = 9; + + // dows(0) = days of week starting at Sunday + const daysOfWeek = require("date_utils").dows(0).concat([/*LANG*/"Every Day", /*LANG*/"Weekdays", /*LANG*/"Weekends"]); + + /** + * Function to load settings + * @returns {SettingsType} settings object + */ + const loadSettings = function() { + const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {}; + settings.enabled = !!settings.enabled; + if (!Array.isArray(settings.sched)) settings.sched = []; + + // Sort by time + settings.sched.sort((a, b) => { + return (a.hour * 60 + a.minute) - (b.hour * 60 + b.minute); + }); + return settings; + }; + + /** + * Function to save settings + * @param {SettingsType} settings + */ + const saveSettings = function(settings) { + require("Storage").writeJSON(SETTINGS_FILE, settings); + require("schedclock.lib.js").syncAlarms(); + }; + + /** + * Get a list of all installed clock faces + * @returns {Array<{name:string,sortorder:number,src:string}>} array of clock face info + **/ + const getClockFaces = function() { + return require("Storage").list(/\.info$/).map(file => { + const info = require("Storage").readJSON(file, 1) || {}; + if (info && info.type === "clock" && info.src) { + return { + name: info.name || info.src.replace(".js",""), + sortorder: info.sortorder || 0, + src: info.src + }; + } + }) + .filter(f => f) // Remove any invalid entries + .sort((a, b) => { // Sort by sortorder, then name (from clkshortcuts) + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + }; + + /** + * Show the main menu + */ + const showMainMenu = function() { + const settings = loadSettings(); + const clockFaces = getClockFaces(); + const menu = { + "": { "title": /*LANG*/"Schedule Clock" }, + "< Back": () => back(), + /*LANG*/"Enabled": { + value: settings.enabled, + onchange: v => { + settings.enabled = v; + saveSettings(settings); + } + }, + }; + + // Add existing schedule items to the menu + settings.sched.forEach((item, index) => { + const faceName = (clockFaces.find(f => f.src === item.face) || {name: /*LANG*/"Unknown"}).name; + const dow = bitmaskToDowIndex(item.dow); + const dayName = daysOfWeek[dow === undefined ? IND_EVERY_DAY : dow]; + const timeStr = require("locale").time(new Date(1999, 1, 1, item.hour, item.minute, 0),1) + menu[`${dayName} ${timeStr} - ${faceName}`] = () => editScheduleItem(index); + }); + + menu[/*LANG*/"Add New"] = () => editScheduleItem(-1); + + E.showMenu(menu); + }; + + /** + * Get the bitmask for a day of week selection from an index in daysOfWeek + * @param {number} index index in daysOfWeek + * @returns bitmask for day of week + */ + const dowIndexToBitmask = function(index) { + switch(index) { + case IND_EVERY_DAY: return BIN_EVERY_DAY; + case IND_WORKDAYS: return BIN_WORKDAYS; + case IND_WEEKEND: return BIN_WEEKEND; + default: + return 1 << index; // Bitmask: Sun=1, Mon=2, Tue=4, Wed=8, Thu=16, Fri=32, Sat=64 + } + }; + + /** + * Get the index in daysOfWeek from a binary day-of-week bitmask + * @param {number} b binary number for day of week + * @returns index in daysOfWeek + */ + const bitmaskToDowIndex = function(b) { + switch(b) { + case 1: return 0; + case 2: return 1; + case 4: return 2; + case 8: return 3; + case 16: return 4; + case 32: return 5; + case 64: return 6; + case BIN_WORKDAYS: return IND_WORKDAYS; + case BIN_WEEKEND: return IND_WEEKEND; + case BIN_EVERY_DAY: + default: return IND_EVERY_DAY; + } + }; + + /** + * Function to edit a schedule item (or add a new one if index is -1) + * @param {number} index index of item to edit, or -1 to add a new item + */ + const editScheduleItem = function(index) { + const settings = loadSettings(); + const clockFaces = getClockFaces(); + const isNew = index === -1; + const defaultFaceSrc = clockFaces.length > 0 ? clockFaces[0].src : ""; + + const currentItem = isNew ? + { hour: 8, minute: 0, face: defaultFaceSrc, dow: BIN_EVERY_DAY } : + Object.assign({}, settings.sched[index]); + + // Default odd items to "Every Day" + if (currentItem.dow === undefined) currentItem.dow = BIN_EVERY_DAY; + + let dow = bitmaskToDowIndex(currentItem.dow); + + const menu = { + "": { "title": isNew ? /*LANG*/"Add Schedule" : /*LANG*/"Edit Schedule" }, + "< Back": () => showMainMenu(), + /*LANG*/"Day": { + value: dow, + min: 0, + max: daysOfWeek.length - 1, + format: v => daysOfWeek[v], + onchange: v => { + currentItem.dow = dowIndexToBitmask(v); + }, + }, + /*LANG*/"Hour": { + value: currentItem.hour, + min: 0, + max: 23, + format: v => { + // Format as 12h time if user has that set + const meridean = require("locale").meridian(new Date(1999, 1, 1, v, 0, 0),1); + return (!meridean) ? v : (v%12||12) + meridean; + }, + onchange: v => { currentItem.hour = v; } + }, + /*LANG*/"Minute": { + value: currentItem.minute, + min: 0, + max: 59, + onchange: v => { currentItem.minute = v; } + }, + /*LANG*/"Clock Face": { + value: Math.max(0, clockFaces.findIndex(f => f.src === currentItem.face)), + min: 0, + max: clockFaces.length - 1, + format: v => (clockFaces[v] && clockFaces[v].name) || /*LANG*/"None", + onchange: v => { + if (clockFaces[v]) currentItem.face = clockFaces[v].src; + } + }, + /*LANG*/"Save": () => { + const validationError = settings.sched.some((item, i) => { + if (!isNew && i === index) return false; // Don't check against self when editing + + const timesMatch = item.hour === currentItem.hour + && item.minute === currentItem.minute; + if (!timesMatch) return false; + + // If times match, check for a day conflict. + return (item.dow & currentItem.dow) !== 0; + }); + + if (validationError) { + E.showAlert( + /*LANG*/"An entry for this time already exists.", + /*LANG*/"Time conflict" + ).then( + ()=>E.showMenu(menu) + ); + return; // Prevent saving + } + + if (isNew) { + settings.sched.push(currentItem); + } else { + settings.sched[index] = currentItem; + } + saveSettings(settings); + showMainMenu(); + } + }; + + if (!isNew) { + menu[/*LANG*/"Delete"] = () => { + E.showPrompt(/*LANG*/"Delete this item?").then(confirm => { + if (confirm) { + settings.sched.splice(index, 1); + saveSettings(settings); + } + showMainMenu(); + }); + }; + } + + E.showMenu(menu); + }; + + Bangle.loadWidgets(); + Bangle.drawWidgets(); + showMainMenu(); +})