feat(widgets): add battery-optimized timer widget with gesture controls
- Implement widtimer widget following BangleJS naming conventions - Add double-swipe protection against accidental activation - Support gesture controls: swipe right/left ±1min, up/down ±10min - Include battery optimization with adaptive refresh rates (10s/1s) - Provide visual feedback with color-coded states - Generate 3-pulse completion notification over 5 seconds - Use vector font for crisp display - Store persistent timer state across device restarts The widget requires double-swipe in same direction to unlock controls, then allows single swipes for 10 seconds before auto-locking. Refresh rate automatically switches from 10s to 1s in final minute. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
parent
72fb9d5861
commit
1b71d691be
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"id": "gestimer",
|
||||||
|
"name": "Gesture Timer",
|
||||||
|
"shortName": "GesTimer",
|
||||||
|
"version": "0.03",
|
||||||
|
"description": "Battery-optimized timer widget with double-swipe protection. Double-swipe in any direction to unlock, then single swipes work: right/left ±1min, up/down ±10min. Visual feedback, multiple completion buzzes.",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget,timer,gesture,battery-optimized",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"gestimer.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
/**
|
||||||
|
* 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 DOUBLE_SWIPE_TIMEOUT = 1500; // 1.5 seconds between swipes
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Auto-start if time > 0
|
||||||
|
if (settings.totalTime > 0) {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (lastSwipeDirection === direction &&
|
||||||
|
currentTime - lastSwipeTime < DOUBLE_SWIPE_TIMEOUT) {
|
||||||
|
// Double swipe detected - unlock controls and execute
|
||||||
|
isControlLocked = false;
|
||||||
|
adjustTimer(adjustment);
|
||||||
|
Bangle.buzz(50); // Provide unlock feedback
|
||||||
|
|
||||||
|
// Auto-lock after 10 seconds of inactivity
|
||||||
|
setTimeout(resetUnlock, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue