From a6c0a9fc990f9911fbf6fbe739363b698e649474 Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Tue, 14 Oct 2025 22:21:19 +0200 Subject: [PATCH 1/4] DarkModeService: Added for automatic dark mode switching. --- Services/DarkModeService.qml | 85 ++++++++++++++++++++++++++++++++++++ Services/LocationService.qml | 2 +- shell.qml | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Services/DarkModeService.qml diff --git a/Services/DarkModeService.qml b/Services/DarkModeService.qml new file mode 100644 index 000000000..afba54814 --- /dev/null +++ b/Services/DarkModeService.qml @@ -0,0 +1,85 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services + +Singleton { + id: root + + property bool initComplete: false + property bool nextDarkModeState: false + + Connections { + target: LocationService.data + function onWeatherChanged() { + if (LocationService.data.weather !== null) { + const changes = root.collectChanges(LocationService.data.weather) + if (!root.initComplete) { + root.initComplete = true + root.resetDarkMode(changes) + } + root.scheduleChange(changes) + } + } + } + + Timer { + id: timer + onTriggered: { + Settings.data.colorSchemes.darkMode = root.nextDarkModeState + if (LocationService.data.weather !== null) { + const changes = root.collectChanges(LocationService.data.weather) + root.scheduleChange(changes) + } + } + } + + function collectChanges(weather) { + const changes = [] + for (var i = 0; i < weather.daily.sunrise.length; i++) { + changes.push({ + "time": Date.parse(weather.daily.sunrise[i]), + "darkMode": false + }) + changes.push({ + "time": Date.parse(weather.daily.sunset[i]), + "darkMode": true + }) + } + return changes + } + + function resetDarkMode(changes) { + const now = Date.now() + + // changes.findLast(change => change.time < now) // not available in QML... + let lastChange = null + for (var i = 0; i < changes.length; i++) { + if (changes[i].time < now) { + lastChange = changes[i] + } + } + + if (lastChange) { + Settings.data.colorSchemes.darkMode = lastChange.darkMode + Logger.log("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`) + } + } + + function scheduleChange(changes) { + const now = Date.now() + const nextChange = changes.find(change => change.time > now) + if (nextChange) { + root.nextDarkModeState = nextChange.darkMode + timer.interval = nextChange.time - now + timer.restart() + Logger.log("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`) + } + } + + function init() { + Logger.log("DarkModeService", "Service started") + } +} diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 0e3c0c9be..729f05bff 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -190,7 +190,7 @@ Singleton { // -------------------------------- function _fetchWeather(latitude, longitude, errorCallback) { Logger.log("Location", "Fetching weather from api.open-meteo.com") - var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto" + var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto" var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { diff --git a/shell.qml b/shell.qml index 4c4291c4a..cc2837ad3 100644 --- a/shell.qml +++ b/shell.qml @@ -86,6 +86,7 @@ ShellRoot { BarWidgetRegistry.init() LocationService.init() NightLightService.apply() + DarkModeService.init() FontService.init() HooksService.init() BluetoothService.init() From 49f4ab114f4f47d5e7c248bcab61616938131a8a Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Thu, 16 Oct 2025 17:23:32 +0200 Subject: [PATCH 2/4] DarkModeService: Add settings and manual scheduling mode. --- Commons/Settings.qml | 3 + Modules/Settings/Tabs/ColorSchemeTab.qml | 89 +++++++++++++++++++ Services/DarkModeService.qml | 108 ++++++++++++++++++++--- Services/LocationService.qml | 2 +- 4 files changed, 189 insertions(+), 13 deletions(-) diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 271cecb72..36b10f2a7 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -354,6 +354,9 @@ Singleton { property bool useWallpaperColors: false property string predefinedScheme: "Noctalia (default)" property bool darkMode: true + property string schedulingMode: "off" + property string manualSunrise: "06:30" + property string manualSunset: "18:30" property string matugenSchemeType: "scheme-fruit-salad" property bool generateTemplatesForPredefined: true } diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 969c87695..74205d9dd 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -13,6 +13,24 @@ ColumnLayout { property var schemeColorsCache: ({}) property int cacheVersion: 0 // Increment to trigger UI updates + // Time dropdown options (00:00 .. 23:30) + ListModel { + id: timeOptions + } + Component.onCompleted: { + for (var h = 0; h < 24; h++) { + for (var m = 0; m < 60; m += 30) { + var hh = ("0" + h).slice(-2) + var mm = ("0" + m).slice(-2) + var key = hh + ":" + mm + timeOptions.append({ + "key": key, + "name": key + }) + } + } + } + spacing: Style.marginL // Helper function to extract scheme name from path @@ -148,6 +166,77 @@ ColumnLayout { } } + NComboBox { + label: "Dark Mode Schedule" + description: "Enables automatic switching between light and dark mode" + + model: [{ + "name": "Off", + "key": "off" + }, { + "name": "Manual", + "key": "manual" + }, { + "name": "Sunrise/Sunset", + "key": "location" + }] + + currentKey: Settings.data.colorSchemes.schedulingMode + + onSelected: key => { + Settings.data.colorSchemes.schedulingMode = key + AppThemeService.generate() + } + } + + // Manual scheduling + ColumnLayout { + spacing: Style.marginS + visible: Settings.data.colorSchemes.schedulingMode === "manual" + + NLabel { + label: I18n.tr("settings.display.night-light.manual-schedule.label") + description: I18n.tr("settings.display.night-light.manual-schedule.description") + } + + RowLayout { + Layout.fillWidth: false + spacing: Style.marginS + + NText { + text: I18n.tr("settings.display.night-light.manual-schedule.sunrise") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: timeOptions + currentKey: Settings.data.colorSchemes.manualSunrise + placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-start") + onSelected: key => Settings.data.colorSchemes.manualSunrise = key + minimumWidth: 120 + } + + Item { + Layout.preferredWidth: 20 + } + + NText { + text: I18n.tr("settings.display.night-light.manual-schedule.sunset") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: timeOptions + currentKey: Settings.data.colorSchemes.manualSunset + placeholder: I18n.tr("settings.display.night-light.manual-schedule.select-stop") + onSelected: key => Settings.data.colorSchemes.manualSunset = key + minimumWidth: 120 + } + } + } + // Use Wallpaper Colors NToggle { label: I18n.tr("settings.color-scheme.color-source.use-wallpaper-colors.label") diff --git a/Services/DarkModeService.qml b/Services/DarkModeService.qml index afba54814..308d393b8 100644 --- a/Services/DarkModeService.qml +++ b/Services/DarkModeService.qml @@ -13,31 +13,118 @@ Singleton { Connections { target: LocationService.data + enabled: Settings.data.colorSchemes.schedulingMode == "location" function onWeatherChanged() { if (LocationService.data.weather !== null) { - const changes = root.collectChanges(LocationService.data.weather) + const changes = root.collectWeatherChanges(LocationService.data.weather) if (!root.initComplete) { root.initComplete = true - root.resetDarkMode(changes) + root.applyCurrentMode(changes) } - root.scheduleChange(changes) + root.scheduleNextMode(changes) } } } + Connections { + target: Settings.data.colorSchemes + enabled: Settings.data.colorSchemes.schedulingMode == "manual" + function onManualSunriseChanged() { + const changes = root.collectManualChanges() + root.applyCurrentMode(changes) + root.scheduleNextMode(changes) + } + function onManualSunsetChanged() { + const changes = root.collectManualChanges() + root.applyCurrentMode(changes) + root.scheduleNextMode(changes) + } + } + + Connections { + target: Settings.data.colorSchemes + function onSchedulingModeChanged() { + root.init() + } + } + Timer { id: timer onTriggered: { Settings.data.colorSchemes.darkMode = root.nextDarkModeState if (LocationService.data.weather !== null) { - const changes = root.collectChanges(LocationService.data.weather) - root.scheduleChange(changes) + const changes = root.collectWeatherChanges(LocationService.data.weather) + root.scheduleNextMode(changes) } } } - function collectChanges(weather) { + function init() { + Logger.log("DarkModeService", "Service started") + + if (Settings.data.colorSchemes.schedulingMode == "manual") { + const changes = collectManualChanges() + initComplete = true + applyCurrentMode(changes) + scheduleNextMode(changes) + } + + if (Settings.data.colorSchemes.schedulingMode == "location" && LocationService.data.weather) { + const changes = collectWeatherChanges(LocationService.data.weather) + initComplete = true + applyCurrentMode(changes) + scheduleNextMode(changes) + } + } + + function parseTime(timeString) { + const parts = timeString.split(":").map(Number) + return { + "hour": parts[0], + "minute": parts[1] + } + } + + function collectManualChanges() { + const sunriseTime = parseTime(Settings.data.colorSchemes.manualSunrise) + const sunsetTime = parseTime(Settings.data.colorSchemes.manualSunset) + + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + const day = now.getDate() + + const yesterdaysSunset = new Date(year, month, day - 1, sunsetTime.hour, sunsetTime.minute) + const todaysSunrise = new Date(year, month, day, sunriseTime.hour, sunriseTime.minute) + const todaysSunset = new Date(year, month, day, sunsetTime.hour, sunsetTime.minute) + const tomorrowsSunrise = new Date(year, month, day + 1, sunriseTime.hour, sunriseTime.minute) + + return [{ + "time": yesterdaysSunset.getTime(), + "darkMode": true + }, { + "time": todaysSunrise.getTime(), + "darkMode": false + }, { + "time": todaysSunset.getTime(), + "darkMode": true + }, { + "time": tomorrowsSunrise.getTime(), + "darkMode": false + }] + } + + function collectWeatherChanges(weather) { const changes = [] + + if (Date.now() < Date.parse(weather.daily.sunrise[0])) { + // The sun has not risen yet + changes.push({ + "time": Date.now() - 1, + "darkMode": true + }) + } + for (var i = 0; i < weather.daily.sunrise.length; i++) { changes.push({ "time": Date.parse(weather.daily.sunrise[i]), @@ -48,10 +135,11 @@ Singleton { "darkMode": true }) } + return changes } - function resetDarkMode(changes) { + function applyCurrentMode(changes) { const now = Date.now() // changes.findLast(change => change.time < now) // not available in QML... @@ -68,7 +156,7 @@ Singleton { } } - function scheduleChange(changes) { + function scheduleNextMode(changes) { const now = Date.now() const nextChange = changes.find(change => change.time > now) if (nextChange) { @@ -78,8 +166,4 @@ Singleton { Logger.log("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`) } } - - function init() { - Logger.log("DarkModeService", "Service started") - } } diff --git a/Services/LocationService.qml b/Services/LocationService.qml index 729f05bff..4c2709bdd 100644 --- a/Services/LocationService.qml +++ b/Services/LocationService.qml @@ -69,7 +69,7 @@ Singleton { Timer { id: updateTimer interval: 20 * 1000 - running: Settings.data.location.weatherEnabled + running: Settings.data.location.weatherEnabled || Settings.data.colorSchemes.schedulingMode == "location" repeat: true onTriggered: { updateWeather() From b82cdefd97a532c92ddb7d4be4e5497e91594883 Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Thu, 16 Oct 2025 17:40:00 +0200 Subject: [PATCH 3/4] ColorSchemeTab: Add translations for DarkModeService settings. --- Assets/Translations/de.json | 17 +++++++++++++---- Assets/Translations/en.json | 19 ++++++++++++++----- Assets/Translations/es.json | 10 ++++++---- Assets/Translations/fr.json | 10 ++++++---- Assets/Translations/pt.json | 10 ++++++---- Assets/Translations/zh-CN.json | 10 ++++++---- Modules/Settings/Tabs/ColorSchemeTab.qml | 14 +++++++------- 7 files changed, 58 insertions(+), 32 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 2be6197bd..2499d5a21 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -475,10 +475,6 @@ "label": "Farbquelle", "description": "Haupteinstellungen für Noctalias Farben." }, - "dark-mode": { - "label": "Dunkler Modus", - "description": "Wechselt zu einem dunkleren Theme für einfachere Betrachtung bei Nacht." - }, "use-wallpaper-colors": { "label": "Hintergrundbild-Farben verwenden", "description": "Farbschemata aus Ihrem Hintergrundbild mit Matugen generieren. Extrahiert automatisch Farben für ein kohärentes Design." @@ -488,6 +484,19 @@ "description": "Wähle einen Farbstil für Matugen aus." } }, + "dark-mode": { + "switch": { + "label": "Dunkler Modus", + "description": "Wechselt zu einem dunkleren Theme für einfachere Betrachtung bei Nacht." + }, + "mode": { + "label": "Automatischer dunkler Modus", + "description": "Ermöglicht automatisches Wechseln zwischen dem hellen und dunklen Modus.", + "off": "Aus", + "manual": "Manuell", + "location": "Standort" + } + }, "predefined": { "section": { "label": "Vordefinierte Farbschemata", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 915b75d14..31b9be467 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -475,10 +475,6 @@ "label": "Color source", "description": "Main settings for Noctalia's colors." }, - "dark-mode": { - "label": "Dark mode", - "description": "Switches to a darker theme for easier viewing at night." - }, "use-wallpaper-colors": { "label": "Use wallpaper colors", "description": "Generate color schemes from your wallpaper using Matugen. Automatically extracts colors to create a cohesive theme." @@ -488,6 +484,19 @@ "description": "Choose the color scheme generation algorithm for Matugen." } }, + "dark-mode": { + "switch": { + "label": "Dark mode", + "description": "Switches to a darker theme for easier viewing at night." + }, + "mode": { + "label": "Dark mode schedule", + "description": "Enables automatic switching between light and dark mode.", + "off": "Off", + "manual": "Manual", + "location": "Location" + } + }, "predefined": { "section": { "label": "Predefined color schemes", @@ -1187,7 +1196,7 @@ "scan-again": "Scan again" } }, - + "tooltips": { "refresh": "Refresh", "close": "Close", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 72251b3f7..d02fa0ece 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -475,10 +475,6 @@ "label": "Fuente de color", "description": "Configuración principal de los colores de Noctalia." }, - "dark-mode": { - "label": "Modo oscuro", - "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche." - }, "use-wallpaper-colors": { "label": "Usar colores del fondo de pantalla", "description": "Generar esquemas de color desde tu fondo de pantalla usando Matugen. Extrae automáticamente colores para crear un tema cohesivo." @@ -488,6 +484,12 @@ "description": "Elige el algoritmo de generación de esquema de colores para Matugen." } }, + "dark-mode": { + "switch": { + "label": "Modo oscuro", + "description": "Cambia a un tema más oscuro para una visualización más fácil por la noche." + } + }, "predefined": { "section": { "label": "Esquemas de colores predefinidos", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 8399a63ae..fdc0455cb 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -475,10 +475,6 @@ "label": "Source des couleurs", "description": "Paramètres principaux pour les couleurs de Noctalia." }, - "dark-mode": { - "label": "Mode sombre", - "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit." - }, "use-wallpaper-colors": { "label": "Utiliser les couleurs du fond d'écran", "description": "Générer des schémas de couleurs à partir de votre fond d'écran avec Matugen. Extrait automatiquement les couleurs pour créer un thème cohérent." @@ -488,6 +484,12 @@ "description": "Choisissez l'algorithme de génération de schéma de couleurs pour Matugen." } }, + "dark-mode": { + "switch": { + "label": "Mode sombre", + "description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit." + } + }, "predefined": { "section": { "label": "Jeux de couleurs prédéfinis", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index c4c3ea525..89113f072 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -437,10 +437,6 @@ "label": "Fonte de cor", "description": "Configurações principais para as cores do Noctalia." }, - "dark-mode": { - "label": "Modo escuro", - "description": "Muda para um tema mais escuro para facilitar a visualização à noite." - }, "use-wallpaper-colors": { "label": "Usar cores do papel de parede", "description": "Gerar esquemas de cores do seu papel de parede usando Matugen. Extrai automaticamente cores para criar um tema coeso." @@ -450,6 +446,12 @@ "description": "Escolha o algoritmo de geração de esquema de cores para Matugen." } }, + "dark-mode": { + "switch": { + "label": "Modo escuro", + "description": "Muda para um tema mais escuro para facilitar a visualização à noite." + } + }, "predefined": { "section": { "label": "Esquemas de cores predefinidos", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 0116a5daf..8d27c6ad0 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -475,10 +475,6 @@ "label": "颜色来源", "description": "Noctalia 颜色的主要设置。" }, - "dark-mode": { - "label": "深色模式", - "description": "切换到更暗的主题,便于夜间观看。" - }, "use-wallpaper-colors": { "label": "使用壁纸颜色", "description": "使用 Matugen 从壁纸生成颜色方案。自动提取颜色以创建一致的主题。" @@ -488,6 +484,12 @@ "description": "为 Matugen 选择配色方案生成算法。" } }, + "dark-mode": { + "switch": { + "label": "深色模式", + "description": "切换到更暗的主题,便于夜间观看。" + } + }, "predefined": { "section": { "label": "预定义配色方案", diff --git a/Modules/Settings/Tabs/ColorSchemeTab.qml b/Modules/Settings/Tabs/ColorSchemeTab.qml index 74205d9dd..fbe5a2967 100644 --- a/Modules/Settings/Tabs/ColorSchemeTab.qml +++ b/Modules/Settings/Tabs/ColorSchemeTab.qml @@ -156,8 +156,8 @@ ColumnLayout { // Dark Mode Toggle NToggle { - label: I18n.tr("settings.color-scheme.color-source.dark-mode.label") - description: I18n.tr("settings.color-scheme.color-source.dark-mode.description") + label: I18n.tr("settings.color-scheme.dark-mode.switch.label") + description: I18n.tr("settings.color-scheme.dark-mode.switch.description") checked: Settings.data.colorSchemes.darkMode enabled: true onToggled: checked => { @@ -167,17 +167,17 @@ ColumnLayout { } NComboBox { - label: "Dark Mode Schedule" - description: "Enables automatic switching between light and dark mode" + label: I18n.tr("settings.color-scheme.dark-mode.mode.label") + description: I18n.tr("settings.color-scheme.dark-mode.mode.description") model: [{ - "name": "Off", + "name": I18n.tr("settings.color-scheme.dark-mode.mode.off"), "key": "off" }, { - "name": "Manual", + "name": I18n.tr("settings.color-scheme.dark-mode.mode.manual"), "key": "manual" }, { - "name": "Sunrise/Sunset", + "name": I18n.tr("settings.color-scheme.dark-mode.mode.location"), "key": "location" }] From e57b565f800567ed27d63973faa92f90a2943369 Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Thu, 16 Oct 2025 17:48:11 +0200 Subject: [PATCH 4/4] DarkModeService: Update to new logging style. --- Services/DarkModeService.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Services/DarkModeService.qml b/Services/DarkModeService.qml index 308d393b8..ad3eef005 100644 --- a/Services/DarkModeService.qml +++ b/Services/DarkModeService.qml @@ -60,7 +60,7 @@ Singleton { } function init() { - Logger.log("DarkModeService", "Service started") + Logger.i("DarkModeService", "Service started") if (Settings.data.colorSchemes.schedulingMode == "manual") { const changes = collectManualChanges() @@ -152,7 +152,7 @@ Singleton { if (lastChange) { Settings.data.colorSchemes.darkMode = lastChange.darkMode - Logger.log("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`) + Logger.d("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`) } } @@ -163,7 +163,7 @@ Singleton { root.nextDarkModeState = nextChange.darkMode timer.interval = nextChange.time - now timer.restart() - Logger.log("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`) + Logger.d("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`) } } }