From 48a78c39e22f5f916f3776a4f1a7ac5f54402757 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 28 Aug 2025 17:18:59 -0400 Subject: [PATCH] Initial commit for nightMode automation --- Common/SessionData.qml | 55 ++ Modules/ControlCenter/ControlCenterPopout.qml | 4 +- Modules/ControlCenter/DisplayTab.qml | 164 ++-- Modules/OSD/BrightnessOSD.qml | 30 +- Modules/Settings/PersonalizationTab.qml | 714 ++++++++++++------ ...ightnessService.qml => DisplayService.qml} | 43 +- Services/NightModeAutomationService.qml | 363 +++++++++ 7 files changed, 1035 insertions(+), 338 deletions(-) rename Services/{BrightnessService.qml => DisplayService.qml} (94%) create mode 100644 Services/NightModeAutomationService.qml diff --git a/Common/SessionData.qml b/Common/SessionData.qml index ed1c46d1..19238e4b 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -18,6 +18,12 @@ Singleton { property bool doNotDisturb: false property bool nightModeEnabled: false property int nightModeTemperature: 4500 + property bool nightModeAutoEnabled: false + property string nightModeAutoMode: "manual" + property string nightModeStartTime: "20:00" + property string nightModeEndTime: "06:00" + property real latitude: 0.0 + property real longitude: 0.0 property var pinnedApps: [] property int selectedGpuIndex: 0 property bool nvidiaGpuTempEnabled: false @@ -52,6 +58,16 @@ Singleton { !== undefined ? settings.nightModeEnabled : false nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500 + nightModeAutoEnabled = settings.nightModeAutoEnabled + !== undefined ? settings.nightModeAutoEnabled : false + nightModeAutoMode = settings.nightModeAutoMode + !== undefined ? settings.nightModeAutoMode : "manual" + nightModeStartTime = settings.nightModeStartTime + !== undefined ? settings.nightModeStartTime : "20:00" + nightModeEndTime = settings.nightModeEndTime + !== undefined ? settings.nightModeEndTime : "06:00" + latitude = settings.latitude !== undefined ? settings.latitude : 0.0 + longitude = settings.longitude !== undefined ? settings.longitude : 0.0 pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : [] selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0 @@ -86,6 +102,12 @@ Singleton { "doNotDisturb": doNotDisturb, "nightModeEnabled": nightModeEnabled, "nightModeTemperature": nightModeTemperature, + "nightModeAutoEnabled": nightModeAutoEnabled, + "nightModeAutoMode": nightModeAutoMode, + "nightModeStartTime": nightModeStartTime, + "nightModeEndTime": nightModeEndTime, + "latitude": latitude, + "longitude": longitude, "pinnedApps": pinnedApps, "selectedGpuIndex": selectedGpuIndex, "nvidiaGpuTempEnabled": nvidiaGpuTempEnabled, @@ -119,6 +141,39 @@ Singleton { saveSettings() } + function setNightModeAutoEnabled(enabled) { + console.log("SessionData: Setting nightModeAutoEnabled to", enabled) + nightModeAutoEnabled = enabled + saveSettings() + } + + function setNightModeAutoMode(mode) { + nightModeAutoMode = mode + saveSettings() + } + + function setNightModeStartTime(time) { + nightModeStartTime = time + saveSettings() + } + + function setNightModeEndTime(time) { + nightModeEndTime = time + saveSettings() + } + + function setLatitude(lat) { + console.log("SessionData: Setting latitude to", lat) + latitude = lat + saveSettings() + } + + function setLongitude(lng) { + console.log("SessionData: Setting longitude to", lng) + longitude = lng + saveSettings() + } + function setWallpaperPath(path) { wallpaperPath = path saveSettings() diff --git a/Modules/ControlCenter/ControlCenterPopout.qml b/Modules/ControlCenter/ControlCenterPopout.qml index e741ec1b..2abc06ba 100644 --- a/Modules/ControlCenter/ControlCenterPopout.qml +++ b/Modules/ControlCenter/ControlCenterPopout.qml @@ -762,9 +762,9 @@ DankPopout { implicitHeight: { let height = Theme.spacingL - if (BrightnessService.brightnessAvailable) { + if (DisplayService.brightnessAvailable) { height += 80 - if (BrightnessService.devices.length > 1) { + if (DisplayService.devices.length > 1) { height += 40 } } diff --git a/Modules/ControlCenter/DisplayTab.qml b/Modules/ControlCenter/DisplayTab.qml index 2d7eaa5a..0ff0de23 100644 --- a/Modules/ControlCenter/DisplayTab.qml +++ b/Modules/ControlCenter/DisplayTab.qml @@ -34,7 +34,9 @@ Item { width: parent.width sourceComponent: settingsComponent } + } + } Component { @@ -43,7 +45,7 @@ Item { Column { width: parent.width spacing: Theme.spacingS - visible: BrightnessService.brightnessAvailable + visible: DisplayService.brightnessAvailable StyledText { text: "Brightness" @@ -54,102 +56,105 @@ Item { DankDropdown { id: deviceDropdown + width: parent.width height: 40 - visible: BrightnessService.devices.length > 1 + visible: DisplayService.devices.length > 1 text: "Device" description: { - const deviceInfo = BrightnessService.getCurrentDeviceInfo() - if (deviceInfo && deviceInfo.class === "ddc") { - return "DDC changes can be slow and unreliable" - } - return "" + const deviceInfo = DisplayService.getCurrentDeviceInfo(); + if (deviceInfo && deviceInfo.class === "ddc") + return "DDC changes can be slow and unreliable"; + + return ""; } - currentValue: BrightnessService.currentDevice - options: BrightnessService.devices.map(function (d) { - return d.name + currentValue: DisplayService.currentDevice + options: DisplayService.devices.map(function(d) { + return d.name; }) - optionIcons: BrightnessService.devices.map(function (d) { + optionIcons: DisplayService.devices.map(function(d) { if (d.class === "backlight") - return "desktop_windows" + return "desktop_windows"; if (d.class === "ddc") - return "tv" + return "tv"; if (d.name.includes("kbd")) - return "keyboard" + return "keyboard"; - return "lightbulb" + return "lightbulb"; }) - onValueChanged: function (value) { - BrightnessService.setCurrentDevice(value, true) + onValueChanged: function(value) { + DisplayService.setCurrentDevice(value, true); } Connections { - target: BrightnessService function onDevicesChanged() { - if (BrightnessService.currentDevice) { - deviceDropdown.currentValue = BrightnessService.currentDevice - } + if (DisplayService.currentDevice) + deviceDropdown.currentValue = DisplayService.currentDevice; // Check if saved device is now available - const lastDevice = SessionData.lastBrightnessDevice - || "" + const lastDevice = SessionData.lastBrightnessDevice || ""; if (lastDevice) { - const deviceExists = BrightnessService.devices.some( - d => d.name === lastDevice) - if (deviceExists - && (!BrightnessService.currentDevice - || BrightnessService.currentDevice !== lastDevice)) { - BrightnessService.setCurrentDevice(lastDevice, - false) - } + const deviceExists = DisplayService.devices.some((d) => { + return d.name === lastDevice; + }); + if (deviceExists && (!DisplayService.currentDevice || DisplayService.currentDevice !== lastDevice)) + DisplayService.setCurrentDevice(lastDevice, false); + } } + function onDeviceSwitched() { // Force update the description when device switches - deviceDropdown.description = Qt.binding(function () { - const deviceInfo = BrightnessService.getCurrentDeviceInfo() - if (deviceInfo && deviceInfo.class === "ddc") { - return "DDC changes can be slow and unreliable" - } - return "" - }) + deviceDropdown.description = Qt.binding(function() { + const deviceInfo = DisplayService.getCurrentDeviceInfo(); + if (deviceInfo && deviceInfo.class === "ddc") + return "DDC changes can be slow and unreliable"; + + return ""; + }); } + + target: DisplayService } + } DankSlider { id: brightnessSlider + width: parent.width - value: BrightnessService.brightnessLevel + value: DisplayService.brightnessLevel leftIcon: "brightness_low" rightIcon: "brightness_high" - enabled: BrightnessService.brightnessAvailable - && BrightnessService.isCurrentDeviceReady() - opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5 - onSliderValueChanged: function (newValue) { - brightnessDebounceTimer.pendingValue = newValue - brightnessDebounceTimer.restart() + enabled: DisplayService.brightnessAvailable && DisplayService.isCurrentDeviceReady() + opacity: DisplayService.isCurrentDeviceReady() ? 1 : 0.5 + onSliderValueChanged: function(newValue) { + brightnessDebounceTimer.pendingValue = newValue; + brightnessDebounceTimer.restart(); } - onSliderDragFinished: function (finalValue) { - brightnessDebounceTimer.stop() - BrightnessService.setBrightnessInternal( - finalValue, BrightnessService.currentDevice) + onSliderDragFinished: function(finalValue) { + brightnessDebounceTimer.stop(); + DisplayService.setBrightnessInternal(finalValue, DisplayService.currentDevice); } Connections { - target: BrightnessService function onBrightnessChanged() { - brightnessSlider.value = BrightnessService.brightnessLevel + brightnessSlider.value = DisplayService.brightnessLevel; } function onDeviceSwitched() { - brightnessSlider.value = BrightnessService.brightnessLevel + brightnessSlider.value = DisplayService.brightnessLevel; } + + target: DisplayService } + } + } + } Component { @@ -174,32 +179,40 @@ Item { width: (parent.width - Theme.spacingM) / 2 height: 80 radius: Theme.cornerRadius - color: BrightnessService.nightModeActive ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: BrightnessService.nightModeActive ? Theme.primary : "transparent" - border.width: BrightnessService.nightModeActive ? 1 : 0 + color: DisplayService.nightModeActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + border.color: DisplayService.nightModeActive ? Theme.primary : "transparent" + border.width: DisplayService.nightModeActive ? 1 : 0 + opacity: SessionData.nightModeAutoEnabled ? 0.6 : 1 Column { anchors.centerIn: parent spacing: Theme.spacingS DankIcon { - name: BrightnessService.nightModeActive ? "nightlight" : "dark_mode" + name: DisplayService.nightModeActive ? "nightlight" : "dark_mode" size: Theme.iconSizeLarge - color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText + color: DisplayService.nightModeActive ? Theme.primary : Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter } StyledText { - text: "Night Mode" + text: SessionData.nightModeAutoEnabled ? "Night Mode (Auto)" : "Night Mode" font.pixelSize: Theme.fontSizeMedium - color: BrightnessService.nightModeActive ? Theme.primary : Theme.surfaceText + color: DisplayService.nightModeActive ? Theme.primary : Theme.surfaceText font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } + + } + + DankIcon { + name: "schedule" + size: 16 + color: Theme.primary + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacingS + visible: SessionData.nightModeAutoEnabled } MouseArea { @@ -209,20 +222,17 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - BrightnessService.toggleNightMode() + DisplayService.toggleNightMode(); } } + } Rectangle { width: (parent.width - Theme.spacingM) / 2 height: 80 radius: Theme.cornerRadius - color: SessionData.isLightMode ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + color: SessionData.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) border.color: SessionData.isLightMode ? Theme.primary : "transparent" border.width: SessionData.isLightMode ? 1 : 0 @@ -244,6 +254,7 @@ Item { font.weight: Font.Medium anchors.horizontalCenter: parent.horizontalCenter } + } MouseArea { @@ -253,7 +264,7 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - Theme.toggleLightMode() + Theme.toggleLightMode(); } } @@ -262,10 +273,15 @@ Item { duration: Theme.shortDuration easing.type: Theme.standardEasing } + } + } + } + } + } brightnessDebounceTimer: Timer { @@ -273,13 +289,13 @@ Item { interval: { // Use longer interval for DDC devices since ddcutil is slow - const deviceInfo = BrightnessService.getCurrentDeviceInfo() - return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50 + const deviceInfo = DisplayService.getCurrentDeviceInfo(); + return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50; } repeat: false onTriggered: { - BrightnessService.setBrightnessInternal( - pendingValue, BrightnessService.currentDevice) + DisplayService.setBrightnessInternal(pendingValue, DisplayService.currentDevice); } } + } diff --git a/Modules/OSD/BrightnessOSD.qml b/Modules/OSD/BrightnessOSD.qml index 05cfee03..d272839a 100644 --- a/Modules/OSD/BrightnessOSD.qml +++ b/Modules/OSD/BrightnessOSD.qml @@ -15,18 +15,18 @@ DankOSD { property int pendingValue: 0 interval: { - const deviceInfo = BrightnessService.getCurrentDeviceInfo() + const deviceInfo = DisplayService.getCurrentDeviceInfo() return (deviceInfo && deviceInfo.class === "ddc") ? 200 : 50 } repeat: false onTriggered: { - BrightnessService.setBrightnessInternal(pendingValue, BrightnessService.lastIpcDevice) + DisplayService.setBrightnessInternal(pendingValue, DisplayService.lastIpcDevice) } } Connections { - target: BrightnessService + target: DisplayService function onBrightnessChanged() { root.show() } @@ -53,7 +53,7 @@ DankOSD { DankIcon { anchors.centerIn: parent name: { - const deviceInfo = BrightnessService.getCurrentDeviceInfo() + const deviceInfo = DisplayService.getCurrentDeviceInfo() if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") return "brightness_medium" else if (deviceInfo.name.includes("kbd")) @@ -75,17 +75,17 @@ DankOSD { anchors.verticalCenter: parent.verticalCenter minimum: 1 maximum: 100 - enabled: BrightnessService.brightnessAvailable + enabled: DisplayService.brightnessAvailable showValue: true unit: "%" Component.onCompleted: { - if (BrightnessService.brightnessAvailable) - value = BrightnessService.brightnessLevel + if (DisplayService.brightnessAvailable) + value = DisplayService.brightnessLevel } onSliderValueChanged: function(newValue) { - if (BrightnessService.brightnessAvailable) { + if (DisplayService.brightnessAvailable) { root.brightnessDebounceTimer.pendingValue = newValue root.brightnessDebounceTimer.restart() resetHideTimer() @@ -97,23 +97,23 @@ DankOSD { } onSliderDragFinished: function(finalValue) { - if (BrightnessService.brightnessAvailable) { + if (DisplayService.brightnessAvailable) { root.brightnessDebounceTimer.stop() - BrightnessService.setBrightnessInternal(finalValue, BrightnessService.lastIpcDevice) + DisplayService.setBrightnessInternal(finalValue, DisplayService.lastIpcDevice) } } Connections { - target: BrightnessService + target: DisplayService function onBrightnessChanged() { if (!brightnessSlider.pressed) - brightnessSlider.value = BrightnessService.brightnessLevel + brightnessSlider.value = DisplayService.brightnessLevel } function onDeviceSwitched() { if (!brightnessSlider.pressed) - brightnessSlider.value = BrightnessService.brightnessLevel + brightnessSlider.value = DisplayService.brightnessLevel } } } @@ -121,10 +121,10 @@ DankOSD { } onOsdShown: { - if (BrightnessService.brightnessAvailable && contentLoader.item) { + if (DisplayService.brightnessAvailable && contentLoader.item) { let slider = contentLoader.item.children[0].children[1] if (slider) - slider.value = BrightnessService.brightnessLevel + slider.value = DisplayService.brightnessLevel } } } \ No newline at end of file diff --git a/Modules/Settings/PersonalizationTab.qml b/Modules/Settings/PersonalizationTab.qml index b66b19fa..fc020219 100644 --- a/Modules/Settings/PersonalizationTab.qml +++ b/Modules/Settings/PersonalizationTab.qml @@ -17,75 +17,56 @@ Item { property bool fontsEnumerated: false function enumerateFonts() { - var fonts = ["Default"] - var availableFonts = Qt.fontFamilies() - var rootFamilies = [] - var seenFamilies = new Set() + var fonts = ["Default"]; + var availableFonts = Qt.fontFamilies(); + var rootFamilies = []; + var seenFamilies = new Set(); for (var i = 0; i < availableFonts.length; i++) { - var fontName = availableFonts[i] + var fontName = availableFonts[i]; if (fontName.startsWith(".")) - continue + continue; if (fontName === SettingsData.defaultFontFamily) - continue + continue; - var rootName = fontName.replace( - / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, - "").replace( - / (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, - "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, - function (match, suffix) { - return match - }).trim() + var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function(match, suffix) { + return match; + }).trim(); if (!seenFamilies.has(rootName) && rootName !== "") { - seenFamilies.add(rootName) - rootFamilies.push(rootName) + seenFamilies.add(rootName); + rootFamilies.push(rootName); } } - cachedFontFamilies = fonts.concat(rootFamilies.sort()) - var monoFonts = ["Default"] - var monoFamilies = [] - var seenMonoFamilies = new Set() + cachedFontFamilies = fonts.concat(rootFamilies.sort()); + var monoFonts = ["Default"]; + var monoFamilies = []; + var seenMonoFamilies = new Set(); for (var j = 0; j < availableFonts.length; j++) { - var fontName2 = availableFonts[j] + var fontName2 = availableFonts[j]; if (fontName2.startsWith(".")) - continue + continue; if (fontName2 === SettingsData.defaultMonoFontFamily) - continue + continue; - var lowerName = fontName2.toLowerCase() - if (lowerName.includes("mono") || lowerName.includes( - "code") || lowerName.includes( - "console") || lowerName.includes( - "terminal") || lowerName.includes( - "courier") || lowerName.includes( - "dejavu sans mono") || lowerName.includes( - "jetbrains") || lowerName.includes( - "fira") || lowerName.includes( - "hack") || lowerName.includes( - "source code") || lowerName.includes( - "ubuntu mono") || lowerName.includes("cascadia")) { - var rootName2 = fontName2.replace( - / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, - "").replace( - / (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, - "").trim() + var lowerName = fontName2.toLowerCase(); + if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes("jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { + var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim(); if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { - seenMonoFamilies.add(rootName2) - monoFamilies.push(rootName2) + seenMonoFamilies.add(rootName2); + monoFamilies.push(rootName2); } } } - cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) + cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()); } Component.onCompleted: { // Access WallpaperCyclingService to ensure it's initialized - WallpaperCyclingService.cyclingActive + WallpaperCyclingService.cyclingActive; if (!fontsEnumerated) { - enumerateFonts() - fontsEnumerated = true + enumerateFonts(); + fontsEnumerated = true; } } @@ -107,10 +88,8 @@ Item { width: parent.width height: wallpaperSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.2) + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 Column { @@ -138,6 +117,7 @@ Item { color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } + } Row { @@ -167,6 +147,7 @@ Item { maskThresholdMin: 0.5 maskSpreadAtMin: 1 } + } Rectangle { @@ -217,12 +198,13 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { if (parentModal) { - parentModal.allowFocusOverride = true - parentModal.shouldHaveFocus = false + parentModal.allowFocusOverride = true; + parentModal.shouldHaveFocus = false; } - wallpaperBrowser.open() + wallpaperBrowser.open(); } } + } Rectangle { @@ -243,24 +225,29 @@ Item { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - if (Theme.currentTheme === Theme.dynamic) { - Theme.switchTheme("blue") - } - SessionData.setWallpaper("") + if (Theme.currentTheme === Theme.dynamic) + Theme.switchTheme("blue"); + + SessionData.setWallpaper(""); } } + } + } + } MouseArea { id: wallpaperMouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor propagateComposedEvents: true acceptedButtons: Qt.NoButton } + } Column { @@ -269,9 +256,7 @@ Item { anchors.verticalCenter: parent.verticalCenter StyledText { - text: SessionData.wallpaperPath ? SessionData.wallpaperPath.split( - '/').pop( - ) : "No wallpaper selected" + text: SessionData.wallpaperPath ? SessionData.wallpaperPath.split('/').pop() : "No wallpaper selected" font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText elide: Text.ElideMiddle @@ -299,15 +284,11 @@ Item { iconSize: Theme.iconSizeSmall enabled: SessionData.wallpaperPath opacity: SessionData.wallpaperPath ? 1 : 0.5 - backgroundColor: Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.5) - hoverColor: Qt.rgba(Theme.primary.r, - Theme.primary.g, - Theme.primary.b, 0.12) + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + hoverColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) iconColor: Theme.surfaceText onClicked: { - WallpaperCyclingService.cyclePrevManually() + WallpaperCyclingService.cyclePrevManually(); } } @@ -317,19 +298,18 @@ Item { iconSize: Theme.iconSizeSmall enabled: SessionData.wallpaperPath opacity: SessionData.wallpaperPath ? 1 : 0.5 - backgroundColor: Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.5) - hoverColor: Qt.rgba(Theme.primary.r, - Theme.primary.g, - Theme.primary.b, 0.12) + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + hoverColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) iconColor: Theme.surfaceText onClicked: { - WallpaperCyclingService.cycleNextManually() + WallpaperCyclingService.cycleNextManually(); } } + } + } + } // Wallpaper Cycling Section - Full Width @@ -375,6 +355,7 @@ Item { color: Theme.surfaceVariantText width: parent.width } + } DankToggle { @@ -382,11 +363,11 @@ Item { anchors.verticalCenter: parent.verticalCenter checked: SessionData.wallpaperCyclingEnabled - onToggled: toggled => { - return SessionData.setWallpaperCyclingEnabled( - toggled) - } + onToggled: (toggled) => { + return SessionData.setWallpaperCyclingEnabled(toggled); + } } + } // Cycling mode and settings @@ -413,17 +394,16 @@ Item { width: 200 height: 32 model: [{ - "text": "Interval" - }, { - "text": "Time" - }] - currentIndex: SessionData.wallpaperCyclingMode - === "time" ? 1 : 0 - onTabClicked: index => { - SessionData.setWallpaperCyclingMode( - index === 1 ? "time" : "interval") - } + "text": "Interval" + }, { + "text": "Time" + }] + currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0 + onTabClicked: (index) => { + SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval"); + } } + } // Interval settings @@ -437,18 +417,16 @@ Item { description: "How often to change wallpaper" options: intervalOptions currentValue: { - const currentSeconds = SessionData.wallpaperCyclingInterval - const index = intervalValues.indexOf( - currentSeconds) - return index >= 0 ? intervalOptions[index] : "5 minutes" + const currentSeconds = SessionData.wallpaperCyclingInterval; + const index = intervalValues.indexOf(currentSeconds); + return index >= 0 ? intervalOptions[index] : "5 minutes"; + } + onValueChanged: (value) => { + const index = intervalOptions.indexOf(value); + if (index >= 0) + SessionData.setWallpaperCyclingInterval(intervalValues[index]); + } - onValueChanged: value => { - const index = intervalOptions.indexOf( - value) - if (index >= 0) - SessionData.setWallpaperCyclingInterval( - intervalValues[index]) - } } // Time settings @@ -473,28 +451,25 @@ Item { topPadding: Theme.spacingS bottomPadding: Theme.spacingS onAccepted: { - var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test( - text) + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); if (isValid) - SessionData.setWallpaperCyclingTime( - text) + SessionData.setWallpaperCyclingTime(text); else - text = SessionData.wallpaperCyclingTime + text = SessionData.wallpaperCyclingTime; } onEditingFinished: { - var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test( - text) + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); if (isValid) - SessionData.setWallpaperCyclingTime( - text) + SessionData.setWallpaperCyclingTime(text); else - text = SessionData.wallpaperCyclingTime + text = SessionData.wallpaperCyclingTime; } anchors.verticalCenter: parent.verticalCenter validator: RegularExpressionValidator { regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ } + } StyledText { @@ -503,10 +478,15 @@ Item { color: Theme.surfaceVariantText anchors.verticalCenter: parent.verticalCenter } + } + } + } + } + } // Dynamic Theme Section @@ -514,10 +494,8 @@ Item { width: parent.width height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.2) + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 Column { @@ -539,8 +517,7 @@ Item { } Column { - width: parent.width - Theme.iconSize - Theme.spacingM - - toggle.width - Theme.spacingM + width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM spacing: Theme.spacingXS anchors.verticalCenter: parent.verticalCenter @@ -558,6 +535,7 @@ Item { wrapMode: Text.WordWrap width: parent.width } + } DankToggle { @@ -566,13 +544,14 @@ Item { anchors.verticalCenter: parent.verticalCenter checked: Theme.wallpaperPath !== "" && Theme.currentTheme === Theme.dynamic enabled: ToastService.wallpaperErrorStatus !== "matugen_missing" && Theme.wallpaperPath !== "" - onToggled: toggled => { - if (toggled) - Theme.switchTheme(Theme.dynamic) - else - Theme.switchTheme("blue") - } + onToggled: (toggled) => { + if (toggled) + Theme.switchTheme(Theme.dynamic); + else + Theme.switchTheme("blue"); + } } + } StyledText { @@ -583,7 +562,9 @@ Item { width: parent.width leftPadding: Theme.iconSize + Theme.spacingM } + } + } // Display Settings @@ -591,10 +572,8 @@ Item { width: parent.width height: displaySection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.2) + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 Column { @@ -622,53 +601,328 @@ Item { color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } + } DankToggle { id: nightModeToggle width: parent.width - text: "Night Mode" - description: "Apply warm color temperature to reduce eye strain" - checked: BrightnessService.nightModeActive - onToggled: checked => { - if (checked !== BrightnessService.nightModeActive) { - if (checked) - BrightnessService.enableNightMode() - else - BrightnessService.disableNightMode() - } - } + text: "Night Mode (Manual)" + description: SessionData.nightModeAutoEnabled ? "Manual control - automation will override when scheduled" : "Apply warm color temperature to reduce eye strain" + checked: DisplayService.nightModeActive + opacity: SessionData.nightModeAutoEnabled ? 0.7 : 1 + onToggled: (checked) => { + if (checked !== DisplayService.nightModeActive) { + if (checked) + DisplayService.enableNightMode(); + else + DisplayService.disableNightMode(); + } + } Connections { function onNightModeActiveChanged() { - nightModeToggle.checked = BrightnessService.nightModeActive + nightModeToggle.checked = DisplayService.nightModeActive; } - target: BrightnessService + target: DisplayService } + } DankDropdown { width: parent.width text: "Night Mode Temperature" - description: BrightnessService.nightModeActive ? "Disable night mode to adjust" : "Set temperature for night mode" - enabled: !BrightnessService.nightModeActive - opacity: !BrightnessService.nightModeActive ? 1 : 0.6 + description: DisplayService.nightModeActive ? "Disable night mode to adjust" : "Set temperature for night mode" + enabled: !DisplayService.nightModeActive + opacity: !DisplayService.nightModeActive ? 1 : 0.6 currentValue: SessionData.nightModeTemperature + "K" options: { - var temps = [] + var temps = []; for (var i = 2500; i <= 6000; i += 500) { - temps.push(i + "K") + temps.push(i + "K"); } - return temps + return temps; } - onValueChanged: value => { - var temp = parseInt( - value.replace("K", "")) - SessionData.setNightModeTemperature( - temp) - } + onValueChanged: (value) => { + var temp = parseInt(value.replace("K", "")); + SessionData.setNightModeTemperature(temp); + } + } + + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + } + + DankToggle { + width: parent.width + text: "Night Mode Automation" + description: "Automatically enable/disable night mode based on time schedule or location. Works independently of manual toggle above." + checked: SessionData.nightModeAutoEnabled + onToggled: (checked) => { + SessionData.setNightModeAutoEnabled(checked); + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + visible: SessionData.nightModeAutoEnabled + leftPadding: Theme.spacingM + + Row { + spacing: Theme.spacingL + width: parent.width - parent.leftPadding + + StyledText { + text: "Mode:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTabBar { + width: 200 + height: 32 + model: [{ + "text": "Time" + }, { + "text": "Location" + }] + currentIndex: SessionData.nightModeAutoMode === "location" ? 1 : 0 + onTabClicked: (index) => { + SessionData.setNightModeAutoMode(index === 1 ? "location" : "time"); + } + } + + } + + StyledText { + text: "Press Enter or click away to save time changes. Border turns green when valid (HH:MM format)." + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + visible: SessionData.nightModeAutoMode === "time" + width: parent.width - parent.leftPadding + wrapMode: Text.WordWrap + } + + Row { + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "time" + width: parent.width - parent.leftPadding + + StyledText { + text: "Start:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + id: startTimeField + width: 80 + height: 32 + text: SessionData.nightModeStartTime + placeholderText: "20:00" + maximumLength: 5 + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + normalBorderColor: { + if (text.length === 0) return Theme.outlineStrong; + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + return isValid ? Theme.success : Theme.error; + } + onAccepted: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + SessionData.setNightModeStartTime(text); + } else { + text = SessionData.nightModeStartTime; + } + } + onEditingFinished: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + SessionData.setNightModeStartTime(text); + } else { + text = SessionData.nightModeStartTime; + } + } + anchors.verticalCenter: parent.verticalCenter + + validator: RegularExpressionValidator { + regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ + } + + } + + StyledText { + text: "End:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + id: endTimeField + width: 80 + height: 32 + text: SessionData.nightModeEndTime + placeholderText: "06:00" + maximumLength: 5 + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + normalBorderColor: { + if (text.length === 0) return Theme.outlineStrong; + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + return isValid ? Theme.success : Theme.error; + } + onAccepted: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + SessionData.setNightModeEndTime(text); + } else { + text = SessionData.nightModeEndTime; + } + } + onEditingFinished: { + var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); + if (isValid) { + SessionData.setNightModeEndTime(text); + } else { + text = SessionData.nightModeEndTime; + } + } + anchors.verticalCenter: parent.verticalCenter + + validator: RegularExpressionValidator { + regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ + } + + } + + } + + Column { + width: parent.width - parent.leftPadding + spacing: Theme.spacingXS + visible: SessionData.nightModeAutoMode === "location" + + StyledText { + text: "Uses automatic location detection for sunrise/sunset times. If automatic detection fails, enter your coordinates manually below (e.g., NYC: 40.7128, -74.0060)." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + wrapMode: Text.WordWrap + } + + StyledText { + text: "Press Enter or click away to save changes. Border turns green when valid." + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + width: parent.width + wrapMode: Text.WordWrap + } + } + + Row { + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "location" + width: parent.width - parent.leftPadding + + StyledText { + text: "Coordinates:" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + id: latitudeField + width: 90 + height: 32 + text: SessionData.latitude ? SessionData.latitude.toString() : "" + placeholderText: "40.7128" + maximumLength: 10 + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + normalBorderColor: { + if (text.length === 0) return Theme.outlineStrong; + var lat = parseFloat(text); + return (!isNaN(lat) && lat >= -90 && lat <= 90) ? Theme.success : Theme.error; + } + onAccepted: { + var lat = parseFloat(text); + if (!isNaN(lat) && lat >= -90 && lat <= 90) { + SessionData.setLatitude(lat); + } else { + text = SessionData.latitude ? SessionData.latitude.toString() : ""; + } + } + onEditingFinished: { + var lat = parseFloat(text); + if (!isNaN(lat) && lat >= -90 && lat <= 90) { + SessionData.setLatitude(lat); + } else { + text = SessionData.latitude ? SessionData.latitude.toString() : ""; + } + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "," + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + DankTextField { + id: longitudeField + width: 90 + height: 32 + text: SessionData.longitude ? SessionData.longitude.toString() : "" + placeholderText: "-74.0060" + maximumLength: 11 + topPadding: Theme.spacingXS + bottomPadding: Theme.spacingXS + normalBorderColor: { + if (text.length === 0) return Theme.outlineStrong; + var lon = parseFloat(text); + return (!isNaN(lon) && lon >= -180 && lon <= 180) ? Theme.success : Theme.error; + } + onAccepted: { + var lon = parseFloat(text); + if (!isNaN(lon) && lon >= -180 && lon <= 180) { + SessionData.setLongitude(lon); + } else { + text = SessionData.longitude ? SessionData.longitude.toString() : ""; + } + } + onEditingFinished: { + var lon = parseFloat(text); + if (!isNaN(lon) && lon >= -180 && lon <= 180) { + SessionData.setLongitude(lon); + } else { + text = SessionData.longitude ? SessionData.longitude.toString() : ""; + } + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "(lat, lng)" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + } + } DankToggle { @@ -676,11 +930,13 @@ Item { text: "Light Mode" description: "Use light theme instead of dark theme" checked: SessionData.isLightMode - onToggled: checked => { - Theme.setLightMode(checked) + onToggled: (checked) => { + Theme.setLightMode(checked); } } + } + } // Font Settings @@ -688,10 +944,8 @@ Item { width: parent.width height: fontSection.implicitHeight + Theme.spacingL * 2 radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, - Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.2) + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.width: 1 Column { @@ -719,6 +973,7 @@ Item { color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } + } DankDropdown { @@ -727,21 +982,20 @@ Item { description: "Select system font family" currentValue: { if (SettingsData.fontFamily === SettingsData.defaultFontFamily) - return "Default" + return "Default"; else - return SettingsData.fontFamily || "Default" + return SettingsData.fontFamily || "Default"; } enableFuzzySearch: true popupWidthOffset: 100 maxPopupHeight: 400 options: cachedFontFamilies - onValueChanged: value => { - if (value.startsWith("Default")) - SettingsData.setFontFamily( - SettingsData.defaultFontFamily) - else - SettingsData.setFontFamily(value) - } + onValueChanged: (value) => { + if (value.startsWith("Default")) + SettingsData.setFontFamily(SettingsData.defaultFontFamily); + else + SettingsData.setFontFamily(value); + } } DankDropdown { @@ -751,64 +1005,64 @@ Item { currentValue: { switch (SettingsData.fontWeight) { case Font.Thin: - return "Thin" + return "Thin"; case Font.ExtraLight: - return "Extra Light" + return "Extra Light"; case Font.Light: - return "Light" + return "Light"; case Font.Normal: - return "Regular" + return "Regular"; case Font.Medium: - return "Medium" + return "Medium"; case Font.DemiBold: - return "Demi Bold" + return "Demi Bold"; case Font.Bold: - return "Bold" + return "Bold"; case Font.ExtraBold: - return "Extra Bold" + return "Extra Bold"; case Font.Black: - return "Black" + return "Black"; default: - return "Regular" + return "Regular"; } } options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"] - onValueChanged: value => { - var weight - switch (value) { - case "Thin": - weight = Font.Thin - break - case "Extra Light": - weight = Font.ExtraLight - break - case "Light": - weight = Font.Light - break - case "Regular": - weight = Font.Normal - break - case "Medium": - weight = Font.Medium - break - case "Demi Bold": - weight = Font.DemiBold - break - case "Bold": - weight = Font.Bold - break - case "Extra Bold": - weight = Font.ExtraBold - break - case "Black": - weight = Font.Black - break - default: - weight = Font.Normal - break - } - SettingsData.setFontWeight(weight) - } + onValueChanged: (value) => { + var weight; + switch (value) { + case "Thin": + weight = Font.Thin; + break; + case "Extra Light": + weight = Font.ExtraLight; + break; + case "Light": + weight = Font.Light; + break; + case "Regular": + weight = Font.Normal; + break; + case "Medium": + weight = Font.Medium; + break; + case "Demi Bold": + weight = Font.DemiBold; + break; + case "Bold": + weight = Font.Bold; + break; + case "Extra Bold": + weight = Font.ExtraBold; + break; + case "Black": + weight = Font.Black; + break; + default: + weight = Font.Normal; + break; + } + SettingsData.setFontWeight(weight); + } } DankDropdown { @@ -817,27 +1071,28 @@ Item { description: "Select monospace font for process list and technical displays" currentValue: { if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily) - return "Default" + return "Default"; - return SettingsData.monoFontFamily || "Default" + return SettingsData.monoFontFamily || "Default"; } enableFuzzySearch: true popupWidthOffset: 100 maxPopupHeight: 400 options: cachedMonoFamilies - onValueChanged: value => { - if (value === "Default") - SettingsData.setMonoFontFamily( - SettingsData.defaultMonoFontFamily) - else - SettingsData.setMonoFontFamily( - value) - } + onValueChanged: (value) => { + if (value === "Default") + SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily); + else + SettingsData.setMonoFontFamily(value); + } } + } + } } + } FileBrowserModal { @@ -847,17 +1102,18 @@ Item { browserIcon: "wallpaper" browserType: "wallpaper" fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] - onFileSelected: path => { - SessionData.setWallpaper(path) - close() - } + onFileSelected: (path) => { + SessionData.setWallpaper(path); + close(); + } onDialogClosed: { if (parentModal) { - parentModal.allowFocusOverride = false + parentModal.allowFocusOverride = false; parentModal.shouldHaveFocus = Qt.binding(() => { - return parentModal.shouldBeVisible - }) + return parentModal.shouldBeVisible; + }); } } } + } diff --git a/Services/BrightnessService.qml b/Services/DisplayService.qml similarity index 94% rename from Services/BrightnessService.qml rename to Services/DisplayService.qml index 29074f33..bbb933ab 100644 --- a/Services/BrightnessService.qml +++ b/Services/DisplayService.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Common +import qs.Services Singleton { id: root @@ -245,6 +246,12 @@ Singleton { } function toggleNightMode() { + // Check if automation is active - show warning if trying to manually toggle + if (SessionData.nightModeAutoEnabled) { + ToastService.showWarning("Night mode is in automatic mode. Disable automation in settings to control manually.") + return + } + if (nightModeActive) { disableNightMode() } else { @@ -271,10 +278,10 @@ Singleton { onExited: function (exitCode) { ddcAvailable = (exitCode === 0) if (ddcAvailable) { - console.log("BrightnessService: ddcutil detected") + console.log("DisplayService: ddcutil detected") ddcDisplayDetectionProcess.running = true } else { - console.log("BrightnessService: ddcutil not available") + console.log("DisplayService: ddcutil not available") } } } @@ -288,7 +295,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { if (!text.trim()) { - console.log("BrightnessService: No DDC displays found") + console.log("DisplayService: No DDC displays found") ddcDevices = [] return } @@ -311,7 +318,7 @@ Singleton { } ddcDevices = newDdcDevices - console.log("BrightnessService: Found", ddcDevices.length, + console.log("DisplayService: Found", ddcDevices.length, "DDC displays") // Queue initial brightness readings for DDC devices @@ -339,7 +346,7 @@ Singleton { } } } catch (error) { - console.warn("BrightnessService: Failed to parse DDC devices:", + console.warn("DisplayService: Failed to parse DDC devices:", error) ddcDevices = [] } @@ -348,7 +355,7 @@ Singleton { onExited: function (exitCode) { if (exitCode !== 0) { - console.warn("BrightnessService: Failed to detect DDC displays:", + console.warn("DisplayService: Failed to detect DDC displays:", exitCode) ddcDevices = [] } @@ -361,7 +368,7 @@ Singleton { command: ["brightnessctl", "-m", "-l"] onExited: function (exitCode) { if (exitCode !== 0) { - console.warn("BrightnessService: Failed to list devices:", + console.warn("DisplayService: Failed to list devices:", exitCode) brightnessAvailable = false } @@ -370,7 +377,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { if (!text.trim()) { - console.warn("BrightnessService: No devices found") + console.warn("DisplayService: No devices found") return } const lines = text.trim().split("\n") @@ -417,7 +424,7 @@ Singleton { running: false onExited: function (exitCode) { if (exitCode !== 0) - console.warn("BrightnessService: Failed to set brightness:", + console.warn("DisplayService: Failed to set brightness:", exitCode) } } @@ -429,7 +436,7 @@ Singleton { onExited: function (exitCode) { if (exitCode !== 0) console.warn( - "BrightnessService: Failed to set DDC brightness:", + "DisplayService: Failed to set DDC brightness:", exitCode) } } @@ -440,7 +447,7 @@ Singleton { running: false onExited: function (exitCode) { if (exitCode !== 0) - console.warn("BrightnessService: Failed to get initial DDC brightness:", + console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode) processNextDdcInit() @@ -470,7 +477,7 @@ Singleton { delete newPending[deviceName] ddcPendingInit = newPending - console.log("BrightnessService: Initial DDC Device", + console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%") } } @@ -484,7 +491,7 @@ Singleton { running: false onExited: function (exitCode) { if (exitCode !== 0) - console.warn("BrightnessService: Failed to get brightness:", + console.warn("DisplayService: Failed to get brightness:", exitCode) } @@ -508,7 +515,7 @@ Singleton { } brightnessInitialized = true - console.log("BrightnessService: Device", currentDevice, + console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%") brightnessChanged() } @@ -523,7 +530,7 @@ Singleton { onExited: function (exitCode) { if (exitCode !== 0) console.warn( - "BrightnessService: Failed to get DDC brightness:", + "DisplayService: Failed to get DDC brightness:", exitCode) } @@ -548,7 +555,7 @@ Singleton { } brightnessInitialized = true - console.log("BrightnessService: DDC Device", currentDevice, + console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%") brightnessChanged() } @@ -569,7 +576,7 @@ Singleton { SessionData.setNightModeEnabled(true) } else { // gammastep not found - console.warn("BrightnessService: gammastep not found") + console.warn("DisplayService: gammastep not found") ToastService.showWarning( "Night mode failed: gammastep not found") } @@ -588,7 +595,7 @@ Singleton { onExited: function (exitCode) { // If process exits with non-zero code while we think it should be running if (nightModeActive && exitCode !== 0) { - console.warn("BrightnessService: Night mode process crashed with exit code:", + console.warn("DisplayService: Night mode process crashed with exit code:", exitCode) nightModeActive = false SessionData.setNightModeEnabled(false) diff --git a/Services/NightModeAutomationService.qml b/Services/NightModeAutomationService.qml new file mode 100644 index 00000000..f7a942d9 --- /dev/null +++ b/Services/NightModeAutomationService.qml @@ -0,0 +1,363 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common + +Singleton { + id: root + + property bool automationAvailable: false + property bool locationProviderAvailable: false + property var availableProviders: [] + property string currentProvider: "" + property bool isAutomaticNightTime: false + property string currentLocation: "" + property real latitude: 0.0 + property real longitude: 0.0 + + Component.onCompleted: { + console.log("NightModeAutomationService: Component completed") + checkAvailability() + updateFromSessionData() + if (SessionData.nightModeAutoEnabled) { + console.log("NightModeAutomationService: Auto-starting automation on init") + startAutomation() + } + } + + function checkAvailability() { + gammaStepTestProcess.running = true + } + + function startAutomation() { + if (!automationAvailable) { + console.warn("NightModeAutomationService: Gammastep not available") + return + } + + const mode = SessionData.nightModeAutoMode || "manual" + + switch (mode) { + case "time": + startTimeBasedMode() + break + case "location": + startLocationBasedMode() + break + case "manual": + default: + stopAutomation() + break + } + } + + function stopAutomation() { + automationTimer.stop() + locationTimer.stop() + if (gammaStepAutomationProcess.running) { + gammaStepAutomationProcess.kill() + } + isAutomaticNightTime = false + } + + function startTimeBasedMode() { + console.log("NightModeAutomationService: Starting time-based automation") + automationTimer.start() + checkTimeBasedMode() + } + + function startLocationBasedMode() { + if (!locationProviderAvailable) { + console.warn("NightModeAutomationService: No location provider available, falling back to time-based mode") + startTimeBasedMode() + return + } + + console.log("NightModeAutomationService: Starting location-based automation") + + const temperature = SessionData.nightModeTemperature || 4500 + const dayTemp = 6500 + + if (latitude !== 0.0 && longitude !== 0.0) { + gammaStepAutomationProcess.command = [ + "gammastep", + "-m", "wayland", + "-l", `${latitude.toFixed(6)}:${longitude.toFixed(6)}`, + "-t", `${dayTemp}:${temperature}`, + "-v" + ] + } else { + gammaStepAutomationProcess.command = [ + "gammastep", + "-m", "wayland", + "-l", currentProvider || "manual", + "-t", `${dayTemp}:${temperature}`, + "-v" + ] + } + + gammaStepAutomationProcess.running = true + locationTimer.start() + } + + function checkTimeBasedMode() { + if (!SessionData.nightModeAutoEnabled || SessionData.nightModeAutoMode !== "time") { + console.log("NightModeAutomationService: checkTimeBasedMode - not enabled or wrong mode") + return + } + + const now = new Date() + const currentHour = now.getHours() + const currentMinute = now.getMinutes() + const currentTime = currentHour * 60 + currentMinute + + const startTime = SessionData.nightModeStartTime || "20:00" + const endTime = SessionData.nightModeEndTime || "06:00" + + const startParts = startTime.split(":") + const endParts = endTime.split(":") + + const startMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]) + const endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]) + + let shouldBeNight = false + + if (startMinutes > endMinutes) { + shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes) + } else { + shouldBeNight = (currentTime >= startMinutes) && (currentTime < endMinutes) + } + + console.log(`NightModeAutomationService: Time check - Current: ${currentHour}:${currentMinute.toString().padStart(2, '0')} (${currentTime}), Range: ${startTime}-${endTime} (${startMinutes}-${endMinutes}), Should be night: ${shouldBeNight}`) + + if (shouldBeNight !== isAutomaticNightTime) { + isAutomaticNightTime = shouldBeNight + console.log("NightModeAutomationService: Automatic night time status changed to:", shouldBeNight) + + if (shouldBeNight) { + requestNightModeActivation() + } else { + requestNightModeDeactivation() + } + } else { + console.log("NightModeAutomationService: No change needed, isAutomaticNightTime already:", isAutomaticNightTime) + } + } + + function requestNightModeActivation() { + console.log("NightModeAutomationService: Requesting night mode activation") + const temperature = SessionData.nightModeTemperature || 4500 + console.log("NightModeAutomationService: Using temperature:", temperature + "K") + + gammaStepOneTimeProcess.command = [ + "gammastep", + "-m", "wayland", + "-O", String(temperature), + "-P" + ] + console.log("NightModeAutomationService: Running gamma command:", gammaStepOneTimeProcess.command.join(" ")) + gammaStepOneTimeProcess.running = true + + SessionData.setNightModeEnabled(true) + } + + function requestNightModeDeactivation() { + console.log("NightModeAutomationService: Requesting night mode deactivation") + + gammaStepResetProcess.command = [ + "gammastep", + "-m", "wayland", + "-O", "6500", + "-P" + ] + gammaStepResetProcess.running = true + + SessionData.setNightModeEnabled(false) + } + + function setLocation(lat, lon) { + latitude = lat + longitude = lon + currentLocation = `${lat.toFixed(6)},${lon.toFixed(6)}` + + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "location") { + startLocationBasedMode() + } + } + + function updateFromSessionData() { + console.log("NightModeAutomationService: Updating from SessionData - lat:", SessionData.latitude, "lng:", SessionData.longitude) + if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { + setLocation(SessionData.latitude, SessionData.longitude) + } + } + + function detectLocationProviders() { + locationProviderDetectionProcess.running = true + } + + function testAutomationNow() { + console.log("NightModeAutomationService: Manual test triggered") + console.log("NightModeAutomationService: Current settings - autoEnabled:", SessionData.nightModeAutoEnabled, "mode:", SessionData.nightModeAutoMode) + if (SessionData.nightModeAutoMode === "time") { + checkTimeBasedMode() + } else if (SessionData.nightModeAutoMode === "location") { + console.log("NightModeAutomationService: Location mode - coordinates:", latitude, longitude) + } + } + + Timer { + id: automationTimer + interval: 60000 + running: false + repeat: true + onTriggered: { + checkTimeBasedMode() + } + } + + Timer { + id: locationTimer + interval: 300000 + running: false + repeat: true + onTriggered: { + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "location") { + detectLocationProviders() + } + } + } + + Process { + id: gammaStepTestProcess + command: ["which", "gammastep"] + running: false + + onExited: function(exitCode) { + automationAvailable = (exitCode === 0) + if (automationAvailable) { + console.log("NightModeAutomationService: Gammastep available") + detectLocationProviders() + } else { + console.warn("NightModeAutomationService: Gammastep not available") + } + } + } + + Process { + id: locationProviderDetectionProcess + command: ["gammastep", "-l", "list"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + availableProviders = text.trim().split('\n').filter(line => line.trim().length > 0) + locationProviderAvailable = availableProviders.length > 0 + + if (locationProviderAvailable && !currentProvider) { + currentProvider = availableProviders[0] + } + + console.log("NightModeAutomationService: Available providers:", availableProviders) + } + } + } + + onExited: function(exitCode) { + if (exitCode !== 0) { + console.warn("NightModeAutomationService: Failed to detect location providers") + locationProviderAvailable = false + } + } + } + + Process { + id: gammaStepAutomationProcess + running: false + + onExited: function(exitCode) { + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "location" && exitCode !== 0) { + console.warn("NightModeAutomationService: Location-based automation failed, exit code:", exitCode) + restartTimer.start() + } + } + } + + Process { + id: gammaStepOneTimeProcess + running: false + + onExited: function(exitCode) { + if (exitCode !== 0) { + console.warn("NightModeAutomationService: Failed to enable night mode, exit code:", exitCode) + } + } + } + + Process { + id: gammaStepResetProcess + running: false + + onExited: function(exitCode) { + if (exitCode !== 0) { + console.warn("NightModeAutomationService: Failed to reset gamma, exit code:", exitCode) + } + } + } + + Timer { + id: restartTimer + interval: 10000 + running: false + repeat: false + onTriggered: { + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "location") { + startLocationBasedMode() + } + } + } + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + console.log("NightModeAutomationService: Auto enabled changed to", SessionData.nightModeAutoEnabled) + if (SessionData.nightModeAutoEnabled) { + startAutomation() + } else { + stopAutomation() + } + } + function onNightModeAutoModeChanged() { + if (SessionData.nightModeAutoEnabled) { + startAutomation() + } + } + function onNightModeStartTimeChanged() { + console.log("NightModeAutomationService: Start time changed to", SessionData.nightModeStartTime) + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "time") { + checkTimeBasedMode() + } + } + function onNightModeEndTimeChanged() { + console.log("NightModeAutomationService: End time changed to", SessionData.nightModeEndTime) + if (SessionData.nightModeAutoEnabled && SessionData.nightModeAutoMode === "time") { + checkTimeBasedMode() + } + } + function onNightModeTemperatureChanged() { + if (SessionData.nightModeAutoEnabled) { + startAutomation() + } + } + function onLatitudeChanged() { + updateFromSessionData() + } + function onLongitudeChanged() { + updateFromSessionData() + } + } +} \ No newline at end of file