diff --git a/main.js b/main.js index c9e4152..c8f6bb4 100644 --- a/main.js +++ b/main.js @@ -6,6 +6,7 @@ const { ipcMain, dialog, screen, + powerMonitor, nativeImage } = require('electron'); const path = require('path'); @@ -17,10 +18,12 @@ const fs = require('fs'); const settingsPath = path.join(app.getPath('userData'), 'settings.json'); const DEFAULT_SETTINGS = { - version: 3, // Increment when defaults change to trigger migration + version: 4, // Increment when defaults change to trigger migration workInterval: 50, // minutes (HSE: 5-10 min break per hour) breakDuration: 300, // seconds (5 minutes - HSE guideline) snoozeDuration: 300, // seconds (5 minutes default) + autoPauseOnIdle: true, + idlePauseThreshold: 180, // seconds (3 minutes) soundEnabled: false, multiMonitor: true, videoPath: '', // empty = use bundled default @@ -74,6 +77,13 @@ function migrateSettings(oldData, currentSettings) { currentSettings.snoozeDuration = DEFAULT_SETTINGS.snoozeDuration; } + // Migration from v3 to v4: Add automatic idle pause + if (version < 4) { + console.log('Adding automatic idle pause defaults (v3 → v4)'); + currentSettings.autoPauseOnIdle = DEFAULT_SETTINGS.autoPauseOnIdle; + currentSettings.idlePauseThreshold = DEFAULT_SETTINGS.idlePauseThreshold; + } + // Update version to latest currentSettings.version = DEFAULT_SETTINGS.version; @@ -147,6 +157,8 @@ let breakSecondsRemaining = 0; let breakSecondsTotal = 0; let isBreakActive = false; let isPaused = false; +let pauseReason = null; // null | 'manual' | 'idle' +let idlePauseStartedAt = null; let audioWindow = null; // persistent hidden window for sound playback // --------------------------------------------------------------------------- @@ -215,7 +227,7 @@ function createSettingsWindow() { settingsWindow = new BrowserWindow({ width: 520, - height: 620, + height: 720, resizable: false, title: 'Cat Gatekeeper Settings', autoHideMenuBar: true, @@ -253,9 +265,12 @@ function startTimer() { workSecondsRemaining = settings.workInterval * 60; isBreakActive = false; isPaused = false; + pauseReason = null; + idlePauseStartedAt = null; broadcastTimerStatus(); timerInterval = setInterval(() => { + updateIdlePauseState(); if (isPaused) return; if (!isBreakActive) { @@ -335,6 +350,8 @@ function resetTimer() { workSecondsRemaining = settings.workInterval * 60; isBreakActive = false; isPaused = false; + pauseReason = null; + idlePauseStartedAt = null; closeOverlayWindows(); startTimer(); updateTrayMenu(); @@ -372,7 +389,8 @@ function broadcastTimerStatus() { breakSecondsRemaining: Math.max(0, breakSecondsRemaining), breakSecondsTotal: breakSecondsTotal, isBreakActive, - isPaused + isPaused, + pauseReason }; for (const win of overlayWindows) { @@ -468,6 +486,12 @@ function getTimeDisplay() { const s = breakSecondsRemaining % 60; return `Break ends in ${m}:${s.toString().padStart(2, '0')}`; } + if (pauseReason === 'idle') { + return 'Paused while away'; + } + if (pauseReason === 'manual') { + return 'Paused'; + } const m = Math.floor(workSecondsRemaining / 60); const s = workSecondsRemaining % 60; return `Next break in ${m}:${s.toString().padStart(2, '0')}`; @@ -475,16 +499,59 @@ function getTimeDisplay() { function pauseTimer() { isPaused = true; + pauseReason = 'manual'; updateTrayMenu(); broadcastTimerStatus(); } function resumeTimer() { isPaused = false; + pauseReason = null; + idlePauseStartedAt = null; updateTrayMenu(); broadcastTimerStatus(); } +function updateIdlePauseState() { + const settings = loadSettings(); + + if (!settings.autoPauseOnIdle || isBreakActive) { + if (pauseReason === 'idle') { + resumeTimer(); + } + return; + } + + let idleSeconds = 0; + try { + idleSeconds = powerMonitor.getSystemIdleTime(); + } catch (_) { + return; + } + + if (pauseReason === 'idle') { + if (idleSeconds <= 1) { + const idlePauseSeconds = idlePauseStartedAt + ? Math.floor((Date.now() - idlePauseStartedAt) / 1000) + : 0; + + if (idlePauseSeconds >= settings.breakDuration) { + workSecondsRemaining = settings.workInterval * 60; + } + resumeTimer(); + } + return; + } + + if (!isPaused && idleSeconds >= settings.idlePauseThreshold) { + isPaused = true; + pauseReason = 'idle'; + idlePauseStartedAt = Date.now(); + updateTrayMenu(); + broadcastTimerStatus(); + } +} + // --------------------------------------------------------------------------- // IPC handlers // --------------------------------------------------------------------------- @@ -509,7 +576,8 @@ function setupIPC() { breakSecondsRemaining: Math.max(0, breakSecondsRemaining), breakSecondsTotal, isBreakActive, - isPaused + isPaused, + pauseReason })); ipcMain.on('dismiss-break', () => { diff --git a/package.json b/package.json index 665b071..20a75b6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ ], "scripts": { "start": "electron .", - "start:dev": "cross-env WORK_INTERVAL=1 BREAK_DURATION=15 electron .", + "start:dev": "cross-env WORK_INTERVAL=2 BREAK_DURATION=180 electron .", "pack": "electron-builder --dir", "dist": "electron-builder --win --mac --linux", "dist:mac": "electron-builder --mac", diff --git a/src/settings.html b/src/settings.html index f485554..eb222b4 100644 --- a/src/settings.html +++ b/src/settings.html @@ -62,6 +62,31 @@