1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-10 07:25:37 -05:00

DankDash: Replace CentCom center with a new widget

This commit is contained in:
bbedward
2025-09-09 20:00:31 -04:00
parent 1c7d8a55f3
commit a67a6c7c1c
26 changed files with 3361 additions and 1818 deletions

View File

@@ -0,0 +1,468 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
property MprisPlayer activePlayer: MprisController.activePlayer
property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
property real displayPosition: currentPosition
readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) return 0
const calculatedRatio = displayPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer?.positionSupported) {
currentPosition = Qt.binding(() => activePlayer?.position || 0)
} else {
currentPosition = 0
}
}
Timer {
interval: 300
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !activePlayer
DankIcon {
name: "music_note"
size: Theme.iconSize
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Media"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingXS * 2
spacing: Theme.spacingL
visible: activePlayer
Item {
width: 110
height: 80
anchors.horizontalCenter: parent.horizontalCenter
Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Shape {
id: morphingBlob
width: 120
height: 120
anchors.centerIn: parent
visible: activePlayer?.playbackState === MprisPlaybackState.Playing
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
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: 40
readonly property int segments: 24
property var audioLevels: {
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
}
return CavaService.values
}
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5]
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.2)
}
const points = []
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * 2 * Math.PI
const audioIndex = i % Math.min(smoothedLevels.length, 6)
const audioLevel = Math.max(0.1, Math.min(1.5, (smoothedLevels[audioIndex] || 0) / 50))
const radius = baseRadius * (1.0 + audioLevel * 0.3)
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: 72
height: 72
radius: 36
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Theme.surfaceContainer
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1
Image {
id: albumArt
source: activePlayer?.trackArtUrl || ""
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: true
asynchronous: true
visible: false
}
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: 68
height: 68
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: 20
color: Theme.surfaceVariantText
visible: albumArt.status !== Image.Ready
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
topPadding: Theme.spacingL
StyledText {
text: activePlayer?.trackTitle || "Unknown"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
}
}
Item {
id: progressSlider
width: parent.width
height: 20
visible: activePlayer?.length > 0
property real value: ratio
property real lineWidth: 2.5
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
Rectangle {
width: parent.width
height: progressSlider.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.trackColor
radius: height / 2
}
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
height: progressSlider.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
Rectangle {
id: playhead
width: 2.5
height: Math.max(progressSlider.lineWidth + 8, 12)
radius: width / 2
color: progressSlider.playheadColor
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressMouseArea.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
Item {
width: parent.width
height: 32
Row {
spacing: Theme.spacingS
anchors.centerIn: parent
Rectangle {
width: 28
height: 28
radius: 14
anchors.verticalCenter: playPauseButton.verticalCenter
color: prevArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
id: playPauseButton
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
anchors.verticalCenter: playPauseButton.verticalCenter
color: nextArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next()
}
}
}
}
}
}