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