From 1b71d691be25927ddaa041305b7acccf7648e3d1 Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Sun, 25 May 2025 22:07:13 +0200 Subject: [PATCH 1/7] feat(widgets): add battery-optimized timer widget with gesture controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/widtimer/metadata.json | 14 ++ apps/widtimer/widget.js | 344 ++++++++++++++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 apps/widtimer/metadata.json create mode 100644 apps/widtimer/widget.js diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json new file mode 100644 index 000000000..1df5fa947 --- /dev/null +++ b/apps/widtimer/metadata.json @@ -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"} + ] +} \ No newline at end of file diff --git a/apps/widtimer/widget.js b/apps/widtimer/widget.js new file mode 100644 index 000000000..75f643689 --- /dev/null +++ b/apps/widtimer/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(); +})(); \ No newline at end of file From 19ab4409544cb6c836e2a11dc718389b113c7e5d Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Sun, 25 May 2025 22:26:51 +0200 Subject: [PATCH 2/7] docs: fix metadata --- apps/widtimer/metadata.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json index 1df5fa947..9aa0b4f64 100644 --- a/apps/widtimer/metadata.json +++ b/apps/widtimer/metadata.json @@ -1,14 +1,14 @@ { - "id": "gestimer", - "name": "Gesture Timer", - "shortName": "GesTimer", + "id": "widtimer", + "name": "Timer Widget", + "shortName": "WidTimer", "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.", + "description": "Timer widget with swipe controls. Double-swipe in any direction to unlock, then single swipes work: right/left ±1min, up/down ±10min. Will buzz upon timer completion.", "icon": "widget.png", "type": "widget", "tags": "widget,timer,gesture,battery-optimized", "supports": ["BANGLEJS2"], "storage": [ - {"name":"gestimer.wid.js","url":"widget.js"} + {"name":"widtimer.wid.js","url":"widget.js"} ] } \ No newline at end of file From 8210e62e0169897d283110391f7af505d8065a73 Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Mon, 26 May 2025 12:03:06 +0200 Subject: [PATCH 3/7] bugfix: add missing logo --- apps/widtimer/metadata.json | 4 ++-- apps/widtimer/widtimer.png | Bin 0 -> 2082 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 apps/widtimer/widtimer.png diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json index 9aa0b4f64..026096e59 100644 --- a/apps/widtimer/metadata.json +++ b/apps/widtimer/metadata.json @@ -4,9 +4,9 @@ "shortName": "WidTimer", "version": "0.03", "description": "Timer widget with swipe controls. Double-swipe in any direction to unlock, then single swipes work: right/left ±1min, up/down ±10min. Will buzz upon timer completion.", - "icon": "widget.png", + "icon": "widtimer.png", "type": "widget", - "tags": "widget,timer,gesture,battery-optimized", + "tags": "widget,timer,gesture", "supports": ["BANGLEJS2"], "storage": [ {"name":"widtimer.wid.js","url":"widget.js"} diff --git a/apps/widtimer/widtimer.png b/apps/widtimer/widtimer.png new file mode 100644 index 0000000000000000000000000000000000000000..d167a2bfb815c53d25e01d263859a6d2547f4685 GIT binary patch literal 2082 zcmb_d`9BkkA0KjNl4BuL&0K}8*c_!WYzu2h=BPB+G`Wv>MC}nPp`>K3SdParVsf8z z&yhLWgdC~RLgjw+=<)p%zOUEw`n=!o&+GmA{P6kV{lohn9)|`>sYn3;03a4)<+!)~ z_81}|yr*d9acuxVh~kez;lmu!mPfJHDC9{KBvS8`{z(8p`%%QB=@WlCD0QDhlH=^0 zJTUsaws)EZAfw)=_+gq(`@qKdfvSOt9h_U>lL7< z&3kasWNA}Nye%HK?3FW#JElb>9 z;=re3TAM}lUtNn;Q?Xf;V5S8>D*5tSwUyhj&{@oDI`9@~+`zp418h8pv?9K`di!>Qx*SkAQJu0+K?DLyOV{?y$^TqU&CjUbA~nnyPr|aMlYFNG{5F2ay~x^|(ua=31?Z(Qp*(08P3r%`;x%!uPBB9m0ymyCcI={k za_2Y}itM{fiRNlCJ+1DaF{<@7P~J_kts5aKO9M4%qYHc}+UXBDy zBeAu6HWs8DI7m(?DcDR;Nn>k_&nTSd?Y_xcigc><%*g>Hw#r%vt)-MCB1ci+QOnZ2 z>TTnVO0i<7bFgA@x?pQnk7r>7YjIUobAQatWS(D+YOvHFwE&O)IS;mO?$qKA3W)5a z>-5+=5%}fxcku9>2HiKP&&d^_7;#xfBrh^72J(E)X~SSiQp_;EKEPoA zZpf!T76G^^Dtk=j?NsKfyJc@A4sm20xl*TtDu=^;7JN%n6K`FFLT!5PgE2B~l79MZ zUAA$HS27w#{-cOGobhl=dwU=$-ndfYr9%IPNtx9e3&d9n8cOM}#VrM*Q~wa%Px%c} zaeZ8#%u49^ONnmn_KkqwCmN!)s`u0)Ge@{F#byxISJWI!c3wsfRu54b@JQ#4XW+tYtec_ z<)ao;X3EDhJY`Y#GV?{=S5qT-Is3~30N?#i5g8mjR1x|<8?yrMB64z)tHRMxVMn_d&70)gThGI%2PAinTLLO znICL8SXiZPKk`00kv7)-fv(ox<>hhjRycB)aM`_kWn~rBRwQ@)mQ1yoa8+c2?L6&C zr!jhffLxx}<$fqFTVni*Xbu6r(;DskVP0Anxy`5IJV)kLqpG=%WjG)ATrZg;6R1U( z46N+J73^V?m-f>kUF+ZAG1;W1T-fsmF*TX_yzgt@@J93EP3>#Ke68<3wpR;UExY1c zz0?x(%q4b0OYY%jOIA!u9&ReC5&AmdN!QH@4OzK{O}%9>bb^j`%Pb2bynmLE8# z9etZgo7qHqLJ&~<3-O?PS#6Uy9Xqaeg*w4yin@i%=WOY>a{kZ`P#4~YoW&ha~G zApZqxp!@^U-X>K9enVpDjfPe}93* Date: Mon, 26 May 2025 12:32:03 +0200 Subject: [PATCH 4/7] feat: use T-gesture for unlocking to prevent accidental unlocks --- apps/widtimer/widget.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/widtimer/widget.js b/apps/widtimer/widget.js index 75f643689..7fce2e753 100644 --- a/apps/widtimer/widget.js +++ b/apps/widtimer/widget.js @@ -28,7 +28,15 @@ const BUZZ_TOTAL_TIME = 5000; // 5 seconds total /** Gesture control constants */ - const DOUBLE_SWIPE_TIMEOUT = 1500; // 1.5 seconds between swipes + 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 @@ -208,6 +216,19 @@ 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 */ @@ -254,15 +275,15 @@ if (!isControlLocked) { // Controls unlocked - execute adjustment immediately adjustTimer(adjustment); - } else if (lastSwipeDirection === direction && - currentTime - lastSwipeTime < DOUBLE_SWIPE_TIMEOUT) { + } else if (isUnlockGesture(direction, lastSwipeDirection) && + currentTime - lastSwipeTime < UNLOCK_GESTURE_TIMEOUT) { // Double swipe detected - unlock controls and execute isControlLocked = false; - adjustTimer(adjustment); + // adjustTimer(adjustment); Bangle.buzz(50); // Provide unlock feedback - // Auto-lock after 10 seconds of inactivity - setTimeout(resetUnlock, 10000); + // Auto-lock after `UNLOCK_CONTROL_TIMEOUT` seconds of inactivity + setTimeout(resetUnlock, UNLOCK_CONTROL_TIMEOUT); } // Update gesture tracking state From 8ff47cc8d54e80a8655141bd1433885bf4708d38 Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Mon, 26 May 2025 12:51:08 +0200 Subject: [PATCH 5/7] docs: make description reflect current UX --- apps/widtimer/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json index 026096e59..38089c272 100644 --- a/apps/widtimer/metadata.json +++ b/apps/widtimer/metadata.json @@ -3,7 +3,7 @@ "name": "Timer Widget", "shortName": "WidTimer", "version": "0.03", - "description": "Timer widget with swipe controls. Double-swipe in any direction to unlock, then single swipes work: right/left ±1min, up/down ±10min. Will buzz upon timer completion.", + "description": "Timer widget with swipe controls. Swipe a T (for timer), then single swipes adjust the time: right/left ±1min, up/down ±10min. Timer will start after the first swipe. Will buzz upon timer completion.", "icon": "widtimer.png", "type": "widget", "tags": "widget,timer,gesture", From 5a5c6a3b5e8b9eb77cbc4539278f276a49e3c010 Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Mon, 2 Jun 2025 11:11:41 +0200 Subject: [PATCH 6/7] feat: make timer start on unlock --- apps/widtimer/metadata.json | 4 ++-- apps/widtimer/widget.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json index 38089c272..d08f7cdae 100644 --- a/apps/widtimer/metadata.json +++ b/apps/widtimer/metadata.json @@ -2,8 +2,8 @@ "id": "widtimer", "name": "Timer Widget", "shortName": "WidTimer", - "version": "0.03", - "description": "Timer widget with swipe controls. Swipe a T (for timer), then single swipes adjust the time: right/left ±1min, up/down ±10min. Timer will start after the first swipe. Will buzz upon timer completion.", + "version": "0.04", + "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", diff --git a/apps/widtimer/widget.js b/apps/widtimer/widget.js index 7fce2e753..5a0e2809d 100644 --- a/apps/widtimer/widget.js +++ b/apps/widtimer/widget.js @@ -188,10 +188,6 @@ settings.totalTime = Math.max(0, settings.totalTime + seconds); remainingTime = settings.totalTime; - // Auto-start if time > 0 - if (settings.totalTime > 0) { - startTimer(); - } } saveSettings(); @@ -282,6 +278,11 @@ // 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); } From d0ff8f8477d599d914d917b14efc420b9f7b702e Mon Sep 17 00:00:00 2001 From: Leon Weber-Genzel Date: Tue, 3 Jun 2025 10:56:37 +0200 Subject: [PATCH 7/7] bugfix: set version of first release to 0.01 --- apps/widtimer/ChangeLog | 1 + apps/widtimer/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 apps/widtimer/ChangeLog diff --git a/apps/widtimer/ChangeLog b/apps/widtimer/ChangeLog new file mode 100644 index 000000000..f451ada48 --- /dev/null +++ b/apps/widtimer/ChangeLog @@ -0,0 +1 @@ +0.01: Official release \ No newline at end of file diff --git a/apps/widtimer/metadata.json b/apps/widtimer/metadata.json index d08f7cdae..18ed76666 100644 --- a/apps/widtimer/metadata.json +++ b/apps/widtimer/metadata.json @@ -2,7 +2,7 @@ "id": "widtimer", "name": "Timer Widget", "shortName": "WidTimer", - "version": "0.04", + "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",