import QtQuick import Quickshell import qs.Common import qs.Services Item { id: service property var daemon: null readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({}) readonly property string alarmSound: String(pluginData.timerSound || "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga").trim() readonly property bool notifyOnFinish: pluginData.timerNotify !== false readonly property bool soundOnFinish: pluginData.timerPlaySound !== false // ── Countdown ──────────────────────────────────────────────────────────── property int durationMs: 5 * 60 * 1000 property int remainingMs: durationMs property double deadline: 0 property bool timerRunning: false property bool timerFinished: false property string timerLabel: "" readonly property real timerProgress: durationMs > 0 ? Math.max(0, Math.min(1, 1 - remainingMs / durationMs)) : 0 // ── Stopwatch ──────────────────────────────────────────────────────────── property double stopwatchStartedAt: 0 property int stopwatchBaseMs: 0 property int stopwatchElapsedMs: 0 property bool stopwatchRunning: false property var laps: [] signal finished(string label) function formatDuration(ms) { const total = Math.max(0, Math.round(ms / 1000)); const hours = Math.floor(total / 3600); const mins = Math.floor((total % 3600) / 60); const secs = total % 60; const pad = value => (value < 10 ? "0" : "") + value; return hours > 0 ? hours + ":" + pad(mins) + ":" + pad(secs) : pad(mins) + ":" + pad(secs); } function formatStopwatch(ms) { const hundredths = Math.floor((Math.max(0, ms) % 1000) / 10); return service.formatDuration(ms) + "." + (hundredths < 10 ? "0" : "") + hundredths; } function setDuration(ms) { service.durationMs = Math.max(1000, Math.round(ms)); if (!service.timerRunning) { service.remainingMs = service.durationMs; service.timerFinished = false; } } function addTime(ms) { if (service.timerRunning) { service.deadline += ms; service.durationMs = Math.max(1000, service.durationMs + ms); service._tick(); return; } service.setDuration(service.durationMs + ms); } function startTimer(ms, label) { if (ms !== undefined && ms !== null) service.setDuration(ms); if (label !== undefined && label !== null) service.timerLabel = String(label); if (service.remainingMs <= 0) service.remainingMs = service.durationMs; service.timerFinished = false; service.deadline = Date.now() + service.remainingMs; service.timerRunning = true; } function pauseTimer() { if (!service.timerRunning) return; service.remainingMs = Math.max(0, service.deadline - Date.now()); service.timerRunning = false; } function toggleTimer() { if (service.timerRunning) service.pauseTimer(); else service.startTimer(); } function resetTimer() { service.timerRunning = false; service.timerFinished = false; service.remainingMs = service.durationMs; } function _tick() { if (!service.timerRunning) return; const left = service.deadline - Date.now(); if (left <= 0) { service.remainingMs = 0; service.timerRunning = false; service.timerFinished = true; service._announce(); return; } service.remainingMs = left; } // Fires whether or not the overlay is open - the service lives in the daemon. function _announce() { const label = service.timerLabel.trim(); const body = label !== "" ? label : "Timer finished after " + service.formatDuration(service.durationMs); if (service.soundOnFinish && service.alarmSound !== "") Quickshell.execDetached(["sh", "-c", '[ -r "$1" ] && exec paplay "$1"', "gameBar", service.alarmSound]); if (service.notifyOnFinish) Quickshell.execDetached(["notify-send", "-a", "Game Bar", "-u", "critical", "-i", "timer", "Timer finished", body]); if (typeof ToastService !== "undefined" && ToastService) ToastService.showInfo("Timer finished", body); service.finished(label); } function dismissFinished() { service.timerFinished = false; service.remainingMs = service.durationMs; } // ── Stopwatch control ──────────────────────────────────────────────────── function startStopwatch() { if (service.stopwatchRunning) return; service.stopwatchStartedAt = Date.now(); service.stopwatchRunning = true; } function pauseStopwatch() { if (!service.stopwatchRunning) return; service.stopwatchBaseMs = service.stopwatchElapsedMs; service.stopwatchRunning = false; } function toggleStopwatch() { if (service.stopwatchRunning) service.pauseStopwatch(); else service.startStopwatch(); } function resetStopwatch() { service.stopwatchRunning = false; service.stopwatchBaseMs = 0; service.stopwatchElapsedMs = 0; service.laps = []; } function lap() { if (!service.stopwatchRunning && service.stopwatchElapsedMs === 0) return; const previous = service.laps.length > 0 ? service.laps[0].total : 0; service.laps = [ { "index": service.laps.length + 1, "total": service.stopwatchElapsedMs, "split": service.stopwatchElapsedMs - previous } ].concat(service.laps); } Timer { running: service.timerRunning interval: 200 repeat: true onTriggered: service._tick() } Timer { running: service.stopwatchRunning interval: 50 repeat: true onTriggered: service.stopwatchElapsedMs = service.stopwatchBaseMs + (Date.now() - service.stopwatchStartedAt) } }