diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index 10eb1796d..b2628482b 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -144,24 +144,6 @@ DankPopout { onCloseRequested: root.__hideDropdowns() onPanelEntered: root.__stopCloseTimer() onPanelExited: root.__startCloseTimer() - onVolumeChanged: volume => { - const player = root.__dropdownPlayer; - const isChrome = player?.identity?.toLowerCase().includes("chrome") || player?.identity?.toLowerCase().includes("chromium"); - const usePlayerVolume = player && player.volumeSupported && !isChrome; - if (usePlayerVolume) { - player.volume = volume; - } else if (AudioService.sink?.audio) { - AudioService.sink.audio.volume = volume; - } - } - onPlayerSelected: player => { - const currentPlayer = MprisController.activePlayer; - if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) { - currentPlayer.pause(); - } - MprisController.setActivePlayer(player); - root.__hideDropdowns(); - } } } diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index d9e2568b3..7f9d477e2 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -60,12 +60,26 @@ Item { } signal closeRequested - signal deviceSelected(var device) - signal playerSelected(var player) - signal volumeChanged(real volume) signal panelEntered signal panelExited + function applyVolume(volume) { + if (usePlayerVolume) { + activePlayer.volume = volume; + return; + } + if (AudioService.sink?.audio) + AudioService.sink.audio.volume = volume; + } + + function selectPlayer(player) { + const current = MprisController.activePlayer; + if (current && current !== player && current.canPause) + current.pause(); + MprisController.setActivePlayer(player); + root.closeRequested(); + } + property int __panelHoverCount: 0 readonly property bool overlayBlurActive: dropdownBlur.active @@ -100,7 +114,7 @@ Item { _wheelAccum -= notches * 120; SessionData.suppressOSDTemporarily(); const next = currentVolume + notches * AudioService.wheelVolumeStep / 100; - root.volumeChanged(Math.max(0, Math.min(1, next))); + root.applyVolume(Math.max(0, Math.min(1, next))); } readonly property Item activePanel: __activePanel @@ -257,7 +271,7 @@ Item { return; const ratio = 1.0 - (mouse.y / height); const volume = Math.max(0, Math.min(1, ratio)); - root.volumeChanged(volume); + root.applyVolume(volume); } } } @@ -464,10 +478,8 @@ Item { } return; } - if (modelData && modelData.name) { + if (modelData && modelData.name) AudioService.setDefaultSinkByName(modelData.name); - root.deviceSelected(modelData); - } } onEntered: panelAreaEntered() onExited: panelAreaExited() @@ -648,9 +660,8 @@ Item { } return; } - if (modelData?.identity) { - root.playerSelected(modelData); - } + if (modelData?.identity) + root.selectPlayer(modelData); } onEntered: panelAreaEntered() onExited: panelAreaExited() diff --git a/quickshell/Modules/DankDash/MediaPlayerIslandChrome.qml b/quickshell/Modules/DankDash/MediaPlayerIslandChrome.qml deleted file mode 100644 index 27311d3dd..000000000 --- a/quickshell/Modules/DankDash/MediaPlayerIslandChrome.qml +++ /dev/null @@ -1,540 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell.Services.Mpris -import Quickshell.Widgets -import qs.Common -import qs.Services -import qs.Widgets -import "../../Common/Format.js" as Format - -Item { - id: root - - required property var player - - property alias volumeButton: volumeButton - property alias playerSelectorButton: sourceButton - property alias audioDevicesButton: outputButton - - implicitHeight: 352 - - readonly property var activePlayer: root.player.activePlayer - readonly property color accent: root.player.accent - readonly property color onAccent: root.player.onAccent - readonly property string artUrl: TrackArtService.resolvedArtUrl - readonly property string title: MprisController.stableTitle || I18n.tr("Unknown Track", "island media player: fallback title") - readonly property string artist: MprisController.stableArtist || I18n.tr("Unknown Artist", "island media player: fallback artist") - readonly property string album: MprisController.stableAlbum || "" - - Component { - id: artBlobComponent - - MediaBlobHalo { - accentColor: root.accent - playing: root.activePlayer?.playbackState === MprisPlaybackState.Playing - } - } - - function volumeIcon() { - return root.player.getVolumeIcon(); - } - - function outputIcon() { - return root.player.getAudioDeviceIcon(AudioService.sink); - } - - ClippingRectangle { - anchors { - fill: parent - margins: 1 - } - radius: 30 - color: Theme.surfaceContainerHigh - antialiasing: true - - Image { - id: backdropImage - - anchors.fill: parent - source: root.visible ? root.artUrl : "" - asynchronous: true - cache: true - fillMode: Image.PreserveAspectCrop - sourceSize: Qt.size(320, 320) - visible: backdropImage.status === Image.Ready - opacity: 0.62 - } - - Rectangle { - anchors.fill: parent - visible: backdropImage.status === Image.Ready - - gradient: Gradient { - GradientStop { - position: 0 - color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.9) - } - - GradientStop { - position: 0.4 - color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.46) - } - - GradientStop { - position: 1 - color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.95) - } - } - } - - Rectangle { - anchors.fill: parent - color: Theme.surfaceContainerHigh - visible: backdropImage.status !== Image.Ready - } - - Rectangle { - anchors.fill: parent - visible: backdropImage.status !== Image.Ready - - gradient: Gradient { - GradientStop { - position: 0 - color: Theme.withAlpha(root.accent, 0.16) - } - - GradientStop { - position: 0.55 - color: Theme.withAlpha(root.accent, 0.04) - } - - GradientStop { - position: 1 - color: "transparent" - } - } - } - - DankIcon { - anchors.centerIn: parent - name: "music_note" - size: 120 - color: Theme.withAlpha(root.accent, 0.10) - visible: backdropImage.status !== Image.Ready - } - } - - Column { - anchors.centerIn: parent - spacing: Theme.spacingM - visible: root.player.showNoPlayerNow - z: 2 - - DankIcon { - name: "music_note" - size: Theme.iconSize * 3 - color: Theme.surfaceTextSecondary - anchors.horizontalCenter: parent.horizontalCenter - } - - StyledText { - text: I18n.tr("No Active Players", "island media player: empty state") - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceTextMedium - anchors.horizontalCenter: parent.horizontalCenter - } - } - - Item { - id: cardBody - - anchors { - fill: parent - margins: 18 - } - visible: !root.player.noneAvailable && !root.player.showNoPlayerNow - - Item { - id: headerRow - - anchors { - left: parent.left - right: parent.right - top: parent.top - } - height: 132 - clip: false - - Item { - id: artCluster - - anchors { - left: parent.left - top: parent.top - } - width: 120 - height: 120 - clip: false - z: 1 - - Loader { - anchors.centerIn: parent - width: 136 - height: 136 - z: 0 - active: root.visible && root.player.live && SettingsData.audioVisualizerEnabled - sourceComponent: artBlobComponent - } - - Rectangle { - id: artFrame - - anchors.fill: parent - z: 1 - radius: 30 - color: "transparent" - border.width: 1 - border.color: Theme.withAlpha(root.accent, 0.3) - - MediaArtwork { - anchors { - fill: parent - margins: 2 - } - cornerRadius: 28 - placeholderIconSize: 46 - artUrl: root.artUrl - } - } - } - - Column { - anchors { - left: artCluster.right - leftMargin: Theme.spacingL - right: parent.right - top: parent.top - topMargin: 2 - } - spacing: Theme.spacingXS - - StyledText { - width: parent.width - text: root.title - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeXLarge - font.weight: Font.Bold - elide: Text.ElideRight - wrapMode: Text.WordWrap - maximumLineCount: 2 - lineHeight: 1.08 - } - - StyledText { - width: parent.width - text: root.artist - color: Theme.surfaceTextMedium - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - } - - StyledText { - width: parent.width - text: root.album - color: Theme.surfaceTextSecondary - font.pixelSize: Theme.fontSizeSmall - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } - } - } - - Item { - id: iconRail - - anchors { - right: parent.right - top: headerRow.bottom - topMargin: Theme.spacingL - bottom: parent.bottom - } - width: 44 - z: 4 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingS - - TonalIconButton { - id: volumeButton - - width: 44 - height: 44 - radius: 22 - iconName: root.volumeIcon() - iconSize: 20 - iconColor: root.player.volumeAvailable && root.player.currentVolume > 0 ? root.accent : Theme.surfaceTextMedium - containerColor: Theme.withAlpha(Theme.surfaceText, 0.08) - interactive: root.player.volumeAvailable - onTriggered: root.player.toggleMute() - onScrolled: notches => root.player.adjustVolume(notches * AudioService.wheelVolumeStep) - - HoverHandler { - enabled: root.player.menusEnabled && root.player.volumeAvailable - onHoveredChanged: { - if (hovered) - root.player.triggerVolumeDropdown(); - else - root.player.dropdownButtonExited(); - } - } - } - - TonalIconButton { - id: outputButton - - width: 44 - height: 44 - radius: 22 - iconName: root.outputIcon() - iconSize: 20 - iconColor: Theme.surfaceText - containerColor: Theme.withAlpha(Theme.surfaceText, 0.08) - onTriggered: root.player.cycleNextSink() - onScrolled: notches => AudioService.cycleAudioOutputDirection(notches < 0) - - HoverHandler { - enabled: root.player.menusEnabled - onHoveredChanged: { - if (hovered) - root.player.triggerDevicesDropdown(); - else - root.player.dropdownButtonExited(); - } - } - } - - TonalIconButton { - id: sourceButton - - width: 44 - height: 44 - radius: 22 - visible: (root.player.allPlayers?.length || 0) > 0 - iconName: "assistant_device" - iconSize: 20 - iconColor: root.player.playersExpanded ? root.accent : Theme.surfaceText - containerColor: Theme.withAlpha(root.accent, root.player.playersExpanded ? 0.16 : 0.08) - onTriggered: { - if (root.player.playersExpanded) { - root.player.cycleNextPlayer(); - return; - } - root.player.triggerPlayersDropdown(); - } - - HoverHandler { - enabled: root.player.menusEnabled && sourceButton.visible - onHoveredChanged: { - if (hovered) - root.player.triggerPlayersDropdown(); - else - root.player.dropdownButtonExited(); - } - } - } - } - } - - Item { - id: playColumn - - anchors { - left: parent.left - right: iconRail.left - rightMargin: Theme.spacingM - top: headerRow.bottom - topMargin: Theme.spacingL - bottom: parent.bottom - } - - Item { - id: seekBlock - - anchors { - left: parent.left - leftMargin: iconRail.width + Theme.spacingM - right: parent.right - bottom: parent.bottom - bottomMargin: 76 - } - height: 40 - - DankSeekbar { - anchors.top: parent.top - width: parent.width - height: 22 - activePlayer: root.activePlayer - stableLength: root.player.stableLength - accentColor: root.accent - accentTrackColor: MediaAccentService.accentTrack - accentSubtleColor: MediaAccentService.accentSubtle - isSeeking: root.player.isSeeking - onIsSeekingChanged: root.player.isSeeking = isSeeking - } - - StyledText { - anchors { - left: parent.left - bottom: parent.bottom - } - text: Format.formatDuration(root.activePlayer?.position || 0) - color: root.accent - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.DemiBold - } - - StyledText { - anchors { - right: parent.right - bottom: parent.bottom - } - text: root.player.stableLength > 0 ? Format.formatDuration(root.player.stableLength) : "--:--" - color: Theme.surfaceTextSecondary - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - } - } - - Row { - anchors { - horizontalCenter: parent.horizontalCenter - horizontalCenterOffset: (iconRail.width + Theme.spacingM) / 2 - bottom: parent.bottom - bottomMargin: 2 - } - spacing: Theme.spacingS - - TonalIconButton { - anchors.verticalCenter: parent.verticalCenter - width: 40 - height: 40 - radius: 20 - visible: !!root.activePlayer?.shuffleSupported - iconName: "shuffle" - iconSize: 18 - iconColor: root.activePlayer?.shuffle ? root.accent : Theme.surfaceText - containerColor: Theme.withAlpha(root.accent, root.activePlayer?.shuffle ? 0.16 : 0) - onTriggered: { - if (root.activePlayer?.canControl && root.activePlayer?.shuffleSupported) - root.activePlayer.shuffle = !root.activePlayer.shuffle; - } - } - - TonalIconButton { - anchors.verticalCenter: parent.verticalCenter - width: 52 - height: 52 - radius: 18 - iconName: "skip_previous" - iconSize: 26 - containerColor: Theme.withAlpha(root.accent, 0.14) - interactive: !!root.activePlayer?.canGoPrevious || !!root.activePlayer?.canSeek - onTriggered: MprisController.previousOrRewind() - } - - Rectangle { - id: playHero - - anchors.verticalCenter: parent.verticalCenter - width: 68 - height: 68 - radius: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? 22 : 34 - color: root.accent - opacity: root.activePlayer?.canTogglePlaying ? 1 : 0.38 - - DankIcon { - anchors.centerIn: parent - name: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" - size: 34 - color: root.onAccent - weight: 500 - } - - StateLayer { - stateColor: root.onAccent - disabled: !root.activePlayer || !root.activePlayer.canTogglePlaying - onClicked: root.activePlayer.togglePlaying() - } - } - - TonalIconButton { - anchors.verticalCenter: parent.verticalCenter - width: 52 - height: 52 - radius: 18 - iconName: "skip_next" - iconSize: 26 - containerColor: Theme.withAlpha(root.accent, 0.14) - interactive: !!root.activePlayer?.canGoNext - onTriggered: MprisController.next() - } - - TonalIconButton { - anchors.verticalCenter: parent.verticalCenter - width: 40 - height: 40 - radius: 20 - visible: !!root.activePlayer?.loopSupported - iconName: { - if (!root.activePlayer) - return "repeat"; - return root.activePlayer.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"; - } - iconSize: 18 - iconColor: root.activePlayer && root.activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText - containerColor: Theme.withAlpha(root.accent, root.activePlayer && root.activePlayer.loopState !== MprisLoopState.None ? 0.16 : 0) - onTriggered: root.player.cycleLoopState() - } - } - } - } - - component TonalIconButton: Rectangle { - id: tonal - - property string iconName: "" - property real iconSize: 22 - property color iconColor: Theme.surfaceText - property color containerColor: Theme.withAlpha(Theme.surfaceText, 0.08) - property bool interactive: true - - signal triggered - signal scrolled(int notches) - - color: tonal.containerColor - opacity: tonal.interactive ? 1 : 0.38 - - DankIcon { - anchors.centerIn: parent - name: tonal.iconName - size: tonal.iconSize - color: tonal.iconColor - } - - StateLayer { - stateColor: tonal.iconColor - disabled: !tonal.interactive - enabled: tonal.interactive - onClicked: tonal.triggered() - onWheel: wheelEvent => { - wheelEvent.accepted = true; - tonal.scrolled(wheelEvent.angleDelta.y > 0 ? 1 : -1); - } - } - } -} diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 6a1720ec4..9fef8f4a7 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -25,10 +25,15 @@ Item { property real contentOffsetY: 0 property string section: "" property int barPosition: SettingsData.Position.Top - property string chrome: "dash" property bool live: Window.window?.visible ?? false property bool menusEnabled: true - readonly property bool islandChrome: chrome === "island" + property bool compact: false + + readonly property real contentTopMargin: compact ? Theme.spacingM : 20 + readonly property real contentHeight: compact ? 320 : 370 + readonly property real artAreaHeight: compact ? 150 : 200 + readonly property real sideButtonTop: contentTopMargin + artAreaHeight / 2 + 10 + readonly property real sideButtonStep: 55 readonly property color accent: MediaAccentService.accent readonly property color onAccent: MediaAccentService.onAccent @@ -121,7 +126,7 @@ Item { } implicitWidth: SettingsData.showWeekNumber ? 736 : 700 - implicitHeight: chromeLoader.item ? chromeLoader.item.implicitHeight : 410 + implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2 Connections { target: activePlayer @@ -197,18 +202,8 @@ Item { } function dropdownAnchor(button) { - if (!button) - return null; - if (root.islandChrome) { - const rightEdge = root.mapToItem(null, root.width, 0).x; - const point = button.mapToItem(null, 0, button.height / 2); - return { - "pos": Qt.point(rightEdge + Theme.spacingS, point.y), - "rightEdge": true - }; - } const buttonsOnRight = !root.isRightEdge; - const btnY = button.mapToItem(root, 0, button.height / 2).y; + const btnY = button.y + button.height / 2; return { "pos": Qt.point(buttonsOnRight ? (root.popoutX + root.popoutWidth) : root.popoutX, root.popoutY + root.contentOffsetY + btnY), "rightEdge": buttonsOnRight @@ -216,42 +211,27 @@ Item { } function triggerVolumeDropdown() { - if (!root.menusEnabled || !volumeAvailable) - return; - if (volumeExpanded) - return; - const button = chromeLoader.item?.volumeButton; - const anchor = dropdownAnchor(button); - if (!anchor) + if (!root.menusEnabled || !volumeAvailable || volumeExpanded) return; + const anchor = dropdownAnchor(volumeButton); hideDropdowns(); volumeExpanded = true; showVolumeDropdown(anchor.pos, targetScreen, anchor.rightEdge, activePlayer, allPlayers); } function triggerPlayersDropdown() { - if (!root.menusEnabled) - return; - const button = chromeLoader.item?.playerSelectorButton; - const anchor = dropdownAnchor(button); - if (!anchor) - return; - if (playersExpanded) + if (!root.menusEnabled || playersExpanded) return; + const anchor = dropdownAnchor(playerSelectorButton); hideDropdowns(); playersExpanded = true; showPlayersDropdown(anchor.pos, targetScreen, anchor.rightEdge, activePlayer, allPlayers); } function triggerDevicesDropdown() { - if (!root.menusEnabled) - return; - const button = chromeLoader.item?.audioDevicesButton; - const anchor = dropdownAnchor(button); - if (!anchor) - return; - if (devicesExpanded) + if (!root.menusEnabled || devicesExpanded) return; + const anchor = dropdownAnchor(audioDevicesButton); hideDropdowns(); devicesExpanded = true; showAudioDevicesDropdown(anchor.pos, targetScreen, anchor.rightEdge); @@ -392,33 +372,8 @@ Item { onTriggered: activePlayer?.positionChanged() } - Loader { - id: chromeLoader - - anchors.fill: parent - sourceComponent: root.islandChrome ? islandChromeComponent : dashChromeComponent - } - - Component { - id: islandChromeComponent - - MediaPlayerIslandChrome { - player: root - } - } - - Component { - id: dashChromeComponent - - Item { - anchors.fill: parent - implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2 - property alias volumeButton: volumeButton - property alias playerSelectorButton: playerSelectorButton - property alias audioDevicesButton: audioDevicesButton - - Item { - id: bgContainer + Item { + id: bgContainer anchors.fill: parent // Fall back to the live mpris url so the background is never blank. @@ -564,15 +519,15 @@ Item { ColumnLayout { id: playerContent width: 484 - height: 370 + height: root.contentHeight spacing: Theme.spacingXS anchors.top: parent.top - anchors.topMargin: 20 + anchors.topMargin: root.contentTopMargin anchors.horizontalCenter: parent.horizontalCenter Item { width: parent.width - height: 200 + height: root.artAreaHeight clip: false DankAlbumArt { @@ -876,21 +831,7 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - if (activePlayer && activePlayer.canControl && activePlayer.loopSupported) { - switch (activePlayer.loopState) { - case MprisLoopState.None: - activePlayer.loopState = MprisLoopState.Playlist; - break; - case MprisLoopState.Playlist: - activePlayer.loopState = MprisLoopState.Track; - break; - case MprisLoopState.Track: - activePlayer.loopState = MprisLoopState.None; - break; - } - } - } + onClicked: root.cycleLoopState() } } } @@ -906,7 +847,7 @@ Item { height: 40 radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM - y: 185 + y: root.sideButtonTop + root.sideButtonStep color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) border.color: Theme.outlineStrong border.width: 1 @@ -927,39 +868,14 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { if (playersExpanded) { - const players = (root.allPlayers || []).filter(p => p && !MprisController.isIdle(p)); - if (players.length > 1) { - let currentIndex = -1; - for (let i = 0; i < players.length; i++) { - if (players[i] === root.activePlayer) { - currentIndex = i; - break; - } - } - const nextIndex = (currentIndex + 1) % players.length; - MprisController.setActivePlayer(players[nextIndex]); - } + root.cycleNextPlayer(); return; } - hideDropdowns(); - playersExpanded = true; - const buttonsOnRight = !isRightEdge; - const btnY = playerSelectorButton.y + playerSelectorButton.height / 2; - const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; - const screenY = popoutY + contentOffsetY + btnY; - showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); + root.triggerPlayersDropdown(); } onEntered: { dropdownButtonEntered(); - if (playersExpanded) - return; - hideDropdowns(); - playersExpanded = true; - const buttonsOnRight = !isRightEdge; - const btnY = playerSelectorButton.y + playerSelectorButton.height / 2; - const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; - const screenY = popoutY + contentOffsetY + btnY; - showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); + root.triggerPlayersDropdown(); } onExited: { if (playersExpanded) @@ -974,7 +890,7 @@ Item { height: 40 radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM - y: 130 + y: root.sideButtonTop color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium border.width: 1 @@ -995,23 +911,13 @@ Item { cursorShape: Qt.PointingHandCursor onEntered: { dropdownButtonEntered(); - if (volumeExpanded) - return; - hideDropdowns(); - volumeExpanded = true; - const buttonsOnRight = !isRightEdge; - const btnY = volumeButton.y + volumeButton.height / 2; - const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; - const screenY = popoutY + contentOffsetY + btnY; - showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers); + root.triggerVolumeDropdown(); } onExited: { if (volumeExpanded) dropdownButtonExited(); } - onClicked: { - toggleMute(); - } + onClicked: root.toggleMute() property real wheelAccum: 0 onWheel: wheelEvent => { wheelEvent.accepted = true; @@ -1031,7 +937,7 @@ Item { height: 40 radius: 20 x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM - y: 240 + y: root.sideButtonTop + root.sideButtonStep * 2 color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) border.color: Theme.outlineStrong border.width: 1 @@ -1050,67 +956,35 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton - onPressed: mouse => { - if (mouse.button === Qt.RightButton) { - mouse.accepted = true; - } - } onWheel: wheelEvent => { const delta = wheelEvent.angleDelta.y; - if (delta !== 0) { - AudioService.cycleAudioOutputDirection(delta < 0); - wheelEvent.accepted = true; - } + if (delta === 0) + return; + AudioService.cycleAudioOutputDirection(delta < 0); + wheelEvent.accepted = true; } onClicked: mouse => { if (mouse.button === Qt.RightButton) { - if (AudioService.sink?.audio) { - SessionData.suppressOSDTemporarily(); - AudioService.sink.audio.muted = !AudioService.sink.audio.muted; - } + if (!AudioService.sink?.audio) + return; + SessionData.suppressOSDTemporarily(); + AudioService.sink.audio.muted = !AudioService.sink.audio.muted; return; } if (devicesExpanded) { - const sinks = AudioService.getAvailableSinks(); - if (sinks && sinks.length > 1) { - let currentIndex = -1; - for (let i = 0; i < sinks.length; i++) { - if (sinks[i]?.name === AudioService.sink?.name) { - currentIndex = i; - break; - } - } - const nextIndex = (currentIndex + 1) % sinks.length; - AudioService.setSink(sinks[nextIndex]); - } + root.cycleNextSink(); return; } - hideDropdowns(); - devicesExpanded = true; - const buttonsOnRight = !isRightEdge; - const btnY = audioDevicesButton.y + audioDevicesButton.height / 2; - const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; - const screenY = popoutY + contentOffsetY + btnY; - showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); + root.triggerDevicesDropdown(); } onEntered: { dropdownButtonEntered(); - if (devicesExpanded) - return; - hideDropdowns(); - devicesExpanded = true; - const buttonsOnRight = !isRightEdge; - const btnY = audioDevicesButton.y + audioDevicesButton.height / 2; - const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX; - const screenY = popoutY + contentOffsetY + btnY; - showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight); + root.triggerDevicesDropdown(); } onExited: { if (devicesExpanded) dropdownButtonExited(); } } - } - } } } diff --git a/quickshell/Modules/DankIsland/Activities/DashTabFace.qml b/quickshell/Modules/DankIsland/Activities/DashTabFace.qml new file mode 100644 index 000000000..72bf182d8 --- /dev/null +++ b/quickshell/Modules/DankIsland/Activities/DashTabFace.qml @@ -0,0 +1,58 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Widgets + +FocusScope { + id: root + + required property var controller + required property string activityId + required property Component tabComponent + property real inset: Theme.spacingM + property bool contentStaged: false + + readonly property bool live: root.controller.expanded && root.controller.activeActivity === root.activityId + readonly property var tab: tabLoader.item + + clip: true + + function focusFace() { + root.forceActiveFocus(); + return true; + } + + function forwardKey(event) { + if (!root.tab || typeof root.tab.handleKeyEvent !== "function") + return false; + return root.tab.handleKeyEvent(event) === true; + } + + Loader { + id: tabLoader + + anchors { + fill: parent + margins: root.inset + } + active: root.contentStaged + asynchronous: true + visible: status === Loader.Ready + sourceComponent: root.tabComponent + onLoaded: { + if (root.live) + root.focusFace(); + } + } + + DankSpinner { + anchors.centerIn: parent + size: 40 + visible: !tabLoader.visible + } + + Keys.onPressed: event => event.accepted = root.forwardKey(event) + + Component.onCompleted: root.contentStaged = true +} diff --git a/quickshell/Modules/DankIsland/Activities/HomeExpanded.qml b/quickshell/Modules/DankIsland/Activities/HomeExpanded.qml index b25ea64a3..58bdea769 100644 --- a/quickshell/Modules/DankIsland/Activities/HomeExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/HomeExpanded.qml @@ -3,62 +3,21 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Common import qs.Modules.DankDash -import qs.Widgets -FocusScope { +DashTabFace { id: root - required property var controller - - property bool contentStaged: false - - readonly property real stageWidth: root.controller.homeExpandedTarget.width - 2 - readonly property real stageHeight: root.controller.homeExpandedTarget.height - 2 - readonly property real faceInset: Theme.spacingM - readonly property var overviewTab: tabLoader.item - - clip: true - - function focusOverview() { - root.forceActiveFocus(); - } - - Loader { - id: tabLoader - - x: root.faceInset - width: root.stageWidth - root.faceInset * 2 - height: tabLoader.item?.implicitHeight || 410 - y: Math.round((root.stageHeight - height) / 2) - active: root.contentStaged - asynchronous: true - visible: status === Loader.Ready - sourceComponent: overviewTabComponent - } - - Component { - id: overviewTabComponent - + activityId: "home" + tabComponent: Component { OverviewTab { - live: root.controller.expanded && root.controller.activeActivity === "home" + live: root.live onCloseDash: root.controller.requestCollapse() - onNavFocusRequested: root.focusOverview() + onNavFocusRequested: root.focusFace() onSwitchToMediaTab: root.controller.requestActivity("media", true, true) onSwitchToWeatherTab: { if (SettingsData.weatherEnabled) root.controller.requestWeather(false); } - Component.onCompleted: root.focusOverview() } } - - DankSpinner { - anchors.centerIn: parent - size: 40 - visible: !tabLoader.visible - } - - Keys.onPressed: event => event.accepted = root.overviewTab?.handleKeyEvent(event) === true - - Component.onCompleted: root.contentStaged = true } diff --git a/quickshell/Modules/DankIsland/Activities/LauncherExpanded.qml b/quickshell/Modules/DankIsland/Activities/LauncherExpanded.qml index f0b795791..bb9a753c7 100644 --- a/quickshell/Modules/DankIsland/Activities/LauncherExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/LauncherExpanded.qml @@ -19,8 +19,9 @@ FocusScope { clip: true - function focusSearch() { + function focusFace() { launcherContent.searchField.forceActiveFocus(); + return true; } function initializeSession() { @@ -42,7 +43,7 @@ FocusScope { launcherController.performSearch(); launcherContent.resetScroll(); - root.focusSearch(); + root.focusFace(); if (!root.controller.launcherPendingQuery) { launcherContent.searchField.selectAll(); return; diff --git a/quickshell/Modules/DankIsland/Activities/MediaCompact.qml b/quickshell/Modules/DankIsland/Activities/MediaCompact.qml index 612c125d4..81f4becc7 100644 --- a/quickshell/Modules/DankIsland/Activities/MediaCompact.qml +++ b/quickshell/Modules/DankIsland/Activities/MediaCompact.qml @@ -69,7 +69,7 @@ Item { } width: root.artworkSize height: width - mediaModel: root.mediaModel + artUrl: root.mediaModel.artUrl } Row { diff --git a/quickshell/Modules/DankIsland/Activities/MediaExpanded.qml b/quickshell/Modules/DankIsland/Activities/MediaExpanded.qml index 09f077866..f1c0a66e6 100644 --- a/quickshell/Modules/DankIsland/Activities/MediaExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/MediaExpanded.qml @@ -2,89 +2,49 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Modules.DankDash -import qs.Widgets -FocusScope { +DashTabFace { id: root - required property var controller property var effectiveScreen: null property real alignedX: 0 property real alignedY: 0 property real alignedWidth: 0 - property real alignedHeight: 0 - - property bool contentStaged: false property bool geometrySettled: false - readonly property bool menusEnabled: root.controller.expanded && geometrySettled - readonly property var playerTab: tabLoader.item + readonly property bool menusEnabled: root.live && root.geometrySettled - signal showVolumeDropdown(point pos) - signal showAudioDevicesDropdown(point pos) - signal showPlayersDropdown(point pos) - signal hideDropdowns + signal dropdownRequested(int dropdownType, point pos) + signal dropdownsHidden + signal dropdownHoverStarted signal dropdownHoverEnded - clip: true - - function focusPlayer() { - root.forceActiveFocus(); - return true; - } - - function resetDropdownStates() { - root.playerTab?.resetDropdownStates(); - } - - Loader { - id: tabLoader - - anchors.fill: parent - active: root.contentStaged - asynchronous: true - visible: status === Loader.Ready - sourceComponent: playerTabComponent - } - - Component { - id: playerTabComponent - + activityId: "media" + tabComponent: Component { MediaPlayerTab { - chrome: "island" - live: root.controller.expanded && root.controller.activeActivity === "media" + compact: true + live: root.live + menusEnabled: root.menusEnabled targetScreen: root.effectiveScreen popoutX: root.alignedX popoutY: root.alignedY popoutWidth: root.alignedWidth - popoutHeight: root.alignedHeight - contentOffsetY: 0 - section: "right" - menusEnabled: root.menusEnabled - onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => root.showVolumeDropdown(pos) - onShowAudioDevicesDropdown: (pos, screen, rightEdge) => root.showAudioDevicesDropdown(pos) - onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => root.showPlayersDropdown(pos) - onHideDropdowns: root.hideDropdowns() + contentOffsetY: root.inset + section: "left" + onShowVolumeDropdown: pos => root.dropdownRequested(1, pos) + onShowAudioDevicesDropdown: pos => root.dropdownRequested(2, pos) + onShowPlayersDropdown: pos => root.dropdownRequested(3, pos) + onHideDropdowns: root.dropdownsHidden() + onDropdownButtonEntered: root.dropdownHoverStarted() onDropdownButtonExited: root.dropdownHoverEnded() - Component.onCompleted: root.focusPlayer() } } - DankSpinner { - anchors.centerIn: parent - size: 40 - visible: !tabLoader.visible - } - - Keys.onPressed: event => event.accepted = root.playerTab?.handleKeyEvent(event) === true - Connections { target: root.controller function onMediaDropdownOpenChanged() { if (!root.controller.mediaDropdownOpen) - root.resetDropdownStates(); + root.tab?.resetDropdownStates(); } } - - Component.onCompleted: root.contentStaged = true } diff --git a/quickshell/Modules/DankIsland/Activities/NotificationCenterExpanded.qml b/quickshell/Modules/DankIsland/Activities/NotificationCenterExpanded.qml index bc69c865f..6051db109 100644 --- a/quickshell/Modules/DankIsland/Activities/NotificationCenterExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/NotificationCenterExpanded.qml @@ -17,7 +17,7 @@ FocusScope { clip: true - function focusList() { + function focusFace() { root.forceActiveFocus(); keyboardController.rebuildFlatNavigation(); return true; diff --git a/quickshell/Modules/DankIsland/Activities/WallpaperExpanded.qml b/quickshell/Modules/DankIsland/Activities/WallpaperExpanded.qml index 5cfe68535..0f79059db 100644 --- a/quickshell/Modules/DankIsland/Activities/WallpaperExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/WallpaperExpanded.qml @@ -1,31 +1,26 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.Common import qs.Modules.DankDash -import qs.Widgets -FocusScope { +DashTabFace { id: root - required property var controller property var effectiveScreen: null - property bool contentStaged: false - - readonly property real stageWidth: root.controller.wallpaperExpandedTarget.width - 2 - readonly property real stageHeight: root.controller.wallpaperExpandedTarget.height - 2 - readonly property var wallpaperTab: tabLoader.item - - clip: true - - function focusGrid() { - root.forceActiveFocus(); + activityId: "wallpaper" + tabComponent: Component { + WallpaperTab { + active: true + pagerCachePages: 0 + targetScreen: root.effectiveScreen + parentPopout: hostContract + } } function beginSession() { - root.wallpaperTab?.collapseSearch(); - root.focusGrid(); + root.tab?.collapseSearch(); + root.focusFace(); } QtObject { @@ -36,39 +31,6 @@ FocusScope { onCustomKeyboardFocusChanged: root.controller.keyboardYielded = hostContract.customKeyboardFocus !== null } - Loader { - id: tabLoader - - x: Theme.spacingM - y: Theme.spacingM - width: root.stageWidth - Theme.spacingM * 2 - height: root.stageHeight - Theme.spacingM - Theme.spacingS - active: root.contentStaged - asynchronous: true - visible: status === Loader.Ready - sourceComponent: wallpaperTabComponent - } - - Component { - id: wallpaperTabComponent - - WallpaperTab { - active: true - pagerCachePages: 0 - targetScreen: root.effectiveScreen - parentPopout: hostContract - Component.onCompleted: root.focusGrid() - } - } - - DankSpinner { - anchors.centerIn: parent - size: 40 - visible: !tabLoader.visible - } - - Keys.onPressed: event => event.accepted = root.wallpaperTab?.handleKeyEvent(event) === true - Connections { target: root.controller @@ -78,11 +40,7 @@ FocusScope { } } - Component.onCompleted: { - root.contentStaged = true; - root.controller.markVisualsReady("wallpaper"); - } - + Component.onCompleted: root.controller.markVisualsReady("wallpaper") Component.onDestruction: { root.controller.keyboardYielded = false; root.controller.setVisualsReady("wallpaper", false); diff --git a/quickshell/Modules/DankIsland/Activities/WeatherExpanded.qml b/quickshell/Modules/DankIsland/Activities/WeatherExpanded.qml index bdaab4436..fe886a527 100644 --- a/quickshell/Modules/DankIsland/Activities/WeatherExpanded.qml +++ b/quickshell/Modules/DankIsland/Activities/WeatherExpanded.qml @@ -1,61 +1,18 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.Common import qs.Modules.DankDash -import qs.Widgets -FocusScope { +DashTabFace { id: root - required property var controller - - property bool contentStaged: false - - readonly property real stageWidth: root.controller.weatherExpandedTarget.width - 2 - readonly property real stageHeight: root.controller.weatherExpandedTarget.height - 2 - readonly property real faceInset: Theme.spacingM - - clip: true - - function focusWeather() { - root.forceActiveFocus(); - } - - Loader { - id: tabLoader - - x: root.faceInset - y: root.faceInset - width: root.stageWidth - root.faceInset * 2 - height: root.stageHeight - root.faceInset * 2 - active: root.contentStaged - asynchronous: true - visible: status === Loader.Ready - sourceComponent: weatherTabComponent - } - - Component { - id: weatherTabComponent - + activityId: "weather" + tabComponent: Component { WeatherTab { - live: root.controller.expanded && root.controller.activeActivity === "weather" - Component.onCompleted: root.focusWeather() + live: root.live } } - DankSpinner { - anchors.centerIn: parent - size: 40 - visible: !tabLoader.visible - } - - Component.onCompleted: { - root.contentStaged = true; - root.controller.markVisualsReady("weather"); - } - - Component.onDestruction: { - root.controller.setVisualsReady("weather", false); - } + Component.onCompleted: root.controller.markVisualsReady("weather") + Component.onDestruction: root.controller.setVisualsReady("weather", false) } diff --git a/quickshell/Modules/DankIsland/DankIslandSurface.qml b/quickshell/Modules/DankIsland/DankIslandSurface.qml index b0b14c7ea..a639a0aeb 100644 --- a/quickshell/Modules/DankIsland/DankIslandSurface.qml +++ b/quickshell/Modules/DankIsland/DankIslandSurface.qml @@ -353,17 +353,6 @@ Item { onCloseRequested: root.hideMediaDropdowns() onPanelEntered: root.stopMediaDropdownCloseTimer() onPanelExited: root.startMediaDropdownCloseTimer() - onVolumeChanged: volume => { - const player = MprisController.activePlayer; - const identity = (player?.identity ?? "").toLowerCase(); - const chrome = identity.includes("chrome") || identity.includes("chromium"); - if (player && player.volumeSupported && !chrome) - player.volume = volume; - else if (AudioService.sink?.audio) - AudioService.sink.audio.volume = volume; - } - onDeviceSelected: device => AudioService.setSink(device) - onPlayerSelected: player => MprisController.setActivePlayer(player) } Component { @@ -401,11 +390,9 @@ Item { alignedX: root.targetVisualX alignedY: root.targetVisualY alignedWidth: root.targetVisualWidth - alignedHeight: root.targetVisualHeight - onShowVolumeDropdown: pos => root.openMediaDropdown(1, pos) - onShowAudioDevicesDropdown: pos => root.openMediaDropdown(2, pos) - onShowPlayersDropdown: pos => root.openMediaDropdown(3, pos) - onHideDropdowns: root.hideMediaDropdowns() + onDropdownRequested: (dropdownType, pos) => root.openMediaDropdown(dropdownType, pos) + onDropdownsHidden: root.hideMediaDropdowns() + onDropdownHoverStarted: root.stopMediaDropdownCloseTimer() onDropdownHoverEnded: root.startMediaDropdownCloseTimer() } } diff --git a/quickshell/Modules/DankIsland/IslandContentHost.qml b/quickshell/Modules/DankIsland/IslandContentHost.qml index f8b1802a3..fdd4fb153 100644 --- a/quickshell/Modules/DankIsland/IslandContentHost.qml +++ b/quickshell/Modules/DankIsland/IslandContentHost.qml @@ -78,34 +78,20 @@ Item { return Math.max(incoming, outgoing); } + readonly property var focusableFaces: ({ + "home": homeExpandedLoader, + "media": mediaExpandedLoader, + "launcher": launcherExpandedLoader, + "wallpaper": wallpaperExpandedLoader, + "weather": weatherExpandedLoader, + "notificationcenter": notificationCenterExpandedLoader + }) + function requestActivityFocus() { - switch (root.activityId) { - case "launcher": - if (!launcherExpandedLoader.item) - return false; - launcherExpandedLoader.item.focusSearch(); - return true; - case "home": - if (!homeExpandedLoader.item) - return false; - homeExpandedLoader.item.focusOverview(); - return true; - case "media": - return mediaExpandedLoader.item?.focusPlayer() === true; - case "wallpaper": - if (!wallpaperExpandedLoader.item) - return false; - wallpaperExpandedLoader.item.focusGrid(); - return true; - case "weather": - if (!weatherExpandedLoader.item) - return false; - weatherExpandedLoader.item.focusWeather(); - return true; - case "notificationcenter": - return notificationCenterExpandedLoader.item?.focusList() === true; - } - return false; + const face = root.focusableFaces[root.activityId]?.item; + if (!face || typeof face.focusFace !== "function") + return false; + return face.focusFace() === true; } function latchHomeExpanded() { diff --git a/quickshell/Modules/DankIsland/IslandController.qml b/quickshell/Modules/DankIsland/IslandController.qml index f5ab96558..4ec7735ff 100644 --- a/quickshell/Modules/DankIsland/IslandController.qml +++ b/quickshell/Modules/DankIsland/IslandController.qml @@ -301,13 +301,11 @@ QtObject { } readonly property var homeCompactTarget: pillTarget(homeCompactWidth, homeCompactFaceHeight) - readonly property var homeExpandedTarget: sheetTarget(SettingsData.showWeekNumber ? 736 : 700, 452) readonly property var mediaCompactTarget: pillTarget(mediaCompactWidth, compactFaceHeight) - readonly property var mediaExpandedTarget: sheetTarget(600, 352) + readonly property var dashSheetTarget: sheetTarget(SettingsData.showWeekNumber ? 736 : 700, 452) + readonly property var mediaExpandedTarget: sheetTarget(600, 384) readonly property var launcherExpandedTarget: sheetTarget(680, 560) readonly property var controlCenterExpandedTarget: sheetTarget(580, controlCenterHeight) - readonly property var wallpaperExpandedTarget: sheetTarget(700, 452) - readonly property var weatherExpandedTarget: sheetTarget(SettingsData.showWeekNumber ? 736 : 700, 452) readonly property var systemCompactTarget: pillTarget(SettingsData.osdAlwaysShowValue ? 330 : 282, compactFaceHeight) readonly property var systemExpandedTarget: sheetTarget(460, 176) readonly property var notificationCompactTarget: pillTarget(Math.ceil(Math.max(notificationCompactMinWidth, Math.min(notificationCompactMaxWidth, notificationContentWidth))), compactFaceHeight) @@ -348,16 +346,12 @@ QtObject { return launcherExpandedTarget; case "controlcenter": return controlCenterExpandedTarget; - case "wallpaper": - return wallpaperExpandedTarget; - case "weather": - return weatherExpandedTarget; case "notificationcenter": return notificationCenterExpandedTarget; case "media": return mediaExpandedTarget; } - return homeExpandedTarget; + return dashSheetTarget; } readonly property var expandedTarget: expandedTargetFor(activeActivity) diff --git a/quickshell/Widgets/DankAlbumArt.qml b/quickshell/Widgets/DankAlbumArt.qml index bde81323b..782a9a91c 100644 --- a/quickshell/Widgets/DankAlbumArt.qml +++ b/quickshell/Widgets/DankAlbumArt.qml @@ -1,6 +1,7 @@ import QtQuick import Quickshell.Services.Mpris import qs.Common +import qs.Services Item { id: root diff --git a/quickshell/Widgets/MediaArtwork.qml b/quickshell/Widgets/MediaArtwork.qml index f86e79f60..9141a8f82 100644 --- a/quickshell/Widgets/MediaArtwork.qml +++ b/quickshell/Widgets/MediaArtwork.qml @@ -7,8 +7,7 @@ import qs.Common Item { id: root - property var mediaModel: null - property string artUrl: mediaModel?.artUrl ?? "" + property string artUrl: "" property real placeholderIconSize: Math.min(width, height) * 0.46 property real cornerRadius: Math.min(width, height) * 0.22 readonly property int artPixelSize: Math.max(1, Math.round(Math.max(width, height) * 2)) diff --git a/quickshell/Widgets/MediaBlobHalo.qml b/quickshell/Widgets/MediaBlobHalo.qml deleted file mode 100644 index 35231f197..000000000 --- a/quickshell/Widgets/MediaBlobHalo.qml +++ /dev/null @@ -1,157 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Common -import qs.Services - -Item { - id: root - - property color accentColor: Theme.primary - property bool playing: false - property real animationScale: 1.0 - - readonly property bool onScreen: visible && enabled && (Window.window?.visible ?? false) - readonly property bool available: CavaService.cavaAvailable && SettingsData.audioVisualizerEnabled - readonly property bool blobActive: root.onScreen && root.playing && root.available - - readonly property real blobBaseRadiusFactor: 0.43 - readonly property real blobAmplitudeFactor: 0.115 - readonly property real blobOvershoot: 1.15 - readonly property real blobEnergySensitivity: 1.15 - readonly property real cavaFullScale: 45 - readonly property real blobAttack: 0.75 - readonly property real blobRelease: 0.2 - readonly property real blobBeatBoost: 2.5 - readonly property real blobBeatKick: 4 - readonly property real blobOnsetThreshold: 1.4 - readonly property real blobSpringStiffness: 220 - readonly property real blobSpringDamping: 19 - readonly property real blobMorphSpeed: 0.05 - readonly property real blobMorphBoost: 1.7 - readonly property real blobSpinSpeed: 0.03 - readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35] - - property var smoothedBands: [0, 0, 0, 0, 0, 0] - property var slowBands: [0, 0, 0, 0, 0, 0] - property var bandTargets: [0, 0, 0, 0, 0, 0] - property var bandDisplay: [0, 0, 0, 0, 0, 0] - property var prevLevels: [0, 0, 0, 0, 0, 0] - property real fluxAvg: 0.02 - property real loudCtx: 0.1 - property int beatCooldown: 0 - property real energyTarget: 0 - property real energyPos: 0 - property real energyVel: 0 - - function updateBands() { - const vals = CavaService.values; - if (!vals || vals.length < 6) - return; - - const s = smoothedBands; - const slow = slowBands; - const out = bandTargets; - const prev = prevLevels; - const w = fluxWeights; - let flux = 0; - for (let i = 0; i < 6; i++) { - const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale; - flux += Math.max(0, level - prev[i]) * w[i]; - prev[i] = level; - const alpha = level > s[i] ? blobAttack : blobRelease; - s[i] += alpha * (level - s[i]); - slow[i] += 0.05 * (level - slow[i]); - const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost; - out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity); - } - - const ratio = flux / Math.max(fluxAvg, 0.004); - fluxAvg += 0.06 * (flux - fluxAvg); - if (beatCooldown > 0) { - beatCooldown--; - } else if (ratio > blobOnsetThreshold && flux > 0.008) { - energyVel += blobBeatKick * Math.min(2.5, ratio - 1); - beatCooldown = 3; - } - - const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]); - loudCtx += 0.03 * (loud - loudCtx); - const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1); - energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge)); - } - - function stepBlob(dt) { - energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt; - energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt)); - blobEffect.energy = energyPos; - - const d = bandDisplay; - const t = bandTargets; - const f = Math.min(1, dt * 14); - for (let i = 0; i < 6; i++) - d[i] += f * (t[i] - d[i]); - blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]); - blobEffect.bandsB = Qt.vector2d(d[4], d[5]); - - const speed = 1 + energyPos * blobMorphBoost; - blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1; - blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718; - } - - Loader { - active: root.blobActive - sourceComponent: Component { - Ref { - service: CavaService - } - } - } - - Connections { - target: CavaService - enabled: root.blobActive - - function onValuesChanged() { - root.updateBands(); - } - } - - Timer { - running: blobEffect.visible && root.onScreen - interval: 33 - repeat: true - onTriggered: root.stepBlob(0.033) - } - - ShaderEffect { - id: blobEffect - - readonly property real span: Math.min(root.width, root.height) - - width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4 - height: width - anchors.centerIn: parent - visible: root.blobActive || activation > 0.004 - - property real phase: 0 - property real spin: 0 - property real sizePx: width - property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale - property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale - property real activation: root.blobActive ? 1 : 0 - property real energy: 0 - property vector4d bandsA: Qt.vector4d(0, 0, 0, 0) - property vector2d bandsB: Qt.vector2d(0, 0) - property vector4d fillColor: Qt.vector4d(root.accentColor.r, root.accentColor.g, root.accentColor.b, root.accentColor.a) - - Behavior on activation { - NumberAnimation { - duration: 550 - easing.type: Easing.InOutQuad - } - } - - fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/blob.frag.qsb") - } -}