mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
404 lines
13 KiB
QML
404 lines
13 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import qs.components
|
|
import qs.components.controls
|
|
import qs.services
|
|
import qs.utils
|
|
import qs.config
|
|
import Caelestia.Services
|
|
import Quickshell
|
|
import Quickshell.Services.Mpris
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Shapes
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property PersistentProperties visibilities
|
|
|
|
property real playerProgress: {
|
|
const active = Players.active;
|
|
return active?.length ? active.position / active.length : 0;
|
|
}
|
|
|
|
function lengthStr(length: int): string {
|
|
if (length < 0)
|
|
return "-1:-1";
|
|
|
|
const hours = Math.floor(length / 3600);
|
|
const mins = Math.floor((length % 3600) / 60);
|
|
const secs = Math.floor(length % 60).toString().padStart(2, "0");
|
|
|
|
if (hours > 0)
|
|
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
|
|
return `${mins}:${secs}`;
|
|
}
|
|
|
|
implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2
|
|
implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2
|
|
|
|
Behavior on playerProgress {
|
|
Anim {
|
|
duration: Appearance.anim.durations.large
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
running: Players.active?.isPlaying ?? false
|
|
interval: Config.dashboard.mediaUpdateInterval
|
|
triggeredOnStart: true
|
|
repeat: true
|
|
onTriggered: Players.active?.positionChanged()
|
|
}
|
|
|
|
ServiceRef {
|
|
service: Audio.cava
|
|
}
|
|
|
|
ServiceRef {
|
|
service: Audio.beatTracker
|
|
}
|
|
|
|
Shape {
|
|
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
|
|
property color colour: Colours.palette.m3primary
|
|
|
|
anchors.fill: cover
|
|
anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize
|
|
|
|
asynchronous: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
data: visualiserBars.instances
|
|
}
|
|
|
|
Variants {
|
|
id: visualiserBars
|
|
|
|
model: Array.from({
|
|
length: Config.services.visualiserBars
|
|
}, (_, i) => i)
|
|
|
|
ShapePath {
|
|
id: visualiserBar
|
|
|
|
required property int modelData
|
|
readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData]))
|
|
|
|
readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars
|
|
readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize
|
|
readonly property real cos: Math.cos(angle)
|
|
readonly property real sin: Math.sin(angle)
|
|
|
|
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4
|
|
strokeColor: Colours.palette.m3primary
|
|
|
|
startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos
|
|
startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin
|
|
|
|
PathLine {
|
|
x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos
|
|
y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin
|
|
}
|
|
|
|
Behavior on strokeColor {
|
|
CAnim {}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledClippingRect {
|
|
id: cover
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize
|
|
|
|
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
|
|
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize
|
|
|
|
color: Colours.tPalette.m3surfaceContainerHigh
|
|
radius: Infinity
|
|
|
|
MaterialIcon {
|
|
anchors.centerIn: parent
|
|
|
|
grade: 200
|
|
text: "art_track"
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font.pointSize: (parent.width * 0.4) || 1
|
|
}
|
|
|
|
Image {
|
|
id: image
|
|
|
|
anchors.fill: parent
|
|
|
|
source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type
|
|
asynchronous: true
|
|
fillMode: Image.PreserveAspectCrop
|
|
sourceSize.width: width
|
|
sourceSize.height: height
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: details
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: visualiser.right
|
|
anchors.leftMargin: Appearance.spacing.normal
|
|
|
|
spacing: Appearance.spacing.small
|
|
|
|
StyledText {
|
|
id: title
|
|
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: parent.implicitWidth
|
|
|
|
animate: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
|
color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
|
font.pointSize: Appearance.font.size.normal
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
StyledText {
|
|
id: album
|
|
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: parent.implicitWidth
|
|
|
|
animate: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
visible: !!Players.active
|
|
text: Players.active?.trackAlbum || qsTr("Unknown album")
|
|
color: Colours.palette.m3outline
|
|
font.pointSize: Appearance.font.size.small
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
StyledText {
|
|
id: artist
|
|
|
|
Layout.fillWidth: true
|
|
Layout.maximumWidth: parent.implicitWidth
|
|
|
|
animate: true
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: (Players.active?.trackArtist ?? qsTr("Play some music for stuff to show up here!")) || qsTr("Unknown artist")
|
|
color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline
|
|
elide: Text.ElideRight
|
|
wrapMode: Players.active ? Text.NoWrap : Text.WordWrap
|
|
}
|
|
|
|
RowLayout {
|
|
id: controls
|
|
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.topMargin: Appearance.spacing.small
|
|
Layout.bottomMargin: Appearance.spacing.smaller
|
|
|
|
spacing: Appearance.spacing.small
|
|
|
|
PlayerControl {
|
|
type: IconButton.Text
|
|
icon: "skip_previous"
|
|
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
|
|
disabled: !Players.active?.canGoPrevious
|
|
onClicked: Players.active?.previous()
|
|
}
|
|
|
|
PlayerControl {
|
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
|
label.animate: true
|
|
toggle: true
|
|
padding: Appearance.padding.small / 2
|
|
checked: Players.active?.isPlaying ?? false
|
|
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
|
|
disabled: !Players.active?.canTogglePlaying
|
|
onClicked: Players.active?.togglePlaying()
|
|
}
|
|
|
|
PlayerControl {
|
|
type: IconButton.Text
|
|
icon: "skip_next"
|
|
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
|
|
disabled: !Players.active?.canGoNext
|
|
onClicked: Players.active?.next()
|
|
}
|
|
}
|
|
|
|
StyledSlider {
|
|
id: slider
|
|
|
|
enabled: !!Players.active
|
|
implicitWidth: 280
|
|
implicitHeight: Appearance.padding.normal * 3
|
|
|
|
onMoved: {
|
|
const active = Players.active;
|
|
if (active?.canSeek && active?.positionSupported)
|
|
active.position = value * active.length;
|
|
}
|
|
|
|
Binding {
|
|
target: slider
|
|
property: "value"
|
|
value: root.playerProgress
|
|
when: !slider.pressed
|
|
}
|
|
|
|
CustomMouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.NoButton
|
|
|
|
function onWheel(event: WheelEvent) {
|
|
const active = Players.active;
|
|
if (!active?.canSeek || !active?.positionSupported)
|
|
return;
|
|
|
|
event.accepted = true;
|
|
const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds
|
|
Qt.callLater(() => {
|
|
active.position = Math.max(0, Math.min(active.length, active.position + delta));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
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
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
spacing: Appearance.spacing.small
|
|
|
|
PlayerControl {
|
|
type: IconButton.Text
|
|
icon: "move_up"
|
|
inactiveOnColour: Colours.palette.m3secondary
|
|
padding: Appearance.padding.small
|
|
font.pointSize: Appearance.font.size.large
|
|
disabled: !Players.active?.canRaise
|
|
onClicked: {
|
|
Players.active?.raise();
|
|
root.visibilities.dashboard = false;
|
|
}
|
|
}
|
|
|
|
SplitButton {
|
|
id: playerSelector
|
|
|
|
disabled: !Players.list.length
|
|
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
|
|
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
|
|
|
|
menuItems: playerList.instances
|
|
fallbackIcon: "music_off"
|
|
fallbackText: qsTr("No players")
|
|
|
|
label.Layout.maximumWidth: slider.implicitWidth * 0.28
|
|
label.elide: Text.ElideRight
|
|
|
|
stateLayer.disabled: true
|
|
menuOnTop: true
|
|
|
|
Variants {
|
|
id: playerList
|
|
|
|
model: Players.list
|
|
|
|
PlayerItem {}
|
|
}
|
|
}
|
|
|
|
PlayerControl {
|
|
type: IconButton.Text
|
|
icon: "delete"
|
|
inactiveOnColour: Colours.palette.m3error
|
|
padding: Appearance.padding.small
|
|
font.pointSize: Appearance.font.size.large
|
|
disabled: !Players.active?.canQuit
|
|
onClicked: Players.active?.quit()
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: bongocat
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: details.right
|
|
anchors.leftMargin: Appearance.spacing.normal
|
|
|
|
implicitWidth: visualiser.width
|
|
implicitHeight: visualiser.height
|
|
|
|
AnimatedImage {
|
|
anchors.centerIn: parent
|
|
|
|
width: visualiser.width * 0.75
|
|
height: visualiser.height * 0.75
|
|
|
|
playing: Players.active?.isPlaying ?? false
|
|
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type
|
|
source: Paths.absolutePath(Config.paths.mediaGif)
|
|
asynchronous: true
|
|
fillMode: AnimatedImage.PreserveAspectFit
|
|
}
|
|
}
|
|
|
|
component PlayerItem: MenuItem {
|
|
required property MprisPlayer modelData
|
|
|
|
icon: modelData === Players.active ? "check" : ""
|
|
text: Players.getIdentity(modelData)
|
|
activeIcon: "animated_images"
|
|
}
|
|
|
|
component PlayerControl: IconButton {
|
|
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
|
|
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2
|
|
radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial
|
|
radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
|
|
Behavior on Layout.preferredWidth {
|
|
Anim {
|
|
duration: Appearance.anim.durations.expressiveFastSpatial
|
|
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
|
}
|
|
}
|
|
}
|
|
}
|