mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
DankDash: Replace CentCom center with a new widget
This commit is contained in:
766
Modules/DankDash/MediaPlayerTab.qml
Normal file
766
Modules/DankDash/MediaPlayerTab.qml
Normal file
@@ -0,0 +1,766 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
property string lastValidTitle: ""
|
||||
property string lastValidArtist: ""
|
||||
property string lastValidAlbum: ""
|
||||
property string lastValidArtUrl: ""
|
||||
property real currentPosition: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0
|
||||
property real displayPosition: currentPosition
|
||||
property var defaultSink: AudioService.sink
|
||||
|
||||
readonly property real ratio: {
|
||||
if (!activePlayer || activePlayer.length <= 0) {
|
||||
return 0
|
||||
}
|
||||
const calculatedRatio = displayPosition / activePlayer.length
|
||||
return Math.max(0, Math.min(1, calculatedRatio))
|
||||
}
|
||||
|
||||
implicitWidth: 700
|
||||
implicitHeight: 410
|
||||
|
||||
onActivePlayerChanged: {
|
||||
if (activePlayer && activePlayer.positionSupported) {
|
||||
currentPosition = Qt.binding(() => activePlayer?.position || 0)
|
||||
} else {
|
||||
currentPosition = 0
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: positionTimer
|
||||
interval: 300
|
||||
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressSliderArea.isSeeking
|
||||
repeat: true
|
||||
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: cleanupTimer
|
||||
interval: 2000
|
||||
running: !activePlayer
|
||||
onTriggered: {
|
||||
lastValidTitle = ""
|
||||
lastValidArtist = ""
|
||||
lastValidAlbum = ""
|
||||
lastValidArtUrl = ""
|
||||
currentPosition = 0
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
|
||||
|
||||
DankIcon {
|
||||
name: "music_note"
|
||||
size: Theme.iconSize * 3
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No Media Playing"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Start playing media to see detailed controls"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: (activePlayer && activePlayer.trackTitle !== "") || lastValidTitle !== ""
|
||||
|
||||
// Left Column: Album Art and Controls (60%)
|
||||
Column {
|
||||
x: 0
|
||||
y: 0
|
||||
width: parent.width * 0.6 - Theme.spacingM
|
||||
height: parent.height
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Album Art Section
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height * 0.55
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Item {
|
||||
width: Math.min(parent.width * 0.8, parent.height * 0.9)
|
||||
height: width
|
||||
anchors.centerIn: parent
|
||||
|
||||
Loader {
|
||||
active: activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||
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
|
||||
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.35
|
||||
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: parent.width * 0.75
|
||||
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: (activePlayer && activePlayer.trackArtUrl) || lastValidArtUrl || ""
|
||||
onSourceChanged: {
|
||||
if (activePlayer && activePlayer.trackArtUrl && albumArt.status !== Image.Error) {
|
||||
lastValidArtUrl = activePlayer.trackArtUrl
|
||||
}
|
||||
}
|
||||
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 && activePlayer.trackArtUrl === source) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Song Info and Controls Section
|
||||
Column {
|
||||
width: parent.width
|
||||
height: parent.height * 0.45
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Song Info
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: (activePlayer && activePlayer.trackTitle) || lastValidTitle || "Unknown Track"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackTitle) {
|
||||
lastValidTitle = activePlayer.trackTitle
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (activePlayer && activePlayer.trackArtist) || lastValidArtist || "Unknown Artist"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackArtist) {
|
||||
lastValidArtist = activePlayer.trackArtist
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: (activePlayer && activePlayer.trackAlbum) || lastValidAlbum || ""
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackAlbum) {
|
||||
lastValidAlbum = activePlayer.trackAlbum
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Progress Bar
|
||||
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
|
||||
readonly property real midY: height / 2
|
||||
|
||||
// Background track
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: progressSlider.lineWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: progressSlider.trackColor
|
||||
radius: height / 2
|
||||
}
|
||||
|
||||
// Filled portion
|
||||
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 } }
|
||||
}
|
||||
|
||||
// Playhead
|
||||
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
|
||||
y: progressSlider.midY - height / 2
|
||||
z: 3
|
||||
Behavior on x { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: progressSliderArea
|
||||
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 (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
|
||||
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
progressSliderArea.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media Controls
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
height: 64
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_previous"
|
||||
size: 18
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevBtnArea
|
||||
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 {
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 22
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 24
|
||||
color: Theme.background
|
||||
weight: 500
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer && activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 6
|
||||
shadowBlur: 1.0
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_next"
|
||||
size: 18
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextBtnArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer && activePlayer.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right Column: Audio Controls (40%)
|
||||
Column {
|
||||
x: parent.width * 0.6 + Theme.spacingM
|
||||
y: 0
|
||||
width: parent.width * 0.4 - Theme.spacingM
|
||||
height: parent.height
|
||||
spacing: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Volume Control
|
||||
Row {
|
||||
x: -Theme.spacingS
|
||||
width: parent.width + Theme.spacingS
|
||||
height: 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize + Theme.spacingS * 2
|
||||
height: Theme.iconSize + Theme.spacingS * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: iconArea
|
||||
anchors.fill: parent
|
||||
visible: defaultSink !== null
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (defaultSink) {
|
||||
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (!defaultSink) return "volume_off"
|
||||
|
||||
let volume = defaultSink.audio.volume
|
||||
let muted = defaultSink.audio.muted
|
||||
|
||||
if (muted || volume === 0.0) return "volume_off"
|
||||
if (volume <= 0.33) return "volume_down"
|
||||
if (volume <= 0.66) return "volume_up"
|
||||
return "volume_up"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingXS
|
||||
enabled: defaultSink !== null
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||
onSliderValueChanged: function(newValue) {
|
||||
if (defaultSink) {
|
||||
defaultSink.audio.volume = newValue / 100.0
|
||||
if (newValue > 0 && defaultSink.audio.muted) {
|
||||
defaultSink.audio.muted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio Devices
|
||||
DankFlickable {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
contentHeight: deviceColumn.height
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: deviceColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && node.isSink && !node.isStream
|
||||
})
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 42
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: modelData === AudioService.sink ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset"
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv"
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset"
|
||||
else
|
||||
return "speaker"
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingS * 2
|
||||
|
||||
StyledText {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
Pipewire.preferredDefaultAudioSink = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: progressMouseArea
|
||||
anchors.fill: parent
|
||||
enabled: false
|
||||
visible: false
|
||||
property bool isSeeking: false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user