534 lines
18 KiB
QML
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
|
|
}
|
|
}
|