1002 lines
33 KiB
JavaScript
1002 lines
33 KiB
JavaScript
(function() {
|
|
/* Tile Clock with Clock Info Integration */
|
|
|
|
// ===== CONSTANTS =====
|
|
const SCALE = 12;
|
|
const SEC_SCALE = 6;
|
|
const FRAC_STEPS = 5;
|
|
const ANIM_DELAY = 16;
|
|
const GAP = 3;
|
|
const MAIN_BORDER_THICKNESS = 2;
|
|
const SEC_BORDER_THICKNESS = 1;
|
|
|
|
// ===== SCREEN DIMENSIONS =====
|
|
const width = g.getWidth();
|
|
const height = g.getHeight();
|
|
|
|
// ===== SETTINGS =====
|
|
const settings = require('Storage').readJSON("tileclk.json", true) || {
|
|
widgets: "show",
|
|
seconds: "hide",
|
|
borders: true,
|
|
borderColor: "theme",
|
|
haptics: true
|
|
};
|
|
const is12Hour = (require('Storage').readJSON("setting.json", true) || {})['12hour'] || false;
|
|
let showSeconds = settings.seconds === "show" || (settings.seconds === "dynamic" && !Bangle.isLocked());
|
|
const showBorders = settings.borders !== false;
|
|
|
|
// ===== STATE VARIABLES =====
|
|
let showingClockInfo = false, clockInfoUnfocused = false, userClockInfoPreference = null;
|
|
let pendingSwitch = false, isDrawing = true, isColonDrawn = false, isSeconds = false;
|
|
let drawTimeout = null, secondsTimeout = null, lastTime = null, lastSeconds = null;
|
|
let clockInfoMenu = null, touchHandler = null, lockHandler = null;
|
|
let animationTimeouts = new Uint16Array(50), animationTimeoutCount = 0;
|
|
// State tracking for conditional saving
|
|
let initialShowingClockInfo = false, initialUserClockInfoPreference = null;
|
|
|
|
|
|
// ===== STATE PERSISTENCE =====
|
|
const loadDisplayState = () => {
|
|
const state = require('Storage').readJSON("tileclk.state.json", true) || {};
|
|
showingClockInfo = state.showingClockInfo || false;
|
|
userClockInfoPreference = state.userClockInfoPreference || null;
|
|
|
|
// Track initial values for conditional saving
|
|
initialShowingClockInfo = showingClockInfo;
|
|
initialUserClockInfoPreference = userClockInfoPreference;
|
|
}
|
|
|
|
const saveDisplayState = () => {
|
|
// Only save if values have changed to reduce flash wear
|
|
if (showingClockInfo !== initialShowingClockInfo ||
|
|
userClockInfoPreference !== initialUserClockInfoPreference) {
|
|
require('Storage').writeJSON("tileclk.state.json", {
|
|
showingClockInfo: showingClockInfo,
|
|
userClockInfoPreference: userClockInfoPreference
|
|
});
|
|
}
|
|
}
|
|
|
|
// ===== DIGIT BITMAPS =====
|
|
const digitBitmaps = new Uint16Array([
|
|
0b000000000000000,0b111101101101111,0b010110010010111,0b111001111100111,0b111001111001111,
|
|
0b101101111001001,0b111100111001111,0b111100111101111,0b111001001001001,0b111101111101111,0b111101111001111
|
|
]);
|
|
|
|
let timeCache = null;
|
|
|
|
const initEssentialCaches = () => {
|
|
timeCache = {
|
|
hourDigits12: new Uint8Array(24 * 2),
|
|
hourDigits24: new Uint8Array(24 * 2),
|
|
minuteDigits: new Uint8Array(60 * 2)
|
|
};
|
|
|
|
for (let h = 0; h < 24; h++) {
|
|
const h12 = h % 12 || 12;
|
|
timeCache.hourDigits12[h * 2] = (h12 / 10) | 0;
|
|
timeCache.hourDigits12[h * 2 + 1] = h12 % 10;
|
|
timeCache.hourDigits24[h * 2] = (h / 10) | 0;
|
|
timeCache.hourDigits24[h * 2 + 1] = h % 10;
|
|
}
|
|
|
|
for (let m = 0; m < 60; m++) {
|
|
timeCache.minuteDigits[m * 2] = (m / 10) | 0;
|
|
timeCache.minuteDigits[m * 2 + 1] = m % 10;
|
|
}
|
|
}
|
|
|
|
// Helper function to get digit index
|
|
const getDigitIndex = (digit) => {
|
|
if (digit === null) return 0; // space/blank
|
|
return (digit >= 0 && digit <= 9) ? digit + 1 : 0;
|
|
}
|
|
|
|
const extractTimeDigits = (time) => {
|
|
if (time === null) {
|
|
return { h1: null, h2: null, m1: null, m2: null };
|
|
}
|
|
|
|
const hours = (time / 100) | 0;
|
|
const minutes = time % 100;
|
|
|
|
if (timeCache && timeCache.minuteDigits) {
|
|
const hourCache = is12Hour ? timeCache.hourDigits12 : timeCache.hourDigits24;
|
|
const minuteCache = timeCache.minuteDigits;
|
|
|
|
return {
|
|
h1: hourCache[hours * 2],
|
|
h2: hourCache[hours * 2 + 1],
|
|
m1: minuteCache[minutes * 2],
|
|
m2: minuteCache[minutes * 2 + 1]
|
|
};
|
|
}
|
|
|
|
return {
|
|
h1: (hours / 10) | 0,
|
|
h2: hours % 10,
|
|
m1: (minutes / 10) | 0,
|
|
m2: minutes % 10
|
|
};
|
|
}
|
|
|
|
const digitWidth = 3 * SCALE, colonWidth = SCALE, secDigitWidth = 3 * SEC_SCALE;
|
|
const totalSecWidth = 2 * secDigitWidth + GAP, secStartX = (width / 2) - (totalSecWidth / 2);
|
|
const widgetYOffset = (settings.widgets === "hide" || settings.widgets === "swipe") ? -SCALE : 0;
|
|
const borderColor = settings.borderColor === "theme" || !settings.borderColor ?
|
|
g.theme.bgH : g.toColor(settings.borderColor);
|
|
|
|
const positions = {
|
|
threeDigit: (() => {
|
|
const totalWidth = 3 * digitWidth + colonWidth + 3 * GAP;
|
|
const startX = (width - totalWidth) / 2;
|
|
return {
|
|
colonX: Math.round(startX + digitWidth + GAP),
|
|
colonY: Math.round(0.35 * height),
|
|
digitX: [
|
|
Math.round(startX),
|
|
Math.round(startX + digitWidth + GAP + colonWidth + GAP),
|
|
Math.round(startX + 2 * (digitWidth + GAP) + colonWidth + GAP)
|
|
],
|
|
digitsY: Math.round(0.41 * height)
|
|
};
|
|
})(),
|
|
fourDigit: (() => {
|
|
const totalWidth = 4 * digitWidth + colonWidth + 4 * GAP;
|
|
const startX = (width - totalWidth) / 2;
|
|
return {
|
|
colonX: Math.round(startX + 2 * (digitWidth + GAP)),
|
|
colonY: Math.round(0.35 * height),
|
|
digitX: [
|
|
Math.round(startX),
|
|
Math.round(startX + digitWidth + GAP),
|
|
Math.round(startX + 2 * (digitWidth + GAP) + colonWidth + GAP),
|
|
Math.round(startX + 3 * (digitWidth + GAP) + colonWidth + GAP)
|
|
],
|
|
digitsY: Math.round(0.41 * height)
|
|
};
|
|
})(),
|
|
seconds: {
|
|
x: [Math.round(secStartX), Math.round(secStartX + secDigitWidth + GAP)],
|
|
y: Math.round(0.8 * height)
|
|
}
|
|
};
|
|
|
|
// Touch areas
|
|
const mainTimeArea = { top: widgetYOffset, bottom: widgetYOffset + Math.round(0.75 * height) };
|
|
const bottomArea = { top: positions.seconds.y + widgetYOffset - 10, bottom: height };
|
|
|
|
const threeDigitLayout = [
|
|
{ type: 'digit', value: 'h2', x: positions.threeDigit.digitX[0], y: positions.threeDigit.digitsY + widgetYOffset, scale: SCALE },
|
|
{ type: 'colon', x: positions.threeDigit.colonX, y: positions.threeDigit.colonY + widgetYOffset, scale: SCALE },
|
|
{ type: 'digit', value: 'm1', x: positions.threeDigit.digitX[1], y: positions.threeDigit.digitsY + widgetYOffset, scale: SCALE },
|
|
{ type: 'digit', value: 'm2', x: positions.threeDigit.digitX[2], y: positions.threeDigit.digitsY + widgetYOffset, scale: SCALE }
|
|
];
|
|
const fourDigitLayout = [
|
|
{ type: 'digit', value: 'h1', x: positions.fourDigit.digitX[0], y: positions.fourDigit.digitsY + widgetYOffset, scale: SCALE },
|
|
{ type: 'digit', value: 'h2', x: positions.fourDigit.digitX[1], y: positions.fourDigit.digitsY + widgetYOffset, scale: SCALE },
|
|
{ type: 'colon', x: positions.fourDigit.colonX, y: positions.fourDigit.colonY + widgetYOffset, scale: SCALE },
|
|
{ type: 'digit', value: 'm1', x: positions.fourDigit.digitX[2], y: positions.fourDigit.digitsY + widgetYOffset, scale: SCALE },
|
|
{ type: 'digit', value: 'm2', x: positions.fourDigit.digitX[3], y: positions.fourDigit.digitsY + widgetYOffset, scale: SCALE }
|
|
];
|
|
|
|
// ===== EFFICIENT COLOR SYSTEM =====
|
|
// Pre-calculated color tables for animations
|
|
let colorOn = null, colorOff = null;
|
|
|
|
const initColorTables = () => {
|
|
// Use Uint16Array since Bangle uses RGB565 (16-bit) colors
|
|
colorOn = new Uint16Array(FRAC_STEPS + 1);
|
|
colorOff = new Uint16Array(FRAC_STEPS + 1);
|
|
|
|
const bgColor = g.theme.bg;
|
|
const fgColor = g.theme.fg;
|
|
|
|
// Simple linear interpolation between color values
|
|
for (let i = 0; i <= FRAC_STEPS; i++) {
|
|
const frac = i / FRAC_STEPS;
|
|
|
|
// Direct interpolation of RGB565 values
|
|
colorOn[i] = (bgColor + (fgColor - bgColor) * frac + 0.5) | 0;
|
|
colorOff[i] = (fgColor + (bgColor - fgColor) * frac + 0.5) | 0;
|
|
}
|
|
}
|
|
|
|
// ===== CONSOLIDATED BORDER DRAWING =====
|
|
const drawBorder = (x, y, s, isMainDigit) => {
|
|
if (!showBorders) return;
|
|
|
|
// Direct variable lookup is faster than object property access
|
|
const thickness = isMainDigit ? MAIN_BORDER_THICKNESS : SEC_BORDER_THICKNESS;
|
|
if (thickness <= 0) return;
|
|
|
|
g.setColor(borderColor);
|
|
for (let i = 0; i < thickness; i++) {
|
|
g.drawRect(x + i, y + i, x + s - 1 - i, y + s - 1 - i);
|
|
}
|
|
}
|
|
|
|
// ===== ANIMATION FUNCTIONS =====
|
|
const animateTransition = (x, y, s, startColor, endColor, drawBorderFunc, callback) => {
|
|
let step = 0;
|
|
const colors = startColor === g.theme.bg ? colorOn : colorOff;
|
|
|
|
const transition = () => {
|
|
if (!isDrawing || (pendingSwitch && isSeconds)) return;
|
|
|
|
// Use pre-calculated color
|
|
g.setColor(colors[step]);
|
|
g.fillRect(x, y, x + s - 1, y + s - 1);
|
|
|
|
step++;
|
|
if (step <= FRAC_STEPS) {
|
|
const timeout = setTimeout(transition, ANIM_DELAY);
|
|
if (animationTimeoutCount < animationTimeouts.length) {
|
|
animationTimeouts[animationTimeoutCount++] = timeout;
|
|
}
|
|
} else {
|
|
g.setColor(endColor);
|
|
g.fillRect(x, y, x + s - 1, y + s - 1);
|
|
if (drawBorderFunc) drawBorderFunc();
|
|
if (callback) callback();
|
|
}
|
|
}
|
|
|
|
transition();
|
|
}
|
|
|
|
const animateTileOn = (x, y, s, callback, isMainDigit) => {
|
|
const borderFunc = showBorders ? () => drawBorder(x, y, s, isMainDigit) : null;
|
|
animateTransition(x, y, s, g.theme.bg, g.theme.fg, borderFunc, callback);
|
|
}
|
|
|
|
const animateTileOff = (x, y, s, callback) => {
|
|
animateTransition(x, y, s, g.theme.fg, g.theme.bg, null, callback);
|
|
}
|
|
|
|
// ===== TILE CALCULATION =====
|
|
const calculateTilesToUpdate = (x, y, s, currentDigit, prevDigit) => {
|
|
const currentPacked = digitBitmaps[getDigitIndex(currentDigit)];
|
|
const prevPacked = digitBitmaps[getDigitIndex(prevDigit)];
|
|
const tiles = [];
|
|
|
|
let yPos = y;
|
|
|
|
// Loop through 5 rows
|
|
for (let row = 0; row < 5; row++) {
|
|
const shift = 12 - row * 3;
|
|
const currentRow = (currentPacked >> shift) & 0b111;
|
|
const prevRow = (prevPacked >> shift) & 0b111;
|
|
const diff = currentRow ^ prevRow;
|
|
if (diff) {
|
|
if (diff & 4) tiles.push({ x: x, y: yPos, state: (currentRow >> 2) & 1 });
|
|
if (diff & 2) tiles.push({ x: x + s, y: yPos, state: (currentRow >> 1) & 1 });
|
|
if (diff & 1) tiles.push({ x: x + s + s, y: yPos, state: currentRow & 1 });
|
|
}
|
|
yPos += s;
|
|
}
|
|
|
|
return tiles;
|
|
}
|
|
|
|
// ===== TILE UPDATE =====
|
|
const updateTile = (tile, s, skipAnimation, isMainDigit, isClearing) => {
|
|
if (tile.state) {
|
|
if (skipAnimation) {
|
|
g.setColor(g.theme.fg);
|
|
g.fillRect(tile.x, tile.y, tile.x + s - 1, tile.y + s - 1);
|
|
drawBorder(tile.x, tile.y, s, isMainDigit);
|
|
} else {
|
|
animateTileOn(tile.x, tile.y, s, null, isMainDigit);
|
|
}
|
|
} else {
|
|
if (skipAnimation || isClearing) {
|
|
g.setColor(g.theme.bg);
|
|
g.fillRect(tile.x, tile.y, tile.x + s - 1, tile.y + s - 1);
|
|
} else {
|
|
animateTileOff(tile.x, tile.y, s, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
const updateTiles = (tiles, s, callback, skipAnimation, isMainDigit, isClearing) => {
|
|
if (!isDrawing || !tiles.length || (pendingSwitch && isSeconds)) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
const tile = tiles.shift();
|
|
updateTile(tile, s, skipAnimation, isMainDigit, isClearing);
|
|
const timeout = setTimeout(() => updateTiles(tiles, s, callback, skipAnimation, isMainDigit, isClearing), ANIM_DELAY);
|
|
if (animationTimeoutCount < animationTimeouts.length) {
|
|
animationTimeouts[animationTimeoutCount++] = timeout;
|
|
}
|
|
}
|
|
|
|
// ===== DIGIT DRAWING =====
|
|
const drawDigit = (x, y, s, num, prevNum, callback, skipAnimation, isMainDigit) => {
|
|
if (num === prevNum) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
|
|
// Check if we should stop seconds animation
|
|
if (isSeconds && pendingSwitch) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
|
|
const tiles = calculateTilesToUpdate(x, y, s, num, prevNum);
|
|
updateTiles(tiles, s, callback, skipAnimation, isMainDigit, num === null);
|
|
}
|
|
|
|
const drawColon = (x, y, callback) => {
|
|
if (!isDrawing || isColonDrawn) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
animateTileOn(x, y + SCALE * 2, SCALE, () => {
|
|
animateTileOn(x, y + SCALE * 4, SCALE, () => {
|
|
isColonDrawn = true;
|
|
if (callback) callback();
|
|
}, true);
|
|
}, true);
|
|
}
|
|
|
|
// ===== TIME UPDATE SCHEDULING =====
|
|
const scheduleNextUpdate = () => {
|
|
if (drawTimeout) clearTimeout(drawTimeout);
|
|
const now = new Date();
|
|
const msUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
|
|
|
|
drawTimeout = setTimeout(updateAndAnimTime, msUntilNextMinute);
|
|
}
|
|
|
|
// ===== CLEARING FUNCTIONS (Direct callback approach for performance) =====
|
|
const clearColon = (callback) => {
|
|
if (!isColonDrawn) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
const layout = is12Hour && lastTime !== null && lastTime < 1000 ? threeDigitLayout : fourDigitLayout;
|
|
const colonItem = layout.find(item => item.type === 'colon');
|
|
|
|
if (colonItem) {
|
|
animateTileOff(colonItem.x, colonItem.y + SCALE * 2, SCALE, () => {
|
|
animateTileOff(colonItem.x, colonItem.y + SCALE * 4, SCALE, () => {
|
|
isColonDrawn = false;
|
|
if (callback) callback();
|
|
});
|
|
});
|
|
} else {
|
|
isColonDrawn = false;
|
|
if (callback) callback();
|
|
}
|
|
}
|
|
|
|
const clearAllDigits = (callback) => {
|
|
const wasThreeDigit = is12Hour && lastTime !== null && lastTime < 1000;
|
|
const layout = wasThreeDigit ? threeDigitLayout : fourDigitLayout;
|
|
|
|
const previousDigits = extractTimeDigits(lastTime);
|
|
|
|
// Direct callback chaining for better performance
|
|
const clearItems = (items, next) => {
|
|
if (!items.length) {
|
|
next();
|
|
return;
|
|
}
|
|
const item = items.shift();
|
|
|
|
if (item.type === 'digit') {
|
|
drawDigit(item.x, item.y, item.scale, null, previousDigits[item.value], () => clearItems(items, next), false, true);
|
|
} else if (item.type === 'colon') {
|
|
clearColon(() => clearItems(items, next));
|
|
}
|
|
}
|
|
|
|
// Clear in order: hours -> colon -> minutes -> seconds
|
|
const hourItems = layout.filter(item => item.type === 'digit' && item.value.startsWith('h'));
|
|
const colonItems = layout.filter(item => item.type === 'colon');
|
|
const minuteItems = layout.filter(item => item.type === 'digit' && item.value.startsWith('m'));
|
|
|
|
clearItems(hourItems.slice(), () => {
|
|
clearItems(colonItems.slice(), () => {
|
|
clearItems(minuteItems.slice(), () => {
|
|
if (showSeconds && !showingClockInfo) {
|
|
clearSeconds(() => {
|
|
lastTime = null;
|
|
if (callback) callback();
|
|
});
|
|
} else {
|
|
lastTime = null;
|
|
if (callback) callback();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== MAIN TIME UPDATE (OPTIMIZED) =====
|
|
function updateAndAnimTime() {
|
|
if (!isDrawing) return;
|
|
|
|
const now = new Date();
|
|
const hoursNum = is12Hour ? now.getHours() % 12 || 12 : now.getHours();
|
|
const minutesNum = now.getMinutes();
|
|
const currentTime = hoursNum * 100 + minutesNum;
|
|
|
|
// Extract current digits ONCE
|
|
const currentDigits = (() => {
|
|
if (timeCache && timeCache.minuteDigits) {
|
|
const hourCache = is12Hour ? timeCache.hourDigits12 : timeCache.hourDigits24;
|
|
const minuteCache = timeCache.minuteDigits;
|
|
|
|
return {
|
|
h1: hourCache[hoursNum * 2],
|
|
h2: hourCache[hoursNum * 2 + 1],
|
|
m1: minuteCache[minutesNum * 2],
|
|
m2: minuteCache[minutesNum * 2 + 1]
|
|
};
|
|
} else {
|
|
return {
|
|
h1: (hoursNum / 10) | 0,
|
|
h2: hoursNum % 10,
|
|
m1: (minutesNum / 10) | 0,
|
|
m2: minutesNum % 10
|
|
};
|
|
}
|
|
})();
|
|
|
|
// Determine layout based on extracted digits
|
|
const isCurrentThreeDigit = is12Hour && currentDigits.h1 === 0;
|
|
const wasLastThreeDigit = is12Hour && lastTime !== null && lastTime < 1000;
|
|
|
|
function drawTime() {
|
|
// Extract previous digits (or null for blank)
|
|
const previousDigits = (isCurrentThreeDigit !== wasLastThreeDigit && lastTime !== null) ?
|
|
{ h1: null, h2: null, m1: null, m2: null } :
|
|
extractTimeDigits(lastTime);
|
|
|
|
const layout = isCurrentThreeDigit ? threeDigitLayout : fourDigitLayout;
|
|
|
|
// Direct drawing without queue abstraction
|
|
function drawLayout(items, onComplete) {
|
|
if (!items.length) {
|
|
if (onComplete) onComplete();
|
|
return;
|
|
}
|
|
|
|
const item = items.shift();
|
|
const next = () => drawLayout(items, onComplete);
|
|
|
|
if (item.type === 'digit') {
|
|
drawDigit(item.x, item.y, item.scale, currentDigits[item.value], previousDigits[item.value], next, false, true);
|
|
} else if (item.type === 'colon') {
|
|
drawColon(item.x, item.y, next);
|
|
}
|
|
}
|
|
|
|
if (isCurrentThreeDigit !== wasLastThreeDigit) {
|
|
clearAllDigits(() => drawLayout(layout.slice(), finishDrawing));
|
|
} else {
|
|
drawLayout(layout.slice(), finishDrawing);
|
|
}
|
|
}
|
|
|
|
function finishDrawing() {
|
|
lastTime = currentTime;
|
|
if (showSeconds && !showingClockInfo) updateSeconds();
|
|
scheduleNextUpdate();
|
|
}
|
|
|
|
drawTime();
|
|
}
|
|
|
|
// ===== SECONDS HANDLING =====
|
|
function updateSeconds() {
|
|
if (isSeconds || !showSeconds || showingClockInfo || pendingSwitch) return;
|
|
isSeconds = true;
|
|
|
|
const now = new Date();
|
|
let secondsNum = now.getSeconds();
|
|
|
|
const skipAnimation = lastSeconds === null;
|
|
|
|
// Declare digit variables once
|
|
let s1, s2, prevS1, prevS2;
|
|
|
|
if (skipAnimation) {
|
|
// Calculate how many tiles need to be drawn from blank using cached lookups
|
|
if (timeCache && timeCache.minuteDigits) {
|
|
s1 = timeCache.minuteDigits[secondsNum * 2];
|
|
s2 = timeCache.minuteDigits[secondsNum * 2 + 1];
|
|
} else {
|
|
s1 = (secondsNum / 10) | 0; // Bitwise OR for integer division
|
|
s2 = secondsNum % 10;
|
|
}
|
|
|
|
let tiles0 = calculateTilesToUpdate(positions.seconds.x[0], positions.seconds.y + widgetYOffset, SEC_SCALE, s1, null);
|
|
let tiles1 = calculateTilesToUpdate(positions.seconds.x[1], positions.seconds.y + widgetYOffset, SEC_SCALE, s2, null);
|
|
let tilesNeeded = tiles0.length + tiles1.length;
|
|
|
|
// Check time again after calculations
|
|
const nowAfterCalc = new Date();
|
|
const timeUntilNextSecond = 1000 - nowAfterCalc.getMilliseconds();
|
|
const estimatedDrawTime = tilesNeeded * ANIM_DELAY * 2; // Double for safety margin
|
|
|
|
// If we can't finish in time, skip to next second
|
|
if (estimatedDrawTime > timeUntilNextSecond) {
|
|
// Get current time again to make sure we skip to the right second
|
|
secondsNum = (new Date().getSeconds() + 1) % 60;
|
|
}
|
|
}
|
|
|
|
// Extract current and previous digits using cached lookups
|
|
if (timeCache && timeCache.minuteDigits) {
|
|
s1 = timeCache.minuteDigits[secondsNum * 2];
|
|
s2 = timeCache.minuteDigits[secondsNum * 2 + 1];
|
|
if (lastSeconds !== null) {
|
|
prevS1 = timeCache.minuteDigits[lastSeconds * 2];
|
|
prevS2 = timeCache.minuteDigits[lastSeconds * 2 + 1];
|
|
} else {
|
|
prevS1 = null;
|
|
prevS2 = null;
|
|
}
|
|
} else {
|
|
s1 = (secondsNum / 10) | 0; // Bitwise OR for integer division
|
|
s2 = secondsNum % 10;
|
|
prevS1 = lastSeconds === null ? null : (lastSeconds / 10) | 0;
|
|
prevS2 = lastSeconds === null ? null : lastSeconds % 10;
|
|
}
|
|
|
|
function updateDigit(index) {
|
|
// Check if we should stop
|
|
if (!isDrawing || pendingSwitch || showingClockInfo) {
|
|
isSeconds = false;
|
|
return;
|
|
}
|
|
|
|
const currentDigit = index === 0 ? s1 : s2;
|
|
const prevDigit = index === 0 ? prevS1 : prevS2;
|
|
|
|
if (currentDigit !== prevDigit) {
|
|
drawSecondDigit(index, currentDigit, prevDigit, () => {
|
|
if (index === 0) {
|
|
updateDigit(1);
|
|
} else {
|
|
finishSeconds();
|
|
}
|
|
}, skipAnimation);
|
|
} else if (index === 0) {
|
|
updateDigit(1);
|
|
} else {
|
|
finishSeconds();
|
|
}
|
|
}
|
|
|
|
function finishSeconds() {
|
|
lastSeconds = secondsNum;
|
|
isSeconds = false;
|
|
|
|
// If we're locked after finishing animation, clear the seconds
|
|
if (settings.seconds === "dynamic" && Bangle.isLocked() && !showingClockInfo) {
|
|
clearSeconds();
|
|
return;
|
|
}
|
|
|
|
// Check if we have a pending switch
|
|
if (pendingSwitch) {
|
|
setTimeout(switchToClockInfo, 10);
|
|
return;
|
|
}
|
|
|
|
if (secondsTimeout) clearTimeout(secondsTimeout);
|
|
secondsTimeout = setTimeout(() => {
|
|
if (showSeconds && !showingClockInfo && !pendingSwitch) updateSeconds();
|
|
}, 1000 - new Date().getMilliseconds());
|
|
}
|
|
|
|
updateDigit(0);
|
|
}
|
|
|
|
const drawSecondDigit = (index, digit, prevDigit, callback, skipAnimation) => {
|
|
drawDigit(positions.seconds.x[index], positions.seconds.y + widgetYOffset, SEC_SCALE, digit, prevDigit, callback, skipAnimation, false);
|
|
}
|
|
|
|
const clearSeconds = (callback) => {
|
|
// If not drawing seconds, just call callback
|
|
if (lastSeconds === null) {
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
|
|
// Cancel any pending seconds update
|
|
if (secondsTimeout) {
|
|
clearTimeout(secondsTimeout);
|
|
secondsTimeout = null;
|
|
}
|
|
|
|
// Always do sequential animated clearing using cached lookups
|
|
isSeconds = true;
|
|
let s1, s2;
|
|
|
|
if (timeCache && timeCache.minuteDigits) {
|
|
s1 = timeCache.minuteDigits[lastSeconds * 2];
|
|
s2 = timeCache.minuteDigits[lastSeconds * 2 + 1];
|
|
} else {
|
|
s1 = (lastSeconds / 10) | 0; // Bitwise OR for integer division
|
|
s2 = lastSeconds % 10;
|
|
}
|
|
|
|
drawDigit(positions.seconds.x[0], positions.seconds.y + widgetYOffset, SEC_SCALE, null, s1, () => {
|
|
drawDigit(positions.seconds.x[1], positions.seconds.y + widgetYOffset, SEC_SCALE, null, s2, () => {
|
|
lastSeconds = null;
|
|
isSeconds = false;
|
|
if (callback) callback();
|
|
}, false, false);
|
|
}, false, false);
|
|
}
|
|
|
|
// ===== ANIMATION CLEANUP =====
|
|
const cancelAllAnimations = () => {
|
|
// Use typed array for better performance
|
|
for (let i = 0; i < animationTimeoutCount; i++) {
|
|
clearTimeout(animationTimeouts[i]);
|
|
}
|
|
animationTimeoutCount = 0;
|
|
}
|
|
|
|
// ===== SWITCHING FUNCTIONS (Optimized with direct state changes) =====
|
|
const switchToClockInfo = () => {
|
|
if (showingClockInfo || !clockInfoMenu) {
|
|
pendingSwitch = false;
|
|
return;
|
|
}
|
|
|
|
// Mark that we want to switch
|
|
pendingSwitch = true;
|
|
|
|
// If seconds are drawing, wait a bit and retry
|
|
if (isSeconds) {
|
|
setTimeout(() => {
|
|
if (pendingSwitch) switchToClockInfo();
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
// Clear pending flag
|
|
pendingSwitch = false;
|
|
|
|
cancelAllAnimations();
|
|
if (secondsTimeout) {
|
|
clearTimeout(secondsTimeout);
|
|
secondsTimeout = null;
|
|
}
|
|
|
|
const show = () => {
|
|
showingClockInfo = true;
|
|
clockInfoUnfocused = false;
|
|
if (clockInfoMenu) {
|
|
clockInfoMenu.focus = true;
|
|
clockInfoMenu.redraw();
|
|
}
|
|
g.flip();
|
|
};
|
|
|
|
if (showSeconds && lastSeconds !== null) {
|
|
clearSeconds(show);
|
|
} else {
|
|
show();
|
|
}
|
|
}
|
|
|
|
const hideClockInfo = () => {
|
|
if (!showingClockInfo) return;
|
|
|
|
pendingSwitch = false;
|
|
cancelAllAnimations();
|
|
showingClockInfo = false;
|
|
clockInfoUnfocused = false;
|
|
|
|
g.setColor(g.theme.bg);
|
|
g.fillRect(0, positions.seconds.y + widgetYOffset - 10, width, positions.seconds.y + widgetYOffset + 50);
|
|
g.flip();
|
|
}
|
|
|
|
const switchToSeconds = () => {
|
|
if (!showingClockInfo || !showSeconds) return;
|
|
|
|
pendingSwitch = false;
|
|
cancelAllAnimations();
|
|
userClockInfoPreference = 'hide';
|
|
showingClockInfo = false;
|
|
clockInfoUnfocused = false;
|
|
lastSeconds = null;
|
|
|
|
g.setColor(g.theme.bg);
|
|
g.fillRect(0, positions.seconds.y + widgetYOffset - 10, width, positions.seconds.y + widgetYOffset + 50);
|
|
g.flip();
|
|
|
|
updateAndAnimTime();
|
|
}
|
|
|
|
// ===== TOUCH HANDLING =====
|
|
const setupTouchHandler = () => {
|
|
// Remove old handler if exists
|
|
if (touchHandler) {
|
|
Bangle.removeListener("touch", touchHandler);
|
|
}
|
|
|
|
touchHandler = (_, e) => {
|
|
// Main time area - cycles through clock info states when clock info is available
|
|
if (e.y >= mainTimeArea.top && e.y <= mainTimeArea.bottom) {
|
|
if (showingClockInfo) {
|
|
if (!clockInfoUnfocused) {
|
|
// First tap: unfocus
|
|
if (settings.haptics !== false) Bangle.buzz(40);
|
|
clockInfoUnfocused = true;
|
|
if (clockInfoMenu) {
|
|
clockInfoMenu.focus = false;
|
|
clockInfoMenu.redraw();
|
|
}
|
|
g.flip();
|
|
} else {
|
|
// Second tap: dismiss
|
|
if (settings.haptics !== false) Bangle.buzz(60);
|
|
if (showSeconds) {
|
|
switchToSeconds();
|
|
} else {
|
|
userClockInfoPreference = 'hide';
|
|
hideClockInfo();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Bottom area - toggles between seconds and clock info
|
|
if (e.y >= bottomArea.top && e.y <= bottomArea.bottom) {
|
|
if (showingClockInfo) {
|
|
// Refocus if unfocused
|
|
if (clockInfoUnfocused) {
|
|
if (settings.haptics !== false) Bangle.buzz(50);
|
|
clockInfoUnfocused = false;
|
|
if (clockInfoMenu) {
|
|
clockInfoMenu.focus = true;
|
|
clockInfoMenu.redraw();
|
|
}
|
|
g.flip();
|
|
}
|
|
// When focused, do nothing (don't hide)
|
|
} else {
|
|
// Switch to clock info if available
|
|
if (clockInfoMenu) {
|
|
if (settings.haptics !== false) Bangle.buzz(50);
|
|
userClockInfoPreference = 'show';
|
|
switchToClockInfo();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add the handler
|
|
Bangle.on("touch", touchHandler);
|
|
}
|
|
|
|
// ===== CLOCK INFO SETUP =====
|
|
const setupClockInfo = () => {
|
|
if (clockInfoMenu) return;
|
|
|
|
try {
|
|
const clockInfoItems = require("clock_info").load();
|
|
|
|
const clockInfoDraw = (_, info, options) => {
|
|
if (!showingClockInfo) return;
|
|
|
|
g.reset().setBgColor(g.theme.bg).setColor(g.theme.fg);
|
|
g.clearRect(options.x, options.y, options.x + options.w - 1, options.y + options.h - 1);
|
|
|
|
if (!clockInfoUnfocused) {
|
|
g.setColor(borderColor);
|
|
g.drawRect(options.x, options.y, options.x + options.w - 1, options.y + options.h - 1);
|
|
}
|
|
|
|
g.setFont("6x8:3");
|
|
|
|
const padding = 8;
|
|
const imgSize = 24;
|
|
const midy = options.y + options.h / 2;
|
|
|
|
let text = info.text;
|
|
const maxTextWidth = options.w - imgSize - padding * 3;
|
|
|
|
if (g.stringWidth(text) > maxTextWidth) {
|
|
if (text.indexOf(' ') === -1) {
|
|
g.setFont("6x8:2");
|
|
if (g.stringWidth(text) > maxTextWidth) g.setFont("6x8:1");
|
|
} else {
|
|
g.setFont("6x8:2");
|
|
const lines = g.wrapString(text, maxTextWidth);
|
|
text = lines.slice(0, 2).join("\n") + (lines.length > 2 ? "..." : "");
|
|
}
|
|
}
|
|
|
|
const textWidth = g.stringWidth(text);
|
|
const totalContentWidth = imgSize + padding + textWidth;
|
|
const startX = options.x + (options.w - totalContentWidth) / 2;
|
|
|
|
if (info.img) {
|
|
g.drawImage(info.img, startX, midy - 12);
|
|
}
|
|
|
|
g.setColor(g.theme.fg);
|
|
g.setFontAlign(-1, 0);
|
|
g.drawString(text, startX + imgSize + padding, midy);
|
|
};
|
|
|
|
clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, {
|
|
app: "tileclk",
|
|
x: 0,
|
|
y: positions.seconds.y + widgetYOffset - 10,
|
|
w: width,
|
|
h: 50,
|
|
draw: clockInfoDraw
|
|
});
|
|
} catch(e) {
|
|
clockInfoMenu = null;
|
|
}
|
|
}
|
|
|
|
// ===== MAIN CLOCK DRAW =====
|
|
const drawClock = () => {
|
|
g.clear(Bangle.appRect);
|
|
if (settings.widgets !== "hide") Bangle.drawWidgets();
|
|
lastTime = null;
|
|
lastSeconds = null;
|
|
isColonDrawn = false;
|
|
|
|
// Initialize essential caches
|
|
if (!timeCache) initEssentialCaches();
|
|
if (!colorOn || !colorOff) initColorTables();
|
|
|
|
// Load saved state
|
|
loadDisplayState();
|
|
|
|
// Determine initial display based on saved preferences and current settings
|
|
if (showingClockInfo && clockInfoMenu) {
|
|
// User was viewing clock info - restore it but unfocused
|
|
clockInfoUnfocused = true;
|
|
clockInfoMenu.focus = false;
|
|
clockInfoMenu.redraw();
|
|
} else if (userClockInfoPreference === 'show' && clockInfoMenu) {
|
|
// User prefers clock info - show it unfocused
|
|
showingClockInfo = true;
|
|
clockInfoUnfocused = true;
|
|
clockInfoMenu.focus = false;
|
|
clockInfoMenu.redraw();
|
|
} else {
|
|
// Default: show nothing in seconds area
|
|
showingClockInfo = false;
|
|
clockInfoUnfocused = false;
|
|
}
|
|
|
|
updateAndAnimTime();
|
|
}
|
|
|
|
// ===== UI SETUP =====
|
|
Bangle.setUI({
|
|
mode: "clock",
|
|
remove: function() {
|
|
// Stop all drawing
|
|
isDrawing = false;
|
|
pendingSwitch = false;
|
|
isSeconds = false;
|
|
|
|
// Save current state
|
|
saveDisplayState();
|
|
|
|
// Clear all timeouts
|
|
if (drawTimeout) {
|
|
clearTimeout(drawTimeout);
|
|
drawTimeout = null;
|
|
}
|
|
if (secondsTimeout) {
|
|
clearTimeout(secondsTimeout);
|
|
secondsTimeout = null;
|
|
}
|
|
cancelAllAnimations();
|
|
|
|
// Remove event handlers
|
|
if (touchHandler) {
|
|
Bangle.removeListener("touch", touchHandler);
|
|
touchHandler = null;
|
|
}
|
|
if (lockHandler) {
|
|
Bangle.removeListener("lock", lockHandler);
|
|
lockHandler = null;
|
|
}
|
|
|
|
// Remove clock info menu
|
|
if (clockInfoMenu) {
|
|
clockInfoMenu.remove();
|
|
clockInfoMenu = null;
|
|
}
|
|
|
|
// Clear state variables
|
|
showingClockInfo = false;
|
|
clockInfoUnfocused = false;
|
|
userClockInfoPreference = null;
|
|
lastTime = null;
|
|
lastSeconds = null;
|
|
isColonDrawn = false;
|
|
isDrawing = false;
|
|
pendingSwitch = false;
|
|
isSeconds = false;
|
|
|
|
// Clear animation system
|
|
colorOn = null;
|
|
colorOff = null;
|
|
|
|
// Clear caches
|
|
timeCache = null;
|
|
|
|
// Restore widgets if hidden
|
|
if (["hide", "swipe"].includes(settings.widgets)) {
|
|
require("widget_utils").show();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ===== WIDGET SETUP =====
|
|
Bangle.loadWidgets();
|
|
if (settings.widgets === "hide") require("widget_utils").hide();
|
|
else if (settings.widgets === "swipe") require("widget_utils").swipeOn();
|
|
|
|
// ===== SETUP (run once) =====
|
|
setupClockInfo();
|
|
setupTouchHandler();
|
|
|
|
// ===== LOCK HANDLER =====
|
|
// Remove old handler if exists
|
|
if (lockHandler) {
|
|
Bangle.removeListener('lock', lockHandler);
|
|
}
|
|
|
|
// Create new handler
|
|
lockHandler = function(isLocked) {
|
|
if (settings.seconds === "dynamic") {
|
|
showSeconds = !isLocked;
|
|
pendingSwitch = false; // Clear any pending switch
|
|
|
|
if (isLocked) {
|
|
if (!showingClockInfo && lastSeconds !== null) {
|
|
if (secondsTimeout) clearTimeout(secondsTimeout);
|
|
clearSeconds(() => {
|
|
if (userClockInfoPreference === 'show') {
|
|
showingClockInfo = true;
|
|
clockInfoUnfocused = true;
|
|
if (clockInfoMenu) {
|
|
clockInfoMenu.focus = false;
|
|
clockInfoMenu.redraw();
|
|
}
|
|
}
|
|
g.flip();
|
|
});
|
|
}
|
|
} else {
|
|
if (showingClockInfo && userClockInfoPreference !== 'show') {
|
|
switchToSeconds();
|
|
} else if (showSeconds && !showingClockInfo) {
|
|
updateSeconds();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add the handler
|
|
Bangle.on('lock', lockHandler);
|
|
|
|
// ===== START CLOCK =====
|
|
drawClock();
|
|
})(); |