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..4dfa7515e 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 @@ -3161,29 +3163,64 @@ Singleton { saveSettings(); } - function addMediaExcludePlayer(identity) { - 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; - 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; + 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(); + } + + 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/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/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() } } 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/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/DisplayService.qml b/quickshell/Services/DisplayService.qml index c7c1d6569..4231d4329 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 @@ -37,9 +39,12 @@ Singleton { signal brightnessChanged(bool showOsd) signal deviceSwitched - 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 +513,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 +863,7 @@ Singleton { Component.onCompleted: { nightModeEnabled = SessionData.nightModeEnabled; + nightModePaused = SessionData.nightModePaused; deviceBrightnessUserSet = Object.assign({}, SessionData.brightnessUserSetValues); if (DMSService.isConnected) { checkGammaControlAvailability(); @@ -880,7 +942,6 @@ Singleton { } } - // Session Data Connections Connections { target: SessionData @@ -922,6 +983,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/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 { 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",