// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Io import QtQuick import Qt.labs.platform as P import "../.." import "../appearance" import "../qtmaildir" // The rows of the notifications settings page. Everything edits mod.draft. Column { id: pg // Not "nm": a property named like the injecting id binds to itself. required property QtObject mod readonly property var d: mod.draft spacing: 8 function setTimeout(key, v) { pg.mod.edit({ timeouts: Object.assign({}, pg.d.timeouts, { [key]: v }) }); } Heading { text: "Timeouts" } Text { width: pg.width; wrapMode: Text.Wrap text: "Seconds a balloon stays when the app leaves it to the daemon. An app's own timeout still wins." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Repeater { model: [["low", "Low"], ["normal", "Normal"], ["critical", "Critical"]] Row { required property var modelData spacing: 10 Text { width: 90; anchors.verticalCenter: parent.verticalCenter text: modelData[1]; color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Field { anchors.verticalCenter: parent.verticalCenter visible: pg.d.timeouts[modelData[0]] !== 0 value: String(pg.d.timeouts[modelData[0]]) placeholder: "seconds" // Typing drops the value binding too: put it back so Discard // shows the draft, and a rejected entry reverts at once. onEdited: { const n = parseInt(value, 10); value = Qt.binding(() => String(pg.d.timeouts[modelData[0]])); if (n > 0 && n <= 86400) pg.setTimeout(modelData[0], n); } } Toggle { anchors.verticalCenter: parent.verticalCenter label: "never" checked: pg.d.timeouts[modelData[0]] === 0 // Toggle flips itself on click, which drops the binding; put // it back so Discard shows the draft again. onToggled: { const on = checked; checked = Qt.binding(() => pg.d.timeouts[modelData[0]] === 0); pg.setTimeout(modelData[0], on ? 0 : 10); } } } } Heading { text: "Sounds" } Text { width: pg.width; wrapMode: Text.Wrap text: "Played when a balloon pops, never for one muted, held by Do Not Disturb or snoozed. An app's own sound, or its request for silence, wins." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Repeater { model: [["low", "Low"], ["normal", "Normal"], ["critical", "Critical"]] Row { required property var modelData spacing: 10 readonly property string file: pg.d.sounds[modelData[0]] Text { width: 90; anchors.verticalCenter: parent.verticalCenter text: modelData[1]; color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Text { width: 180; anchors.verticalCenter: parent.verticalCenter elide: Text.ElideMiddle text: parent.file ? parent.file.split("/").pop() : "none" color: parent.file ? Theme.text : Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Button { text: "Choose\u2026" onClicked: { picker.key = modelData[0]; picker.open(); } } Button { icon: "\uf04b" enabled: parent.file !== "" onClicked: { preview.command = ["pw-play", parent.file]; preview.running = false; preview.running = true; } } Button { icon: "\uf00d" enabled: parent.file !== "" onClicked: pg.mod.edit({ sounds: Object.assign({}, pg.d.sounds, { [modelData[0]]: "" }) }) } } } Text { topPadding: 6 text: "Theme for sounds apps ask for by name, also set as the system sound theme" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } } Flow { width: pg.width; spacing: 6 Repeater { model: pg.mod.themes Button { required property string modelData text: modelData border.color: pg.d.soundTheme === modelData ? Theme.accent : Qt.alpha(Theme.text, 0.2) onClicked: pg.mod.edit({ soundTheme: modelData }) } } } Row { spacing: 10 visible: pg.mod.systemTheme !== "" && pg.mod.systemTheme !== pg.d.soundTheme Text { anchors.verticalCenter: parent.verticalCenter text: "System sound theme is " + pg.mod.systemTheme + ", changed outside this page" color: Theme.yellow font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Button { text: "Use " + pg.mod.systemTheme onClicked: pg.mod.edit({ soundTheme: pg.mod.systemTheme }) } } Process { id: preview } // Qt.labs.platform, not QtQuick.Dialogs: the Quick dialog opens light on // this desktop (AGENTS.md); this one takes the widget style. P.FileDialog { id: picker property string key: "" folder: "file://" + Quickshell.env("HOME") + "/.local/share/sounds" nameFilters: ["Sounds (*.oga *.ogg *.wav)"] onAccepted: pg.mod.edit({ sounds: Object.assign({}, pg.d.sounds, { [picker.key]: decodeURIComponent(file.toString().replace(/^file:\/\//, "")) }) }) } Heading { text: "Limits" } Text { width: pg.width; wrapMode: Text.Wrap text: "How many notifications are kept live and in history, 1 to 100 each. Past the live limit the oldest moves to history; past the history limit it is dropped." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Repeater { model: [["liveCap", "On screen"], ["historyCap", "History"]] Row { required property var modelData spacing: 10 Text { width: 90; anchors.verticalCenter: parent.verticalCenter text: modelData[1]; color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Field { anchors.verticalCenter: parent.verticalCenter value: String(pg.d[modelData[0]]) border.color: pg.d[modelData[0]] >= 1 && pg.d[modelData[0]] <= 100 ? Qt.alpha(Theme.text, 0.12) : Theme.red onEdited: { const n = parseInt(value, 10) || 0; value = Qt.binding(() => String(pg.d[modelData[0]])); pg.mod.edit({ [modelData[0]]: n }); } } } } Heading { text: "Placement" } Flow { width: pg.width; spacing: 6 Repeater { model: Quickshell.screens.map(s => s.name) Button { required property string modelData text: modelData border.color: pg.d.balloons.screen === modelData ? Theme.accent : Qt.alpha(Theme.text, 0.2) onClicked: pg.mod.edit({ balloons: Object.assign({}, pg.d.balloons, { screen: modelData }) }) } } } // The four corners as a miniature screen: click the one balloons go to. Grid { columns: 2; spacing: 4 Repeater { model: ["top-left", "top-right", "bottom-left", "bottom-right"] Rectangle { required property string modelData width: 60; height: 34; radius: 5 color: pg.d.balloons.corner === modelData ? Qt.alpha(Theme.accent, 0.6) : Qt.alpha(Theme.surface, 0.6) border.width: 1; border.color: Qt.alpha(Theme.text, 0.15) MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor onClicked: pg.mod.edit({ balloons: Object.assign({}, pg.d.balloons, { corner: modelData }) }) } } } } Heading { text: "Suppression" } Toggle { label: "Critical notifications still pop up under Do Not Disturb" checked: pg.d.dndPassCritical onToggled: { const on = checked; checked = Qt.binding(() => pg.d.dndPassCritical); pg.mod.edit({ dndPassCritical: on }); } } Text { width: pg.width; wrapMode: Text.Wrap text: "Muted apps get no balloon. They still appear in the drawer and in history." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } // Two rows told apart by a caption and by look: muted apps in red, a // click unmutes; recent apps dimmed, a click mutes. Text { text: pg.d.muted.length ? "Muted, click to unmute" : "No muted apps" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } } Flow { width: pg.width; spacing: 6 Repeater { model: pg.d.muted Button { required property string modelData text: modelData; icon: "\uf00d"; danger: true onClicked: pg.mod.edit({ muted: pg.d.muted.filter(a => a !== modelData) }) } } } // Apps seen in the queue and history, newest first (both lists are), not // yet muted. Bounded by the two caps already; the slice keeps the row short. Text { visible: recent.count > 0 topPadding: 6 text: "Recently seen, click to mute" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } } Flow { width: pg.width; spacing: 6 Repeater { id: recent model: [...new Set(Notify.queue.concat(Notify.history).map(n => n.app))] .filter(a => a && pg.d.muted.indexOf(a) < 0).slice(0, 8) Button { required property string modelData text: modelData; icon: "\uf067" opacity: 0.7 onClicked: pg.mod.edit({ muted: pg.d.muted.concat([modelData]) }) } } } Field { width: 200 placeholder: "mute another app by name" onEdited: { const a = value.trim(); if (a !== "" && pg.d.muted.indexOf(a) < 0) pg.mod.edit({ muted: pg.d.muted.concat([a]) }); value = ""; } } Text { width: pg.width; wrapMode: Text.Wrap visible: pg.mod.notice !== "" text: pg.mod.notice color: pg.mod.notice === "saved" ? Theme.green : Theme.yellow font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } }