F2: NsNotifications.qml "Clear" mirrors the service (Notifs.list.slice() → n.close()) instead of n.notification?.dismiss() — cards now actually leave the list, and it works for disk-restored notifications (whose notification is null). C2: Toggles.qml gameMode delegate launches the guarded switch-to-gaming via Quickshell.execDetached (debounced + disabled + 5s timer so a double-click can't fire two SDDM restarts), and the tile is hidden entirely when DeckShift isn't installed (Process probe for the gamescope session file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
207 lines
7.5 KiB
QML
207 lines
7.5 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Bluetooth
|
|
import Caelestia.Components
|
|
import Caelestia.Config
|
|
import qs.components
|
|
import qs.components.controls
|
|
import qs.services
|
|
import qs.modules.nexus
|
|
import qs.modules.bar.popouts as BarPopouts
|
|
|
|
StyledRect {
|
|
id: root
|
|
|
|
required property DrawerVisibilities visibilities
|
|
required property BarPopouts.Wrapper popouts
|
|
|
|
// NoSignal: the gamepad "Game Mode" toggle launches DeckShift gaming. Hide it
|
|
// entirely when DeckShift isn't installed (no gamescope session file) — checked
|
|
// once at load by deckshiftProbe below.
|
|
property bool deckshiftInstalled: false
|
|
|
|
Process {
|
|
id: deckshiftProbe
|
|
running: true
|
|
command: ["test", "-f", "/usr/share/wayland-sessions/gamescope-session-steam-nm.desktop"]
|
|
onExited: (exitCode, exitStatus) => root.deckshiftInstalled = exitCode === 0
|
|
}
|
|
|
|
readonly property var quickToggles: {
|
|
const seenIds = new Set();
|
|
|
|
return Config.utilities.quickToggles.filter(item => {
|
|
if (!(item.enabled ?? true))
|
|
return false;
|
|
|
|
if (seenIds.has(item.id)) {
|
|
return false;
|
|
}
|
|
|
|
if (item.id === "vpn") {
|
|
return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false);
|
|
}
|
|
|
|
// NoSignal: drop the Game Mode tile unless DeckShift is installed.
|
|
if (item.id === "gameMode") {
|
|
return root.deckshiftInstalled;
|
|
}
|
|
|
|
seenIds.add(item.id);
|
|
return true;
|
|
});
|
|
}
|
|
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
|
|
readonly property bool needExtraRow: quickToggles.length > 6
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
|
|
|
|
radius: Tokens.rounding.large
|
|
color: Colours.tPalette.m3surfaceContainer
|
|
|
|
ColumnLayout {
|
|
id: layout
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.large
|
|
spacing: Tokens.spacing.medium
|
|
|
|
StyledText {
|
|
text: qsTr("Quick Toggles")
|
|
font: Tokens.font.body.medium
|
|
}
|
|
|
|
QuickToggleRow {
|
|
model: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles
|
|
}
|
|
|
|
QuickToggleRow {
|
|
visible: root.needExtraRow
|
|
model: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : []
|
|
}
|
|
}
|
|
|
|
component QuickToggleRow: ButtonRow {
|
|
property alias model: repeater.model
|
|
|
|
Layout.fillWidth: true
|
|
spacing: Tokens.spacing.small
|
|
|
|
Repeater {
|
|
id: repeater
|
|
|
|
delegate: DelegateChooser {
|
|
role: "id"
|
|
|
|
DelegateChoice {
|
|
roleValue: "wifi"
|
|
delegate: Toggle {
|
|
icon: "wifi"
|
|
checked: Nmcli.wifiEnabled
|
|
onClicked: Nmcli.toggleWifi()
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "bluetooth"
|
|
delegate: Toggle {
|
|
icon: "bluetooth"
|
|
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
|
|
onClicked: {
|
|
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
|
if (adapter)
|
|
adapter.enabled = !adapter.enabled;
|
|
}
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "mic"
|
|
delegate: Toggle {
|
|
icon: "mic"
|
|
checked: !Audio.sourceMuted
|
|
onClicked: {
|
|
const audio = Audio.source?.audio;
|
|
if (audio)
|
|
audio.muted = !audio.muted;
|
|
}
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "settings"
|
|
delegate: Toggle {
|
|
icon: "settings"
|
|
inactiveOnColour: Colours.palette.m3onSurfaceVariant
|
|
isToggle: false
|
|
onClicked: {
|
|
root.visibilities.utilities = false;
|
|
WindowFactory.create();
|
|
}
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "gameMode"
|
|
delegate: Toggle {
|
|
id: gmTog
|
|
// NoSignal: launch the DeckShift gaming session if installed;
|
|
// otherwise do nothing. (Was caelestia's cosmetic Game Mode.)
|
|
// Kept styled as an off-toggle (muted, outline icon) — the
|
|
// gamepad is a momentary LAUNCHER, not a stateful toggle.
|
|
// DEBOUNCED: a fast double-click must not fire two
|
|
// `switch-to-gaming` runs — two SDDM restarts race the
|
|
// one-shot autologin and drop you at the password greeter.
|
|
// Guard mirrors the Super+Shift+S bind + the deckshift-login
|
|
// install-check, so it no-ops if DeckShift is absent.
|
|
property bool launching: false
|
|
icon: "gamepad"
|
|
disabled: launching
|
|
onClicked: {
|
|
if (gmTog.launching)
|
|
return;
|
|
gmTog.launching = true;
|
|
gmTog.internalChecked = false; // don't latch "on"
|
|
relockTimer.start();
|
|
Quickshell.execDetached(["sh", "-c", "[ -x /usr/local/bin/switch-to-gaming ] && [ -f /usr/share/wayland-sessions/gamescope-session-steam-nm.desktop ] && exec /usr/local/bin/switch-to-gaming"]);
|
|
}
|
|
|
|
Timer {
|
|
id: relockTimer
|
|
interval: 5000
|
|
onTriggered: gmTog.launching = false
|
|
}
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "dnd"
|
|
delegate: Toggle {
|
|
icon: "notifications_off"
|
|
checked: Notifs.dnd
|
|
onClicked: Notifs.dnd = !Notifs.dnd
|
|
}
|
|
}
|
|
DelegateChoice {
|
|
roleValue: "vpn"
|
|
delegate: Toggle {
|
|
icon: "vpn_key"
|
|
checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
|
|
enabled: !VPN.connecting
|
|
isToggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error"
|
|
inactiveOnColour: Colours.palette.m3onSurfaceVariant
|
|
onClicked: VPN.toggle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component Toggle: IconButton {
|
|
inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
|
fillWidth: true
|
|
isToggle: true
|
|
isRound: true
|
|
shapeMorph: true
|
|
}
|
|
}
|