diff --git a/apps/boxclk/ChangeLog b/apps/boxclk/ChangeLog index f7ee41904..62d1b4875 100644 --- a/apps/boxclk/ChangeLog +++ b/apps/boxclk/ChangeLog @@ -5,3 +5,10 @@ 0.05: Fixes step count not resetting after a new day starts 0.06: Added clockbackground app functionality 0.07: Allow custom backgrounds per boxclk config and from the clockbg module +0.08: Improves performance, responsiveness, and bug fixes +- [+] Added box size caching to reduce calculations +- [+] Improved step count with real-time updates +- [+] Improved battery level update logic to reduce unnecessary refreshes +- [+] Fixed optional seconds not displaying in time +- [+] Fixed drag handler by adding E.stopEventPropagation() +- [+] General code optimization and cleanup \ No newline at end of file diff --git a/apps/boxclk/app.js b/apps/boxclk/app.js index 67493aad9..8287b64c8 100644 --- a/apps/boxclk/app.js +++ b/apps/boxclk/app.js @@ -1,59 +1,125 @@ { - /** - * --------------------------------------------------------------- - * 1. Module dependencies and initial configurations - * --------------------------------------------------------------- - */ - + // 1. Module dependencies and initial configurations let background = require("clockbg"); let storage = require("Storage"); let locale = require("locale"); let widgets = require("widget_utils"); - let date = new Date(); let bgImage; let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; - // Add a condition to check if the file exists, if it does not, default to 'boxclk.json' if (!storage.read(fileName)) { fileName = 'boxclk.json'; } let boxesConfig = storage.readJSON(fileName, 1) || {}; let boxes = {}; - let boxPos = {}; - let isDragging = {}; - let wasDragging = {}; + let isDragging = false; let doubleTapTimer = null; let g_setColor; let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA")); - /** - * --------------------------------------------------------------- - * 2. Graphical and visual configurations - * --------------------------------------------------------------- - */ - + // 2. Graphical and visual configurations let w = g.getWidth(); let h = g.getHeight(); - let totalWidth, totalHeight; let drawTimeout; - /** - * --------------------------------------------------------------- - * 3. Touchscreen Handlers - * --------------------------------------------------------------- - */ + // 3. Event handlers + let eventHandlers = { + touchHandler: function(zone, e) { + let boxTouched = false; + let touchedBox = null; + + for (let boxKey in boxes) { + if (touchInText(e, boxes[boxKey])) { + touchedBox = boxKey; + boxTouched = true; + break; + } + } + + if (boxTouched) { + // Toggle the selected state of the touched box + boxes[touchedBox].selected = !boxes[touchedBox].selected; + + // Update isDragging based on whether any box is selected + isDragging = Object.values(boxes).some(box => box.selected); + + if (isDragging) { + widgets.hide(); + } else { + deselectAllBoxes(); + } + } else { + // If tapped outside any box, deselect all boxes + deselectAllBoxes(); + } + + // Always redraw after a touch event + draw(); + + // Handle double tap for saving + if (!boxTouched && !isDragging) { + if (doubleTapTimer) { + clearTimeout(doubleTapTimer); + doubleTapTimer = null; + for (let boxKey in boxes) { + boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3); + boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3); + } + storage.write(fileName, JSON.stringify(boxesConfig)); + displaySaveIcon(); + return; + } + + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 500); + } + }, - let touchHandler; - let dragHandler; - let movementDistance = 0; + dragHandler: function(e) { + if (!isDragging) return; + + // Stop propagation of the drag event to prevent other handlers + E.stopEventPropagation(); + + for (let key in boxes) { + if (boxes[key].selected) { + let boxItem = boxes[key]; + calcBoxSize(boxItem); + let newX = boxItem.pos.x + e.dx; + let newY = boxItem.pos.y + e.dy; + + if (newX - boxItem.cachedSize.width / 2 >= 0 && + newX + boxItem.cachedSize.width / 2 <= w && + newY - boxItem.cachedSize.height / 2 >= 0 && + newY + boxItem.cachedSize.height / 2 <= h) { + boxItem.pos.x = newX; + boxItem.pos.y = newY; + } + } + } + + draw(); + }, - /** - * --------------------------------------------------------------- - * 4. Font loading function - * --------------------------------------------------------------- - */ + stepHandler: function(up) { + if (boxes.step && !isDragging) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + boxes.step.cachedSize = null; + draw(); + } + }, + lockHandler: function(isLocked) { + if (isLocked) { + deselectAllBoxes(); + draw(); + } + } + }; + + // 4. Font loading function let loadCustomFont = function() { Graphics.prototype.setFontBrunoAce = function() { // Actual height 23 (24 - 2) @@ -61,50 +127,43 @@ E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))), 46, atob("CBEdChgYGhgaGBsaCQ=="), - 32|65536 + 32 | 65536 ); }; }; - /** - * --------------------------------------------------------------- - * 5. Initial settings of boxes and their positions - * --------------------------------------------------------------- - */ + // 5. Initial settings of boxes and their positions + let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal; for (let key in boxesConfig) { if (key === 'bg' && boxesConfig[key].img) { bgImage = storage.read(boxesConfig[key].img); } else if (key !== 'selectedConfig') { boxes[key] = Object.assign({}, boxesConfig[key]); + // Set default values for short, shortMonth, and disableSuffix + boxes[key].short = isBool(boxes[key].short, true); + boxes[key].shortMonth = isBool(boxes[key].shortMonth, true); + boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false); + + // Set box position + boxes[key].pos = { + x: w * boxes[key].boxPos.x, + y: h * boxes[key].boxPos.y + }; + // Cache box size + boxes[key].cachedSize = null; } } - let boxKeys = Object.keys(boxes); + // 6. Text and drawing functions - boxKeys.forEach((key) => { - let boxConfig = boxes[key]; - boxPos[key] = { - x: w * boxConfig.boxPos.x, - y: h * boxConfig.boxPos.y - }; - isDragging[key] = false; - wasDragging[key] = false; - }); - - /** - * --------------------------------------------------------------- - * 6. Text and drawing functions - * --------------------------------------------------------------- + /* + Overwrite the setColor function to allow the + use of (x) in g.theme.x as a string + in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") */ - - // Overwrite the setColor function to allow the - // use of (x) in g.theme.x as a string - // in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") let modSetColor = function() { - // Save the original setColor function g_setColor = g.setColor; - // Overwrite setColor with the new function g.setColor = function(color) { if (typeof color === "string" && color in g.theme) { g_setColor.call(g, g.theme[color]); @@ -115,7 +174,6 @@ }; let restoreSetColor = function() { - // Restore the original setColor function if (g_setColor) { g.setColor = g_setColor; } @@ -139,25 +197,6 @@ } }; - let calcBoxSize = function(boxItem) { - g.reset(); - g.setFontAlign(0,0); - g.setFont(boxItem.font, boxItem.fontSize); - let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; - let fontHeight = g.getFontHeight() + 2 * boxItem.outline; - totalWidth = strWidth + 2 * boxItem.xPadding; - totalHeight = fontHeight + 2 * boxItem.yPadding; - }; - - let calcBoxPos = function(boxKey) { - return { - x1: boxPos[boxKey].x - totalWidth / 2, - y1: boxPos[boxKey].y - totalHeight / 2, - x2: boxPos[boxKey].x + totalWidth / 2, - y2: boxPos[boxKey].y + totalHeight / 2 - }; - }; - let displaySaveIcon = function() { draw(boxes); g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24); @@ -168,33 +207,15 @@ }, 2000); }; - /** - * --------------------------------------------------------------- - * 7. String forming helper functions - * --------------------------------------------------------------- - */ - - let isBool = function(val, defaultVal) { - return typeof val !== 'undefined' ? Boolean(val) : defaultVal; - }; - + // 7. String forming helper functions let getDate = function(short, shortMonth, disableSuffix) { const date = new Date(); const dayOfMonth = date.getDate(); const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const year = date.getFullYear(); - let suffix; - if ([1, 21, 31].includes(dayOfMonth)) { - suffix = "st"; - } else if ([2, 22].includes(dayOfMonth)) { - suffix = "nd"; - } else if ([3, 23].includes(dayOfMonth)) { - suffix = "rd"; - } else { - suffix = "th"; - } - let dayOfMonthStr = disableSuffix ? dayOfMonth : dayOfMonth + suffix; - return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version + let suffix = ["st", "nd", "rd"][(dayOfMonth - 1) % 10] || "th"; + let dayOfMonthStr = disableSuffix ? dayOfMonth : `${dayOfMonth}${suffix}`; + return `${month} ${dayOfMonthStr}${short ? '' : `, ${year}`}`; }; let getDayOfWeek = function(date, short) { @@ -207,195 +228,215 @@ return short ? meridian[0] : meridian; }; - let modString = function(boxItem, data) { - let prefix = boxItem.prefix || ''; - let suffix = boxItem.suffix || ''; - return prefix + data + suffix; + let formatStr = function(boxItem, data) { + return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`; }; - /** - * --------------------------------------------------------------- - * 8. Main draw function - * --------------------------------------------------------------- - */ + // 8. Main draw function and update logic + let lastDay = -1; + const BATTERY_UPDATE_INTERVAL = 300000; - let draw = (function() { - let updatePerMinute = true; + let updateBoxData = function() { + let date = new Date(); + let currentDay = date.getDate(); + let now = Date.now(); - return function(boxes) { - date = new Date(); - g.clear(); - - // Always draw backgrounds full screen - - if (bgImage) { // Check for bg in boxclk config - g.drawImage(bgImage, 0, 0); - } else { // Otherwise use clockbg module - background.fillRect(0, 0, g.getWidth(), g.getHeight()); - } - + if (boxes.time || boxes.meridian || boxes.date || boxes.dow) { if (boxes.time) { - boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0).trim()); - updatePerMinute = isBool(boxes.time.short, true); - } - if (boxes.meridian) { - boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true))); - } - if (boxes.date) { - boxes.date.string = ( - modString(boxes.date, - getDate(isBool(boxes.date.short, true), - isBool(boxes.date.shortMonth, true), - isBool(boxes.date.disableSuffix, false) - ))); - } - if (boxes.dow) { - boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true))); - } - if (boxes.batt) { - boxes.batt.string = modString(boxes.batt, E.getBattery()); - } - if (boxes.step) { - boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps); - } - boxKeys.forEach((boxKey) => { - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); - if (isDragging[boxKey]) { - g.setColor(boxItem.border); - g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + let showSeconds = !boxes.time.short; + let timeString = locale.time(date, 1).trim(); + if (showSeconds) { + let seconds = date.getSeconds().toString().padStart(2, '0'); + timeString += ':' + seconds; + } + let newTimeString = formatStr(boxes.time, timeString); + if (newTimeString !== boxes.time.string) { + boxes.time.string = newTimeString; + boxes.time.cachedSize = null; } - g.drawString( - boxItem, - boxItem.string, - boxPos[boxKey].x + boxItem.xOffset, - boxPos[boxKey].y + boxItem.yOffset - ); - }); - if (!Object.values(isDragging).some(Boolean)) { - if (drawTimeout) clearTimeout(drawTimeout); - let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000; - drawTimeout = setTimeout(() => draw(boxes), interval); } - }; - })(); - /** - * --------------------------------------------------------------- - * 9. Helper function for touch event - * --------------------------------------------------------------- - */ + if (boxes.meridian) { + let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short)); + if (newMeridianString !== boxes.meridian.string) { + boxes.meridian.string = newMeridianString; + boxes.meridian.cachedSize = null; + } + } - let touchInText = function(e, boxItem, boxKey) { + if (boxes.date && currentDay !== lastDay) { + let newDateString = formatStr(boxes.date, + getDate(boxes.date.short, + boxes.date.shortMonth, + boxes.date.noSuffix) + ); + if (newDateString !== boxes.date.string) { + boxes.date.string = newDateString; + boxes.date.cachedSize = null; + } + } + + if (boxes.dow) { + let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short)); + if (newDowString !== boxes.dow.string) { + boxes.dow.string = newDowString; + boxes.dow.cachedSize = null; + } + } + + lastDay = currentDay; + } + + if (boxes.step) { + let newStepCount = Bangle.getHealthStatus("day").steps; + let newStepString = formatStr(boxes.step, newStepCount); + if (newStepString !== boxes.step.string) { + boxes.step.string = newStepString; + boxes.step.cachedSize = null; + } + } + + if (boxes.batt) { + if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) { + let currentLevel = E.getBattery(); + if (currentLevel !== boxes.batt.lastLevel) { + let newBattString = formatStr(boxes.batt, currentLevel); + if (newBattString !== boxes.batt.string) { + boxes.batt.string = newBattString; + boxes.batt.cachedSize = null; + boxes.batt.lastLevel = currentLevel; + } + } + boxes.batt.lastUpdate = now; + } + } + }; + + let draw = function() { + g.clear(); + + // Always draw backgrounds full screen + if (bgImage) { // Check for bg in boxclk config + g.drawImage(bgImage, 0, 0); + } else { // Otherwise use clockbg module + background.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + if (!isDragging) { + updateBoxData(); + } + + for (let boxKey in boxes) { + let boxItem = boxes[boxKey]; + + // Set font and alignment for each box individually + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + calcBoxSize(boxItem); + + const pos = calcBoxPos(boxItem); + + if (boxItem.selected) { + g.setColor(boxItem.border); + g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + } + + g.drawString( + boxItem, + boxItem.string, + boxItem.pos.x + boxItem.xOffset, + boxItem.pos.y + boxItem.yOffset + ); + } + + if (!isDragging) { + if (drawTimeout) clearTimeout(drawTimeout); + let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000); + drawTimeout = setTimeout(draw, updateInterval); + } + }; + + // 9. Helper function for touch event + let calcBoxPos = function(boxItem) { calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); + return { + x1: boxItem.pos.x - boxItem.cachedSize.width / 2, + y1: boxItem.pos.y - boxItem.cachedSize.height / 2, + x2: boxItem.pos.x + boxItem.cachedSize.width / 2, + y2: boxItem.pos.y + boxItem.cachedSize.height / 2 + }; + }; + + // Use cached size if available, otherwise calculate and cache + let calcBoxSize = function(boxItem) { + if (boxItem.cachedSize) { + return boxItem.cachedSize; + } + + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; + let fontHeight = g.getFontHeight() + 2 * boxItem.outline; + let totalWidth = strWidth + 2 * boxItem.xPadding; + let totalHeight = fontHeight + 2 * boxItem.yPadding; + + boxItem.cachedSize = { + width: totalWidth, + height: totalHeight + }; + + return boxItem.cachedSize; + }; + + let touchInText = function(e, boxItem) { + calcBoxSize(boxItem); + const pos = calcBoxPos(boxItem); return e.x >= pos.x1 && - e.x <= pos.x2 && - e.y >= pos.y1 && - e.y <= pos.y2; + e.x <= pos.x2 && + e.y >= pos.y1 && + e.y <= pos.y2; }; let deselectAllBoxes = function() { - Object.keys(isDragging).forEach((boxKey) => { - isDragging[boxKey] = false; - }); + isDragging = false; + for (let boxKey in boxes) { + boxes[boxKey].selected = false; + } restoreSetColor(); widgets.show(); widgets.swipeOn(); modSetColor(); }; - /** - * --------------------------------------------------------------- - * 10. Setup function to configure event handlers - * --------------------------------------------------------------- - */ - + // 10. Setup function to configure event handlers let setup = function() { - // ------------------------------------ - // Define the touchHandler function - // ------------------------------------ - touchHandler = function(zone, e) { - wasDragging = Object.assign({}, isDragging); - let boxTouched = false; - boxKeys.forEach((boxKey) => { - if (touchInText(e, boxes[boxKey], boxKey)) { - isDragging[boxKey] = true; - wasDragging[boxKey] = true; - boxTouched = true; - } - }); - if (!boxTouched) { - if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged - deselectAllBoxes(); - if (doubleTapTimer) { - clearTimeout(doubleTapTimer); - doubleTapTimer = null; - // Save boxesConfig on double tap outside of any box and when no boxes are being dragged - Object.keys(boxPos).forEach((boxKey) => { - boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3); - boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3); - }); - storage.write(fileName, JSON.stringify(boxesConfig)); - displaySaveIcon(); - return; - } - } else { - // if any box is being dragged, just deselect all without saving - deselectAllBoxes(); - } - } - if (Object.values(wasDragging).some(Boolean) || !boxTouched) { - draw(boxes); - } - doubleTapTimer = setTimeout(() => { - doubleTapTimer = null; - }, 500); // Increase or decrease this value based on the desired double tap timing - movementDistance = 0; - }; - - // ------------------------------------ - // Define the dragHandler function - // ------------------------------------ - dragHandler = function(e) { - // Check if any box is being dragged - if (!Object.values(isDragging).some(Boolean)) return; - // Calculate the movement distance - movementDistance += Math.abs(e.dx) + Math.abs(e.dy); - // Check if the movement distance exceeds a threshold - if (movementDistance > 1) { - boxKeys.forEach((boxKey) => { - if (isDragging[boxKey]) { - widgets.hide(); - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - let newX = boxPos[boxKey].x + e.dx; - let newY = boxPos[boxKey].y + e.dy; - if (newX - totalWidth / 2 >= 0 && - newX + totalWidth / 2 <= w && - newY - totalHeight / 2 >= 0 && - newY + totalHeight / 2 <= h ) { - boxPos[boxKey].x = newX; - boxPos[boxKey].y = newY; - } - const pos = calcBoxPos(boxKey); - g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2); - } - }); - draw(boxes); - } - }; - - Bangle.on('touch', touchHandler); - Bangle.on('drag', dragHandler); - + Bangle.on('lock', eventHandlers.lockHandler); + Bangle.on('touch', eventHandlers.touchHandler); + Bangle.on('drag', eventHandlers.dragHandler); + + if (boxes.step) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + Bangle.on('step', eventHandlers.stepHandler); + } + + if (boxes.batt) { + boxes.batt.lastLevel = E.getBattery(); + boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel); + boxes.batt.lastUpdate = Date.now(); + } + Bangle.setUI({ - mode : "clock", - remove : function() { - // Remove event handlers, stop draw timer, remove custom font if used - Bangle.removeListener('touch', touchHandler); - Bangle.removeListener('drag', dragHandler); + mode: "clock", + remove: function() { + // Remove event handlers, stop draw timer, remove custom font + Bangle.removeListener('touch', eventHandlers.touchHandler); + Bangle.removeListener('drag', eventHandlers.dragHandler); + Bangle.removeListener('lock', eventHandlers.lockHandler); + if (boxes.step) { + Bangle.removeListener('step', eventHandlers.stepHandler); + } if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; delete Graphics.prototype.setFontBrunoAce; @@ -405,16 +446,12 @@ widgets.show(); } }); + loadCustomFont(); - draw(boxes); + draw(); }; - /** - * --------------------------------------------------------------- - * 11. Main execution part - * --------------------------------------------------------------- - */ - + // 11. Main execution Bangle.loadWidgets(); widgets.swipeOn(); modSetColor(); diff --git a/apps/boxclk/metadata.json b/apps/boxclk/metadata.json index b4055f160..27e43c3be 100644 --- a/apps/boxclk/metadata.json +++ b/apps/boxclk/metadata.json @@ -1,7 +1,7 @@ { "id": "boxclk", "name": "Box Clock", - "version": "0.07", + "version": "0.08", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "icon": "app.png", "dependencies" : { "clockbg":"module" }, @@ -24,4 +24,4 @@ "data": [ {"name":"boxclk.json","url":"boxclk.json"} ] -} +} \ No newline at end of file