commit
4be6bb2cff
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Official release
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"id": "widtimer",
|
||||||
|
"name": "Timer Widget",
|
||||||
|
"shortName": "WidTimer",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Timer widget with swipe controls. Swipe a T (for timer) to start the timer and unlock the controls. Then single swipes adjust the time: right/left ±1min, up/down ±10min. Will buzz upon timer completion.",
|
||||||
|
"icon": "widtimer.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget,timer,gesture",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"widtimer.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
/**
|
||||||
|
* Timer Widget for BangleJS 2
|
||||||
|
*
|
||||||
|
* A battery-optimized timer widget with gesture-based controls and accidental activation protection.
|
||||||
|
* Features double-swipe unlock mechanism, visual feedback, and adaptive refresh rates.
|
||||||
|
*
|
||||||
|
* @author Claude AI Assistant
|
||||||
|
* @version 0.03
|
||||||
|
*/
|
||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONSTANTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/** Timer adjustment constants (in seconds) */
|
||||||
|
const ONE_MINUTE = 60;
|
||||||
|
const TEN_MINUTES = 600;
|
||||||
|
const DEFAULT_TIME = 300; // 5 minutes
|
||||||
|
|
||||||
|
/** Refresh rate constants for battery optimization */
|
||||||
|
const COUNTDOWN_INTERVAL_NORMAL = 10000; // 10 seconds when > 1 minute
|
||||||
|
const COUNTDOWN_INTERVAL_FINAL = 1000; // 1 second when <= 1 minute
|
||||||
|
|
||||||
|
/** Completion notification constants */
|
||||||
|
const BUZZ_COUNT = 3;
|
||||||
|
const BUZZ_TOTAL_TIME = 5000; // 5 seconds total
|
||||||
|
|
||||||
|
/** Gesture control constants */
|
||||||
|
const UNLOCK_GESTURE_TIMEOUT = 1500; // milliseconds before unlock gesture has to be started from scratcb
|
||||||
|
const UNLOCK_CONTROL_TIMEOUT = 5000; // milliseconds before gesture control locks again
|
||||||
|
const DIRECTION_LEFT = "left";
|
||||||
|
const DIRECTION_RIGHT = "right";
|
||||||
|
const DIRECTION_UP = "up";
|
||||||
|
const DIRECTION_DOWN = "down";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STATE VARIABLES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
var settings;
|
||||||
|
var interval = 0;
|
||||||
|
var remainingTime = 0; // in seconds
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UTILITY FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format time as MM:SS (allowing MM > 59)
|
||||||
|
* @param {number} seconds - Time in seconds
|
||||||
|
* @returns {string} Formatted time string
|
||||||
|
*/
|
||||||
|
function formatTime(seconds) {
|
||||||
|
var mins = Math.floor(seconds / 60);
|
||||||
|
var secs = seconds % 60;
|
||||||
|
return mins.toString().padStart(2, '0') + ':' + secs.toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SETTINGS MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current settings to storage
|
||||||
|
*/
|
||||||
|
function saveSettings() {
|
||||||
|
require('Storage').writeJSON('widtimer.json', settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load settings from storage and calculate current timer state
|
||||||
|
*/
|
||||||
|
function loadSettings() {
|
||||||
|
settings = require('Storage').readJSON('widtimer.json', 1) || {
|
||||||
|
totalTime: DEFAULT_TIME,
|
||||||
|
running: false,
|
||||||
|
startTime: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate remaining time if timer was running
|
||||||
|
if (settings.running && settings.startTime) {
|
||||||
|
var elapsed = Math.floor((Date.now() - settings.startTime) / 1000);
|
||||||
|
remainingTime = Math.max(0, settings.totalTime - elapsed);
|
||||||
|
if (remainingTime === 0) {
|
||||||
|
settings.running = false;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remainingTime = settings.totalTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TIMER CONTROL FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main countdown function - handles timer progression and battery optimization
|
||||||
|
*/
|
||||||
|
function countdown() {
|
||||||
|
if (!settings.running) return;
|
||||||
|
|
||||||
|
var elapsed = Math.floor((Date.now() - settings.startTime) / 1000);
|
||||||
|
var oldRemainingTime = remainingTime;
|
||||||
|
remainingTime = Math.max(0, settings.totalTime - elapsed);
|
||||||
|
|
||||||
|
// Switch to faster refresh when entering final minute for better accuracy
|
||||||
|
if (oldRemainingTime > 60 && remainingTime <= 60 && interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = setInterval(countdown, COUNTDOWN_INTERVAL_FINAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTime <= 0) {
|
||||||
|
// Timer finished - provide completion notification
|
||||||
|
buzzMultiple();
|
||||||
|
settings.running = false;
|
||||||
|
remainingTime = settings.totalTime; // Reset to original time
|
||||||
|
saveSettings();
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WIDGETS["widtimer"].draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate multiple buzzes for timer completion notification
|
||||||
|
*/
|
||||||
|
function buzzMultiple() {
|
||||||
|
var buzzInterval = BUZZ_TOTAL_TIME / BUZZ_COUNT;
|
||||||
|
for (var i = 0; i < BUZZ_COUNT; i++) {
|
||||||
|
(function(delay) {
|
||||||
|
setTimeout(function() {
|
||||||
|
Bangle.buzz(300);
|
||||||
|
}, delay);
|
||||||
|
})(i * buzzInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the timer with battery-optimized refresh rate
|
||||||
|
*/
|
||||||
|
function startTimer() {
|
||||||
|
if (remainingTime > 0 && !settings.running) {
|
||||||
|
settings.running = true;
|
||||||
|
settings.startTime = Date.now();
|
||||||
|
saveSettings();
|
||||||
|
if (!interval) {
|
||||||
|
// Use different intervals based on remaining time for battery optimization
|
||||||
|
var intervalTime = remainingTime <= 60 ? COUNTDOWN_INTERVAL_FINAL : COUNTDOWN_INTERVAL_NORMAL;
|
||||||
|
interval = setInterval(countdown, intervalTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust timer by specified number of seconds
|
||||||
|
* @param {number} seconds - Positive or negative adjustment in seconds
|
||||||
|
*/
|
||||||
|
function adjustTimer(seconds) {
|
||||||
|
if (settings.running) {
|
||||||
|
// For running timer, adjust both total time and remaining time
|
||||||
|
settings.totalTime = Math.max(0, settings.totalTime + seconds);
|
||||||
|
remainingTime = Math.max(0, remainingTime + seconds);
|
||||||
|
|
||||||
|
// If remaining time becomes 0 or negative, stop the timer
|
||||||
|
if (remainingTime <= 0) {
|
||||||
|
settings.running = false;
|
||||||
|
remainingTime = 0;
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = 0;
|
||||||
|
}
|
||||||
|
// Provide feedback if timer finished due to negative adjustment
|
||||||
|
if (remainingTime === 0) {
|
||||||
|
buzzMultiple();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Adjust stopped timer
|
||||||
|
settings.totalTime = Math.max(0, settings.totalTime + seconds);
|
||||||
|
remainingTime = settings.totalTime;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
WIDGETS["widtimer"].draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// GESTURE CONTROL SYSTEM
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Gesture state variables
|
||||||
|
var drag = null;
|
||||||
|
var lastSwipeTime = 0;
|
||||||
|
var lastSwipeDirection = null;
|
||||||
|
var isControlLocked = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset gesture controls to locked state
|
||||||
|
*/
|
||||||
|
function resetUnlock() {
|
||||||
|
isControlLocked = true;
|
||||||
|
WIDGETS["widtimer"].draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHorizontal(direction) {
|
||||||
|
return (direction == DIRECTION_LEFT) || (direction == DIRECTION_RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVertical(direction) {
|
||||||
|
return (direction == DIRECTION_UP) || (direction == DIRECTION_DOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnlockGesture(first_direction, second_direction) {
|
||||||
|
return (isHorizontal(first_direction) && isVertical(second_direction)
|
||||||
|
|| isVertical(first_direction) && isHorizontal(second_direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up gesture handlers with double-swipe protection against accidental activation
|
||||||
|
*/
|
||||||
|
function setupGestures() {
|
||||||
|
Bangle.on("drag", function(e) {
|
||||||
|
if (!drag) {
|
||||||
|
// Start tracking drag gesture
|
||||||
|
drag = {x: e.x, y: e.y};
|
||||||
|
} else if (!e.b) {
|
||||||
|
// Drag gesture completed
|
||||||
|
var dx = e.x - drag.x;
|
||||||
|
var dy = e.y - drag.y;
|
||||||
|
drag = null;
|
||||||
|
|
||||||
|
// Only process significant gestures
|
||||||
|
if (Math.abs(dx) > 20 || Math.abs(dy) > 20) {
|
||||||
|
var currentTime = Date.now();
|
||||||
|
var direction = null;
|
||||||
|
var adjustment = 0;
|
||||||
|
|
||||||
|
// Determine gesture direction and timer adjustment
|
||||||
|
if (Math.abs(dx) > Math.abs(dy) + 10) {
|
||||||
|
// Horizontal swipe detected
|
||||||
|
if (dx > 0) {
|
||||||
|
direction = 'right';
|
||||||
|
adjustment = ONE_MINUTE;
|
||||||
|
} else {
|
||||||
|
direction = 'left';
|
||||||
|
adjustment = -ONE_MINUTE;
|
||||||
|
}
|
||||||
|
} else if (Math.abs(dy) > Math.abs(dx) + 10) {
|
||||||
|
// Vertical swipe detected
|
||||||
|
if (dy > 0) {
|
||||||
|
direction = 'down';
|
||||||
|
adjustment = -TEN_MINUTES;
|
||||||
|
} else {
|
||||||
|
direction = 'up';
|
||||||
|
adjustment = TEN_MINUTES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction) {
|
||||||
|
// Process gesture based on lock state
|
||||||
|
if (!isControlLocked) {
|
||||||
|
// Controls unlocked - execute adjustment immediately
|
||||||
|
adjustTimer(adjustment);
|
||||||
|
} else if (isUnlockGesture(direction, lastSwipeDirection) &&
|
||||||
|
currentTime - lastSwipeTime < UNLOCK_GESTURE_TIMEOUT) {
|
||||||
|
// Double swipe detected - unlock controls and execute
|
||||||
|
isControlLocked = false;
|
||||||
|
// adjustTimer(adjustment);
|
||||||
|
Bangle.buzz(50); // Provide unlock feedback
|
||||||
|
|
||||||
|
// Auto-start if time > 0
|
||||||
|
if (settings.totalTime > 0) {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-lock after `UNLOCK_CONTROL_TIMEOUT` seconds of inactivity
|
||||||
|
setTimeout(resetUnlock, UNLOCK_CONTROL_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update gesture tracking state
|
||||||
|
lastSwipeDirection = direction;
|
||||||
|
lastSwipeTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// WIDGET DEFINITION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main widget object following BangleJS widget conventions
|
||||||
|
*/
|
||||||
|
WIDGETS["widtimer"] = {
|
||||||
|
area: "tl",
|
||||||
|
width: 58, // Optimized width for vector font display
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the widget with current timer state and visual feedback
|
||||||
|
*/
|
||||||
|
draw: function() {
|
||||||
|
g.reset();
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
g.clearRect(this.x, this.y, this.x + this.width, this.y + 23);
|
||||||
|
|
||||||
|
// Use vector font for crisp, scalable display
|
||||||
|
g.setFont("Vector", 16);
|
||||||
|
var timeStr = formatTime(remainingTime);
|
||||||
|
|
||||||
|
// Set color based on current timer state
|
||||||
|
if (settings.running && remainingTime > 0) {
|
||||||
|
g.setColor("#ffff00"); // Yellow when running (visible on colored backgrounds)
|
||||||
|
} else if (remainingTime === 0) {
|
||||||
|
g.setColor("#ff0000"); // Red when finished
|
||||||
|
} else if (!isControlLocked) {
|
||||||
|
g.setColor("#00ff88"); // Light green when controls unlocked
|
||||||
|
} else {
|
||||||
|
g.setColor("#ffffff"); // White when stopped/locked
|
||||||
|
}
|
||||||
|
|
||||||
|
g.drawString(timeStr, this.x + this.width/2, this.y + 12);
|
||||||
|
g.setColor("#ffffff"); // Reset graphics color
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload widget state from storage and restart timer if needed
|
||||||
|
*/
|
||||||
|
reload: function() {
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
|
// Clear any existing countdown interval
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart countdown if timer was previously running
|
||||||
|
if (settings.running && remainingTime > 0) {
|
||||||
|
var intervalTime = remainingTime <= 60 ? COUNTDOWN_INTERVAL_FINAL : COUNTDOWN_INTERVAL_NORMAL;
|
||||||
|
interval = setInterval(countdown, intervalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INITIALIZATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Initialize widget and set up gesture handlers
|
||||||
|
WIDGETS["widtimer"].reload();
|
||||||
|
setupGestures();
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Loading…
Reference in New Issue