357 lines
9.2 KiB
QML
357 lines
9.2 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Shapes
|
|
import QtQuick.Effects
|
|
import ZShell.Services
|
|
import qs.Daemons
|
|
import qs.Components
|
|
import qs.Config
|
|
import qs.Helpers
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property real playerProgress: {
|
|
const active = Players.active;
|
|
return active?.length ? (active.position % active.length) / active.length : 0;
|
|
}
|
|
property int rowHeight: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
implicitHeight: cover.height + rowHeight * 2
|
|
|
|
Behavior on playerProgress {
|
|
Anim {
|
|
duration: Appearance.anim.durations.large
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: Config.dashboard.mediaUpdateInterval
|
|
repeat: true
|
|
running: Players.active?.isPlaying ?? false
|
|
triggeredOnStart: true
|
|
|
|
onTriggered: Players.active?.positionChanged()
|
|
}
|
|
|
|
ServiceRef {
|
|
service: Audio.cava
|
|
}
|
|
|
|
Shape {
|
|
id: visualizer
|
|
|
|
readonly property int bars: Config.services.visualizerBars
|
|
property color color: DynamicColors.palette.m3tertiary
|
|
property color fillColor: Qt.alpha(color, 0.25)
|
|
|
|
anchors.fill: layout
|
|
anchors.leftMargin: -(shape.strokeWidth / 2)
|
|
layer.enabled: true
|
|
asynchronous: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
id: shape
|
|
strokeColor: visualizer.color
|
|
fillColor: visualizer.fillColor
|
|
strokeWidth: 2
|
|
|
|
PathSvg {
|
|
path: shape.curvePath
|
|
}
|
|
|
|
property string curvePath: {
|
|
const values = Audio.cava.values;
|
|
const n = values.length;
|
|
|
|
if (n < 2)
|
|
return "";
|
|
|
|
const h = layout.height + shape.strokeWidth / 2;
|
|
const w = layout.width + shape.strokeWidth;
|
|
|
|
function x(i) {
|
|
return i * w / (n - 1);
|
|
}
|
|
|
|
function y(i) {
|
|
return h - values[i] * Config.dashboard.sizes.mediaVisualizerSize;
|
|
}
|
|
|
|
let d = `M 0 ${h} `;
|
|
d += `L ${x(0)} ${y(0)} `;
|
|
|
|
for (let i = 0; i < n - 1; ++i) {
|
|
const x0 = x(i);
|
|
const y0 = y(i);
|
|
|
|
const x1 = x(i + 1);
|
|
const y1 = y(i + 1);
|
|
|
|
const prev = Math.max(0, i - 1);
|
|
const next = Math.min(n - 1, i + 2);
|
|
|
|
const c1x = x0 + (x1 - x(prev)) / 6;
|
|
const c1y = y0 + (y1 - y(prev)) / 6;
|
|
|
|
const c2x = x1 - (x(next) - x0) / 6;
|
|
const c2y = y1 - (y(next) - y0) / 6;
|
|
|
|
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
|
|
}
|
|
|
|
d += `L ${w} ${h} `;
|
|
d += `L 0 ${h} Z`;
|
|
|
|
return d;
|
|
}
|
|
}
|
|
}
|
|
|
|
CustomRect {
|
|
anchors.fill: visualizer
|
|
|
|
color: Qt.alpha(DynamicColors.palette.m3shadow, 0.2)
|
|
layer.enabled: true
|
|
layer.effect: MultiEffect {
|
|
maskEnabled: true
|
|
maskSource: visualizer
|
|
}
|
|
}
|
|
|
|
Shape {
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
fillColor: "transparent"
|
|
strokeColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
|
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
|
|
|
|
Behavior on strokeColor {
|
|
CAnim {
|
|
}
|
|
}
|
|
|
|
PathAngleArc {
|
|
centerX: cover.x + cover.width / 2
|
|
centerY: cover.y + cover.height / 2
|
|
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
|
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
|
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
|
|
sweepAngle: Config.dashboard.sizes.mediaProgressSweep
|
|
}
|
|
}
|
|
|
|
ShapePath {
|
|
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
fillColor: "transparent"
|
|
strokeColor: DynamicColors.palette.m3primary
|
|
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
|
|
|
|
Behavior on strokeColor {
|
|
CAnim {
|
|
}
|
|
}
|
|
|
|
PathAngleArc {
|
|
centerX: cover.x + cover.width / 2
|
|
centerY: cover.y + cover.height / 2
|
|
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
|
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
|
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
|
|
sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
id: layout
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
implicitHeight: root.implicitHeight
|
|
|
|
CustomClippingRect {
|
|
id: cover
|
|
|
|
Layout.alignment: Qt.AlignLeft
|
|
Layout.bottomMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
|
|
Layout.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
|
|
Layout.preferredHeight: Config.dashboard.sizes.mediaCoverArtSize
|
|
Layout.preferredWidth: Config.dashboard.sizes.mediaCoverArtSize
|
|
Layout.topMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
|
|
color: DynamicColors.tPalette.m3surfaceContainerHigh
|
|
radius: Infinity
|
|
|
|
MaterialIcon {
|
|
anchors.centerIn: parent
|
|
color: DynamicColors.palette.m3onSurfaceVariant
|
|
font.pointSize: (parent.width * 0.4) || 1
|
|
grade: 200
|
|
text: "art_track"
|
|
}
|
|
|
|
Image {
|
|
id: image
|
|
|
|
anchors.fill: parent
|
|
asynchronous: true
|
|
fillMode: Image.PreserveAspectCrop
|
|
source: Players.active?.trackArtUrl ?? ""
|
|
sourceSize.height: Math.floor(height)
|
|
sourceSize.width: Math.floor(width)
|
|
}
|
|
}
|
|
|
|
CustomRect {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: childrenRect.height
|
|
|
|
MarqueeText {
|
|
id: title
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: parent.top
|
|
color: DynamicColors.palette.m3primary
|
|
font.pointSize: Appearance.font.size.normal
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
|
width: parent.width - Appearance.padding.large * 4
|
|
}
|
|
|
|
MarqueeText {
|
|
id: album
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: title.bottom
|
|
anchors.topMargin: Appearance.spacing.small
|
|
color: DynamicColors.palette.m3outline
|
|
font.pointSize: Appearance.font.size.small
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
|
|
width: parent.width - Appearance.padding.large * 4
|
|
}
|
|
|
|
MarqueeText {
|
|
id: artist
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: album.bottom
|
|
anchors.topMargin: Appearance.spacing.small
|
|
color: DynamicColors.palette.m3secondary
|
|
horizontalAlignment: Text.AlignHCenter
|
|
pauseMs: 4000
|
|
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
|
width: parent.width - Appearance.padding.large * 4
|
|
}
|
|
|
|
RowLayout {
|
|
id: controls
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: artist.bottom
|
|
anchors.topMargin: Appearance.spacing.smaller
|
|
spacing: Appearance.spacing.small
|
|
|
|
Control {
|
|
function onClicked(): void {
|
|
Players.active?.previous();
|
|
}
|
|
|
|
canUse: Players.active?.canGoPrevious ?? false
|
|
icon: "skip_previous"
|
|
}
|
|
|
|
Control {
|
|
function onClicked(): void {
|
|
Players.active?.togglePlaying();
|
|
}
|
|
|
|
canUse: Players.active?.canTogglePlaying ?? false
|
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
|
}
|
|
|
|
Control {
|
|
function onClicked(): void {
|
|
Players.active?.next();
|
|
}
|
|
|
|
canUse: Players.active?.canGoNext ?? false
|
|
icon: "skip_next"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component Control: CustomRect {
|
|
id: control
|
|
|
|
required property bool canUse
|
|
required property string icon
|
|
property int level: 1
|
|
property string set_color: "Secondary"
|
|
|
|
function onClicked(): void {
|
|
}
|
|
|
|
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : 0)
|
|
color: canUse ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
|
implicitHeight: implicitWidth
|
|
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
|
|
radius: Appearance.rounding.full
|
|
|
|
Behavior on Layout.preferredWidth {
|
|
Anim {
|
|
duration: Appearance.anim.durations.expressiveFastSpatial
|
|
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
}
|
|
}
|
|
Behavior on radius {
|
|
Anim {
|
|
duration: Appearance.anim.durations.expressiveFastSpatial
|
|
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
}
|
|
}
|
|
|
|
Elevation {
|
|
anchors.fill: parent
|
|
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
|
radius: parent.radius
|
|
z: -1
|
|
}
|
|
|
|
StateLayer {
|
|
id: controlState
|
|
|
|
color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
|
enabled: control.canUse
|
|
|
|
onClicked: {
|
|
control.onClicked();
|
|
}
|
|
// radius: Appearance.rounding.full
|
|
}
|
|
|
|
MaterialIcon {
|
|
id: icon
|
|
|
|
anchors.centerIn: parent
|
|
anchors.verticalCenterOffset: font.pointSize * 0.05
|
|
animate: true
|
|
color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
|
fill: control.canUse ? 1 : 0
|
|
font.pointSize: Appearance.font.size.large
|
|
text: control.icon
|
|
}
|
|
}
|
|
}
|