pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.Common import qs.Services Singleton { id: root readonly property var log: Log.scoped("ThemeAutoService") property bool active: false Component.onCompleted: { if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) { start(); } } Connections { target: SessionData enabled: typeof SessionData !== "undefined" function onThemeModeAutoEnabledChanged() { if (SessionData.themeModeAutoEnabled) { root.start(); } else { root.stop(); } } function onThemeModeAutoModeChanged() { if (root.active) { root.evaluate(); root.syncTimeSchedule(); root.syncLocationSchedule(); } } function onThemeModeStartHourChanged() { if (root.active && !SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onThemeModeStartMinuteChanged() { if (root.active && !SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onThemeModeEndHourChanged() { if (root.active && !SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onThemeModeEndMinuteChanged() { if (root.active && !SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onThemeModeShareGammaSettingsChanged() { if (root.active) { root.evaluate(); root.syncTimeSchedule(); root.syncLocationSchedule(); } } function onNightModeStartHourChanged() { if (root.active && SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onNightModeStartMinuteChanged() { if (root.active && SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onNightModeEndHourChanged() { if (root.active && SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onNightModeEndMinuteChanged() { if (root.active && SessionData.themeModeShareGammaSettings) { root.evaluate(); root.syncTimeSchedule(); } } function onLatitudeChanged() { if (root.active && SessionData.themeModeAutoMode === "location") { if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }); } root.evaluate(); root.syncLocationSchedule(); } } function onLongitudeChanged() { if (root.active && SessionData.themeModeAutoMode === "location") { if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0 && typeof DMSService !== "undefined") { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }); } root.evaluate(); root.syncLocationSchedule(); } } function onNightModeUseIPLocationChanged() { if (root.active && SessionData.themeModeAutoMode === "location") { if (typeof DMSService !== "undefined") { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": SessionData.nightModeUseIPLocation }, response => { if (!response.error && !SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }); } }); } root.evaluate(); root.syncLocationSchedule(); } } } Connections { target: DisplayService enabled: typeof DisplayService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location" && !root.backendAvailable() function onGammaIsDayChanged() { if (Theme.isLightMode !== DisplayService.gammaIsDay) { Theme.setLightMode(DisplayService.gammaIsDay, true, true); } } } Connections { target: DMSService function onThemeAutoStateUpdate(data) { if (!SessionData.themeModeAutoEnabled) { return; } root.applyBackendState(data); } function onConnectionStateChanged() { if (DMSService.isConnected && SessionData.themeModeAutoMode === "time") { root.syncTimeSchedule(); } if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") { root.syncLocationSchedule(); } if (root.backendAvailable() && SessionData.themeModeAutoEnabled) { DMSService.sendRequest("theme.auto.getState", null, response => { if (response && response.result) { root.applyBackendState(response.result); } }); } if (!SessionData.themeModeAutoEnabled) { return; } if (DMSService.isConnected && SessionData.themeModeAutoMode === "location") { if (SessionData.nightModeUseIPLocation) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": true }, response => { if (!response.error) { root.log.info("Theme automation: IP location enabled after connection"); } }); } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": false }, response => { if (!response.error) { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }, locationResponse => { if (locationResponse?.error) { root.log.warn("Theme automation: Failed to set location", locationResponse.error); } }); } }); } else { root.log.warn("Theme automation: No location configured"); } } } } Connections { target: SessionService enabled: SessionData.themeModeAutoEnabled function onSessionUnlocked() { root.refresh(); } function onSessionResumed() { root.refresh(); } } function refresh() { if (!backendAvailable()) { evaluate(); return; } DMSService.sendRequest("theme.auto.trigger", {}); } function backendAvailable() { return typeof DMSService !== "undefined" && DMSService.isConnected && Array.isArray(DMSService.capabilities) && DMSService.capabilities.includes("theme.auto"); } function applyBackendState(state) { if (!state) { return; } if (state.config && state.config.mode && state.config.mode !== SessionData.themeModeAutoMode) { return; } if (typeof SessionData !== "undefined" && state.nextTransition !== undefined) { SessionData.themeModeNextTransition = state.nextTransition || ""; } if (state.isLight !== undefined && Theme.isLightMode !== state.isLight) { Theme.setLightMode(state.isLight, true, true); } } function syncTimeSchedule() { if (typeof SessionData === "undefined" || typeof DMSService === "undefined") { return; } if (!DMSService.isConnected) { return; } const timeModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "time"; if (!timeModeActive) { return; } DMSService.sendRequest("theme.auto.setMode", { "mode": "time" }); const shareSettings = SessionData.themeModeShareGammaSettings; const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour; const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute; const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour; const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute; DMSService.sendRequest("theme.auto.setSchedule", { "startHour": startHour, "startMinute": startMinute, "endHour": endHour, "endMinute": endMinute }, response => { if (response && response.error) { log.error("Theme automation: Failed to sync time schedule:", response.error); } }); DMSService.sendRequest("theme.auto.setEnabled", { "enabled": true }); DMSService.sendRequest("theme.auto.trigger", {}); } function syncLocationSchedule() { if (typeof SessionData === "undefined" || typeof DMSService === "undefined") { return; } if (!DMSService.isConnected) { return; } const locationModeActive = SessionData.themeModeAutoEnabled && SessionData.themeModeAutoMode === "location"; if (!locationModeActive) { return; } DMSService.sendRequest("theme.auto.setMode", { "mode": "location" }); if (SessionData.nightModeUseIPLocation) { DMSService.sendRequest("theme.auto.setUseIPLocation", { "use": true }); } else { DMSService.sendRequest("theme.auto.setUseIPLocation", { "use": false }); if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("theme.auto.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }); } } DMSService.sendRequest("theme.auto.setEnabled", { "enabled": true }); DMSService.sendRequest("theme.auto.trigger", {}); } function evaluate() { if (typeof SessionData === "undefined" || !SessionData.themeModeAutoEnabled) { return; } if (backendAvailable()) { DMSService.sendRequest("theme.auto.getState", null, response => { if (response && response.result) { applyBackendState(response.result); } }); return; } const mode = SessionData.themeModeAutoMode; if (mode === "location") { evaluateLocation(); } else { evaluateTime(); } } function evaluateLocation() { if (typeof DisplayService !== "undefined") { const shouldBeLight = DisplayService.gammaIsDay; if (Theme.isLightMode !== shouldBeLight) { Theme.setLightMode(shouldBeLight, true, true); } return; } if (!SessionData.nightModeUseIPLocation && SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { const shouldBeLight = calculateIsDaytime(SessionData.latitude, SessionData.longitude); if (Theme.isLightMode !== shouldBeLight) { Theme.setLightMode(shouldBeLight, true, true); } return; } if (root.active) { if (SessionData.nightModeUseIPLocation) { log.warn("Theme automation: Waiting for IP location from backend"); } else { log.warn("Theme automation: Location mode requires coordinates"); } } } function evaluateTime() { const shareSettings = SessionData.themeModeShareGammaSettings; const startHour = shareSettings ? SessionData.nightModeStartHour : SessionData.themeModeStartHour; const startMinute = shareSettings ? SessionData.nightModeStartMinute : SessionData.themeModeStartMinute; const endHour = shareSettings ? SessionData.nightModeEndHour : SessionData.themeModeEndHour; const endMinute = shareSettings ? SessionData.nightModeEndMinute : SessionData.themeModeEndMinute; const now = new Date(); const currentMinutes = now.getHours() * 60 + now.getMinutes(); const startMinutes = startHour * 60 + startMinute; const endMinutes = endHour * 60 + endMinute; let shouldBeLight; if (startMinutes < endMinutes) { shouldBeLight = currentMinutes < startMinutes || currentMinutes >= endMinutes; } else { shouldBeLight = currentMinutes >= endMinutes && currentMinutes < startMinutes; } if (Theme.isLightMode !== shouldBeLight) { Theme.setLightMode(shouldBeLight, true, true); } } function calculateIsDaytime(lat, lng) { const now = new Date(); const start = new Date(now.getFullYear(), 0, 0); const diff = now - start; const dayOfYear = Math.floor(diff / 86400000); const latRad = lat * Math.PI / 180; const declination = 23.45 * Math.sin((360 / 365) * (dayOfYear - 81) * Math.PI / 180); const declinationRad = declination * Math.PI / 180; const cosHourAngle = -Math.tan(latRad) * Math.tan(declinationRad); if (cosHourAngle > 1) { return false; } if (cosHourAngle < -1) { return true; } const hourAngle = Math.acos(cosHourAngle); const hourAngleDeg = hourAngle * 180 / Math.PI; const sunriseHour = 12 - hourAngleDeg / 15; const sunsetHour = 12 + hourAngleDeg / 15; const timeZoneOffset = now.getTimezoneOffset() / 60; const localSunrise = sunriseHour - lng / 15 - timeZoneOffset; const localSunset = sunsetHour - lng / 15 - timeZoneOffset; const currentHour = now.getHours() + now.getMinutes() / 60; const normalizeSunrise = ((localSunrise % 24) + 24) % 24; const normalizeSunset = ((localSunset % 24) + 24) % 24; return currentHour >= normalizeSunrise && currentHour < normalizeSunset; } function sendLocationToBackend() { if (typeof SessionData === "undefined" || typeof DMSService === "undefined") { return false; } if (!DMSService.isConnected) { return false; } if (SessionData.nightModeUseIPLocation) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": true }, response => { if (response?.error) { log.warn("Theme automation: Failed to enable IP location", response.error); } }); return true; } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { DMSService.sendRequest("wayland.gamma.setUseIPLocation", { "use": false }, response => { if (!response.error) { DMSService.sendRequest("wayland.gamma.setLocation", { "latitude": SessionData.latitude, "longitude": SessionData.longitude }, locResp => { if (locResp?.error) { log.warn("Theme automation: Failed to set location", locResp.error); } }); } }); return true; } return false; } Timer { id: locationRetryTimer interval: 1000 repeat: true running: false property int retryCount: 0 onTriggered: { if (root.sendLocationToBackend()) { stop(); retryCount = 0; root.evaluate(); } else { retryCount++; if (retryCount >= 10) { stop(); retryCount = 0; } } } } function start() { root.active = true; syncTimeSchedule(); syncLocationSchedule(); const sent = sendLocationToBackend(); if (!sent && typeof SessionData !== "undefined" && SessionData.themeModeAutoMode === "location") { locationRetryTimer.start(); } else { evaluate(); } } function stop() { root.active = false; if (typeof DMSService !== "undefined" && DMSService.isConnected) { DMSService.sendRequest("theme.auto.setEnabled", { "enabled": false }); } } }