diff options
Diffstat (limited to 'modules/dashboard/Media.qml')
| -rw-r--r-- | modules/dashboard/Media.qml | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml new file mode 100644 index 0000000..bf44e17 --- /dev/null +++ b/modules/dashboard/Media.qml @@ -0,0 +1,366 @@ +pragma ComponentBehavior: Bound + +import "root:/widgets" +import "root:/services" +import "root:/config" +import Quickshell.Io +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls + +Item { + id: root + + property real playerProgress: { + const active = Players.active; + return active?.length ? active.position / active.length : 0; + } + + property list<int> cava: [] + + function lengthStr(length: int): string { + if (length < 0) + return "-1:-1"; + return `${Math.floor(length / 60)}:${Math.floor(length % 60).toString().padStart(2, "0")}`; + } + + implicitWidth: cover.implicitWidth + DashboardConfig.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 + implicitHeight: Math.max(cover.implicitHeight + DashboardConfig.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + + Behavior on playerProgress { + NumberAnimation { + duration: Appearance.anim.durations.large + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Timer { + running: root.visible && (Players.active?.isPlaying ?? false) + interval: DashboardConfig.mediaUpdateInterval + triggeredOnStart: true + repeat: true + onTriggered: Players.active?.positionChanged() + } + + Process { + running: true + command: ["sh", "-c", `printf '[general]\nframerate=60\nbars=${DashboardConfig.visualiserBars}\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`] + stdout: SplitParser { + onRead: data => { + root.cava = data.slice(0, -1).split(";").map(v => parseInt(v, 10)); + if (visualiser.visible) + visualiser.requestPaint(); + } + } + } + + Canvas { + id: visualiser + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 + readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small + readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small + + anchors.fill: cover + anchors.margins: -DashboardConfig.sizes.mediaVisualiserSize + + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + + const values = root.cava; + const len = values.length; + const size = DashboardConfig.sizes.mediaVisualiserSize; + const cx = centerX; + const cy = centerY; + const rx = innerX; + const ry = innerY; + + for (let i = 0; i < len; i++) { + const v = Math.max(1, Math.min(100, values[i])); + + const angle = i * 2 * Math.PI / len; + const magnitude = v / 100 * size; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + ctx.moveTo(cx + rx * cos, cy + ry * sin); + ctx.lineTo(cx + (rx + magnitude) * cos, cy + (ry + magnitude) * sin); + } + + ctx.strokeStyle = Colours.palette.m3primary; + ctx.lineWidth = 360 / len - Appearance.spacing.small / 4; + ctx.lineCap = "round"; + + ctx.stroke(); + } + } + + ClippingRectangle { + id: cover + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.large + DashboardConfig.sizes.mediaVisualiserSize + + implicitWidth: DashboardConfig.sizes.mediaCoverArtSize + implicitHeight: DashboardConfig.sizes.mediaCoverArtSize + + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + MaterialIcon { + anchors.centerIn: parent + + text: "art_track" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: (parent.width * 0.4) || 1 + } + + Image { + id: image + + anchors.fill: parent + + source: Players.active?.trackArtUrl ?? "" + asynchronous: true + fillMode: Image.PreserveAspectCrop + sourceSize.width: width + sourceSize.height: height + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + Column { + id: details + + anchors.verticalCenter: parent.verticalCenter + anchors.left: visualiser.right + anchors.leftMargin: Appearance.spacing.normal + + spacing: Appearance.spacing.small + + StyledText { + id: title + + anchors.horizontalCenter: parent.horizontalCenter + + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.normal + + width: parent.implicitWidth + elide: Text.ElideRight + } + + StyledText { + id: album + + anchors.horizontalCenter: parent.horizontalCenter + + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + + width: parent.implicitWidth + elide: Text.ElideRight + } + + StyledText { + id: artist + + anchors.horizontalCenter: parent.horizontalCenter + + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") + color: Colours.palette.m3secondary + + width: parent.implicitWidth + elide: Text.ElideRight + } + + Row { + id: controls + + anchors.horizontalCenter: parent.horizontalCenter + + spacing: Appearance.spacing.small + + Control { + icon: "skip_previous" + canUse: Players.active?.canGoPrevious ?? false + + function onClicked(): void { + Players.active?.previous(); + } + } + + Control { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + canUse: Players.active?.canTogglePlaying ?? false + + function onClicked(): void { + Players.active?.togglePlaying(); + } + } + + Control { + icon: "skip_next" + canUse: Players.active?.canGoNext ?? false + + function onClicked(): void { + Players.active?.next(); + } + } + } + + Slider { + id: slider + + implicitWidth: controls.implicitWidth * 2 + implicitHeight: Appearance.padding.normal * 3 + + value: root.playerProgress + onMoved: { + const active = Players.active; + if (active?.canSeek && active?.positionSupported) + active.position = value * active.length; + } + + background: Item { + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.topMargin: slider.implicitHeight / 3 + anchors.bottomMargin: slider.implicitHeight / 3 + + implicitWidth: slider.handle.x - slider.implicitHeight / 6 + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + topRightRadius: slider.implicitHeight / 15 + bottomRightRadius: slider.implicitHeight / 15 + } + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.topMargin: slider.implicitHeight / 3 + anchors.bottomMargin: slider.implicitHeight / 3 + + implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6 + + color: Colours.palette.m3surfaceContainer + radius: Appearance.rounding.full + topLeftRadius: slider.implicitHeight / 15 + bottomLeftRadius: slider.implicitHeight / 15 + } + } + + handle: StyledRect { + id: rect + + x: slider.visualPosition * slider.availableWidth + + implicitWidth: slider.implicitHeight / 4.5 + implicitHeight: slider.implicitHeight + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onPressed: event => event.accepted = false + } + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + + implicitHeight: Math.max(position.implicitHeight, length.implicitHeight) + + StyledText { + id: position + + anchors.left: parent.left + + text: root.lengthStr(Players.active?.position ?? -1) + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + + StyledText { + id: length + + anchors.right: parent.right + + text: root.lengthStr(Players.active?.length ?? -1) + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + } + } + + AnimatedImage { + id: bongocat + + anchors.verticalCenter: parent.verticalCenter + anchors.left: details.right + anchors.leftMargin: Appearance.spacing.large + + playing: visible && (Players.active?.isPlaying ?? false) + speed: BeatDetector.bpm / 300 + source: "root:/assets/bongocat.gif" + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit + } + + component Control: StyledRect { + id: control + + required property string icon + required property bool canUse + function onClicked(): void { + } + + implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small + implicitHeight: implicitWidth + + StateLayer { + disabled: !control.canUse + radius: Appearance.rounding.full + + function onClicked(): void { + control.onClicked(); + } + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + anchors.verticalCenterOffset: font.pointSize * 0.05 + + animate: true + text: control.icon + color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline + font.pointSize: Appearance.font.size.large + } + } +} |