1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

media player cleanups

This commit is contained in:
bbedward
2025-09-14 19:16:54 -04:00
parent 76ee483b27
commit 59c7246989
4 changed files with 576 additions and 871 deletions

222
Widgets/DankAlbumArt.qml Normal file
View File

@@ -0,0 +1,222 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
Item {
id: root
property MprisPlayer activePlayer
property string artUrl: (activePlayer?.trackArtUrl) || ""
property string lastValidArtUrl: ""
property alias albumArtStatus: albumArt.status
property real albumSize: Math.min(width, height) * 0.88
property bool showAnimation: true
property real animationScale: 1.0
onArtUrlChanged: {
if (artUrl && albumArt.status !== Image.Error) {
lastValidArtUrl = artUrl
}
}
Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Shape {
id: morphingBlob
width: parent.width * 1.1
height: parent.height * 1.1
anchors.centerIn: parent
visible: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
z: 0
layer.enabled: true
layer.smooth: true
layer.samples: 4
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
readonly property int segments: 28
property var audioLevels: {
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
}
return CavaService.values
}
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
property var cubics: []
onAudioLevelsChanged: updatePath()
Timer {
running: morphingBlob.visible
interval: 16
repeat: true
onTriggered: morphingBlob.updatePath()
}
Component {
id: cubicSegment
PathCubic {}
}
Component.onCompleted: {
shapePath.pathElements.push(Qt.createQmlObject(
'import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath
))
for (let i = 0; i < segments; i++) {
const seg = cubicSegment.createObject(shapePath)
shapePath.pathElements.push(seg)
cubics.push(seg)
}
updatePath()
}
function expSmooth(prev, next, alpha) {
return prev + alpha * (next - prev)
}
function updatePath() {
if (cubics.length === 0) return
for (let i = 0; i < Math.min(smoothedLevels.length, audioLevels.length); i++) {
smoothedLevels[i] = expSmooth(smoothedLevels[i], audioLevels[i], 0.35)
}
const points = []
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI
const audioIndex = i % Math.min(smoothedLevels.length, 10)
const rawLevel = smoothedLevels[audioIndex] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const normalizedLevel = scaledLevel / 100
const audioLevel = Math.max(0.15, normalizedLevel) * 0.5
const radius = baseRadius * (1.0 + audioLevel)
const x = centerX + Math.cos(angle) * radius
const y = centerY + Math.sin(angle) * radius
points.push({x: x, y: y})
}
const startMove = shapePath.pathElements[0]
startMove.x = points[0].x
startMove.y = points[0].y
const tension = 0.5
for (let i = 0; i < segments; i++) {
const p0 = points[(i - 1 + segments) % segments]
const p1 = points[i]
const p2 = points[(i + 1) % segments]
const p3 = points[(i + 2) % segments]
const c1x = p1.x + (p2.x - p0.x) * tension / 3
const c1y = p1.y + (p2.y - p0.y) * tension / 3
const c2x = p2.x - (p3.x - p1.x) * tension / 3
const c2y = p2.y - (p3.y - p1.y) * tension / 3
const seg = cubics[i]
seg.control1X = c1x
seg.control1Y = c1y
seg.control2X = c2x
seg.control2Y = c2y
seg.x = p2.x
seg.y = p2.y
}
}
ShapePath {
id: shapePath
fillColor: Theme.primary
strokeColor: "transparent"
strokeWidth: 0
joinStyle: ShapePath.RoundJoin
fillRule: ShapePath.WindingFill
}
}
Rectangle {
width: albumSize
height: width
radius: width / 2
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Theme.surfaceContainer
border.width: 1
anchors.centerIn: parent
z: 1
Image {
id: albumArt
source: artUrl || lastValidArtUrl || ""
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: true
asynchronous: true
visible: false
onStatusChanged: {
if (status === Image.Error) {
console.warn("Failed to load album art:", source)
source = ""
if (activePlayer?.trackArtUrl === source) {
root.lastValidArtUrl = ""
}
}
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: albumArt
maskEnabled: true
maskSource: circularMask
visible: albumArt.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: parent.width - 4
height: parent.height - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
DankIcon {
anchors.centerIn: parent
name: "album"
size: parent.width * 0.3
color: Theme.surfaceVariantText
visible: albumArt.status !== Image.Ready
}
}
}

181
Widgets/DankSeekbar.qml Normal file
View File

@@ -0,0 +1,181 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property MprisPlayer activePlayer
property real value: {
if (!activePlayer || activePlayer.length <= 0) return 0
const calculatedRatio = (activePlayer.position || 0) / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
property bool isSeeking: false
implicitHeight: 20
Loader {
anchors.fill: parent
visible: activePlayer && activePlayer.length > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
z: 1
Component {
id: waveProgressComponent
M3WaveProgress {
value: root.value
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
Timer {
id: waveSeekDebounceTimer
interval: 150
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
waveSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}
Component {
id: flatProgressComponent
Item {
property real lineWidth: 3
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
readonly property real midY: height / 2
Rectangle {
width: parent.width
height: parent.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: parent.trackColor
radius: height / 2
}
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * root.value))
height: parent.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
Rectangle {
id: playhead
width: 3
height: Math.max(parent.lineWidth + 8, 14)
radius: width / 2
color: parent.playheadColor
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
y: parent.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
Timer {
id: flatSeekDebounceTimer
interval: 150
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
flatSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}
}
}