Merge pull request #482 from luleyleo/auto-night-mode

Implement scheduled light/dark mode switching
This commit is contained in:
Lysec
2025-10-18 21:05:44 +02:00
committed by GitHub
11 changed files with 316 additions and 28 deletions

View File

@@ -500,10 +500,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."
@@ -513,6 +509,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",

View File

@@ -500,10 +500,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."
@@ -513,6 +509,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",

View File

@@ -500,10 +500,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."
@@ -513,6 +509,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",

View File

@@ -500,10 +500,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."
@@ -513,6 +509,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",

View File

@@ -462,10 +462,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."
@@ -475,6 +471,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",

View File

@@ -500,10 +500,6 @@
"label": "颜色来源",
"description": "Noctalia 颜色的主要设置。"
},
"dark-mode": {
"label": "深色模式",
"description": "切换到更暗的主题,便于夜间观看。"
},
"use-wallpaper-colors": {
"label": "使用壁纸颜色",
"description": "使用 Matugen 从壁纸生成颜色方案。自动提取颜色以创建一致的主题。"
@@ -513,6 +509,12 @@
"description": "为 Matugen 选择配色方案生成算法。"
}
},
"dark-mode": {
"switch": {
"label": "深色模式",
"description": "切换到更暗的主题,便于夜间观看。"
}
},
"predefined": {
"section": {
"label": "预定义配色方案",

View File

@@ -357,6 +357,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
}

View File

@@ -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
@@ -138,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 => {
@@ -148,6 +166,77 @@ ColumnLayout {
}
}
NComboBox {
label: I18n.tr("settings.color-scheme.dark-mode.mode.label")
description: I18n.tr("settings.color-scheme.dark-mode.mode.description")
model: [{
"name": I18n.tr("settings.color-scheme.dark-mode.mode.off"),
"key": "off"
}, {
"name": I18n.tr("settings.color-scheme.dark-mode.mode.manual"),
"key": "manual"
}, {
"name": I18n.tr("settings.color-scheme.dark-mode.mode.location"),
"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")

View File

@@ -0,0 +1,169 @@
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
enabled: Settings.data.colorSchemes.schedulingMode == "location"
function onWeatherChanged() {
if (LocationService.data.weather !== null) {
const changes = root.collectWeatherChanges(LocationService.data.weather)
if (!root.initComplete) {
root.initComplete = true
root.applyCurrentMode(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.collectWeatherChanges(LocationService.data.weather)
root.scheduleNextMode(changes)
}
}
}
function init() {
Logger.i("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]),
"darkMode": false
})
changes.push({
"time": Date.parse(weather.daily.sunset[i]),
"darkMode": true
})
}
return changes
}
function applyCurrentMode(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.d("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`)
}
}
function scheduleNextMode(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.d("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`)
}
}
}

View File

@@ -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()
@@ -190,7 +190,7 @@ Singleton {
// --------------------------------
function _fetchWeather(latitude, longitude, errorCallback) {
Logger.d("Location", "Fetching weather from api.open-meteo.com")
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "&current_weather=true&current=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 + "&current_weather=true&current=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) {

View File

@@ -86,6 +86,7 @@ ShellRoot {
BarWidgetRegistry.init()
LocationService.init()
NightLightService.apply()
DarkModeService.init()
FontService.init()
HooksService.init()
BluetoothService.init()