nosignal-shell/modules/dashboard/Media.qml
2025-05-26 00:10:05 +08:00

366 lines
11 KiB
QML

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
}
}
}