{ // 1. Module dependencies and initial configurations let background = require("clockbg"); let storage = require("Storage"); let locale = require("locale"); let widgets = require("widget_utils"); let bgImage; let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; if (!storage.read(fileName)) { fileName = 'boxclk.json'; } let boxesConfig = storage.readJSON(fileName, 1) || {}; let boxes = {}; 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 let w = g.getWidth(); let h = g.getHeight(); let drawTimeout; // 3. Event handlers let 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 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(); }; let stepHandler = function(up) { if (boxes.step && !isDragging) { boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); boxes.step.cachedSize = null; draw(); } }; let lockHandler = function(isLocked) { if (isLocked) { deselectAllBoxes(); draw(); } }; // 4. Font loading function let loadCustomFont = function() { Graphics.prototype.setFontBrunoAce = function() { // Actual height 23 (24 - 2) return this.setFontCustom( 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 ); }; }; // 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; } } // 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") */ let modSetColor = function() { g_setColor = g.setColor; g.setColor = function(color) { if (typeof color === "string" && color in g.theme) { g_setColor.call(g, g.theme[color]); } else { g_setColor.call(g, color); } }; }; let restoreSetColor = function() { if (g_setColor) { g.setColor = g_setColor; } }; // Overwrite the drawString function let g_drawString = g.drawString; g.drawString = function(box, str, x, y) { outlineText(box, str, x, y); g.setColor(box.color); g_drawString.call(g, str, x, y); }; let outlineText = function(box, str, x, y) { let px = box.outline; let dx = [-px, 0, px, -px, px, -px, 0, px]; let dy = [-px, -px, -px, 0, 0, px, px, px]; g.setColor(box.outlineColor); for (let i = 0; i < dx.length; i++) { g_drawString.call(g, str, x + dx[i], y + dy[i]); } }; let displaySaveIcon = function() { draw(boxes); g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24); // Display save icon for 2 seconds setTimeout(() => { g.clearRect(w / 2 - 24, h / 2 - 24, w / 2 + 24, h / 2 + 24); draw(boxes); }, 2000); }; // 7. String forming helper functions let getDate = function(short, shortMonth, disableSuffix) { const date = new Date(); const day = date.getDate(); const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const year = date.getFullYear(); const getSuffix = (day) => { if (day >= 11 && day <= 13) return 'th'; const lastDigit = day % 10; switch (lastDigit) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } }; const dayStr = disableSuffix ? day : `${day}${getSuffix(day)}`; return `${month} ${dayStr}${short ? '' : `, ${year}`}`; // not including year for short version }; let getDayOfWeek = function(date, short) { return locale.dow(date, short ? 1 : 0); }; locale.meridian = function(date, short) { let hours = date.getHours(); let meridian = hours >= 12 ? 'PM' : 'AM'; return short ? meridian[0] : meridian; }; let formatStr = function(boxItem, data) { return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`; }; // 8. Main draw function and update logic let lastDay = -1; const BATTERY_UPDATE_INTERVAL = 300000; let updateBoxData = function() { let date = new Date(); let currentDay = date.getDate(); let now = Date.now(); if (boxes.time || boxes.meridian || boxes.date || boxes.dow) { if (boxes.time) { 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; } } 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; } } 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); 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; }; let deselectAllBoxes = function() { isDragging = false; for (let boxKey in boxes) { boxes[boxKey].selected = false; } restoreSetColor(); widgets.show(); widgets.swipeOn(); modSetColor(); }; // 10. Setup function to configure event handlers let setup = function() { Bangle.on('lock', lockHandler); Bangle.on('touch', touchHandler); Bangle.on('drag', dragHandler); if (boxes.step) { boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); Bangle.on('step', 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 Bangle.removeListener('touch', touchHandler); Bangle.removeListener('drag', dragHandler); Bangle.removeListener('lock', lockHandler); if (boxes.step) { Bangle.removeListener('step', stepHandler); } if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; delete Graphics.prototype.setFontBrunoAce; // Restore original drawString function (no outlines) g.drawString = g_drawString; restoreSetColor(); widgets.show(); } }); loadCustomFont(); draw(); }; // 11. Main execution Bangle.loadWidgets(); widgets.swipeOn(); modSetColor(); setup(); }