quickshell-toki-night/modules/dashboard/Media.qml

532 lines
17 KiB
QML

pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
Item {
id: root
required property PersistentProperties uiState
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: slider.width + 32
implicitHeight: childrenRect.height
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Item {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: Config.dashboard.mediaCoverArtHeight
CustomClippingRect {
id: cover
anchors.horizontalCenter: parent.horizontalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.verticalCenter: parent.top
anchors.verticalCenterOffset: Config.dashboard.mediaCoverArtHeight / 2
implicitWidth: image.paintedWidth > 0 ? image.paintedWidth : Config.dashboard.mediaCoverArtHeight
implicitHeight: image.paintedHeight > 0 ? image.paintedHeight : Config.dashboard.mediaCoverArtHeight
color: Config.colors.containerDash
radius: 12
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: Config.colors.tertiary
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.centerIn: parent
width: Config.dashboard.mediaCoverArtWidth
height: Config.dashboard.mediaCoverArtHeight
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
}
}
ColumnLayout {
id: details
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 12 + Config.dashboard.mediaCoverArtHeight
spacing: 8
CustomText {
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: Config.colors.secondary
font.pointSize: Config.font.size.normal
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
CustomText {
id: artist
Layout.fillWidth: true
visible: Players.active
animate: true
horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackArtist || qsTr("Unknown artist")
opacity: Players.active ? 1 : 0
color: Config.colors.primary
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
CustomText {
id: album
Layout.fillWidth: true
Layout.topMargin: -5
visible: text !== ""
animate: true
horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackAlbum ?? ""
color: Config.colors.tertiary
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
id: controls
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 12
Layout.bottomMargin: 10
spacing: 7
PlayerControl {
icon.text: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
CustomRect {
id: playBtn
implicitWidth: Math.max(playIcon.implicitWidth, playIcon.implicitHeight)
implicitHeight: implicitWidth
radius: Players.active?.isPlaying ? 12 : implicitHeight / 2
color: {
if (!Players.active?.canTogglePlaying)
return "transparent";
return Players.active?.isPlaying ? Config.colors.media : Config.colors.container;
}
StateLayer {
anchors.fill: parent
disabled: !Players.active?.canTogglePlaying
color: Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.primary
radius: parent.radius
function onClicked(): void {
Players.active?.togglePlaying();
}
}
MaterialIcon {
id: playIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -font.pointSize * 0.02
anchors.verticalCenterOffset: font.pointSize * 0.02
animate: true
fill: 1
text: Players.active?.isPlaying ? "pause" : "play_arrow"
color: {
if (!Players.active?.canTogglePlaying)
return Config.colors.inactive;
return Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.media;
}
font.pointSize: Config.font.size.largest
}
Behavior on radius {
Anim {}
}
}
PlayerControl {
icon.text: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
CustomSlider {
id: slider
enabled: !!Players.active
progressColor: Config.colors.media
implicitWidth: controls.implicitWidth * 1.5
implicitHeight: 20
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
Layout.topMargin: -6
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
opacity: Players.active ? 1 : 0
visible: opacity > 0
CustomText {
id: position
anchors.left: parent.left
text: root.lengthStr(Players.active?.position ?? 0)
color: Config.colors.primary
font.pointSize: Config.font.size.small
}
CustomText {
id: length
anchors.right: parent.right
text: root.lengthStr(Players.active?.length ?? 0)
color: Config.colors.primary
font.pointSize: Config.font.size.small
}
Behavior on opacity {
Anim { duration: Config.anim.durations.small }
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 12
spacing: 7
PlayerControl {
icon.text: "flip_to_front"
canUse: Players.active?.canRaise ?? false
fontSize: Config.font.size.larger
size: playerSelector.height
fill: 0
color: Config.colors.container
activeColor: Config.colors.secondary
function onClicked(): void {
Players.active?.raise();
root.uiState.dashboard = false;
}
}
CustomRect {
id: playerSelector
property bool expanded
Layout.alignment: Qt.AlignVCenter
implicitWidth: slider.implicitWidth * 0.6
implicitHeight: currentPlayer.implicitHeight + 14
radius: 17
color: Config.colors.container
z: 1
StateLayer {
anchors.fill: parent
disabled: Players.list.length <= 1
function onClicked(): void {
playerSelector.expanded = !playerSelector.expanded;
}
}
RowLayout {
id: currentPlayer
anchors.centerIn: parent
spacing: 7
PlayerIcon {
player: Players.active
}
CustomText {
Layout.fillWidth: true
Layout.maximumWidth: playerSelector.implicitWidth - implicitHeight - parent.spacing - 20
text: Players.active ? Players.getName(Players.active) : qsTr("No players")
color: Players.active ? Config.colors.primary : Config.colors.tertiary
elide: Text.ElideRight
}
}
Elevation {
anchors.fill: playerSelectorBg
radius: playerSelectorBg.radius
opacity: playerSelector.expanded ? 1 : 0
level: 2
Behavior on opacity {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
}
}
}
CustomClippingRect {
id: playerSelectorBg
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: playerSelector.expanded ? playerList.implicitWidth : playerSelector.implicitWidth
implicitHeight: playerSelector.expanded ? playerList.implicitHeight : playerSelector.implicitHeight
color: Config.colors.containerAlt
radius: 17
opacity: playerSelector.expanded ? 1 : 0
ColumnLayout {
id: playerList
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: 0
Repeater {
model: [...Players.list].sort((a, b) => (a === Players.active) - (b === Players.active))
Item {
id: player
required property MprisPlayer modelData
Layout.fillWidth: true
Layout.minimumWidth: playerSelector.implicitWidth
implicitWidth: playerInner.implicitWidth + 20
implicitHeight: playerInner.implicitHeight + 14
StateLayer {
disabled: !playerSelector.expanded
function onClicked(): void {
playerSelector.expanded = false;
Players.manualActive = player.modelData;
}
}
RowLayout {
id: playerInner
anchors.centerIn: parent
spacing: 7
PlayerIcon {
player: player.modelData
}
CustomText {
text: Players.getName(player.modelData)
color: Config.colors.primary
}
}
}
}
}
Behavior on opacity {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
}
}
Behavior on implicitWidth {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}
Behavior on implicitHeight {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}
}
}
PlayerControl {
icon.text: "close"
icon.anchors.horizontalCenterOffset: 0
canUse: Players.active?.canQuit ?? false
size: playerSelector.height
fontSize: Config.font.size.larger
fill: 1
color: Config.colors.container
activeColor: Config.colors.error
function onClicked(): void {
Players.active?.quit();
}
}
}
}
component PlayerIcon: Loader {
id: loader
required property MprisPlayer player
readonly property string icon: player ? Icons.getAppIcon(Players.getIdentity(player)) : ""
Layout.fillHeight: true
sourceComponent: !player || icon === "image://icon/" ? fallbackIcon : playerImage
Component {
id: playerImage
IconImage {
implicitWidth: height
source: loader.icon
}
}
Component {
id: fallbackIcon
MaterialIcon {
text: loader.player ? "animated_images" : "music_off"
}
}
}
component PlayerControl: CustomRect {
id: control
required property bool canUse
property alias icon: icon
property int fontSize: Config.font.size.largest
property int size: Math.max(icon.implicitWidth, icon.implicitHeight)
property real fill: 1
property color activeColor: Config.colors.media
function onClicked() {}
implicitWidth: size
implicitHeight: implicitWidth
radius: 1000
StateLayer {
anchors.fill: parent
disabled: !control.canUse
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -font.pointSize * 0.02
anchors.verticalCenterOffset: font.pointSize * 0.02
animate: true
fill: control.fill
text: control.icon
color: control.canUse ? control.activeColor : Config.colors.inactive
font.pointSize: control.fontSize
}
}
}