feat: improve dashboard media tab
This commit is contained in:
parent
8457438f14
commit
a28316ad5a
10 changed files with 662 additions and 491 deletions
|
|
@ -28,6 +28,8 @@ Row {
|
||||||
readonly property alias iconLabel: iconLabel
|
readonly property alias iconLabel: iconLabel
|
||||||
readonly property alias label: label
|
readonly property alias label: label
|
||||||
readonly property alias stateLayer: stateLayer
|
readonly property alias stateLayer: stateLayer
|
||||||
|
readonly property alias textRow: textRow
|
||||||
|
readonly property alias expandBtn: expandBtn
|
||||||
|
|
||||||
property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
|
property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
|
||||||
property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
|
property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Item {
|
||||||
const count = repeater.count;
|
const count = repeater.count;
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const item = repeater.itemAt(i) as Loader;
|
const item = repeater.itemAt(i) as Loader;
|
||||||
if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard)
|
if (item?.sourceComponent === mediaComponent && (item?.item as Media)?.needsKeyboard)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -174,7 +174,7 @@ Item {
|
||||||
Component {
|
Component {
|
||||||
id: mediaComponent
|
id: mediaComponent
|
||||||
|
|
||||||
MediaWrapper {
|
Media {
|
||||||
visibilities: root.visibilities
|
visibilities: root.visibilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,495 +1,36 @@
|
||||||
pragma ComponentBehavior: Bound
|
import "media"
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Shapes
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import Caelestia.Services
|
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.controls
|
|
||||||
import qs.components.widgets
|
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.utils
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property DrawerVisibilities visibilities
|
required property DrawerVisibilities visibilities
|
||||||
readonly property bool needsKeyboard: lyricMenuOpen
|
readonly property bool needsKeyboard: false // TODO
|
||||||
|
|
||||||
readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.extraLargeIncreased
|
implicitWidth: Tokens.sizes.dashboard.mediaTabWidth
|
||||||
readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight
|
implicitHeight: Tokens.sizes.dashboard.mediaTabHeight
|
||||||
|
|
||||||
property bool lyricMenuOpen: false
|
|
||||||
property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0
|
|
||||||
property bool lyricsShowingDebounced: false
|
|
||||||
|
|
||||||
property real playerProgress: {
|
|
||||||
const active = Players.active;
|
|
||||||
return active?.length ? (active.position % active.length) / 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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
onLyricsShowingChanged: {
|
|
||||||
if (lyricsShowing) {
|
|
||||||
lyricsHideDelay.stop();
|
|
||||||
lyricsShowingDebounced = true;
|
|
||||||
} else {
|
|
||||||
lyricsHideDelay.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.extraLargeIncreased
|
|
||||||
implicitHeight: nonAnimHeight
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on playerProgress {
|
|
||||||
Anim {
|
|
||||||
type: Anim.StandardLarge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
running: Players.active?.isPlaying ?? false
|
|
||||||
interval: GlobalConfig.dashboard.mediaUpdateInterval
|
|
||||||
triggeredOnStart: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
if (!Players.active)
|
|
||||||
return;
|
|
||||||
LyricsService.updatePosition();
|
|
||||||
Players.active?.positionChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: lyricsHideDelay
|
|
||||||
|
|
||||||
interval: 300
|
|
||||||
repeat: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onTriggered() {
|
|
||||||
root.lyricsShowingDebounced = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: lyricsHideDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
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 coverMaxRadius: {
|
|
||||||
const bounds = cover.shape.pathBounds();
|
|
||||||
return Math.max(bounds.width, bounds.height) / 2;
|
|
||||||
}
|
|
||||||
readonly property real spacing: Tokens.spacing.medium
|
|
||||||
|
|
||||||
anchors.fill: cover
|
|
||||||
anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize
|
|
||||||
|
|
||||||
asynchronous: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
data: visualiserBars.instances
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
id: visualiserBars
|
|
||||||
|
|
||||||
model: Array.from({
|
|
||||||
length: GlobalConfig.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 / GlobalConfig.services.visualiserBars
|
|
||||||
readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize
|
|
||||||
readonly property real shapeEdgeDist: {
|
|
||||||
cover.shape.rotation; // Update when shape rotation changes
|
|
||||||
return cover.shape.distanceAtAngle(modelData * 360 / GlobalConfig.services.visualiserBars);
|
|
||||||
}
|
|
||||||
readonly property real cos: Math.cos(angle)
|
|
||||||
readonly property real sin: Math.sin(angle)
|
|
||||||
|
|
||||||
capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
||||||
strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4
|
|
||||||
strokeColor: Colours.palette.m3primary
|
|
||||||
|
|
||||||
startX: visualiser.centerX + (shapeEdgeDist + visualiser.spacing + strokeWidth / 2) * cos
|
|
||||||
startY: visualiser.centerY + (shapeEdgeDist + visualiser.spacing + strokeWidth / 2) * sin
|
|
||||||
|
|
||||||
PathLine {
|
|
||||||
x: visualiser.centerX + (visualiser.coverMaxRadius + visualiser.spacing + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos
|
|
||||||
y: visualiser.centerY + (visualiser.coverMaxRadius + visualiser.spacing + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on strokeColor {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CoverArt {
|
|
||||||
id: cover
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize
|
|
||||||
|
|
||||||
implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize
|
|
||||||
implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
LyricsService.toggleVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: details
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.left: visualiser.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.medium
|
|
||||||
|
|
||||||
spacing: Tokens.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: Tokens.font.title.small
|
|
||||||
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: Tokens.font.body.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
|
|
||||||
}
|
|
||||||
|
|
||||||
LyricsView {
|
|
||||||
id: lyricsViewInDetails
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 200
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: controls
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.topMargin: Tokens.spacing.small
|
|
||||||
Layout.bottomMargin: Tokens.spacing.medium
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
type: IconButton.Text
|
|
||||||
icon: Players.active?.shuffle ? "shuffle_on" : "shuffle"
|
|
||||||
font: Tokens.font.icon.size(Math.round(Tokens.font.icon.large.pointSize)).build()
|
|
||||||
disabled: !Players.active?.shuffleSupported
|
|
||||||
onClicked: Players.active.shuffle = !Players.active?.shuffle
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
type: IconButton.Text
|
|
||||||
icon: "skip_previous"
|
|
||||||
font: Tokens.font.icon.builders.large.scale(1.5).build()
|
|
||||||
disabled: !Players.active?.canGoPrevious
|
|
||||||
onClicked: Players.active?.previous()
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
|
||||||
label.animate: true
|
|
||||||
isToggle: true
|
|
||||||
padding: Tokens.padding.extraSmall / 2
|
|
||||||
checked: Players.active?.isPlaying ?? false
|
|
||||||
font: Tokens.font.icon.builders.large.scale(1.5).build()
|
|
||||||
disabled: !Players.active?.canTogglePlaying
|
|
||||||
onClicked: Players.active?.togglePlaying()
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
type: IconButton.Text
|
|
||||||
icon: "skip_next"
|
|
||||||
font: Tokens.font.icon.builders.large.scale(1.5).build()
|
|
||||||
disabled: !Players.active?.canGoNext
|
|
||||||
onClicked: Players.active?.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
type: IconButton.Text
|
|
||||||
icon: "lyrics"
|
|
||||||
font: Tokens.font.icon.size(Math.round(Tokens.font.icon.large.pointSize)).build()
|
|
||||||
onClicked: root.lyricMenuOpen = !root.lyricMenuOpen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledSlider {
|
|
||||||
id: slider
|
|
||||||
|
|
||||||
enabled: !!Players.active
|
|
||||||
implicitWidth: 280
|
|
||||||
implicitHeight: Tokens.padding.medium * 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 {
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.NoButton
|
anchors.margins: Tokens.padding.large
|
||||||
}
|
spacing: Tokens.spacing.extraLarge
|
||||||
|
|
||||||
|
CoverVisualiser {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Details {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: position
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
|
|
||||||
text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1)
|
|
||||||
color: Colours.palette.m3onSurfaceVariant
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
LyricsAndSelector {
|
||||||
id: length
|
Layout.fillHeight: true
|
||||||
|
implicitWidth: Tokens.sizes.dashboard.mediaSectionWidth
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
text: root.lengthStr(Players.active?.length ?? -1)
|
|
||||||
color: Colours.palette.m3onSurfaceVariant
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: leftSection
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0
|
|
||||||
anchors.left: details.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.medium
|
|
||||||
|
|
||||||
visible: lyricMenu.height === 0 || opacity > 0
|
|
||||||
opacity: lyricMenu.height === 0 ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Tokens.anim.durations.normal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: bongocat
|
|
||||||
|
|
||||||
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 / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type
|
|
||||||
source: Paths.absolutePath(Config.paths.mediaGif)
|
|
||||||
asynchronous: true
|
|
||||||
fillMode: AnimatedImage.PreserveAspectFit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LyricMenu {
|
|
||||||
id: lyricMenu
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: details.right
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.medium
|
|
||||||
|
|
||||||
contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight
|
|
||||||
|
|
||||||
visible: root.lyricMenuOpen || height > 0
|
|
||||||
height: root.lyricMenuOpen ? implicitHeight : 0
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Tokens.anim.durations.normal
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: playerChanger
|
|
||||||
|
|
||||||
parent: !root.lyricsShowingDebounced ? details : leftSection
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
PlayerControl {
|
|
||||||
type: IconButton.Text
|
|
||||||
icon: "move_up"
|
|
||||||
inactiveOnColour: Colours.palette.m3secondary
|
|
||||||
padding: Tokens.padding.small
|
|
||||||
font: Tokens.font.icon.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: Tokens.padding.small
|
|
||||||
font: Tokens.font.icon.large
|
|
||||||
disabled: !Players.active?.canQuit
|
|
||||||
onClicked: Players.active?.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ? Tokens.padding.large : internalChecked ? Tokens.padding.small : 0)
|
|
||||||
radius: stateLayer.pressed ? Tokens.rounding.medium / 2 : internalChecked ? Tokens.rounding.medium : implicitHeight / 2
|
|
||||||
radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial
|
|
||||||
radiusAnim.easing: Tokens.anim.expressiveFastSpatial
|
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
|
||||||
Anim {
|
|
||||||
type: Anim.FastSpatial
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Item {
|
|
||||||
property alias visibilities: media.visibilities
|
|
||||||
readonly property alias needsKeyboard: media.needsKeyboard
|
|
||||||
|
|
||||||
implicitWidth: media.implicitWidth
|
|
||||||
implicitHeight: media.nonAnimHeight
|
|
||||||
|
|
||||||
Media {
|
|
||||||
id: media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
84
modules/dashboard/media/CoverVisualiser.qml
Normal file
84
modules/dashboard/media/CoverVisualiser.qml
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell
|
||||||
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
|
import qs.components
|
||||||
|
import qs.components.widgets
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property real centerX: width / 2
|
||||||
|
readonly property real centerY: height / 2
|
||||||
|
readonly property real coverMaxRadius: {
|
||||||
|
const bounds = cover.shape.pathBounds();
|
||||||
|
return Math.max(bounds.width, bounds.height) / 2;
|
||||||
|
}
|
||||||
|
readonly property real spacing: Tokens.spacing.medium
|
||||||
|
readonly property real maxMagnitude: (implicitWidth - cover.implicitWidth) / 2 - spacing
|
||||||
|
|
||||||
|
ServiceRef {
|
||||||
|
service: Audio.cava
|
||||||
|
}
|
||||||
|
|
||||||
|
Shape {
|
||||||
|
anchors.fill: parent
|
||||||
|
asynchronous: true
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
data: bars.instances
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
id: bars
|
||||||
|
|
||||||
|
model: Array.from({
|
||||||
|
length: GlobalConfig.services.visualiserBars
|
||||||
|
}, (_, i) => i)
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
id: bar
|
||||||
|
|
||||||
|
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 / GlobalConfig.services.visualiserBars
|
||||||
|
readonly property real dist: root.coverMaxRadius + root.spacing + strokeWidth / 2 + value * root.maxMagnitude
|
||||||
|
readonly property real shapeEdgeDist: {
|
||||||
|
cover.shape.rotation; // Update when shape rotation changes
|
||||||
|
const sDist = cover.shape.distanceAtAngle(modelData * 360 / GlobalConfig.services.visualiserBars);
|
||||||
|
return sDist + root.spacing + strokeWidth / 2;
|
||||||
|
}
|
||||||
|
readonly property real cos: Math.cos(angle)
|
||||||
|
readonly property real sin: Math.sin(angle)
|
||||||
|
|
||||||
|
asynchronous: true
|
||||||
|
capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
||||||
|
strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4
|
||||||
|
strokeColor: Colours.palette.m3primary
|
||||||
|
|
||||||
|
startX: root.centerX + shapeEdgeDist * cos
|
||||||
|
startY: root.centerY + shapeEdgeDist * sin
|
||||||
|
|
||||||
|
PathLine {
|
||||||
|
x: root.centerX + bar.dist * bar.cos
|
||||||
|
y: root.centerY + bar.dist * bar.sin
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on strokeColor {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CoverArt {
|
||||||
|
id: cover
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize
|
||||||
|
implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize
|
||||||
|
}
|
||||||
|
}
|
||||||
177
modules/dashboard/media/Details.qml
Normal file
177
modules/dashboard/media/Details.qml
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import Caelestia.Components
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
running: Players.active?.isPlaying ?? false
|
||||||
|
interval: GlobalConfig.dashboard.mediaUpdateInterval
|
||||||
|
triggeredOnStart: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (!Players.active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LyricsService.updatePosition();
|
||||||
|
Players.active.positionChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Players.active?.trackTitle ?? ""
|
||||||
|
font: Tokens.font.title.large
|
||||||
|
elide: Text.ElideRight
|
||||||
|
animate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Players.active?.trackArtist ?? qsTr("Unknown artist")
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: Tokens.font.title.medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
animate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: Players.active?.trackAlbum ?? qsTr("Unknown album")
|
||||||
|
color: Colours.palette.m3secondary
|
||||||
|
font: Tokens.font.title.medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
animate: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.topMargin: Tokens.spacing.extraLargeIncreased
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: timeMetrics
|
||||||
|
|
||||||
|
text: Players.active ? root.lengthStr(Math.max(Players.active.position, Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
|
||||||
|
font: Tokens.font.label.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.preferredWidth: timeMetrics.width
|
||||||
|
text: root.lengthStr(Players.active?.position ?? -1)
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: timeMetrics.font
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledSlider {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
|
||||||
|
enabled: Players.active?.canSeek ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.preferredWidth: timeMetrics.width
|
||||||
|
text: root.lengthStr(Players.active?.length ?? -1)
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: timeMetrics.font
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
Layout.topMargin: Tokens.spacing.largeIncreased
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
type: IconButton.Tonal
|
||||||
|
icon: "shuffle"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
checked: Players.active?.shuffle ?? false
|
||||||
|
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
|
||||||
|
disabled: !Players.active?.shuffleSupported
|
||||||
|
onClicked: Players.active.shuffle = !Players.active?.shuffle
|
||||||
|
implicitWidth: Math.round(implicitHeight * 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: previousBtn
|
||||||
|
|
||||||
|
type: IconButton.Tonal
|
||||||
|
icon: "skip_previous"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
font: Tokens.font.icon.large
|
||||||
|
disabled: !Players.active?.canGoPrevious
|
||||||
|
onClicked: Players.active?.previous()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: playPauseBtn
|
||||||
|
|
||||||
|
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
fillWidth: true
|
||||||
|
checked: Players.active?.isPlaying ?? false
|
||||||
|
font: Tokens.font.icon.large
|
||||||
|
disabled: !Players.active?.canTogglePlaying
|
||||||
|
onClicked: Players.active?.togglePlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: nextBtn
|
||||||
|
|
||||||
|
type: IconButton.Tonal
|
||||||
|
icon: "skip_next"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
font: Tokens.font.icon.large
|
||||||
|
disabled: !Players.active?.canGoNext
|
||||||
|
onClicked: Players.active?.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
type: IconButton.Tonal
|
||||||
|
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
|
||||||
|
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
|
||||||
|
disabled: !Players.active?.loopSupported
|
||||||
|
onClicked: {
|
||||||
|
const state = Players.active.loopState;
|
||||||
|
if (state === MprisLoopState.None)
|
||||||
|
Players.active.loopState = MprisLoopState.Track;
|
||||||
|
else if (state === MprisLoopState.Track)
|
||||||
|
Players.active.loopState = MprisLoopState.Playlist;
|
||||||
|
else
|
||||||
|
Players.active.loopState = MprisLoopState.None;
|
||||||
|
}
|
||||||
|
implicitWidth: Math.round(implicitHeight * 0.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
293
modules/dashboard/media/LyricList.qml
Normal file
293
modules/dashboard/media/LyricList.qml
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import Caelestia.Config
|
||||||
|
import Caelestia.Services
|
||||||
|
import qs.components
|
||||||
|
import qs.components.containers
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.components.effects
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Funny binding hack to make lyrics update
|
||||||
|
readonly property var _: {
|
||||||
|
const p = Players.active;
|
||||||
|
if (p)
|
||||||
|
Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length);
|
||||||
|
else
|
||||||
|
Lyrics.clearTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real fadeAmount: 0.1
|
||||||
|
property bool flag
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: Mask {
|
||||||
|
maskSource: mask
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: mask
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
implicitWidth: root.width
|
||||||
|
implicitHeight: root.height
|
||||||
|
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Vertical
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.alpha("black", 0)
|
||||||
|
position: 0
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.alpha("black", 1)
|
||||||
|
position: root.fadeAmount
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.alpha("black", 1)
|
||||||
|
position: 1 - root.fadeAmount
|
||||||
|
}
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.alpha("black", 0)
|
||||||
|
position: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state: {
|
||||||
|
flag; // For some reason it doesn't update sometimes, so use this to force an update
|
||||||
|
if (Lyrics.hasLyrics)
|
||||||
|
return "hasLyrics";
|
||||||
|
if (Lyrics.loading)
|
||||||
|
return "loading";
|
||||||
|
return "noLyrics";
|
||||||
|
}
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "loading"
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
loadingIndicator.opacity: 1
|
||||||
|
lyrics.opacity: 0
|
||||||
|
noLyrics.opacity: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "hasLyrics"
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
loadingIndicator.opacity: 0
|
||||||
|
lyrics.opacity: 1
|
||||||
|
noLyrics.opacity: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "noLyrics"
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
loadingIndicator.opacity: 0
|
||||||
|
lyrics.opacity: 0
|
||||||
|
noLyrics.opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: "loading"
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
Anim {
|
||||||
|
target: loadingIndicator
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
targets: [lyrics, noLyrics]
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "hasLyrics"
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
Anim {
|
||||||
|
target: lyrics
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
targets: [loadingIndicator, noLyrics]
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "noLyrics"
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
Anim {
|
||||||
|
target: noLyrics
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
targets: [loadingIndicator, lyrics]
|
||||||
|
property: "opacity"
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onHasLyricsChanged() {
|
||||||
|
root.flag = !root.flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Lyrics
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: loadingIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
asynchronous: true
|
||||||
|
active: opacity > 0
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
sourceComponent: ColumnLayout {
|
||||||
|
spacing: Tokens.spacing.large
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
|
||||||
|
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
|
||||||
|
color: Colours.palette.m3primaryContainer
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
LoadingIndicator {
|
||||||
|
id: shape
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitSize: Math.round(Tokens.sizes.dashboard.mediaSectionWidth / 5)
|
||||||
|
containsIcon: true // This removes the pentagon, which is not centered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Loading lyrics...")
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
font: Tokens.font.title.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: noLyrics
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
asynchronous: true
|
||||||
|
active: opacity > 0
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
sourceComponent: ColumnLayout {
|
||||||
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: "sentiment_sad"
|
||||||
|
fontStyle: Tokens.font.icon.builders.large.scale(2).build()
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("No lyrics found")
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
font: Tokens.font.title.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView {
|
||||||
|
id: lyrics
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: parent.height * root.fadeAmount / 2
|
||||||
|
anchors.bottomMargin: parent.height * root.fadeAmount / 2
|
||||||
|
|
||||||
|
displayMarginBeginning: anchors.topMargin
|
||||||
|
displayMarginEnd: anchors.bottomMargin
|
||||||
|
|
||||||
|
model: Lyrics.lyrics
|
||||||
|
Component.onCompleted: {
|
||||||
|
currentIndex = Qt.binding(() => {
|
||||||
|
Lyrics.lyrics; // Force update when lyrics change
|
||||||
|
return Lyrics.indexForTime(Players.active?.position ?? 0);
|
||||||
|
});
|
||||||
|
positionViewAtIndex(currentIndex, ListView.Center);
|
||||||
|
}
|
||||||
|
onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center))
|
||||||
|
|
||||||
|
highlightRangeMode: ListView.ApplyRange
|
||||||
|
highlightMoveDuration: Tokens.anim.durations.large
|
||||||
|
highlightMoveVelocity: -1
|
||||||
|
preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2
|
||||||
|
preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2
|
||||||
|
|
||||||
|
spacing: Tokens.spacing.small
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
delegate: StyledText {
|
||||||
|
id: lyric
|
||||||
|
|
||||||
|
required property string modelData
|
||||||
|
property real effectScale: ListView.isCurrentItem ? 1 : 0
|
||||||
|
|
||||||
|
text: modelData || ". . ."
|
||||||
|
color: ListView.isCurrentItem ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||||
|
font: Tokens.font.body.medium
|
||||||
|
|
||||||
|
layer.enabled: effectScale > 0
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
shadowEnabled: true
|
||||||
|
shadowColor: Colours.palette.m3primary
|
||||||
|
shadowOpacity: 0.5 * lyric.effectScale
|
||||||
|
shadowBlur: 0.6 * lyric.effectScale
|
||||||
|
blur: 0.4 * lyric.effectScale
|
||||||
|
autoPaddingEnabled: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on effectScale {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
modules/dashboard/media/LyricsAndSelector.qml
Normal file
83
modules/dashboard/media/LyricsAndSelector.qml
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import Caelestia.Config
|
||||||
|
import qs.components
|
||||||
|
import qs.components.controls
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Tokens.padding.medium
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.bottomMargin: -Tokens.spacing.medium
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||||
|
text: "lyrics"
|
||||||
|
fontStyle: Tokens.font.icon.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Lyrics")
|
||||||
|
font: Tokens.font.title.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
icon: "settings"
|
||||||
|
isToggle: true
|
||||||
|
isRound: true
|
||||||
|
type: IconButton.Tonal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LyricList {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitButton {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
|
type: SplitButton.Tonal
|
||||||
|
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")
|
||||||
|
|
||||||
|
minLeftWidth: layout.width - expandBtn.implicitWidth - spacing
|
||||||
|
label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2
|
||||||
|
label.elide: Text.ElideRight
|
||||||
|
|
||||||
|
stateLayer.disabled: true
|
||||||
|
menuOnTop: true
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
id: playerList
|
||||||
|
|
||||||
|
model: Players.list
|
||||||
|
|
||||||
|
PlayerItem {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component PlayerItem: MenuItem {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
|
||||||
|
icon: modelData === Players.active ? "check" : ""
|
||||||
|
text: Players.getIdentity(modelData)
|
||||||
|
activeIcon: "animated_images"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -187,8 +187,10 @@ class DashboardTokens : public ConfigObject {
|
||||||
CONFIG_PROPERTY(int, mediaProgressThickness, 6)
|
CONFIG_PROPERTY(int, mediaProgressThickness, 6)
|
||||||
CONFIG_PROPERTY(int, resourceProgressThickness, 6)
|
CONFIG_PROPERTY(int, resourceProgressThickness, 6)
|
||||||
CONFIG_PROPERTY(int, weatherWidth, 275)
|
CONFIG_PROPERTY(int, weatherWidth, 275)
|
||||||
CONFIG_PROPERTY(int, mediaCoverArtSize, 150)
|
CONFIG_PROPERTY(int, mediaCoverArtSize, 200)
|
||||||
CONFIG_PROPERTY(int, mediaVisualiserSize, 80)
|
CONFIG_PROPERTY(int, mediaTabWidth, 1000)
|
||||||
|
CONFIG_PROPERTY(int, mediaTabHeight, 320)
|
||||||
|
CONFIG_PROPERTY(int, mediaSectionWidth, 300)
|
||||||
CONFIG_PROPERTY(int, perfHeroCardWidth, 400)
|
CONFIG_PROPERTY(int, perfHeroCardWidth, 400)
|
||||||
CONFIG_PROPERTY(int, perfUsageShapeSize, 100)
|
CONFIG_PROPERTY(int, perfUsageShapeSize, 100)
|
||||||
CONFIG_PROPERTY(int, perfStorageTextWidth, 160)
|
CONFIG_PROPERTY(int, perfStorageTextWidth, 160)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ Singleton {
|
||||||
property alias manualActive: props.manualActive
|
property alias manualActive: props.manualActive
|
||||||
|
|
||||||
function getIdentity(player: MprisPlayer): string {
|
function getIdentity(player: MprisPlayer): string {
|
||||||
|
if (!player)
|
||||||
|
return "";
|
||||||
const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity);
|
const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity);
|
||||||
return alias?.to ?? player.identity;
|
return alias?.to ?? player.identity;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue