init: working version
This commit is contained in:
commit
7d8d7dacae
109 changed files with 15066 additions and 0 deletions
532
modules/dashboard/Media.qml
Normal file
532
modules/dashboard/Media.qml
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue