201 lines
6.5 KiB
QML
201 lines
6.5 KiB
QML
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)
|
|
}
|
|
}
|