From d5b50d58de1e8b87d00ec0808c5dd0cd1ddb6e64 Mon Sep 17 00:00:00 2001 From: euletheia <154215350+euletheia@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:48:46 +0200 Subject: [PATCH 1/4] feat(NightMode): add exceptions (fullscreen and custom app list) Drawing heavily on hthienloc@de3e8cffa073dcae2c66c40565ba12d1c1412340 (excluded media players), give the possibility to specify a list of applications excluded from NightMode, alongside the ability to exclude fullscreen applications. (Logic is implemented on newly focused windows, and integrates with compositors overview mode so that NightMode is resumed on overview.) + Update GammaControlTab main section to use a SettingsCard component. --- quickshell/Common/Paths.qml | 29 + quickshell/Common/SessionData.qml | 22 + quickshell/Common/SettingsData.qml | 49 + quickshell/Common/settings/SessionSpec.js | 2 + quickshell/Common/settings/SettingsSpec.js | 2 + .../Modals/Settings/SettingsContent.qml | 4 +- .../Modules/Settings/GammaControlTab.qml | 1129 ++++++++++------- quickshell/Services/DisplayService.qml | 79 +- .../translations/settings_search_index.json | 71 +- 9 files changed, 910 insertions(+), 477 deletions(-) diff --git a/quickshell/Common/Paths.qml b/quickshell/Common/Paths.qml index a0d690b68..bcebd00b0 100644 --- a/quickshell/Common/Paths.qml +++ b/quickshell/Common/Paths.qml @@ -150,4 +150,33 @@ Singleton { return desktopEntry && desktopEntry.name ? desktopEntry.name : appId; } + + function isAppIdMatch(id: string, target: string, desktopId: string): bool { + id = id ? String(id).toLowerCase().trim() : ""; + desktopId = desktopId ? String(desktopId).toLowerCase().trim() : ""; + target = target ? String(target).toLowerCase().trim() : ""; + if (!target) + return false; + + // 1. Substring match + if (id.includes(target) || desktopId.includes(target)) { + return true; + } + + // 2. Match reverse-DNS segments (e.g. app.zen_browser.zen -> zen) + if (target.indexOf(".") !== -1) { + const parts = target.split("."); + const lastPart = parts[parts.length - 1]; + if (lastPart && (id.includes(lastPart) || desktopId.includes(lastPart))) { + return true; + } + } + + // 3. Bidirectional match (longer excluded name contains shorter app id) + if (id.length >= 3 && target.includes(id)) { + return true; + } + + return false; + } } diff --git a/quickshell/Common/SessionData.qml b/quickshell/Common/SessionData.qml index 12fb3477f..fbdad8c65 100644 --- a/quickshell/Common/SessionData.qml +++ b/quickshell/Common/SessionData.qml @@ -163,6 +163,8 @@ Singleton { property real longitude: 0.0 property bool nightModeUseIPLocation: false property string nightModeLocationProvider: "" + property bool nightModePaused: false + property var nightModeExcludedAppsMatchesCache: [] property bool themeModeAutoEnabled: false property string themeModeAutoMode: "time" @@ -795,6 +797,26 @@ Singleton { saveSettings(); } + function cacheNightModeExcludedAppMatch(app: string) { + app = app ? String(app).toLowerCase().trim() : ""; + var moddedList = nightModeExcludedAppsMatchesCache ? nightModeExcludedAppsMatchesCache.slice() : []; + if (!moddedList.includes(app)) { + moddedList.push(app); + nightModeExcludedAppsMatchesCache = moddedList; + saveSettings(); + } + } + + function resetNightModeExcludedAppsMatchesCache() { + nightModeExcludedAppsMatchesCache = []; + saveSettings(); + } + + function setNightModePaused(paused: bool) { + nightModePaused = paused; + saveSettings(); + } + function setNightModeEnabled(enabled) { nightModeEnabled = enabled; saveSettings(); diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 60f85149c..044128881 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -196,6 +196,8 @@ Singleton { property bool useFahrenheit: false property string windSpeedUnit: "kmh" property bool nightModeEnabled: false + property bool nightModeExcludeFullscreen: false + property var nightModeExcludedApps: [] property int animationSpeed: SettingsData.AnimationSpeed.Short property int customAnimationDuration: 500 property bool syncComponentAnimationSpeeds: true @@ -3165,7 +3167,54 @@ Singleton { if (identity === undefined || identity === null) return; var normalizedIdentity = identity.toString().trim().toLowerCase(); + function setNightModeExcludeFullscreen(enabled) { + nightModeExcludeFullscreen = enabled; + saveSettings(); + } + + function normalizeAppId(id: string): string { + return (id && id.toString().toLowerCase().trim()) || ""; + } + + function addAppIdToList(identity: string, appList: list): list { + identity = identity ?? ""; + appList = appList ?? []; + if (!identity) + return appList; + + var normalizedIdentity = normalizeAppId(identity); if (!normalizedIdentity) + return appList; + + var cleanList = appList.map(id => id ? normalizeAppId(id) : "").filter(id => id !== ""); + if (cleanList.includes(normalizedIdentity)) + return cleanList; + + cleanList.push(normalizedIdentity); + return cleanList; + } + + function removeAppIdFromList(index: int, appList: list): list { + var moddedList = appList ? appList.slice() : []; + if (index < 0 || index >= moddedList.length) + return moddedList; + moddedList.splice(index, 1); + return moddedList; + } + + function addNightModeExcludedApp(identity: string) { + var newList = addAppIdToList(identity, nightModeExcludedApps); + nightModeExcludedApps = newList; + SessionData.resetNightModeExcludedAppsMatchesCache(); + saveSettings(); + } + + function removeNightModeExcludedApp(index: int) { + var newList = removeAppIdFromList(index, nightModeExcludedApps); + nightModeExcludedApps = newList; + SessionData.resetNightModeExcludedAppsMatchesCache(); + saveSettings(); + } return; var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; var normalizedList = list.map(function (id) { diff --git a/quickshell/Common/settings/SessionSpec.js b/quickshell/Common/settings/SessionSpec.js index c11bcf788..78fcb0ba3 100644 --- a/quickshell/Common/settings/SessionSpec.js +++ b/quickshell/Common/settings/SessionSpec.js @@ -37,6 +37,8 @@ var SPEC = { longitude: { def: 0.0 }, nightModeUseIPLocation: { def: false }, nightModeLocationProvider: { def: "" }, + nightModePaused: { def: "" }, + nightModeExcludedAppsMatchesCache: { def: [] }, themeModeAutoEnabled: { def: false }, themeModeAutoMode: { def: "time" }, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index a53bc6b49..ec5817b78 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -48,6 +48,8 @@ var SPEC = { useFahrenheit: { def: false }, windSpeedUnit: { def: "kmh" }, nightModeEnabled: { def: false }, + nightModeExcludeFullscreen: { def: false }, + nightModeExcludedApps: { def: [] }, animationSpeed: { def: 1 }, customAnimationDuration: { def: 500 }, syncComponentAnimationSpeeds: { def: true }, diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index 4de081855..7ac71bae0 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -203,7 +203,9 @@ FocusScope { visible: active focus: active - sourceComponent: GammaControlTab {} + sourceComponent: GammaControlTab { + parentModal: root.parentModal + } onActiveChanged: { if (active && item) diff --git a/quickshell/Modules/Settings/GammaControlTab.qml b/quickshell/Modules/Settings/GammaControlTab.qml index c6b2f6bc8..10ec74924 100644 --- a/quickshell/Modules/Settings/GammaControlTab.qml +++ b/quickshell/Modules/Settings/GammaControlTab.qml @@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets Item { id: root + property var desktopApps: [] + property var parentModal: root + function formatGammaTime(isoString) { if (!isoString) return ""; @@ -20,6 +23,48 @@ Item { } } + Component.onCompleted: { + desktopApps = AppSearchService.getVisibleApplications() || []; + } + + Component.onDestruction: { + desktopApps = []; + } + + Loader { + id: popupLoader + active: false + sourceComponent: AppBrowserPopup { + id: appBrowserPopup + appsModel: root.desktopApps + parentModal: root.parentModal + onAppSelected: appId => { + var name = appId; + if (name.endsWith(".desktop")) { + name = name.slice(0, -8); + } + SettingsData.addNightModeExcludedApp(name); + } + } + + function recreatePopup() { + log.debug("Recreating popup"); + popupLoader.active = false; + popupLoader.active = true; + } + + function showPopup() { + if (!popupLoader.item) { + popupLoader.active = true; + } + popupLoader.item.show(); + if (!popupLoader.item.visible) { + recreatePopup(); + popupLoader.item.show(); + } + } + } + DankFlickable { anchors.fill: parent clip: true @@ -34,628 +79,766 @@ Item { anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXL - StyledRect { + SettingsCard { width: parent.width - height: gammaSection.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - border.color: Theme.outlineHeavy - border.width: 0 - - Column { - id: gammaSection - - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - Row { - width: parent.width - spacing: Theme.spacingM + iconName: "brightness_6" + title: I18n.tr("Gamma Control") + settingKey: "gammaControl" + + DankToggle { + id: nightModeToggle + + width: parent.width + text: I18n.tr("Night Mode") + description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr("Gamma control not available. Requires DMS API v6+.") + checked: DisplayService.nightModeEnabled + enabled: DisplayService.gammaControlAvailable + onToggled: checked => { + DisplayService.toggleNightMode(); + } - DankIcon { - name: "brightness_6" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter + Connections { + function onNightModeEnabledChanged() { + nightModeToggle.checked = DisplayService.nightModeEnabled; } - StyledText { - text: I18n.tr("Gamma Control") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } + target: DisplayService } + } - DankToggle { - id: nightModeToggle - - width: parent.width - text: I18n.tr("Night Mode") - description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr("Gamma control not available. Requires DMS API v6+.") - checked: DisplayService.nightModeEnabled - enabled: DisplayService.gammaControlAvailable - onToggled: checked => { - DisplayService.toggleNightMode(); + Column { + width: parent.width + spacing: Theme.spacingS + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + visible: DisplayService.gammaControlAvailable + + SettingsSliderRow { + id: nightTempSlider + settingKey: "nightModeTemperature" + tags: ["gamma", "night", "temperature", "kelvin", "warm", "color", "blue light"] + width: parent.width - parent.leftPadding - parent.rightPadding + text: SessionData.nightModeAutoEnabled ? I18n.tr("Night Temperature") : I18n.tr("Color Temperature") + description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply") + minimum: 2500 + maximum: 6000 + step: 100 + unit: "K" + value: SessionData.nightModeTemperature + onSliderValueChanged: newValue => { + SessionData.setNightModeTemperature(newValue); + if (SessionData.nightModeHighTemperature < newValue) + SessionData.setNightModeHighTemperature(newValue); } + } - Connections { - function onNightModeEnabledChanged() { - nightModeToggle.checked = DisplayService.nightModeEnabled; - } + SettingsSliderRow { + id: dayTempSlider + settingKey: "nightModeHighTemperature" + tags: ["gamma", "day", "temperature", "kelvin", "color"] + width: parent.width - parent.leftPadding - parent.rightPadding + text: I18n.tr("Day Temperature") + description: I18n.tr("Color temperature for day time") + minimum: SessionData.nightModeTemperature + maximum: 10000 + step: 100 + unit: "K" + value: Math.max(SessionData.nightModeHighTemperature, SessionData.nightModeTemperature) + visible: SessionData.nightModeAutoEnabled + onSliderValueChanged: newValue => SessionData.setNightModeHighTemperature(newValue) + } + } - target: DisplayService + DankToggle { + id: automaticToggle + width: parent.width + text: I18n.tr("Automatic Control") + description: I18n.tr("Only adjust gamma based on time or location rules.") + checked: SessionData.nightModeAutoEnabled + visible: DisplayService.gammaControlAvailable + onToggled: checked => { + if (checked && !DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode(); + } else if (!checked && DisplayService.nightModeEnabled) { + DisplayService.toggleNightMode(); } + SessionData.setNightModeAutoEnabled(checked); } - Column { - width: parent.width - spacing: Theme.spacingS - leftPadding: Theme.spacingM - rightPadding: Theme.spacingM - visible: DisplayService.gammaControlAvailable - - SettingsSliderRow { - id: nightTempSlider - settingKey: "nightModeTemperature" - tags: ["gamma", "night", "temperature", "kelvin", "warm", "color", "blue light"] - width: parent.width - parent.leftPadding - parent.rightPadding - text: SessionData.nightModeAutoEnabled ? I18n.tr("Night Temperature") : I18n.tr("Color Temperature") - description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply") - minimum: 2500 - maximum: 6000 - step: 100 - unit: "K" - value: SessionData.nightModeTemperature - onSliderValueChanged: newValue => { - SessionData.setNightModeTemperature(newValue); - if (SessionData.nightModeHighTemperature < newValue) - SessionData.setNightModeHighTemperature(newValue); - } + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticToggle.checked = SessionData.nightModeAutoEnabled; } + } + } - SettingsSliderRow { - id: dayTempSlider - settingKey: "nightModeHighTemperature" - tags: ["gamma", "day", "temperature", "kelvin", "color"] - width: parent.width - parent.leftPadding - parent.rightPadding - text: I18n.tr("Day Temperature") - description: I18n.tr("Color temperature for day time") - minimum: SessionData.nightModeTemperature - maximum: 10000 - step: 100 - unit: "K" - value: Math.max(SessionData.nightModeHighTemperature, SessionData.nightModeTemperature) - visible: SessionData.nightModeAutoEnabled - onSliderValueChanged: newValue => SessionData.setNightModeHighTemperature(newValue) + Column { + id: automaticSettings + width: parent.width + spacing: Theme.spacingS + visible: SessionData.nightModeAutoEnabled && DisplayService.gammaControlAvailable + + Connections { + target: SessionData + function onNightModeAutoEnabledChanged() { + automaticSettings.visible = SessionData.nightModeAutoEnabled; } } - DankToggle { - id: automaticToggle + Item { width: parent.width - text: I18n.tr("Automatic Control") - description: I18n.tr("Only adjust gamma based on time or location rules.") - checked: SessionData.nightModeAutoEnabled - visible: DisplayService.gammaControlAvailable - onToggled: checked => { - if (checked && !DisplayService.nightModeEnabled) { - DisplayService.toggleNightMode(); - } else if (!checked && DisplayService.nightModeEnabled) { - DisplayService.toggleNightMode(); + height: 45 + Theme.spacingM + + DankTabBar { + id: modeTabBarNight + width: 200 + height: 45 + anchors.horizontalCenter: parent.horizontalCenter + model: [ + { + "text": I18n.tr("Time"), + "icon": "access_time" + }, + { + "text": I18n.tr("Location"), + "icon": "place" + } + ] + + Component.onCompleted: { + currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; + Qt.callLater(updateIndicator); + } + + onTabClicked: index => { + DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time"); + currentIndex = index; } - SessionData.setNightModeAutoEnabled(checked); - } - Connections { - target: SessionData - function onNightModeAutoEnabledChanged() { - automaticToggle.checked = SessionData.nightModeAutoEnabled; + Connections { + target: SessionData + function onNightModeAutoModeChanged() { + modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; + Qt.callLater(modeTabBarNight.updateIndicator); + } } } } Column { - id: automaticSettings width: parent.width - spacing: Theme.spacingS - visible: SessionData.nightModeAutoEnabled && DisplayService.gammaControlAvailable + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "time" - Connections { - target: SessionData - function onNightModeAutoEnabledChanged() { - automaticSettings.visible = SessionData.nightModeAutoEnabled; - } - } + Column { + spacing: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter - Item { - width: parent.width - height: 45 + Theme.spacingM - - DankTabBar { - id: modeTabBarNight - width: 200 - height: 45 - anchors.horizontalCenter: parent.horizontalCenter - model: [ - { - "text": I18n.tr("Time"), - "icon": "access_time" - }, - { - "text": I18n.tr("Location"), - "icon": "place" - } - ] + Row { + spacing: Theme.spacingM - Component.onCompleted: { - currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; - Qt.callLater(updateIndicator); + StyledText { + text: "" + width: 50 + height: 20 } - onTabClicked: index => { - DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time"); - currentIndex = index; + StyledText { + text: I18n.tr("Hour") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter } - Connections { - target: SessionData - function onNightModeAutoModeChanged() { - modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; - Qt.callLater(modeTabBarNight.updateIndicator); - } + StyledText { + text: I18n.tr("Minute") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 70 + horizontalAlignment: Text.AlignHCenter } } - } - - Column { - width: parent.width - spacing: Theme.spacingM - visible: SessionData.nightModeAutoMode === "time" - - Column { - spacing: Theme.spacingXS - anchors.horizontalCenter: parent.horizontalCenter - Row { - spacing: Theme.spacingM + Row { + spacing: Theme.spacingM - StyledText { - text: "" - width: 50 - height: 20 - } + StyledText { + text: I18n.tr("Start") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } - StyledText { - text: I18n.tr("Hour") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: 70 - horizontalAlignment: Text.AlignHCenter + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeStartHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) { + hours.push(i.toString()); + } + return hours; } - - StyledText { - text: I18n.tr("Minute") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: 70 - horizontalAlignment: Text.AlignHCenter + onValueChanged: value => { + SessionData.setNightModeStartHour(parseInt(value)); } } - Row { - spacing: Theme.spacingM - - StyledText { - text: I18n.tr("Start") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - width: 50 - height: 40 - verticalAlignment: Text.AlignVCenter - } - - DankDropdown { - dropdownWidth: 70 - currentValue: SessionData.nightModeStartHour.toString() - options: { - var hours = []; - for (var i = 0; i < 24; i++) { - hours.push(i.toString()); - } - return hours; - } - onValueChanged: value => { - SessionData.setNightModeStartHour(parseInt(value)); + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); } + return minutes; } - - DankDropdown { - dropdownWidth: 70 - currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') - options: { - var minutes = []; - for (var i = 0; i < 60; i += 5) { - minutes.push(i.toString().padStart(2, '0')); - } - return minutes; - } - onValueChanged: value => { - SessionData.setNightModeStartMinute(parseInt(value)); - } + onValueChanged: value => { + SessionData.setNightModeStartMinute(parseInt(value)); } } + } - Row { - spacing: Theme.spacingM + Row { + spacing: Theme.spacingM - StyledText { - text: I18n.tr("End") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - width: 50 - height: 40 - verticalAlignment: Text.AlignVCenter - } + StyledText { + text: I18n.tr("End") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + width: 50 + height: 40 + verticalAlignment: Text.AlignVCenter + } - DankDropdown { - dropdownWidth: 70 - currentValue: SessionData.nightModeEndHour.toString() - options: { - var hours = []; - for (var i = 0; i < 24; i++) { - hours.push(i.toString()); - } - return hours; - } - onValueChanged: value => { - SessionData.setNightModeEndHour(parseInt(value)); + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeEndHour.toString() + options: { + var hours = []; + for (var i = 0; i < 24; i++) { + hours.push(i.toString()); } + return hours; + } + onValueChanged: value => { + SessionData.setNightModeEndHour(parseInt(value)); } + } - DankDropdown { - dropdownWidth: 70 - currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') - options: { - var minutes = []; - for (var i = 0; i < 60; i += 5) { - minutes.push(i.toString().padStart(2, '0')); - } - return minutes; - } - onValueChanged: value => { - SessionData.setNightModeEndMinute(parseInt(value)); + DankDropdown { + dropdownWidth: 70 + currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') + options: { + var minutes = []; + for (var i = 0; i < 60; i += 5) { + minutes.push(i.toString().padStart(2, '0')); } + return minutes; + } + onValueChanged: value => { + SessionData.setNightModeEndMinute(parseInt(value)); } } } } + } - Column { - property bool isLocationMode: SessionData.nightModeAutoMode === "location" - visible: isLocationMode - spacing: Theme.spacingM - width: parent.width + Column { + property bool isLocationMode: SessionData.nightModeAutoMode === "location" + visible: isLocationMode + spacing: Theme.spacingM + width: parent.width - DankToggle { - id: ipLocationToggle - width: parent.width - text: I18n.tr("Use IP Location") - description: I18n.tr("Automatically detect location based on IP address") - checked: SessionData.nightModeUseIPLocation || false - onToggled: checked => { - SessionData.setNightModeUseIPLocation(checked); - } + DankToggle { + id: ipLocationToggle + width: parent.width + text: I18n.tr("Use IP Location") + description: I18n.tr("Automatically detect location based on IP address") + checked: SessionData.nightModeUseIPLocation || false + onToggled: checked => { + SessionData.setNightModeUseIPLocation(checked); + } - Connections { - target: SessionData - function onNightModeUseIPLocationChanged() { - ipLocationToggle.checked = SessionData.nightModeUseIPLocation; - } + Connections { + target: SessionData + function onNightModeUseIPLocationChanged() { + ipLocationToggle.checked = SessionData.nightModeUseIPLocation; } } + } - Column { - width: parent.width - spacing: Theme.spacingM - leftPadding: Theme.spacingM - visible: !SessionData.nightModeUseIPLocation + Column { + width: parent.width + spacing: Theme.spacingM + leftPadding: Theme.spacingM + visible: !SessionData.nightModeUseIPLocation - StyledText { - text: I18n.tr("Manual Coordinates") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - } + StyledText { + text: I18n.tr("Manual Coordinates") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + } - Row { - spacing: Theme.spacingL + Row { + spacing: Theme.spacingL - Column { - spacing: Theme.spacingXS + Column { + spacing: Theme.spacingXS - StyledText { - text: I18n.tr("Latitude") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } + StyledText { + text: I18n.tr("Latitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } - DankTextField { - width: 120 - height: 40 - text: SessionData.latitude.toString() - placeholderText: "0.0" - onEditingFinished: { - const lat = parseFloat(text); - if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) { - SessionData.setLatitude(lat); - } + DankTextField { + width: 120 + height: 40 + text: SessionData.latitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lat = parseFloat(text); + if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) { + SessionData.setLatitude(lat); } } } + } - Column { - spacing: Theme.spacingXS + Column { + spacing: Theme.spacingXS - StyledText { - text: I18n.tr("Longitude") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } + StyledText { + text: I18n.tr("Longitude") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } - DankTextField { - width: 120 - height: 40 - text: SessionData.longitude.toString() - placeholderText: "0.0" - onEditingFinished: { - const lon = parseFloat(text); - if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) { - SessionData.setLongitude(lon); - } + DankTextField { + width: 120 + height: 40 + text: SessionData.longitude.toString() + placeholderText: "0.0" + onEditingFinished: { + const lon = parseFloat(text); + if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) { + SessionData.setLongitude(lon); } } } } + } - StyledText { - text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - parent.leftPadding - wrapMode: Text.WordWrap - } + StyledText { + text: I18n.tr("Uses sunrise/sunset times to automatically adjust night mode based on your location.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width - parent.leftPadding + wrapMode: Text.WordWrap } } + } - Rectangle { + Rectangle { + width: parent.width + height: 1 + color: Theme.outline + opacity: 0.2 + visible: gammaStatusSection.visible + } + + Column { + id: gammaStatusSection + width: parent.width + spacing: Theme.spacingM + visible: DisplayService.nightModeEnabled && DisplayService.gammaCurrentTemp > 0 + + Row { width: parent.width - height: 1 - color: Theme.outline - opacity: 0.2 - visible: gammaStatusSection.visible + spacing: Theme.spacingS + + DankIcon { + name: DisplayService.gammaIsDay ? "light_mode" : "dark_mode" + size: Theme.iconSizeSmall + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Current Status") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } } - Column { - id: gammaStatusSection + Row { width: parent.width spacing: Theme.spacingM - visible: DisplayService.nightModeEnabled && DisplayService.gammaCurrentTemp > 0 - Row { - width: parent.width - spacing: Theme.spacingS + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: tempColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh - DankIcon { - name: DisplayService.gammaIsDay ? "light_mode" : "dark_mode" - size: Theme.iconSizeSmall - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter + Column { + id: tempColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: "device_thermostat" + size: Theme.iconSize + color: Theme.primary + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: DisplayService.gammaCurrentTemp + "K" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Current Temp") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } } + } - StyledText { - text: I18n.tr("Current Status") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: periodColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + + Column { + id: periodColumn + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: DisplayService.gammaIsDay ? "wb_sunny" : "nightlight" + size: Theme.iconSize + color: DisplayService.gammaIsDay ? "#FFA726" : "#7E57C2" + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: DisplayService.gammaIsDay ? I18n.tr("Daytime") : I18n.tr("Night") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("Current Period") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } } } + } - Row { - width: parent.width - spacing: Theme.spacingM + Row { + width: parent.width + spacing: Theme.spacingM + visible: SessionData.nightModeAutoMode === "location" && (DisplayService.gammaSunriseTime || DisplayService.gammaSunsetTime) - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: tempColumn.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: sunriseColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaSunriseTime - Column { - id: tempColumn - anchors.centerIn: parent - spacing: Theme.spacingXS + Column { + id: sunriseColumn + anchors.centerIn: parent + spacing: Theme.spacingXS - DankIcon { - name: "device_thermostat" - size: Theme.iconSize - color: Theme.primary - anchors.horizontalCenter: parent.horizontalCenter - } + DankIcon { + name: "wb_twilight" + size: Theme.iconSize + color: "#FF7043" + anchors.horizontalCenter: parent.horizontalCenter + } - StyledText { - text: DisplayService.gammaCurrentTemp + "K" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } + StyledText { + text: root.formatGammaTime(DisplayService.gammaSunriseTime) + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } - StyledText { - text: I18n.tr("Current Temp") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } + StyledText { + text: I18n.tr("Sunrise") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter } } + } - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: periodColumn.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: sunsetColumn.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaSunsetTime - Column { - id: periodColumn - anchors.centerIn: parent - spacing: Theme.spacingXS + Column { + id: sunsetColumn + anchors.centerIn: parent + spacing: Theme.spacingXS - DankIcon { - name: DisplayService.gammaIsDay ? "wb_sunny" : "nightlight" - size: Theme.iconSize - color: DisplayService.gammaIsDay ? "#FFA726" : "#7E57C2" - anchors.horizontalCenter: parent.horizontalCenter - } + DankIcon { + name: "wb_twilight" + size: Theme.iconSize + color: "#5C6BC0" + anchors.horizontalCenter: parent.horizontalCenter + } - StyledText { - text: DisplayService.gammaIsDay ? I18n.tr("Daytime") : I18n.tr("Night") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } + StyledText { + text: root.formatGammaTime(DisplayService.gammaSunsetTime) + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } - StyledText { - text: I18n.tr("Current Period") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } + StyledText { + text: I18n.tr("Sunset") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter } } } + } + + Rectangle { + width: parent.width + height: nextChangeRow.implicitHeight + Theme.spacingM * 2 + radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh + visible: DisplayService.gammaNextTransition Row { - width: parent.width + id: nextChangeRow + anchors.centerIn: parent spacing: Theme.spacingM - visible: SessionData.nightModeAutoMode === "location" && (DisplayService.gammaSunriseTime || DisplayService.gammaSunsetTime) - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: sunriseColumn.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - visible: DisplayService.gammaSunriseTime + DankIcon { + name: "schedule" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("Next Transition") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } Column { - id: sunriseColumn - anchors.centerIn: parent - spacing: Theme.spacingXS + spacing: Theme.spacingXXS + anchors.verticalCenter: parent.verticalCenter - DankIcon { - name: "wb_twilight" - size: Theme.iconSize - color: "#FF7043" - anchors.horizontalCenter: parent.horizontalCenter + StyledText { + text: I18n.tr("Next Transition") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText } StyledText { - text: root.formatGammaTime(DisplayService.gammaSunriseTime) - font.pixelSize: Theme.fontSizeLarge + text: root.formatGammaTime(DisplayService.gammaNextTransition) + font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("Sunrise") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter } } } + } + } + } + } + } - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: sunsetColumn.implicitHeight + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - visible: DisplayService.gammaSunsetTime + SettingsCard { + width: parent.width + iconName: "settings_night_sight" + title: I18n.tr("Night Mode Exceptions") + settingKey: "nightModeExceptions" + readonly property var log: Log.scoped("NightModeExceptions") + + SettingsToggleRow { + settingKey: "nightModeExcludeFullscreen" + tags: ["gamma", "night", "mode", "fullscreen"] + text: I18n.tr("Fullscreen Applications") + description: I18n.tr("Disable night mode when using fullscreen applications.") + checked: SettingsData.nightModeExcludeFullscreen + onToggled: checked => { + SettingsData.set("nightModeExcludeFullscreen", checked); + } + } - Column { - id: sunsetColumn - anchors.centerIn: parent - spacing: Theme.spacingXS + Column { + width: parent.width + spacing: Theme.spacingS + leftPadding: Theme.spacingM + rightPadding: Theme.spacingM + visible: DisplayService.gammaControlAvailable + + StyledText { + text: I18n.tr("Excluded Applications") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + topPadding: Theme.spacingM + width: parent.width + } - DankIcon { - name: "wb_twilight" - size: Theme.iconSize - color: "#5C6BC0" - anchors.horizontalCenter: parent.horizontalCenter - } + StyledText { + text: I18n.tr("Disable night mode when focusing on specific applications (media player, editor, game, ...)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.Wrap + width: parent.width + lineHeight: 1.2 + } - StyledText { - text: root.formatGammaTime(DisplayService.gammaSunsetTime) - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } + Row { + width: parent.width + spacing: Theme.spacingS - StyledText { - text: I18n.tr("Sunset") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.horizontalCenter: parent.horizontalCenter - } - } + DankTextField { + id: newNightModeExcludedAppField + width: parent.width - addBtn.width - selectAppBtn.width - Theme.spacingS * 2 - Theme.spacingM * 2 + height: 36 + placeholderText: I18n.tr("App name or identity (e.g., GIMP)") + font.pixelSize: Theme.fontSizeSmall + onAccepted: { + const cleanInput = text.trim(); + if (cleanInput !== "") { + SettingsData.addNightModeExcludedApp(cleanInput); } + text = ""; } + } - Rectangle { - width: parent.width - height: nextChangeRow.implicitHeight + Theme.spacingM * 2 + DankActionButton { + id: addBtn + buttonSize: 36 + iconName: "add" + iconSize: 20 + backgroundColor: Theme.primary + iconColor: Theme.onPrimary + onClicked: { + const cleanInput = newNightModeExcludedAppField.text.trim(); + if (cleanInput !== "") { + SettingsData.addNightModeExcludedApp(cleanInput); + } + newNightModeExcludedAppField.text = ""; + } + } + + DankActionButton { + id: selectAppBtn + buttonSize: 36 + iconName: "apps" + iconSize: 20 + backgroundColor: Theme.surfaceContainer + iconColor: Theme.primary + onClicked: popupLoader.showPopup() + } + } + + Column { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: SettingsData.nightModeExcludedApps + + delegate: Rectangle { + width: parent.width - Theme.spacingM * 2 + // width: parent.width + height: 48 radius: Theme.cornerRadius - color: Theme.surfaceContainerHigh - visible: DisplayService.gammaNextTransition + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) Row { - id: nextChangeRow - anchors.centerIn: parent + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM spacing: Theme.spacingM - DankIcon { - name: "schedule" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: Theme.spacingXXS - anchors.verticalCenter: parent.verticalCenter + Row { + width: parent.width - deleteBtn.width - Theme.spacingXS + height: parent.height + spacing: Theme.spacingS - StyledText { - text: I18n.tr("Next Transition") - font.pixelSize: Theme.fontSizeSmall + DankIcon { + name: "bedtime_off" + size: 20 color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter } StyledText { - text: root.formatGammaTime(DisplayService.gammaNextTransition) - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium + text: modelData + font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter } } + + DankActionButton { + id: deleteBtn + buttonSize: 32 + iconName: "delete" + iconSize: 18 + iconColor: Theme.error + backgroundColor: "transparent" + anchors.verticalCenter: parent.verticalCenter + onClicked: SettingsData.removeNightModeExcludedApp(index) + } } } } } + + StyledText { + visible: !SettingsData.nightModeExcludedApps || SettingsData.nightModeExcludedApps.length === 0 + text: I18n.tr("No excluded application configured") + font.pixelSize: Theme.fontSizeSmall + font.italic: true + color: Theme.surfaceVariantText + horizontalAlignment: Text.AlignHCenter + width: parent.width + topPadding: Theme.spacingS + } } } } diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index c7c1d6569..f8353d170 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.Wayland import Quickshell.Io import qs.Common import qs.Services +import qs.Modules.WorkspaceOverlays Singleton { id: root @@ -40,6 +42,11 @@ Singleton { property bool nightModeActive: nightModeEnabled property bool nightModeEnabled: false + property bool nightModePaused: false + property var nightModeExcludedAppsMatchesCache: [] + + property var hyprlandOverviewLoader: null + property bool automationAvailable: false property bool gammaControlAvailable: false property int resumeRecoveryAttempt: 0 @@ -508,6 +515,62 @@ Singleton { } } + function pauseNightMode() { + disableNightMode(); + nightModePaused = true; + SessionData.setNightModePaused(true); + } + function resumeNightMode() { + enableNightMode(); + nightModePaused = false; + SessionData.setNightModePaused(false); + } + + function isNightModeExcludedApp(appId: string): bool { + const excludedApps = SettingsData.nightModeExcludedApps || ""; + if (excludedApps.length === 0) { + return false; + } + + const exclusionCache = nightModeExcludedAppsMatchesCache; + if (exclusionCache.includes(appId)) { + return true; + } + + const moddedId = Paths.moddedAppId(appId); + const desktopId = DesktopEntries.heuristicLookup(moddedId)?.id ?? ""; + const isExcludedAppException = excludedApps.some(excludedId => Paths.isAppIdMatch(appId, excludedId, desktopId)); + if (isExcludedAppException) { + SessionData.cacheNightModeExcludedAppMatch(appId); + } + return isExcludedAppException; + } + + function handleNightModeExceptions(topLevel) { + if (!nightModePaused && !nightModeEnabled) { + return; + } + + if (nightModePaused) { + const isInOverview = (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item?.overviewOpen) || (CompositorService.isNiri && NiriService.inOverview) || (CompositorService.isMango && MangoService.inOverview); + if (isInOverview) { + resumeNightMode(); + return; + } + } + + const activeApp = ToplevelManager.activeToplevel; + if (activeApp) { + const isFullscreenExcluded = SettingsData.nightModeExcludeFullscreen || false; + const shouldPause = (isFullscreenExcluded && activeApp.fullscreen) || isNightModeExcludedApp(activeApp.appId); + if (shouldPause && !nightModePaused && nightModeEnabled) { + pauseNightMode(); + } else if (!shouldPause && nightModePaused) { + resumeNightMode(); + } + } + } + function applyNightModeDirectly() { const temperature = SessionData.nightModeTemperature || 4000; @@ -802,6 +865,7 @@ Singleton { Component.onCompleted: { nightModeEnabled = SessionData.nightModeEnabled; + nightModePaused = SessionData.nightModePaused; deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues); if (DMSService.isConnected) { checkGammaControlAvailability(); @@ -880,7 +944,6 @@ Singleton { } } - // Session Data Connections Connections { target: SessionData @@ -922,6 +985,20 @@ Singleton { function onNightModeUseIPLocationChanged() { evaluateNightMode(); } + function onNightModePausedChanged() { + nightModePaused = SessionData.nightModePaused; + } + function onNightModeExcludedAppsMatchesCacheChanged() { + nightModeExcludedAppsMatchesCache = SessionData.nightModeExcludedAppsMatchesCache; + } + } + + Connections { + target: CompositorService + + function onToplevelsChanged() { + root.handleNightModeExceptions(); + } } // IPC Handler for external control diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index b14197437..cc7e7d73a 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -7496,23 +7496,90 @@ "description": "Color temperature for day time" }, { - "section": "_tab_25", + "section": "nightModeExcludeFullscreen", + "label": "Fullscreen Applications", + "tabIndex": 25, + "category": "Displays", + "keywords": [ + "app", + "applications", + "apps", + "disable", + "displays", + "fullscreen", + "gamma", + "mode", + "monitor", + "night", + "program", + "programs", + "resolution", + "screen" + ], + "description": "Disable night mode when using fullscreen applications." + }, + { + "section": "gammaControl", "label": "Gamma Control", "tabIndex": 25, "category": "Displays", "keywords": [ + "activates", + "apply", + "automation", + "below", "blue light", + "celsius", + "color", "color temperature", + "colour", "control", "displays", + "fahrenheit", "gamma", + "hue", + "kelvin", "monitor", + "night", "night light", "redshift", + "reduce", + "resolution", + "screen", + "settings", + "strain", + "temp", + "temperature", + "tint", + "warm" + ], + "icon": "brightness_6", + "description": "Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates." + }, + { + "section": "nightModeExceptions", + "label": "Night Mode Exceptions", + "tabIndex": 25, + "category": "Displays", + "keywords": [ + "app", + "applications", + "apps", + "disable", + "displays", + "exceptions", + "fullscreen", + "gamma", + "mode", + "monitor", + "night", + "program", + "programs", "resolution", "screen" ], - "icon": "brightness_6" + "icon": "settings_night_sight", + "description": "Disable night mode when using fullscreen applications." }, { "section": "nightModeTemperature", From bae0f1ff24261f59cd13807b54a1592a73b01785 Mon Sep 17 00:00:00 2001 From: euletheia <154215350+euletheia@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:20:16 +0200 Subject: [PATCH 2/4] refactor(NightMode): remove unused DisplayService property nightModeActive --- quickshell/Services/DisplayService.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/quickshell/Services/DisplayService.qml b/quickshell/Services/DisplayService.qml index f8353d170..4231d4329 100644 --- a/quickshell/Services/DisplayService.qml +++ b/quickshell/Services/DisplayService.qml @@ -39,8 +39,6 @@ Singleton { signal brightnessChanged(bool showOsd) signal deviceSwitched - property bool nightModeActive: nightModeEnabled - property bool nightModeEnabled: false property bool nightModePaused: false property var nightModeExcludedAppsMatchesCache: [] From 6482e8afb2dcea21fec91bc07e1b187938c090c9 Mon Sep 17 00:00:00 2001 From: euletheia <154215350+euletheia@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:56:00 +0200 Subject: [PATCH 3/4] refactor(MediaPlayerSettings): integrate changes done on NightMode exceptions - Use the appId matching function abstracted to Paths.qml. - Use common appId normalization functions to append/remove from settings list. - Align with GammaControlTab app list input fields to fix bug where white spaces only input would prevent field clearing on accept. - Align with GammaControlTab and use a Loader for AppBrowserPopup so that it can be recreated in case the popup is not closed with the dedicated button (i.e., via the compositor). --- quickshell/Common/SettingsData.qml | 24 ++----- .../Modules/Settings/MediaPlayerTab.qml | 63 +++++++++++++------ quickshell/Services/MprisController.qml | 28 +-------- 3 files changed, 51 insertions(+), 64 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 044128881..4dfa7515e 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -3163,10 +3163,6 @@ Singleton { saveSettings(); } - function addMediaExcludePlayer(identity) { - if (identity === undefined || identity === null) - return; - var normalizedIdentity = identity.toString().trim().toLowerCase(); function setNightModeExcludeFullscreen(enabled) { nightModeExcludeFullscreen = enabled; saveSettings(); @@ -3215,24 +3211,16 @@ Singleton { SessionData.resetNightModeExcludedAppsMatchesCache(); saveSettings(); } - return; - var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; - var normalizedList = list.map(function (id) { - return id ? id.toString().trim().toLowerCase() : ""; - }); - if (normalizedList.indexOf(normalizedIdentity) >= 0) - return; - list.push(normalizedIdentity); - mediaExcludePlayers = list; + + function addMediaExcludePlayer(identity) { + var newList = addAppIdToList(identity, mediaExcludePlayers); + mediaExcludePlayers = newList; saveSettings(); } function removeMediaExcludePlayer(index) { - var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : []; - if (index < 0 || index >= list.length) - return; - list.splice(index, 1); - mediaExcludePlayers = list; + var newList = removeAppIdFromList(index, mediaExcludePlayers); + mediaExcludePlayers = newList; saveSettings(); } diff --git a/quickshell/Modules/Settings/MediaPlayerTab.qml b/quickshell/Modules/Settings/MediaPlayerTab.qml index 402739f6c..be1c74c02 100644 --- a/quickshell/Modules/Settings/MediaPlayerTab.qml +++ b/quickshell/Modules/Settings/MediaPlayerTab.qml @@ -18,6 +18,40 @@ Item { desktopApps = []; } + Loader { + id: popupLoader + active: false + sourceComponent: AppBrowserPopup { + id: appBrowserPopup + appsModel: root.desktopApps + parentModal: root.parentModal + onAppSelected: appId => { + var name = appId; + if (name.endsWith(".desktop")) { + name = name.slice(0, -8); + } + SettingsData.addMediaExcludePlayer(name); + } + } + + function recreatePopup() { + log.debug("Recreating popup"); + popupLoader.active = false; + popupLoader.active = true; + } + + function showPopup() { + if (!popupLoader.item) { + popupLoader.active = true; + } + popupLoader.item.show(); + if (!popupLoader.item.visible) { + recreatePopup(); + popupLoader.item.show(); + } + } + } + DankFlickable { anchors.fill: parent clip: true @@ -166,10 +200,11 @@ Item { placeholderText: I18n.tr("App name or identity (e.g., firefox)") font.pixelSize: Theme.fontSizeSmall onAccepted: { - if (text.trim() !== "") { - SettingsData.addMediaExcludePlayer(text.trim()); - text = ""; + var cleanInput = newExcludePlayerField.text.trim(); + if (cleanInput !== "") { + SettingsData.addMediaExcludePlayer(cleanInput); } + text = ""; } } @@ -181,10 +216,11 @@ Item { backgroundColor: Theme.primary iconColor: Theme.onPrimary onClicked: { - if (newExcludePlayerField.text.trim() !== "") { - SettingsData.addMediaExcludePlayer(newExcludePlayerField.text.trim()); - newExcludePlayerField.text = ""; + var cleanInput = newExcludePlayerField.text.trim(); + if (cleanInput !== "") { + SettingsData.addMediaExcludePlayer(cleanInput); } + newExcludePlayerField.text = ""; } } @@ -195,7 +231,7 @@ Item { iconSize: 20 backgroundColor: Theme.surfaceContainer iconColor: Theme.primary - onClicked: appBrowserPopup.show() + onClicked: popupLoader.showPopup() } } @@ -267,17 +303,4 @@ Item { } } } - - AppBrowserPopup { - id: appBrowserPopup - appsModel: root.desktopApps - parentModal: root.parentModal - onAppSelected: appId => { - var name = appId; - if (name.endsWith(".desktop")) { - name = name.slice(0, -8); - } - SettingsData.addMediaExcludePlayer(name); - } - } } diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml index a139a288f..9dd022385 100644 --- a/quickshell/Services/MprisController.qml +++ b/quickshell/Services/MprisController.qml @@ -17,28 +17,7 @@ Singleton { return players.filter(p => { const identity = (p.identity || "").toLowerCase(); const desktopEntry = ("desktopEntry" in p && p.desktopEntry) ? String(p.desktopEntry).toLowerCase() : ""; - return !excluded.some(ex => { - const exLower = String(ex).toLowerCase().trim(); - if (!exLower) return false; - - // 1. Substring match - if (identity.includes(exLower) || desktopEntry.includes(exLower)) - return true; - - // 2. Match reverse-DNS segments (e.g. app.zen_browser.zen -> zen) - if (exLower.indexOf(".") !== -1) { - const parts = exLower.split("."); - const lastPart = parts[parts.length - 1]; - if (lastPart && (identity.includes(lastPart) || desktopEntry.includes(lastPart))) - return true; - } - - // 3. Bidirectional match (longer excluded name contains shorter player identity) - if (identity.length >= 3 && exLower.includes(identity)) - return true; - - return false; - }); + return !excluded.some(ex => Paths.isAppIdMatch(identity, ex, desktopEntry)); }); } property MprisPlayer activePlayer: null @@ -86,10 +65,7 @@ Singleton { } function isIdle(player: MprisPlayer): bool { - return player - && player.playbackState === MprisPlaybackState.Stopped - && !player.trackTitle - && !player.trackArtist; + return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist; } function _resolveActivePlayer(): void { From 43a09171b1ca3a3a9d6b8bc3137b45141d77733a Mon Sep 17 00:00:00 2001 From: euletheia <154215350+euletheia@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:19:10 +0200 Subject: [PATCH 4/4] fix(AutoStartTab): use Loader for AppBrowserPopup so that it can be recreated if closed (Align with GammaControlTab changes) --- quickshell/Modules/Settings/AutoStartTab.qml | 37 +++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/quickshell/Modules/Settings/AutoStartTab.qml b/quickshell/Modules/Settings/AutoStartTab.qml index 1e388bd53..386a03c81 100644 --- a/quickshell/Modules/Settings/AutoStartTab.qml +++ b/quickshell/Modules/Settings/AutoStartTab.qml @@ -353,19 +353,40 @@ Item { desktopApps = []; } - DankFlickable { - anchors.fill: parent - clip: true - contentHeight: mainColumn.height + Theme.spacingXL - contentWidth: width - - AppBrowserPopup { + Loader { + id: popupLoader + active: false + sourceComponent: AppBrowserPopup { id: appBrowserPopup appsModel: root.desktopApps parentModal: root.parentModal onAppSelected: appId => root.newEntryDesktopId = appId } + function recreatePopup() { + log.debug("Recreating popup"); + popupLoader.active = false; + popupLoader.active = true; + } + + function showPopup() { + if (!popupLoader.item) { + popupLoader.active = true; + } + popupLoader.item.show(); + if (!popupLoader.item.visible) { + recreatePopup(); + popupLoader.item.show(); + } + } + } + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: mainColumn.height + Theme.spacingXL + contentWidth: width + Column { id: mainColumn topPadding: 4 @@ -486,7 +507,7 @@ Item { id: browseButton text: I18n.tr("Browse") iconName: "search" - onClicked: appBrowserPopup.show() + onClicked: popupLoader.showPopup() } }