summaryrefslogtreecommitdiff
path: root/.config/noctalia/plugins/timer/Panel.qml
diff options
context:
space:
mode:
Diffstat (limited to '.config/noctalia/plugins/timer/Panel.qml')
-rw-r--r--.config/noctalia/plugins/timer/Panel.qml593
1 files changed, 593 insertions, 0 deletions
diff --git a/.config/noctalia/plugins/timer/Panel.qml b/.config/noctalia/plugins/timer/Panel.qml
new file mode 100644
index 0000000..b79389e
--- /dev/null
+++ b/.config/noctalia/plugins/timer/Panel.qml
@@ -0,0 +1,593 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import qs.Commons
+import qs.Services.System
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var pluginApi: null
+ readonly property var geometryPlaceholder: panelContainer
+ property real contentPreferredWidth: (compactMode ? 340 : 380) * Style.uiScaleRatio
+ property real contentPreferredHeight: (compactMode ? 230 : 350) * Style.uiScaleRatio
+ readonly property bool allowAttach: true
+
+
+
+ readonly property bool compactMode:
+ pluginApi?.pluginSettings?.compactMode ??
+ pluginApi?.manifest?.metadata?.defaultSettings?.compactMode ??
+ false
+
+
+
+ anchors.fill: parent
+
+ readonly property var mainInstance: pluginApi?.mainInstance
+
+ readonly property bool isRunning: mainInstance ? mainInstance.timerRunning : false
+ property bool isStopwatchMode: mainInstance ? mainInstance.timerStopwatchMode : false
+ readonly property int remainingSeconds: mainInstance ? mainInstance.timerRemainingSeconds : 0
+ readonly property int totalSeconds: mainInstance ? mainInstance.timerTotalSeconds : 0
+ readonly property int elapsedSeconds: mainInstance ? mainInstance.timerElapsedSeconds : 0
+ readonly property bool soundPlaying: mainInstance ? mainInstance.timerSoundPlaying : false
+
+ function startTimer() { if (mainInstance) mainInstance.timerStart(); }
+ function pauseTimer() { if (mainInstance) mainInstance.timerPause(); }
+ function resetTimer() {
+ if (mainInstance) {
+ mainInstance.timerReset();
+ // Do not apply default duration here. User wants 00:00:00 on reset.
+ }
+ }
+
+ function setTimerStopwatchMode(mode) {
+ if (mainInstance) {
+ mainInstance.timerStopwatchMode = mode;
+ }
+ }
+
+ function setTimerRemainingSeconds(seconds) {
+ if (mainInstance) mainInstance.cdRemainingSeconds = seconds;
+ }
+
+ function formatTime(seconds, totalTimeSeconds) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ if (seconds === 0 && totalTimeSeconds > 0) {
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ if (!totalTimeSeconds || totalTimeSeconds === 0) {
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ if (totalTimeSeconds < 3600) {
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ function formatTimeFromDigits(digits) {
+ const len = digits.length;
+ let seconds = 0;
+ let minutes = 0;
+ let hours = 0;
+
+ if (len > 0) {
+ seconds = parseInt(digits.substring(Math.max(0, len - 2))) || 0;
+ }
+ if (len > 2) {
+ minutes = parseInt(digits.substring(Math.max(0, len - 4), len - 2)) || 0;
+ }
+ if (len > 4) {
+ hours = parseInt(digits.substring(0, len - 4)) || 0;
+ }
+
+ seconds = Math.min(59, seconds);
+ minutes = Math.min(59, minutes);
+ hours = Math.min(99, hours);
+
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ }
+
+ function parseDigitsToTime(digits) {
+ const len = digits.length;
+ let seconds = 0;
+ let minutes = 0;
+ let hours = 0;
+
+ if (len > 0) {
+ seconds = parseInt(digits.substring(Math.max(0, len - 2))) || 0;
+ }
+ if (len > 2) {
+ minutes = parseInt(digits.substring(Math.max(0, len - 4), len - 2)) || 0;
+ }
+ if (len > 4) {
+ hours = parseInt(digits.substring(0, len - 4)) || 0;
+ }
+
+ seconds = Math.min(59, seconds);
+ minutes = Math.min(59, minutes);
+ hours = Math.min(99, hours);
+
+ setTimerRemainingSeconds((hours * 3600) + (minutes * 60) + seconds);
+ }
+
+ Component.onCompleted: {
+ // Do not auto-set default duration on load
+ }
+
+ function applyTimeFromBuffer() {
+ if (timerDisplayItem.inputBuffer !== "") {
+ parseDigitsToTime(timerDisplayItem.inputBuffer);
+ timerDisplayItem.inputBuffer = "";
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ if (!isRunning && !isStopwatchMode && totalSeconds === 0) {
+ timerInput.forceActiveFocus();
+ }
+ }
+ }
+
+ Rectangle {
+ id: panelContainer
+ anchors.fill: parent
+ color: "transparent"
+
+ ColumnLayout {
+ anchors {
+ fill: parent
+ margins: Style.marginM
+ }
+ spacing: Style.marginL
+
+ NBox {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ColumnLayout {
+ id: content
+ anchors.fill: parent
+ anchors.margins: Style.marginM
+ spacing: Style.marginM
+ clip: true
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ NIcon {
+ icon: isStopwatchMode ? "clock" : "hourglass"
+ pointSize: Style.fontSizeL
+ color: Color.mPrimary
+ }
+
+ NText {
+ text: pluginApi?.tr("panel.title") || "Timer"
+ pointSize: Style.fontSizeL
+ font.weight: Style.fontWeightBold
+ color: Color.mOnSurface
+ Layout.fillWidth: true
+ }
+ }
+
+ Item {
+ id: timerDisplayItem
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.alignment: Qt.AlignHCenter
+
+ property string inputBuffer: ""
+ property bool isEditing: false
+
+ WheelHandler {
+ target: timerDisplayItem
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ enabled: !isRunning && !isStopwatchMode && totalSeconds === 0
+ onWheel: function (event) {
+ if (!enabled || !mainInstance) {
+ return;
+ }
+ const step = 5;
+ if (event.angleDelta.y > 0) {
+ mainInstance.cdRemainingSeconds = Math.max(0, mainInstance.cdRemainingSeconds + step);
+ event.accepted = true;
+ } else if (event.angleDelta.y < 0) {
+ mainInstance.cdRemainingSeconds = Math.max(0, mainInstance.cdRemainingSeconds - step);
+ event.accepted = true;
+ }
+ }
+ }
+
+ Rectangle {
+ id: textboxBorder
+ anchors.centerIn: parent
+ width: Math.max(timerInput.implicitWidth + Style.marginM * 2, parent.width * 0.8)
+ height: timerInput.implicitHeight + Style.marginM * 2
+ radius: Style.iRadiusM
+ color: Color.mSurfaceVariant
+ border.color: (timerInput.activeFocus || timerDisplayItem.isEditing) ? Color.mPrimary : Color.mOutline
+ border.width: Style.borderS
+ visible: !isRunning && !isStopwatchMode && totalSeconds === 0
+ z: 0
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Style.animationFast
+ }
+ }
+ }
+
+ Canvas {
+ id: progressRing
+ anchors.centerIn: parent
+ width: Math.min(parent.width, parent.height) * 1.0
+ height: width
+ visible: !isStopwatchMode && totalSeconds > 0 && !compactMode && (isRunning || elapsedSeconds > 0)
+ z: -1
+
+ property real progressRatio: {
+ if (totalSeconds <= 0)
+ return 0;
+ const ratio = remainingSeconds / totalSeconds;
+ return Math.max(0, Math.min(1, ratio));
+ }
+
+ onProgressRatioChanged: requestPaint()
+
+ onPaint: {
+ var ctx = getContext("2d");
+ if (width <= 0 || height <= 0) {
+ return;
+ }
+
+ var centerX = width / 2;
+ var centerY = height / 2;
+ var radius = Math.min(width, height) / 2 - 5;
+
+ if (radius <= 0) {
+ return;
+ }
+
+ ctx.reset();
+
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
+ ctx.lineWidth = 10;
+ ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.1);
+ ctx.stroke();
+
+ if (progressRatio > 0) {
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progressRatio * 2 * Math.PI);
+ ctx.lineWidth = 10;
+ ctx.strokeStyle = Color.mPrimary;
+ ctx.lineCap = "round";
+ ctx.stroke();
+ }
+ }
+ }
+
+ Item {
+ id: timerContainer
+ anchors.centerIn: parent
+ width: timerInput.implicitWidth
+ height: timerInput.implicitHeight + 8
+
+ TextInput {
+ id: timerInput
+ anchors.centerIn: parent
+ width: Math.max(implicitWidth, timerDisplayItem.width * 0.8)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ selectByMouse: false
+ cursorVisible: false
+ cursorDelegate: Item {}
+ readOnly: isStopwatchMode || isRunning || totalSeconds > 0
+ enabled: !isRunning && !isStopwatchMode && totalSeconds === 0
+ font.family: Settings.data.ui.fontFixed
+
+ readonly property bool showingHours: {
+ if (isStopwatchMode) {
+ return elapsedSeconds >= 3600;
+ }
+ if (timerDisplayItem.isEditing) {
+ return true;
+ }
+ return totalSeconds >= 3600;
+ }
+
+ font.pointSize: {
+ const scale = compactMode ? 0.8 : 1.0;
+ if (totalSeconds === 0) {
+ return Style.fontSizeXXXL * 1.5 * scale;
+ }
+ return (showingHours ? Style.fontSizeXXL * 1.3 : (Style.fontSizeXXL * 1.8)) * scale;
+ }
+
+ font.weight: Style.fontWeightBold
+ color: {
+ if (totalSeconds > 0) {
+ return Color.mPrimary;
+ }
+ if (timerDisplayItem.isEditing) {
+ return Color.mPrimary;
+ }
+ return Color.mOnSurface;
+ }
+
+ property string _cachedText: ""
+ property int _textUpdateCounter: 0
+
+ function updateText() {
+ if (isStopwatchMode) {
+ _cachedText = formatTime(elapsedSeconds, elapsedSeconds);
+ } else if (timerDisplayItem.isEditing && totalSeconds === 0 && timerDisplayItem.inputBuffer !== "") {
+ _cachedText = formatTimeFromDigits(timerDisplayItem.inputBuffer);
+ } else if (timerDisplayItem.isEditing && totalSeconds === 0) {
+ _cachedText = formatTime(0, 0);
+ } else {
+ _cachedText = formatTime(remainingSeconds, totalSeconds);
+ }
+ _textUpdateCounter = _textUpdateCounter + 1;
+ }
+
+ text: {
+ const counter = _textUpdateCounter;
+ return _cachedText;
+ }
+
+ Connections {
+ target: root
+ function onRemainingSecondsChanged() { timerInput.updateText(); }
+ function onTotalSecondsChanged() { timerInput.updateText(); }
+ function onIsRunningChanged() { timerInput.updateText(); Qt.callLater(() => { timerInput.updateText(); }); }
+ function onElapsedSecondsChanged() { timerInput.updateText(); }
+ function onIsStopwatchModeChanged() { timerInput.updateText(); }
+ }
+
+ Connections {
+ target: timerDisplayItem
+ function onIsEditingChanged() {
+ timerInput.updateText();
+ }
+ }
+
+ Component.onCompleted: updateText()
+
+ Keys.onPressed: event => {
+ if (isRunning || isStopwatchMode || totalSeconds > 0) {
+ if (event.key === Qt.Key_Space) {
+ if (isRunning) mainInstance.timerPause();
+ else mainInstance.timerStart();
+ event.accepted = true;
+ return;
+ }
+ event.accepted = true;
+ return;
+ }
+
+ const keyText = event.text.toLowerCase();
+
+ if (event.key === Qt.Key_Backspace) {
+ if (timerDisplayItem.isEditing && timerDisplayItem.inputBuffer.length > 0) {
+ timerDisplayItem.inputBuffer = timerDisplayItem.inputBuffer.slice(0, -1);
+ if (timerDisplayItem.inputBuffer !== "") {
+ parseDigitsToTime(timerDisplayItem.inputBuffer);
+ } else {
+ setTimerRemainingSeconds(0);
+ }
+ }
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Delete) {
+ if (timerDisplayItem.isEditing) {
+ timerDisplayItem.inputBuffer = "";
+ setTimerRemainingSeconds(0);
+ }
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
+ applyTimeFromBuffer();
+ timerDisplayItem.isEditing = false;
+ timerInput.focus = false;
+ if (remainingSeconds > 0) {
+ mainInstance.timerStart();
+ }
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Escape) {
+ timerDisplayItem.inputBuffer = "";
+ setTimerRemainingSeconds(0);
+ timerDisplayItem.isEditing = false;
+ timerInput.focus = false;
+ event.accepted = true;
+ return;
+ }
+
+ if (keyText === 'h' || keyText === 'm' || keyText === 's') {
+ if (timerDisplayItem.inputBuffer.length > 0) {
+ let val = parseInt(timerDisplayItem.inputBuffer) || 0;
+ let secs = 0;
+ if (keyText === 'h') secs = val * 3600;
+ else if (keyText === 'm') secs = val * 60;
+ else if (keyText === 's') secs = val;
+
+ setTimerRemainingSeconds(Math.min(99 * 3600 + 59 * 60 + 59, secs));
+ timerDisplayItem.inputBuffer = "";
+ timerDisplayItem.isEditing = false;
+ timerInput.focus = false;
+ }
+ event.accepted = true;
+ return;
+ }
+
+ const isDigitKey = event.key >= Qt.Key_0 && event.key <= Qt.Key_9;
+ const isDigitText = keyText.length === 1 && keyText >= '0' && keyText <= '9';
+
+ if (isDigitKey && isDigitText) {
+ if (timerDisplayItem.inputBuffer.length >= 6) {
+ event.accepted = true;
+ return;
+ }
+ timerDisplayItem.inputBuffer += keyText;
+ parseDigitsToTime(timerDisplayItem.inputBuffer);
+ event.accepted = true;
+ } else {
+ event.accepted = true;
+ }
+ }
+
+ onActiveFocusChanged: {
+ if (activeFocus) {
+ timerDisplayItem.isEditing = true;
+ timerDisplayItem.inputBuffer = "";
+ } else {
+ applyTimeFromBuffer();
+ timerDisplayItem.isEditing = false;
+ timerDisplayItem.inputBuffer = "";
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: !isRunning && !isStopwatchMode && totalSeconds === 0
+ cursorShape: enabled ? Qt.IBeamCursor : Qt.ArrowCursor
+ preventStealing: true
+ onPressed: (mouse) => {
+ if (!isRunning && !isStopwatchMode && totalSeconds === 0) {
+ timerInput.forceActiveFocus();
+ mouse.accepted = true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ id: buttonRow
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredWidth: 0
+ implicitHeight: startButton.implicitHeight
+ color: "transparent"
+
+ NButton {
+ id: startButton
+ anchors.fill: parent
+ text: {
+ if (isRunning) return pluginApi?.tr("panel.pause") || "Pause";
+ if (isStopwatchMode && elapsedSeconds > 0) return pluginApi?.tr("panel.resume") || "Resume";
+ if (!isStopwatchMode && totalSeconds > 0) return pluginApi?.tr("panel.resume") || "Resume";
+ return pluginApi?.tr("panel.start") || "Start";
+ }
+ icon: isRunning ? "player-pause" : "player-play"
+ enabled: isStopwatchMode || remainingSeconds > 0
+ onClicked: {
+ if (isRunning) {
+ pauseTimer();
+ } else {
+ startTimer();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredWidth: 0
+ implicitHeight: resetButton.implicitHeight
+ color: "transparent"
+
+ NButton {
+ id: resetButton
+ anchors.fill: parent
+ text: pluginApi?.tr("panel.reset") || "Reset"
+ icon: "refresh"
+ enabled: (isStopwatchMode && (elapsedSeconds > 0 || isRunning)) || (!isStopwatchMode && (remainingSeconds > 0 || isRunning || soundPlaying))
+ onClicked: {
+ resetTimer();
+ }
+ }
+ }
+ }
+
+ NTabBar {
+ id: modeTabBar
+ Layout.fillWidth: true
+ Layout.preferredHeight: Style.baseWidgetSize
+ currentIndex: isStopwatchMode ? 1 : 0
+ margins: 0
+ distributeEvenly: true
+ onCurrentIndexChanged: {
+ const newMode = currentIndex === 1;
+ if (newMode !== isStopwatchMode) {
+ timerInput.focus = false;
+ setTimerStopwatchMode(newMode);
+ }
+ }
+ spacing: Style.marginS
+
+
+ Component.onCompleted: {
+ Qt.callLater(() => {
+ if (modeTabBar.children && modeTabBar.children.length > 0) {
+ for (var i = 0; i < modeTabBar.children.length; i++) {
+ var child = modeTabBar.children[i];
+ if (child && typeof child.spacing !== 'undefined' && child.anchors) {
+ child.anchors.margins = 0;
+ break;
+ }
+ }
+ }
+ });
+ }
+
+ NTabButton {
+ text: pluginApi?.tr("panel.countdown") || "Timer"
+ tabIndex: 0
+ checked: !isStopwatchMode
+ radius: Style.iRadiusS
+ }
+
+ NTabButton {
+ text: pluginApi?.tr("panel.stopwatch") || "Stopwatch"
+ tabIndex: 1
+ checked: isStopwatchMode
+ radius: Style.iRadiusS
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Shortcut {
+ sequence: "Space"
+ onActivated: {
+ if (!timerInput.activeFocus) {
+ if (isRunning) mainInstance.timerPause();
+ else if (isStopwatchMode || remainingSeconds > 0) mainInstance.timerStart();
+ }
+ }
+ }
+}
+