island: re-use media controls from dankdash

This commit is contained in:
bbedward committed 2026-09-02 13:36:09 -04:00
1 parent a781b7b236
commit de24b1515a
18 files changed
+187 -1157

No files matched your search

@@ -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();
}
}
}
@@ -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()
@@ -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);
}
}
}
}
+40 -166
View File
@@ -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();
}
}
}
}
}
}
@@ -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
}
@@ -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
}
@@ -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;
@@ -69,7 +69,7 @@ Item {
}
width: root.artworkSize
height: width
mediaModel: root.mediaModel
artUrl: root.mediaModel.artUrl
}
Row {
@@ -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
}
@@ -17,7 +17,7 @@ FocusScope {
clip: true
function focusList() {
function focusFace() {
root.forceActiveFocus();
keyboardController.rebuildFlatNavigation();
return true;
@@ -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);
@@ -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)
}
@@ -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()
}
}
@@ -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() {
@@ -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)
+1
View File
@@ -1,6 +1,7 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
Item {
id: root
+1 -2
View File
@@ -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))
-157
View File
@@ -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")
}
}