Files
DeltaruneQuickshell/Startup/shell.qml
2026-03-25 21:25:12 +02:00

534 lines
18 KiB
QML

import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Services.Mpris
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 var bluetoothTargetNames: ["JBL Tune 525BT", "JBL GO"]
property int bluetoothConnectAttemptMs: 3000
property bool waitingForBluetooth: true
property bool animationLaunchScheduled: false
property bool animationStarted: false
property bool isEnding: false
property bool postIntroSongTriggered: false
property bool waitingForPostIntroSong: false
property bool waitingForChromiumSong: false
property bool finishDelayScheduled: false
property bool bluetoothConnectRequested: false
property int bluetoothTargetAttemptIndex: 0
property string bluetoothActiveTargetName: ""
function bluetoothModelCount() {
if (bluetoothDeviceRepeater)
return bluetoothDeviceRepeater.count;
return 0;
}
function bluetoothDeviceAt(index) {
const item = bluetoothDeviceRepeater.itemAt(index);
if (!item)
return null;
return item.device || 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 findBluetoothDeviceByName(targetName) {
for (var i = 0; i < bluetoothModelCount(); i++) {
var device = bluetoothDeviceAt(i);
var deviceName = bluetoothDeviceName(device);
console.log("Startup BT: candidate", i, deviceName, device ? Boolean(device.connected) : false);
if (deviceName === targetName)
return device;
}
console.log("Startup BT: target not found", targetName, "count", bluetoothModelCount());
return null;
}
function findConnectedPreferredBluetoothDevice() {
for (var i = 0; i < bluetoothTargetNames.length; i++) {
var device = findBluetoothDeviceByName(bluetoothTargetNames[i]);
if (device && device.connected)
return device;
}
return null;
}
function resetBluetoothAttemptState() {
bluetoothConnectRequested = false;
bluetoothTargetAttemptIndex = 0;
bluetoothActiveTargetName = "";
bluetoothConnectAttemptTimer.stop();
}
function maybeStartBluetoothOrSkip() {
console.log("Startup BT: maybeStartBluetoothOrSkip", "waiting", waitingForBluetooth, "requested", bluetoothConnectRequested, "active", bluetoothActiveTargetName, "attemptIndex", bluetoothTargetAttemptIndex);
if (!waitingForBluetooth)
return;
const connectedDevice = findConnectedPreferredBluetoothDevice();
if (connectedDevice) {
console.log("Startup BT: preferred device connected, starting animation", bluetoothDeviceName(connectedDevice));
beginAnimation();
return;
}
if (bluetoothConnectRequested) {
console.log("Startup BT: connect already requested, waiting on", bluetoothActiveTargetName);
return;
}
for (var i = bluetoothTargetAttemptIndex; i < bluetoothTargetNames.length; i++) {
var targetName = bluetoothTargetNames[i];
var targetDevice = findBluetoothDeviceByName(targetName);
if (!targetDevice)
continue;
bluetoothConnectRequested = true;
bluetoothTargetAttemptIndex = i;
bluetoothActiveTargetName = targetName;
bluetoothConnectAttemptTimer.restart();
console.log("Startup BT: requesting connect", targetName);
targetDevice.connect();
return;
}
console.log("Startup BT: no preferred devices available yet");
}
function beginAnimation() {
console.log("Startup: beginAnimation", "started", animationStarted, "scheduled", animationLaunchScheduled);
if (animationStarted || animationLaunchScheduled)
return;
waitingForBluetooth = false;
bluetoothRetryTimer.stop();
bluetoothConnectAttemptTimer.stop();
animationLaunchScheduled = true;
startupDelayTimer.restart();
}
function handlePrimaryAction() {
console.log("Startup: handlePrimaryAction", "animationStarted", animationStarted, "waitingForBluetooth", waitingForBluetooth);
if (!animationStarted)
beginAnimation();
else
finishStartup();
}
function finishStartup() {
console.log("Startup: finishStartup", "playbackState", player.playbackState);
if (player.playbackState !== MediaPlayer.StoppedState)
player.stop();
else
Qt.quit();
}
function triggerPostIntroSong() {
if (postIntroSongTriggered)
return;
postIntroSongTriggered = true;
isEnding = true;
waitingForPostIntroSong = true;
waitingForChromiumSong = true;
console.log("Startup: Triggering post-intro song...");
postIntroSongTimeout.restart();
chromiumSongTimeout.restart();
postIntroSongProcess.exec(["curl", "-s", "-X", "POST", "-H", "Content-Type: application/json", "-d", "{\"type\":\"songs\",\"id\":\"i.WmYR0zXC68B2g50\"}", "http://localhost:10767/api/v1/playback/play-item"]);
}
function chromiumSongReady() {
for (var i = 0; i < mprisPlayerRepeater.count; i++) {
var item = mprisPlayerRepeater.itemAt(i);
if (!item || !item.player)
continue;
var player = item.player;
var identity = String(player.identity || "").toLowerCase();
var desktopEntry = String(player.desktopEntry || "").toLowerCase();
var dbusName = String(player.dbusName || "").toLowerCase();
var isChromium = identity.indexOf("chromium") !== -1 || desktopEntry.indexOf("chromium") !== -1 || dbusName.indexOf("chromium") !== -1;
if (!isChromium)
continue;
var album = String(player.trackAlbum || "");
var artist = String(player.trackArtist || "");
var title = String(player.trackTitle || "");
console.log("Startup: chromium metadata", identity, album, artist, title);
if (album === "Deltarune Chapters 3+4 (Original Game Soundtrack)" && artist === "Toby Fox" && title === "With Hope Crossed on Our Hearts")
return true;
}
return false;
}
function maybeFinishAfterSongReady() {
if (!isEnding)
return;
if (waitingForPostIntroSong)
return;
if (chromiumSongReady()) {
console.log("Startup: chromium MPRIS metadata matched, waiting 1s before closing overlay");
waitingForChromiumSong = false;
chromiumSongTimeout.stop();
if (!finishDelayScheduled) {
finishDelayScheduled = true;
finishDelayTimer.restart();
}
return;
}
console.log("Startup: waiting for chromium MPRIS metadata");
}
HyprlandFocusGrab {
active: true
windows: [overlay]
}
Component.onCompleted: {
console.log("Startup: overlay completed", "btCount", bluetoothModelCount(), "targets", JSON.stringify(bluetoothTargetNames));
maybeStartBluetoothOrSkip();
}
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) {
console.log("Startup BT: devices count changed", overlay.bluetoothModelCount(), "repeater", bluetoothDeviceRepeater.count);
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
function onModelReset() {
if (overlay.waitingForBluetooth) {
console.log("Startup BT: devices model reset");
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
function onRowsInserted() {
if (overlay.waitingForBluetooth) {
console.log("Startup BT: device rows inserted");
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
function onRowsRemoved() {
if (overlay.waitingForBluetooth) {
console.log("Startup BT: device rows removed");
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
}
Repeater {
id: bluetoothDeviceRepeater
model: Bluetooth.devices
delegate: Item {
property var device: modelData
Component.onCompleted: {
console.log("Startup BT: delegate ready", index, overlay.bluetoothDeviceName(device), Boolean(device ? device.connected : false), "repeaterCount", bluetoothDeviceRepeater.count);
overlay.maybeStartBluetoothOrSkip();
}
Connections {
target: device
ignoreUnknownSignals: true
function onConnectedChanged() {
console.log("Startup BT: connected changed", overlay.bluetoothDeviceName(device), Boolean(device ? device.connected : false));
if (overlay.waitingForBluetooth) {
if (Boolean(device ? device.connected : false))
overlay.bluetoothConnectAttemptTimer.stop();
overlay.maybeStartBluetoothOrSkip();
}
}
function onNameChanged() {
if (overlay.waitingForBluetooth) {
console.log("Startup BT: name changed", overlay.bluetoothDeviceName(device));
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
function onDeviceNameChanged() {
if (overlay.waitingForBluetooth) {
console.log("Startup BT: deviceName changed", overlay.bluetoothDeviceName(device));
overlay.resetBluetoothAttemptState();
overlay.maybeStartBluetoothOrSkip();
}
}
}
}
}
Repeater {
id: mprisPlayerRepeater
model: Mpris.players
delegate: Item {
required property var modelData
property var player: modelData
visible: false
Connections {
target: player
function onTrackChanged() {
overlay.maybeFinishAfterSongReady();
}
function onPostTrackChanged() {
overlay.maybeFinishAfterSongReady();
}
function onMetadataChanged() {
overlay.maybeFinishAfterSongReady();
}
function onTrackTitleChanged() {
overlay.maybeFinishAfterSongReady();
}
function onTrackArtistChanged() {
overlay.maybeFinishAfterSongReady();
}
function onTrackAlbumChanged() {
overlay.maybeFinishAfterSongReady();
}
function onIdentityChanged() {
overlay.maybeFinishAfterSongReady();
}
function onDesktopEntryChanged() {
overlay.maybeFinishAfterSongReady();
}
}
}
}
Connections {
target: Mpris.players
ignoreUnknownSignals: true
function onCountChanged() {
overlay.maybeFinishAfterSongReady();
}
function onModelReset() {
overlay.maybeFinishAfterSongReady();
}
function onRowsInserted() {
overlay.maybeFinishAfterSongReady();
}
function onRowsRemoved() {
overlay.maybeFinishAfterSongReady();
}
}
MediaPlayer {
id: player
source: "deltarune.mp4"
autoPlay: false
videoOutput: videoOutput
playbackRate: 1
audioOutput: startupAudioOutput
onMediaStatusChanged: {
console.log("Startup: Media Status:", player.mediaStatus);
if (player.mediaStatus === MediaPlayer.EndOfMedia)
overlay.triggerPostIntroSong();
}
onPlaybackStateChanged: {
if (player.playbackState === MediaPlayer.StoppedState && !overlay.isEnding) {
console.log("Startup: Video stopped unexpectedly, shutting down.");
overlay.finishStartup();
}
}
}
Process {
id: postIntroSongProcess
onExited: (exitCode, exitStatus) => {
console.log("Startup: post-intro song request exited", exitCode, exitStatus);
postIntroSongTimeout.stop();
overlay.waitingForPostIntroSong = false;
overlay.maybeFinishAfterSongReady();
}
}
Timer {
id: postIntroSongTimeout
interval: 3000
repeat: false
onTriggered: {
console.log("Startup: post-intro song request timed out");
if (overlay.waitingForPostIntroSong) {
postIntroSongProcess.running = false;
overlay.waitingForPostIntroSong = false;
overlay.maybeFinishAfterSongReady();
}
}
}
Timer {
id: chromiumSongTimeout
interval: 8000
repeat: false
onTriggered: {
console.log("Startup: chromium MPRIS metadata wait timed out");
if (overlay.waitingForChromiumSong) {
overlay.waitingForChromiumSong = false;
overlay.finishStartup();
}
}
}
Timer {
id: finishDelayTimer
interval: 1000
repeat: false
onTriggered: {
overlay.finishDelayScheduled = false;
overlay.finishStartup();
}
}
Timer {
id: startupDelayTimer
interval: 1500
repeat: false
onTriggered: {
console.log("Startup: startup delay elapsed, playing video");
overlay.animationLaunchScheduled = false;
overlay.animationStarted = true;
overlay.isEnding = false;
overlay.finishDelayScheduled = false;
player.play();
}
}
Timer {
id: bluetoothRetryTimer
interval: 250
running: true
repeat: true
onTriggered: {
if (!overlay.waitingForBluetooth) {
console.log("Startup BT: retry timer stopping");
stop();
return;
}
console.log("Startup BT: retry timer fired");
overlay.maybeStartBluetoothOrSkip();
}
}
Timer {
id: bluetoothConnectAttemptTimer
interval: overlay.bluetoothConnectAttemptMs
running: false
repeat: false
onTriggered: {
console.log("Startup BT: connect attempt timed out", overlay.bluetoothActiveTargetName);
overlay.bluetoothConnectRequested = false;
overlay.bluetoothActiveTargetName = "";
overlay.bluetoothTargetAttemptIndex = Math.min(overlay.bluetoothTargetAttemptIndex + 1, overlay.bluetoothTargetNames.length);
overlay.maybeStartBluetoothOrSkip();
}
}
MediaDevices {
id: mediaDevices
onDefaultAudioOutputChanged: {
console.log("Startup Audio: default output changed", mediaDevices.defaultAudioOutput.description);
startupAudioOutput.device = mediaDevices.defaultAudioOutput;
}
}
AudioOutput {
id: startupAudioOutput
device: mediaDevices.defaultAudioOutput
}
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
// }
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: Qt.BlankCursor
}
}