diff --git a/Shell/Notifications/NotificationLayer.qml b/Shell/Notifications/NotificationLayer.qml index edc00f0..79d234f 100644 --- a/Shell/Notifications/NotificationLayer.qml +++ b/Shell/Notifications/NotificationLayer.qml @@ -17,9 +17,9 @@ PanelWindow { right: theme.panelMarginRight } - // Top layer keeps notifications above normal windows while still usually - // below fullscreen overlays. Overlay would be too aggressive here. - WlrLayershell.layer: WlrLayer.Top + // Match the media status overlay behavior so notifications stay visible + // even when an app is fullscreen. + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.focusable: false WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.namespace: "deltarune-quickshell-notifications" diff --git a/Shell/Overlays/MprisOverlay.qml b/Shell/Overlays/MprisOverlay.qml index f5d5e36..946e4a7 100644 --- a/Shell/Overlays/MprisOverlay.qml +++ b/Shell/Overlays/MprisOverlay.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import Quickshell.Services.Mpris +import ".." PanelWindow { id: overlay @@ -22,7 +23,8 @@ PanelWindow { color: '#00ffffff' - visible: true + readonly property bool overlayEnabled: Boolean(ShellStateManager.global("shell.mprisOverlayEnabled", true)) + visible: overlayEnabled Repeater { model: Mpris.players diff --git a/Shell/ShellStateManager.qml b/Shell/ShellStateManager.qml index 1b2fc16..38a5cec 100644 --- a/Shell/ShellStateManager.qml +++ b/Shell/ShellStateManager.qml @@ -19,9 +19,16 @@ QtObject { property bool appUsageLoaded: false property var windowRequests: ({}) property var quickSettingsPayload: ({}) - property var globals: ({ - "notifications.stackPreviewEnabled": true + property var defaultGlobals: ({ + "notifications.stackPreviewEnabled": true, + "shell.mprisOverlayEnabled": true }) + property var globals: Object.assign({}, defaultGlobals) + property string globalsPath: { + var homeDir = Quickshell.env("HOME"); + return homeDir ? homeDir + "/.local/share/deltarunequickshell/globals.json" : ""; + } + property bool globalsLoaded: false signal shellOpened signal shellClosed @@ -175,7 +182,17 @@ QtObject { appUsageFile.writeAdapter(); } + function persistGlobals() { + if (!globalsLoaded) + return; + if (!globalsFile || !globalsAdapter) + return; + globalsAdapter.values = globals || ({}); + globalsFile.writeAdapter(); + } + onAppUsageCountsChanged: persistAppUsage() + onGlobalsChanged: persistGlobals() property FileView appUsageFile: FileView { id: appUsageFile @@ -199,6 +216,28 @@ QtObject { } } + property FileView globalsFile: FileView { + id: globalsFile + path: manager.globalsPath + watchChanges: true + onFileChanged: reload() + onLoaded: { + var loaded = globalsAdapter && globalsAdapter.values ? globalsAdapter.values : ({}); + manager.globals = Object.assign({}, manager.defaultGlobals, loaded); + manager.globalsLoaded = true; + } + onLoadFailed: { + manager.globals = Object.assign({}, manager.defaultGlobals); + manager.globalsLoaded = true; + manager.persistGlobals(); + } + + JsonAdapter { + id: globalsAdapter + property var values: ({}) + } + } + function setGlobal(key, value) { var updated = Object.assign({}, globals); updated[key] = value; @@ -206,7 +245,7 @@ QtObject { } function global(key, defaultValue) { - return globals.hasOwnProperty(key) ? globals[key] : defaultValue; + return Object.prototype.hasOwnProperty.call(globals, key) ? globals[key] : defaultValue; } function notificationStackPreviewEnabled() { @@ -220,4 +259,16 @@ QtObject { function toggleNotificationStackPreviewEnabled() { setNotificationStackPreviewEnabled(!notificationStackPreviewEnabled()); } + + function mprisOverlayEnabled() { + return Boolean(global("shell.mprisOverlayEnabled", true)); + } + + function setMprisOverlayEnabled(enabled) { + setGlobal("shell.mprisOverlayEnabled", Boolean(enabled)); + } + + function toggleMprisOverlayEnabled() { + setMprisOverlayEnabled(!mprisOverlayEnabled()); + } } diff --git a/Shell/Windows/QuickSettings/QuickSettingsApp.qml b/Shell/Windows/QuickSettings/QuickSettingsApp.qml index 3e5037a..4395989 100644 --- a/Shell/Windows/QuickSettings/QuickSettingsApp.qml +++ b/Shell/Windows/QuickSettings/QuickSettingsApp.qml @@ -36,7 +36,7 @@ Item { property int activeSelection: 0 property bool inBluetoothMenu: false property bool inWallpaperMenu: false - property bool inNotificationsMenu: false + property bool inShellMenu: false property bool inPowerMenu: false property bool inPowerConfirm: false @@ -84,7 +84,7 @@ Item { } function currentAction() { - if (root.inBluetoothMenu || root.inWallpaperMenu || root.inNotificationsMenu || root.inPowerMenu) + if (root.inBluetoothMenu || root.inWallpaperMenu || root.inShellMenu || root.inPowerMenu) return null; return menuLength() > 0 ? menuAt(activeSelection) : null; } @@ -198,7 +198,7 @@ Item { property int bluetoothActionIndex: 1 property int wallpaperActionIndex: 2 - property int notificationsActionIndex: 3 + property int shellActionIndex: 3 property int powerActionIndex: 4 property int pendingPowerActionIndex: 0 property var pendingPowerCommand: [] @@ -225,7 +225,7 @@ Item { ent: function () { root.inBluetoothMenu = true; root.inWallpaperMenu = false; - root.inNotificationsMenu = false; + root.inShellMenu = false; root.isSelected = false; root.activeSelection = 0; root.clampSelection(); @@ -239,7 +239,7 @@ Item { ent: function () { root.inWallpaperMenu = true; root.inBluetoothMenu = false; - root.inNotificationsMenu = false; + root.inShellMenu = false; root.isSelected = false; var currentIndex = root.currentWallpaperIndex(); root.activeSelection = currentIndex >= 0 ? currentIndex : 0; @@ -252,9 +252,9 @@ Item { } }, { - name: "Notifications", + name: "Shell", ent: function () { - root.inNotificationsMenu = true; + root.inShellMenu = true; root.inBluetoothMenu = false; root.inWallpaperMenu = false; root.inPowerMenu = false; @@ -274,7 +274,7 @@ Item { root.inPowerConfirm = false; root.inBluetoothMenu = false; root.inWallpaperMenu = false; - root.inNotificationsMenu = false; + root.inShellMenu = false; root.isSelected = false; root.activeSelection = 0; root.pendingPowerActionIndex = 0; @@ -315,7 +315,7 @@ Item { } ] - property var notificationActions: [ + property var shellActions: [ { name: "Stack Preview", ent: function () { @@ -327,10 +327,22 @@ Item { return "ON"; return manager.notificationStackPreviewEnabled() ? "ON" : "OFF"; } + }, + { + name: "Media Overlay", + ent: function () { + if (manager && manager.toggleMprisOverlayEnabled) + manager.toggleMprisOverlayEnabled(); + }, + getState: function () { + if (!manager || !manager.mprisOverlayEnabled) + return "ON"; + return manager.mprisOverlayEnabled() ? "ON" : "OFF"; + } } ] - property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : (root.inNotificationsMenu ? notificationActions : (root.inPowerMenu ? (root.inPowerConfirm ? powerConfirmActions : powerActions) : actions))) + property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : (root.inShellMenu ? shellActions : (root.inPowerMenu ? (root.inPowerConfirm ? powerConfirmActions : powerActions) : actions))) FolderListModel { id: wallpaperFolderModel @@ -427,7 +439,7 @@ Item { } Text { - text: root.inPowerConfirm ? "CONFIRM" : (root.inPowerMenu ? "POWER" : (root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : (root.inNotificationsMenu ? "NOTIFICATIONS" : "CONFIG")))) + text: root.inPowerConfirm ? "CONFIRM" : (root.inPowerMenu ? "POWER" : (root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : (root.inShellMenu ? "SHELL" : "CONFIG")))) font.family: "8bitoperator JVE" font.pixelSize: 71 renderType: Text.NativeRendering @@ -561,10 +573,10 @@ Item { const wallpaperPath = wallpaperPathAt(activeSelection); if (wallpaperPath && wallpaperPath.length > 0) root.applyWallpaper(wallpaperPath); - } else if (root.inNotificationsMenu) { - const notificationAction = menuAt(activeSelection); - if (notificationAction && notificationAction.ent) - notificationAction.ent(); + } else if (root.inShellMenu) { + const shellAction = menuAt(activeSelection); + if (shellAction && shellAction.ent) + shellAction.ent(); } else if (root.inPowerConfirm) { const confirmAction = menuAt(activeSelection); if (confirmAction && confirmAction.name === "Yes") { @@ -615,10 +627,10 @@ Item { root.pendingPowerCommand = []; root.activeSelection = root.powerActionIndex; root.clampSelection(); - } else if (root.inNotificationsMenu) { - root.inNotificationsMenu = false; + } else if (root.inShellMenu) { + root.inShellMenu = false; root.isSelected = false; - root.activeSelection = root.notificationsActionIndex; + root.activeSelection = root.shellActionIndex; root.clampSelection(); } else if (root.inBluetoothMenu) { root.inBluetoothMenu = false; @@ -642,7 +654,7 @@ Item { root.isSelected = false; root.inBluetoothMenu = false; root.inWallpaperMenu = false; - root.inNotificationsMenu = false; + root.inShellMenu = false; root.inPowerMenu = false; root.inPowerConfirm = false; root.pendingPowerActionIndex = 0; diff --git a/Startup/deltarune.webm b/Startup/deltarune.webm new file mode 100644 index 0000000..6d78625 Binary files /dev/null and b/Startup/deltarune.webm differ diff --git a/Startup/shell.qml b/Startup/shell.qml new file mode 100644 index 0000000..7c42564 --- /dev/null +++ b/Startup/shell.qml @@ -0,0 +1,236 @@ +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtMultimedia + +PanelWindow { + id: overlay + anchors { + top: true + left: true + right: true + bottom: true + } + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.focusable: true + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "deltarune-startup-overlay" + + color: "#000000" + exclusionMode: ExclusionMode.Ignore + aboveWindows: true + focusable: true + + property string bluetoothTargetName: "JBL Tune 525BT" + property int bluetoothTimeoutMs: 5000 + property bool waitingForBluetooth: true + property bool animationLaunchScheduled: false + property bool animationStarted: false + + function bluetoothModelCount() { + if (!Bluetooth.devices) + return 0; + if (typeof Bluetooth.devices.count === "function") + return Bluetooth.devices.count(); + if (Bluetooth.devices.count !== undefined) + return Bluetooth.devices.count; + if (Bluetooth.devices.length !== undefined) + return Bluetooth.devices.length; + return 0; + } + + function bluetoothModelGet(index) { + if (!Bluetooth.devices || index < 0 || index >= bluetoothModelCount()) + return null; + if (Bluetooth.devices.get) + return Bluetooth.devices.get(index); + if (Bluetooth.devices.length !== undefined) + return Bluetooth.devices[index]; + return null; + } + + function bluetoothDeviceName(device) { + if (!device) + return ""; + if (device.name && device.name.length > 0) + return String(device.name); + if (device.deviceName && device.deviceName.length > 0) + return String(device.deviceName); + return ""; + } + + function findTargetBluetoothDevice() { + for (var i = 0; i < bluetoothModelCount(); i++) { + var device = bluetoothModelGet(i); + if (bluetoothDeviceName(device) === bluetoothTargetName) + return device; + } + return null; + } + + function beginAnimation() { + if (animationStarted || animationLaunchScheduled) + return; + waitingForBluetooth = false; + bluetoothRetryTimer.stop(); + bluetoothTimeoutTimer.stop(); + animationLaunchScheduled = true; + startupDelayTimer.restart(); + } + + function handlePrimaryAction() { + if (!animationStarted) + beginAnimation(); + else + finishStartup(); + } + + function finishStartup() { + if (player.playbackState !== MediaPlayer.StoppedState) + player.stop(); + else + Qt.quit(); + } + + HyprlandFocusGrab { + active: true + windows: [overlay] + } + + FocusScope { + id: overlayFocus + anchors.fill: parent + focus: true + + Component.onCompleted: overlayFocus.forceActiveFocus() + onVisibleChanged: if (visible) + overlayFocus.forceActiveFocus() + onFocusChanged: if (focus) + overlayFocus.forceActiveFocus() + + Keys.onReleased: function (event) { + switch (event.key) { + case Qt.Key_Z: + case Qt.Key_Return: + case Qt.Key_Enter: + event.accepted = true; + overlay.handlePrimaryAction(); + return; + } + } + } + + Connections { + target: Bluetooth.devices + ignoreUnknownSignals: true + + function onCountChanged() { + if (overlay.waitingForBluetooth) + bluetoothRetryTimer.restart(); + } + + function onModelReset() { + if (overlay.waitingForBluetooth) + bluetoothRetryTimer.restart(); + } + + function onRowsInserted() { + if (overlay.waitingForBluetooth) + bluetoothRetryTimer.restart(); + } + } + + MediaPlayer { + id: player + source: "deltarune.webm" // Local file or URL + autoPlay: false + videoOutput: videoOutput + playbackRate: 1 + audioOutput: AudioOutput {} + onPlaybackStateChanged: a => { + if (player.playbackState === MediaPlayer.StoppedState) + overlay.finishStartup(); + } + } + + Timer { + id: startupDelayTimer + interval: 1000 + repeat: false + onTriggered: { + overlay.animationLaunchScheduled = false; + overlay.animationStarted = true; + player.play(); + } + } + + Timer { + id: bluetoothRetryTimer + interval: 250 + running: true + repeat: true + onTriggered: { + if (!overlay.waitingForBluetooth) { + stop(); + return; + } + + const device = overlay.findTargetBluetoothDevice(); + if (!device) + return; + + if (device.connected) { + overlay.beginAnimation(); + return; + } + + device.connect(); + } + } + + Timer { + id: bluetoothTimeoutTimer + interval: overlay.bluetoothTimeoutMs + running: true + repeat: false + onTriggered: overlay.beginAnimation() + } + + VideoOutput { + id: videoOutput + anchors.fill: parent + } + + Text { + visible: overlay.waitingForBluetooth + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: "#ffffff" + font.family: "Determination Mono" + font.pixelSize: 28 + text: "CONNECTING TO " + overlay.bluetoothTargetName.toUpperCase() + z: 1 + } + + Text { + visible: !overlay.animationStarted + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 56 + color: "#ffffff" + font.family: "Determination Mono" + font.pixelSize: 28 + text: overlay.waitingForBluetooth ? "PRESS Z OR ENTER TO SKIP WAIT" : "STARTING..." + z: 1 + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + cursorShape: Qt.BlankCursor + } +}