diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 38bea8b6b..6e950b172 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -263,11 +263,11 @@ Singleton { property real barHeight: 40 property real barCenterSideModuleWidth: 360 property real barPreferredSideSectionWidth: 400 - property real sidebarWidth: 450 + property real sidebarWidth: 460 property real sidebarWidthExtended: 750 property real osdWidth: 200 - property real mediaControlsWidth: 430 - property real mediaControlsHeight: 150 + property real mediaControlsWidth: 440 + property real mediaControlsHeight: 160 property real notificationPopupWidth: 410 property real searchWidthCollapsed: 260 property real searchWidth: 450 diff --git a/.config/quickshell/modules/common/widgets/StyledSlider.qml b/.config/quickshell/modules/common/widgets/StyledSlider.qml index 5a61182ac..23a6179f4 100644 --- a/.config/quickshell/modules/common/widgets/StyledSlider.qml +++ b/.config/quickshell/modules/common/widgets/StyledSlider.qml @@ -17,6 +17,9 @@ Slider { property real handleHeight: 44 * scale property real handleLimit: slider.backgroundDotMargins * scale property real trackHeight: 15 * scale + property color highlightColor: Appearance.m3colors.m3primary + property color trackColor: Appearance.m3colors.m3secondaryContainer + property color handleColor: Appearance.m3colors.m3onSecondaryContainer property real limitedHandleRangeWidth: (slider.availableWidth - handleWidth - slider.handleLimit * 2) property string tooltipContent: `${Math.round(value * 100)}%` @@ -54,7 +57,7 @@ Slider { anchors.left: parent.left width: slider.handleLimit + slider.visualPosition * slider.limitedHandleRangeWidth - (slider.handleMargins + slider.handleWidth / 2) height: trackHeight - color: Appearance.m3colors.m3primary + color: slider.highlightColor topLeftRadius: Appearance.rounding.full bottomLeftRadius: Appearance.rounding.full topRightRadius: Appearance.rounding.unsharpen @@ -67,7 +70,7 @@ Slider { anchors.right: parent.right width: slider.handleLimit + (1 - slider.visualPosition) * slider.limitedHandleRangeWidth - (slider.handleMargins + slider.handleWidth / 2) height: trackHeight - color: Appearance.m3colors.m3secondaryContainer + color: slider.trackColor topLeftRadius: Appearance.rounding.unsharpen bottomLeftRadius: Appearance.rounding.unsharpen topRightRadius: Appearance.rounding.full @@ -82,7 +85,7 @@ Slider { width: slider.backgroundDotSize height: slider.backgroundDotSize radius: Appearance.rounding.full - color: Appearance.m3colors.m3onSecondaryContainer + color: slider.handleColor } } @@ -93,7 +96,7 @@ Slider { implicitWidth: slider.handleWidth implicitHeight: slider.handleHeight radius: Appearance.rounding.full - color: Appearance.m3colors.m3onSecondaryContainer + color: slider.handleColor Behavior on implicitWidth { NumberAnimation { diff --git a/.config/quickshell/modules/mediaControls/MediaControls.qml b/.config/quickshell/modules/mediaControls/MediaControls.qml index 01d9c84d3..6d640b4dd 100644 --- a/.config/quickshell/modules/mediaControls/MediaControls.qml +++ b/.config/quickshell/modules/mediaControls/MediaControls.qml @@ -22,11 +22,25 @@ Scope { readonly property real osdWidth: Appearance.sizes.osdWidth readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight - property real contentPadding: 12 + property real contentPadding: 13 property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1 property real artRounding: Appearance.rounding.verysmall property string baseCoverArtDir: FileUtils.trimFileProtocol(`${XdgDirectories.cache}/media/coverart`) + // property bool hasPlasmaIntegration: true + function isRealPlayer(player) { + // return true + return ( + // Remove unecessary native buses from browsers if there's plasma integration + // !(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.firefox')) && + // !(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.chromium')) && + // playerctld just copies other buses and we don't need duplicates + !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && + // Non-instance mpd bus + !(player.dbusName?.endsWith('.mpd') && !player.busName.endsWith('MediaPlayer2.mpd')) + ); + } + Component.onCompleted: { Hyprland.dispatch(`exec rm -rf ${baseCoverArtDir} && mkdir -p ${baseCoverArtDir}`) } @@ -53,6 +67,9 @@ Scope { top: true left: true } + mask: Region { + item: playerColumnLayout + } ColumnLayout { id: playerColumnLayout @@ -62,180 +79,16 @@ Scope { - (osdWidth / 2) // Dodge OSD - (widgetWidth) // Account for widget width + (Appearance.sizes.elevationMargin) // It's fine for shadows to overlap + spacing: -Appearance.sizes.elevationMargin // Shadow overlap okay - Item { // Player instance - id: playerController - property MprisPlayer player: root.activePlayer - - implicitWidth: widgetWidth - implicitHeight: widgetHeight - property string fileName: Qt.md5(activePlayer?.trackArtUrl) + ".jpg" - property string filePath: `${root.baseCoverArtDir}/${fileName}` - - Process { - id: downloadProcess - running: false - command: ["bash", "-c", `[ -f ${playerController.filePath} ] || curl '${playerController.player?.trackArtUrl}' -o '${playerController.filePath}'`] - onExited: (exitCode, exitStatus) => { - colorQuantizer.source = playerController.filePath - } + Repeater { + model: { + // console.log(JSON.stringify(Mpris.players, null, 2)) + return Mpris.players.values.filter(player => isRealPlayer(player)) } - - ColorQuantizer { - id: colorQuantizer - depth: 1 // 2^1 colors - rescaleSize: 64 // Rescale to 64x64 for faster processing - } - - property QtObject blendedColors: QtObject { - // property color colLayer0: Appearance.mix(Appearance.colors.colLayer0, colorQuantizer.colors[0], 0.5) - } - - Rectangle { - id: background - anchors.fill: parent - anchors.margins: Appearance.sizes.elevationMargin - color: Appearance.colors.colLayer0 - radius: root.popupRounding - - RowLayout { - anchors.fill: parent - anchors.margins: root.contentPadding - spacing: 10 - - Rectangle { // Art backgrounmd - Layout.fillHeight: true - implicitWidth: height - radius: root.artRounding - color: Appearance.colors.colLayer1 - - Image { // Art image - id: mediaArt - property int size: parent.height - anchors.fill: parent - - source: playerController.player?.trackArtUrl - fillMode: Image.PreserveAspectCrop - cache: false - antialiasing: true - asynchronous: true - - width: size - height: size - sourceSize.width: size - sourceSize.height: size - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: mediaArt.size - height: mediaArt.size - radius: root.artRounding - } - } - } - } - - ColumnLayout { // Info & controls - Layout.fillHeight: true - spacing: 2 - - StyledText { - id: trackTitle - Layout.fillWidth: true - font.pixelSize: Appearance.font.pixelSize.large - color: Appearance.colors.colOnLayer0 - elide: Text.ElideRight - text: StringUtils.cleanMusicTitle(playerController.player?.trackTitle) || "No media" - } - StyledText { - id: trackArtist - Layout.fillWidth: true - font.pixelSize: Appearance.font.pixelSize.smaller - color: Appearance.colors.colSubtext - elide: Text.ElideRight - text: playerController.player?.trackArtist - } - Item { Layout.fillHeight: true } - Item { - Layout.fillWidth: true - implicitHeight: trackTime.implicitHeight + slider.implicitHeight - - StyledText { - id: trackTime - anchors.bottom: slider.top - anchors.bottomMargin: -4 - anchors.left: parent.left - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.colors.colSubtext - elide: Text.ElideRight - text: `${StringUtils.friendlyTimeForSeconds(playerController.player?.position)} / ${StringUtils.friendlyTimeForSeconds(playerController.player?.length)}` - } - StyledSlider { - id: slider - anchors { - bottom: parent.bottom - left: parent.left - right: parent.right - bottomMargin: -8 - } - scale: 0.7 - value: playerController.player?.position / playerController.player?.length - onMoved: playerController.player.position = value * playerController.player.length - tooltipContent: StringUtils.friendlyTimeForSeconds(playerController.player?.position) - } - - Button { - id: playPauseButton - anchors.right: parent.right - anchors.bottom: slider.top - anchors.bottomMargin: -1 - implicitWidth: 44 - implicitHeight: 44 - onClicked: playerController.player.togglePlaying(); - - PointingHandInteraction {} - - background: Rectangle { - color: playerController.player?.isPlaying ? - (playPauseButton.pressed ? Appearance.colors.colPrimaryActive : - playPauseButton.hovered ? Appearance.colors.colPrimaryHover : - Appearance.m3colors.m3primary) : - (playPauseButton.pressed ? Appearance.colors.colSecondaryContainerActive : - playPauseButton.hovered ? Appearance.colors.colSecondaryContainerHover : - Appearance.m3colors.m3secondaryContainer) - radius: Appearance.rounding.full - - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - } - - contentItem: MaterialSymbol { - iconSize: Appearance.font.pixelSize.huge - fill: 1 - horizontalAlignment: Text.AlignHCenter - color: playerController.player?.isPlaying ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onSecondaryContainer - text: playerController.player?.isPlaying ? "pause" : "play_arrow" - - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - } - } - } - } - } - } - - DropShadow { - anchors.fill: background - source: background - horizontalOffset: 0 - verticalOffset: 2 - radius: Appearance.sizes.elevationMargin - samples: Appearance.sizes.elevationMargin * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs - color: Appearance.colors.colShadow + delegate: PlayerControl { + required property MprisPlayer modelData + player: modelData } } } diff --git a/.config/quickshell/modules/mediaControls/PlayerControl.qml b/.config/quickshell/modules/mediaControls/PlayerControl.qml new file mode 100644 index 000000000..00475a786 --- /dev/null +++ b/.config/quickshell/modules/mediaControls/PlayerControl.qml @@ -0,0 +1,207 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Services.Mpris +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { // Player instance + id: playerController + required property MprisPlayer player + // property var artUrl: player?.metadata["xesam:url"] || player?.metadata["mpris:artUrl"] || player?.trackArtUrl + property var artUrl: player?.trackArtUrl + property string localArt + property color artDominantColor: "#00000000" + + implicitWidth: widgetWidth + implicitHeight: widgetHeight + + onArtUrlChanged: { + colorQuantizer.running = true + } + + Process { // Average Color Runner + id: colorQuantizer + command: [ "sh", "-c", `magick ${playerController.player.trackArtUrl} -scale 1x1\\! -format '%[fx:int(255*r+.5)],%[fx:int(255*g+.5)],%[fx:int(255*b+.5)]' info: | sed 's/,/\\n/g' | xargs -L 1 printf '%02x' ; echo` ] + stdout: SplitParser { + onRead: data => { + playerController.artDominantColor = "#" + data + } + } + } + + property QtObject blendedColors: QtObject { + property color colLayer0: Appearance.mix(Appearance.colors.colLayer0, artDominantColor, 0.7) + property color colLayer1: Appearance.mix(Appearance.colors.colLayer1, artDominantColor, 0.5) + property color colOnLayer0: Appearance.mix(Appearance.colors.colOnLayer0, artDominantColor, 0.7) + property color colOnLayer1: Appearance.mix(Appearance.colors.colOnLayer1, artDominantColor, 0.5) + property color colSubtext: Appearance.mix(Appearance.colors.colSubtext, artDominantColor, 0.5) + property color colPrimary: Appearance.mix(Appearance.m3colors.m3primary, artDominantColor, 0.3) + property color colPrimaryHover: Appearance.mix(Appearance.colors.colPrimaryHover, artDominantColor, 0.3) + property color colPrimaryActive: Appearance.mix(Appearance.colors.colPrimaryActive, artDominantColor, 0.3) + property color colSecondaryContainer: Appearance.mix(Appearance.m3colors.m3secondaryContainer, artDominantColor, 0.5) + property color colSecondaryContainerHover: Appearance.mix(Appearance.colors.colSecondaryContainerHover, artDominantColor, 0.3) + property color colSecondaryContainerActive: Appearance.mix(Appearance.colors.colSecondaryContainerActive, artDominantColor, 0.3) + property color colOnPrimary: Appearance.mix(Appearance.colors.colOnPrimary, artDominantColor, 0.5) + property color colOnSecondaryContainer: Appearance.mix(Appearance.m3colors.m3onSecondaryContainer, artDominantColor, 0.2) + + } + + Rectangle { + id: background + anchors.fill: parent + anchors.margins: Appearance.sizes.elevationMargin + color: blendedColors.colLayer0 + radius: root.popupRounding + + RowLayout { + anchors.fill: parent + anchors.margins: root.contentPadding + spacing: 15 + + Rectangle { // Art backgrounmd + Layout.fillHeight: true + implicitWidth: height + radius: root.artRounding + color: blendedColors.colLayer1 + + Image { // Art image + id: mediaArt + property int size: parent.height + anchors.fill: parent + + source: playerController.artUrl + fillMode: Image.PreserveAspectCrop + cache: false + antialiasing: true + asynchronous: true + + width: size + height: size + sourceSize.width: size + sourceSize.height: size + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: mediaArt.size + height: mediaArt.size + radius: root.artRounding + } + } + } + } + + ColumnLayout { // Info & controls + Layout.fillHeight: true + spacing: 2 + + StyledText { + id: trackTitle + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.large + color: blendedColors.colOnLayer0 + elide: Text.ElideRight + text: StringUtils.cleanMusicTitle(playerController.player?.trackTitle) || "Untitled" + } + StyledText { + id: trackArtist + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.smaller + color: blendedColors.colSubtext + elide: Text.ElideRight + text: playerController.player?.trackArtist + } + Item { Layout.fillHeight: true } + Item { + Layout.fillWidth: true + implicitHeight: trackTime.implicitHeight + slider.implicitHeight + + StyledText { + id: trackTime + anchors.bottom: slider.top + anchors.bottomMargin: -4 + anchors.left: parent.left + font.pixelSize: Appearance.font.pixelSize.small + color: blendedColors.colSubtext + elide: Text.ElideRight + text: `${StringUtils.friendlyTimeForSeconds(playerController.player?.position)} / ${StringUtils.friendlyTimeForSeconds(playerController.player?.length)}` + } + StyledSlider { + id: slider + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + bottomMargin: -8 + } + highlightColor: blendedColors.colPrimary + trackColor: blendedColors.colSecondaryContainer + handleColor: blendedColors.colOnSecondaryContainer + scale: 0.6 + value: playerController.player?.position / playerController.player?.length + onMoved: playerController.player.position = value * playerController.player.length + tooltipContent: StringUtils.friendlyTimeForSeconds(playerController.player?.position) + } + + Button { + id: playPauseButton + anchors.right: parent.right + anchors.bottom: slider.top + anchors.bottomMargin: -1 + implicitWidth: 44 + implicitHeight: 44 + onClicked: playerController.player.togglePlaying(); + + PointingHandInteraction {} + + background: Rectangle { + color: playerController.player?.isPlaying ? + (playPauseButton.pressed ? blendedColors.colPrimaryActive : + playPauseButton.hovered ? blendedColors.colPrimaryHover : + blendedColors.colPrimary) : + (playPauseButton.pressed ? blendedColors.colSecondaryContainerActive : + playPauseButton.hovered ? blendedColors.colSecondaryContainerHover : + blendedColors.colSecondaryContainer) + radius: Appearance.rounding.full + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + + contentItem: MaterialSymbol { + iconSize: Appearance.font.pixelSize.huge + fill: 1 + horizontalAlignment: Text.AlignHCenter + color: playerController.player?.isPlaying ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onSecondaryContainer + text: playerController.player?.isPlaying ? "pause" : "play_arrow" + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + } + } + } + } + } + + DropShadow { + anchors.fill: background + source: background + horizontalOffset: 0 + verticalOffset: 2 + radius: Appearance.sizes.elevationMargin + samples: Appearance.sizes.elevationMargin * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs + color: Appearance.colors.colShadow + } +} \ No newline at end of file