summaryrefslogtreecommitdiff
path: root/modules/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'modules/dashboard')
-rw-r--r--modules/dashboard/Content.qml34
-rw-r--r--modules/dashboard/Media.qml366
-rw-r--r--modules/dashboard/dash/Media.qml13
3 files changed, 402 insertions, 11 deletions
diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml
index 5f65e7f..fc2046f 100644
--- a/modules/dashboard/Content.qml
+++ b/modules/dashboard/Content.qml
@@ -48,7 +48,39 @@ Item {
currentIndex: tabs.currentIndex
- Dash {}
+ ClippingWrapperRectangle {
+ radius: Appearance.rounding.normal
+ color: "transparent"
+
+ Dash {
+ clip: true
+ }
+ }
+
+ ClippingWrapperRectangle {
+ radius: Appearance.rounding.normal
+ color: "transparent"
+
+ Media {
+ clip: true
+ }
+ }
+ }
+ }
+
+ Behavior on implicitWidth {
+ NumberAnimation {
+ duration: Appearance.anim.durations.large
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.emphasized
+ }
+ }
+
+ Behavior on implicitHeight {
+ NumberAnimation {
+ duration: Appearance.anim.durations.large
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
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
+ }
+ }
+}
diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml
index 9f366c3..075844c 100644
--- a/modules/dashboard/dash/Media.qml
+++ b/modules/dashboard/dash/Media.qml
@@ -12,7 +12,7 @@ Item {
property real playerProgress: {
const active = Players.active;
- return active ? active.position / active.length : 0;
+ return active?.length ? active.position / active.length : 0;
}
anchors.top: parent.top
@@ -223,18 +223,11 @@ Item {
anchors.bottomMargin: Appearance.padding.large
anchors.margins: Appearance.padding.large * 2
- playing: Players.active?.isPlaying ?? false
+ playing: visible && (Players.active?.isPlaying ?? false)
+ speed: BeatDetector.bpm / 300
source: "root:/assets/bongocat.gif"
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
-
- Process {
- running: true
- command: [`${Quickshell.shellRoot}/assets/realtime-beat-detector.py`]
- stdout: SplitParser {
- onRead: data => bongocat.speed = parseFloat(data) / 300
- }
- }
}
component Control: StyledRect {