feat: dashboard show on hover, media and weather tabs added
This commit is contained in:
+137
-38
@@ -5,78 +5,177 @@ import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties dashState
|
||||
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
|
||||
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
|
||||
required property PersistentProperties visibilities
|
||||
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
|
||||
|
||||
required property ScreenState screenState
|
||||
|
||||
readonly property var dashboardTabs: {
|
||||
const allTabs = [
|
||||
{
|
||||
component: dashComponent,
|
||||
iconName: "dashboard",
|
||||
text: qsTr("Dashboard"),
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
component: weatherComponent,
|
||||
iconName: "cloud",
|
||||
text: qsTr("Weather"),
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
component: mediaComponent,
|
||||
iconName: "queue_music",
|
||||
text: qsTr("Media"),
|
||||
enabled: true
|
||||
},
|
||||
// {
|
||||
// component: testingComponent,
|
||||
// iconName: "speed",
|
||||
// text: qsTr("Testing"),
|
||||
// enabled: true
|
||||
// },
|
||||
];
|
||||
return allTabs.filter(tab => tab.enabled);
|
||||
}
|
||||
|
||||
implicitHeight: nonAnimHeight
|
||||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Tabs {
|
||||
id: tabs
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
|
||||
anchors.margins: Tokens.padding.large
|
||||
|
||||
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2
|
||||
screenState: root.screenState
|
||||
tabs: root.dashboardTabs
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: viewWrapper
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.top: tabs.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.small
|
||||
color: "transparent"
|
||||
radius: Tokens.rounding.extraLarge - anchors.margins
|
||||
|
||||
Item {
|
||||
Flickable {
|
||||
id: view
|
||||
|
||||
readonly property int currentIndex: root.dashState?.currentTab
|
||||
readonly property Item currentItem: row.children[currentIndex]
|
||||
readonly property int currentIndex: root.screenState.dashboardTab
|
||||
readonly property Item currentItem: {
|
||||
repeater.count;
|
||||
return repeater.itemAt(currentIndex);
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
|
||||
implicitWidth: currentItem?.implicitWidth ?? 0
|
||||
implicitHeight: currentItem?.implicitHeight ?? 0
|
||||
|
||||
contentX: currentItem?.x ?? 0
|
||||
contentWidth: row.implicitWidth
|
||||
contentHeight: row.implicitHeight
|
||||
|
||||
interactive: false
|
||||
|
||||
onContentXChanged: {
|
||||
if (!moving || !currentItem)
|
||||
return;
|
||||
|
||||
const x = contentX - currentItem.x;
|
||||
if (x > currentItem.implicitWidth / 2)
|
||||
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, tabs.count - 1);
|
||||
else if (x < -currentItem.implicitWidth / 2)
|
||||
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
Pane {
|
||||
index: 0
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
sourceComponent: Dash {
|
||||
dashState: root.dashState
|
||||
visibilities: root.visibilities
|
||||
model: ScriptModel {
|
||||
values: root.dashboardTabs
|
||||
}
|
||||
|
||||
delegate: Loader {
|
||||
id: paneLoader
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
sourceComponent: modelData.component
|
||||
|
||||
Component.onCompleted: active = Qt.binding(() => {
|
||||
if (index === view.currentIndex)
|
||||
return true;
|
||||
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
|
||||
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
|
||||
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dashComponent
|
||||
|
||||
Dash {
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: weatherComponent
|
||||
|
||||
WeatherTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mediaComponent
|
||||
|
||||
Media {
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
|
||||
// Component {
|
||||
// id: testingComponent
|
||||
//
|
||||
// WeatherTest {}
|
||||
// }
|
||||
|
||||
Behavior on contentX {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pane: Loader {
|
||||
id: pane
|
||||
|
||||
required property int index
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Component.onCompleted: active = Qt.binding(() => {
|
||||
// Always keep current tab loaded
|
||||
if (pane.index === view.currentIndex)
|
||||
return true;
|
||||
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
|
||||
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
|
||||
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,9 @@ import qs.Services
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties dashState
|
||||
readonly property bool dashboardVisible: visibilities.dashboard
|
||||
readonly property bool dashboardVisible: screenState.dashboard
|
||||
property int radius: Tokens.rounding.medium
|
||||
required property PersistentProperties visibilities
|
||||
required property ScreenState screenState
|
||||
|
||||
columnSpacing: Tokens.spacing.small
|
||||
rowSpacing: Tokens.spacing.small
|
||||
@@ -61,7 +60,7 @@ GridLayout {
|
||||
User {
|
||||
id: user
|
||||
|
||||
dashState: root.dashState
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +71,7 @@ GridLayout {
|
||||
Layout.row: 0
|
||||
radius: root.radius
|
||||
|
||||
Weather {
|
||||
}
|
||||
Weather {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
@@ -87,7 +85,7 @@ GridLayout {
|
||||
Calendar {
|
||||
id: calendar
|
||||
|
||||
dashState: root.dashState
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,20 +102,6 @@ GridLayout {
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.column: 0
|
||||
Layout.columnSpan: 5
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: media.implicitHeight
|
||||
Layout.row: 2
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: root.radius
|
||||
|
||||
Media {
|
||||
id: media
|
||||
}
|
||||
}
|
||||
|
||||
component Rect: CustomRect {
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ CustomMouseArea {
|
||||
id: root
|
||||
|
||||
property int activeGrid: 1
|
||||
required property PersistentProperties dashState
|
||||
required property ScreenState screenState
|
||||
property bool initialized: false
|
||||
property int month1
|
||||
property int month2
|
||||
readonly property int realCurrMonth: dashState.currentDate.getMonth()
|
||||
readonly property int realCurrYear: dashState.currentDate.getFullYear()
|
||||
readonly property int realCurrMonth: screenState.dashboardDate.getMonth()
|
||||
readonly property int realCurrYear: screenState.dashboardDate.getFullYear()
|
||||
property int year1
|
||||
property int year2
|
||||
|
||||
@@ -43,9 +43,9 @@ CustomMouseArea {
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
if (event.angleDelta.y > 0)
|
||||
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
|
||||
root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
|
||||
else if (event.angleDelta.y < 0)
|
||||
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
|
||||
root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
|
||||
}
|
||||
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
@@ -61,7 +61,7 @@ CustomMouseArea {
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
onClicked: root.dashState.currentDate = new Date()
|
||||
onClicked: root.screenState.dashboardDate = new Date()
|
||||
onRealCurrMonthChanged: handleDateChange()
|
||||
onRealCurrYearChanged: handleDateChange()
|
||||
|
||||
@@ -135,7 +135,7 @@ CustomMouseArea {
|
||||
padding: Tokens.padding.extraSmall
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
|
||||
onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -158,7 +158,7 @@ CustomMouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.dashState.currentDate = new Date()
|
||||
onClicked: root.screenState.dashboardDate = new Date()
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -206,7 +206,7 @@ CustomMouseArea {
|
||||
padding: Tokens.padding.extraSmall
|
||||
type: IconButton.Text
|
||||
|
||||
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
|
||||
onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Effects
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import ZShell.Config
|
||||
import qs.Modules.Dashboard.Dash.Media
|
||||
import qs.Daemons
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
|
||||
property real playerProgress: {
|
||||
const active = Players.active;
|
||||
return active?.length ? (active.position % active.length) / active.length : 0;
|
||||
}
|
||||
property int rowHeight: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
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}`;
|
||||
}
|
||||
|
||||
Behavior on playerProgress {
|
||||
Anim {
|
||||
duration: Tokens.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
|
||||
}
|
||||
|
||||
BackgroundShapes {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: visualizer
|
||||
|
||||
readonly property int bars: Config.services.visualizerBars
|
||||
property color color: Qt.alpha(Colors.palette.m3primary, 0.5)
|
||||
property color fillColor: Qt.alpha(color, 0.1)
|
||||
|
||||
anchors.fill: parent
|
||||
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 = root.height + shape.strokeWidth / 2;
|
||||
const w = root.width + shape.strokeWidth;
|
||||
|
||||
function x(i) {
|
||||
return i * w / (n - 1);
|
||||
}
|
||||
|
||||
function y(i) {
|
||||
return h - (values[i] * Config.dashboard.sizes.mediaVisualizerSize) / 2;
|
||||
}
|
||||
|
||||
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(Colors.palette.m3shadow, 0.2)
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: visualizer
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.leftMargin: Tokens.padding.extraLarge
|
||||
|
||||
CoverVisualizer {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.largeIncreased
|
||||
Layout.rightMargin: Tokens.spacing.largeIncreased
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MarqueeText {
|
||||
id: title
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
color: Colors.palette.m3primary
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
pauseMs: 4000
|
||||
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
implicitWidth: parent.width - Tokens.padding.large * 4
|
||||
}
|
||||
|
||||
MarqueeText {
|
||||
id: album
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
color: Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
pauseMs: 4000
|
||||
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
|
||||
implicitWidth: parent.width - Tokens.padding.large * 4
|
||||
}
|
||||
|
||||
MarqueeText {
|
||||
id: artist
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
color: Colors.palette.m3secondary
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
pauseMs: 4000
|
||||
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
||||
implicitWidth: parent.width - Tokens.padding.large * 4
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: positionSliderLayout
|
||||
Layout.topMargin: Tokens.spacing.large
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
TextMetrics {
|
||||
id: timeMetrics
|
||||
|
||||
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: positionLabel
|
||||
|
||||
Layout.preferredWidth: timeMetrics.width
|
||||
text: root.lengthStr(Players.active?.position ?? -1)
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: timeMetrics.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
id: positionSlider
|
||||
|
||||
Layout.fillWidth: true
|
||||
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
|
||||
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
|
||||
wavy: true
|
||||
animateWave: Players.active?.isPlaying ?? false
|
||||
waveFrequency: 5
|
||||
waveDuration: 2000
|
||||
interactionOnMove: false
|
||||
onInteraction: value => {
|
||||
const active = Players.active;
|
||||
if (active?.canSeek && active?.positionSupported)
|
||||
active.position = value * active.length;
|
||||
}
|
||||
Binding {
|
||||
target: positionLabel
|
||||
property: "text"
|
||||
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
|
||||
when: positionSlider.dragging
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.preferredWidth: timeMetrics.width
|
||||
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: timeMetrics.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
ButtonRow {
|
||||
Layout.topMargin: Tokens.spacing.largeIncreased
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
|
||||
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
|
||||
|
||||
IconButton {
|
||||
type: IconButton.Tonal
|
||||
icon: "shuffle"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
checked: Players.active?.shuffle ?? false
|
||||
enabled: Players.active?.shuffleSupported
|
||||
onClicked: Players.active.shuffle = !Players.active?.shuffle
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: previousBtn
|
||||
|
||||
type: IconButton.Tonal
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
enabled: Players.active?.canGoPrevious
|
||||
onClicked: Players.active?.previous()
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: playPauseBtn
|
||||
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
fillWidth: true
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: nextBtn
|
||||
|
||||
type: IconButton.Tonal
|
||||
icon: "skip_next"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
enabled: Players.active?.canGoNext
|
||||
onClicked: Players.active?.next()
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
|
||||
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
|
||||
enabled: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import qs.Helpers
|
||||
Row {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties dashState
|
||||
required property ScreenState screenState
|
||||
|
||||
padding: 20
|
||||
spacing: 12
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell.Config
|
||||
import qs.Modules.Dashboard.Media
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ScreenState screenState
|
||||
|
||||
implicitWidth: 1000
|
||||
implicitHeight: 320
|
||||
|
||||
BackgroundShapes {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.large
|
||||
spacing: Tokens.spacing.extraLarge
|
||||
|
||||
CoverVisualizer {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 250
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
state: Players.active ? "" : "noMedia"
|
||||
|
||||
states: State {
|
||||
name: "noMedia"
|
||||
|
||||
PropertyChanges {
|
||||
noMedia.opacity: 1
|
||||
content.opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: content
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
Anim {
|
||||
target: noMedia
|
||||
property: "opacity"
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: noMedia
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
Anim {
|
||||
target: content
|
||||
property: "opacity"
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: noMedia
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2
|
||||
asynchronous: true
|
||||
active: opacity > 0
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialShape {
|
||||
Layout.topMargin: (pathBounds().height - implicitSize) / 2
|
||||
Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3primaryContainer
|
||||
implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2
|
||||
shape: MaterialShape.ClamShell
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "queue_music"
|
||||
font.pointSize: Tokens.font.size.large
|
||||
color: Colors.palette.m3onPrimaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Nothing playing")
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Play something for it to show up here!")
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
active: opacity > 0
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: Tokens.spacing.extraLarge
|
||||
|
||||
Details {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LyricsAndSelector {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -11,8 +11,8 @@ Item {
|
||||
id: cover
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize
|
||||
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
|
||||
implicitHeight: 200
|
||||
implicitWidth: 200
|
||||
shape.shape: MaterialShape.Cookie9Sided
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
|
||||
|
||||
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: Config.dashboard.mediaUpdateInterval
|
||||
triggeredOnStart: true
|
||||
repeat: true
|
||||
onTriggered: Players.active?.positionChanged()
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: Players.active?.trackTitle ?? ""
|
||||
font.pointSize: Tokens.font.size.large
|
||||
elide: Text.ElideRight
|
||||
animate: true
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: Players.active?.trackArtist || qsTr("Unknown artist")
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
elide: Text.ElideRight
|
||||
animate: true
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: Players.active?.trackAlbum || qsTr("Unknown album")
|
||||
color: Colors.palette.m3secondary
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
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, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: positionLabel
|
||||
|
||||
Layout.preferredWidth: timeMetrics.width
|
||||
text: root.lengthStr(Players.active?.position ?? -1)
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: timeMetrics.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
id: positionSlider
|
||||
|
||||
Layout.fillWidth: true
|
||||
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
|
||||
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
|
||||
wavy: true
|
||||
animateWave: Players.active?.isPlaying ?? false
|
||||
waveFrequency: 5
|
||||
waveDuration: 2000
|
||||
interactionOnMove: false
|
||||
onInteraction: value => {
|
||||
const active = Players.active;
|
||||
if (active?.canSeek && active?.positionSupported)
|
||||
active.position = value * active.length;
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: positionLabel
|
||||
property: "text"
|
||||
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
|
||||
when: positionSlider.dragging
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.preferredWidth: timeMetrics.width
|
||||
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
|
||||
color: Colors.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.pointSize: Tokens.font.size.large
|
||||
enabled: 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.pointSize: Tokens.font.size.extraLarge
|
||||
enabled: 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.pointSize: Tokens.font.size.extraLarge
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: nextBtn
|
||||
|
||||
type: IconButton.Tonal
|
||||
icon: "skip_next"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
enabled: 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.pointSize: Tokens.font.size.large
|
||||
enabled: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Effects
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
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 list<string> lyricList: Lyrics.lyrics
|
||||
|
||||
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: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
asynchronous: true
|
||||
active: opacity > 0
|
||||
opacity: 0
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomRect {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
|
||||
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
|
||||
color: Colors.palette.m3primaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: shape
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitSize: Math.round(300 / 5)
|
||||
containsIcon: true // This removes the pentagon, which is not centered
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Loading lyrics...")
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
color: Colors.palette.m3outline
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("No lyrics found")
|
||||
color: Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
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: root.lyricList
|
||||
Component.onCompleted: {
|
||||
currentIndex = Qt.binding(() => {
|
||||
model; // 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
|
||||
enabled: opacity > 0
|
||||
|
||||
delegate: CustomText {
|
||||
id: lyric
|
||||
|
||||
required property string modelData
|
||||
required property int index
|
||||
property real effectScale: ListView.isCurrentItem ? 1 : 0
|
||||
|
||||
anchors.left: lyrics.contentItem.left
|
||||
anchors.right: lyrics.contentItem.right
|
||||
|
||||
text: modelData || ". . ."
|
||||
color: ListView.isCurrentItem ? Colors.palette.m3primary : mouse.containsMouse ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
font.pointSize: Tokens.font.size.medium
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
layer.enabled: effectScale > 0
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowColor: Colors.palette.m3primary
|
||||
shadowOpacity: 0.5 * lyric.effectScale
|
||||
shadowBlur: 0.6 * lyric.effectScale
|
||||
blur: 0.4 * lyric.effectScale
|
||||
}
|
||||
|
||||
Behavior on effectScale {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
const p = Players.active;
|
||||
if (p)
|
||||
p.position = Lyrics.timeForIndex(lyric.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on lyricList {
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: lyrics
|
||||
property: "opacity"
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
PropertyAction {}
|
||||
Anim {
|
||||
target: lyrics
|
||||
property: "opacity"
|
||||
to: 1
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
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
|
||||
z: 1
|
||||
|
||||
MaterialIcon {
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||
text: "lyrics"
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Lyrics")
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
|
||||
LyricsInfo {}
|
||||
}
|
||||
|
||||
LyricList {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
SplitButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
type: SplitButton.Tonal
|
||||
enabled: 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", "no media players active")
|
||||
|
||||
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.enabled: 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell
|
||||
import ZShell.Blobs
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool open
|
||||
readonly property real padding: Tokens.padding.large
|
||||
|
||||
implicitWidth: btn.implicitWidth * 0.9
|
||||
implicitHeight: btn.implicitHeight * 0.9
|
||||
|
||||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
color: Colors.palette.m3surfaceContainerHighest
|
||||
smoothing: Tokens.rounding.medium
|
||||
cornerFill: false
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
BlobRect {
|
||||
id: btnRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
|
||||
group: blobGroup
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
Behavior on anchors.margins {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
BlobRect {
|
||||
id: rect
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: parent.height
|
||||
|
||||
group: blobGroup
|
||||
radius: Tokens.rounding.medium
|
||||
deformScale: 0.00001
|
||||
|
||||
states: State {
|
||||
name: "open"
|
||||
when: root.open
|
||||
|
||||
PropertyChanges {
|
||||
rect.anchors.rightMargin: root.width - Tokens.spacing.small
|
||||
rect.anchors.topMargin: -Tokens.padding.medium
|
||||
rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2
|
||||
rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2
|
||||
content.opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "rightMargin,implicitWidth"
|
||||
}
|
||||
Anim {
|
||||
properties: "topMargin,implicitHeight"
|
||||
easing: Tokens.anim.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
opacity: 0
|
||||
state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics"
|
||||
|
||||
states: State {
|
||||
name: "hasLyrics"
|
||||
|
||||
PropertyChanges {
|
||||
layout.opacity: 1
|
||||
placeholder.opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "hasLyrics"
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: layout
|
||||
property: "opacity"
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
Anim {
|
||||
target: placeholder
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
to: "hasLyrics"
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
target: placeholder
|
||||
property: "opacity"
|
||||
type: Anim.FastEffects
|
||||
}
|
||||
Anim {
|
||||
target: layout
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
opacity: 0
|
||||
|
||||
CustomText {
|
||||
// TRANSLATORS: %1 = lyrics backend name, e.g. LRCLIB
|
||||
text: qsTr("Backend: %1").arg(ZUtils.enumToString(Lyrics, "backend"))
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
animate: true
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.maximumWidth: 1000 / 2
|
||||
// TRANSLATORS: %1/%2/%3 = matched track title, artist and album
|
||||
text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album)
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
animate: true
|
||||
}
|
||||
|
||||
CustomText {
|
||||
// TRANSLATORS: %1 = lyric timing offset; ms is the millisecond unit, leave untranslated
|
||||
text: qsTr("Offset: %1 ms").arg(Lyrics.offset)
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
animate: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: placeholder
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: placeholderText.implicitWidth
|
||||
implicitHeight: placeholderText.implicitHeight
|
||||
|
||||
CustomText {
|
||||
id: placeholderText
|
||||
|
||||
text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found")
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
animate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btn
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: root.open = !root.open
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "more_vert"
|
||||
font.pointSize: Tokens.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
-144
@@ -1,23 +1,24 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Templates
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property alias count: bar.count
|
||||
required property real nonAnimWidth
|
||||
required property PersistentProperties state
|
||||
required property ScreenState screenState
|
||||
required property var tabs
|
||||
|
||||
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
|
||||
readonly property alias count: bar.count
|
||||
readonly property int tabIndicatorSpacing: Tokens.spacing.small
|
||||
|
||||
implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
|
||||
|
||||
TabBar {
|
||||
id: bar
|
||||
@@ -25,85 +26,99 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.tabIndicatorSpacing
|
||||
|
||||
currentIndex: root.screenState.dashboardTab
|
||||
onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex
|
||||
|
||||
implicitHeight: contentHeight
|
||||
background: null
|
||||
currentIndex: root.state.currentTab
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
|
||||
onCurrentIndexChanged: root.state.currentTab = currentIndex
|
||||
|
||||
Tab {
|
||||
iconName: "dashboard"
|
||||
text: qsTr("Dashboard")
|
||||
Repeater {
|
||||
model: bar.contentModel
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "queue_music"
|
||||
text: qsTr("Media")
|
||||
}
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.tabs
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "speed"
|
||||
text: qsTr("Performance")
|
||||
}
|
||||
delegate: Tab {
|
||||
required property var modelData
|
||||
|
||||
Tab {
|
||||
iconName: "cloud"
|
||||
text: qsTr("Weather")
|
||||
iconName: modelData.iconName
|
||||
text: modelData.text
|
||||
}
|
||||
}
|
||||
|
||||
// Tab {
|
||||
// iconName: "workspaces"
|
||||
// text: qsTr("Workspaces")
|
||||
// }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: indicator
|
||||
|
||||
anchors.top: bar.bottom
|
||||
clip: true
|
||||
implicitHeight: 40
|
||||
implicitWidth: bar.currentItem.implicitWidth
|
||||
anchors.topMargin: root.tabIndicatorSpacing
|
||||
|
||||
implicitWidth: {
|
||||
const tab = bar.currentItem;
|
||||
if (tab)
|
||||
return tab.implicitWidth;
|
||||
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
|
||||
return width;
|
||||
}
|
||||
implicitHeight: 3
|
||||
|
||||
x: {
|
||||
const tab = bar.currentItem;
|
||||
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
|
||||
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
|
||||
const tabWidth = tab?.implicitWidth ?? width;
|
||||
return width * bar.currentIndex + (width - tabWidth) / 2;
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
CustomRect {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: parent.implicitHeight * 2
|
||||
|
||||
color: Colors.palette.m3primary
|
||||
radius: Tokens.rounding.full
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3primary
|
||||
implicitHeight: parent.implicitHeight * 2
|
||||
radius: Tokens.rounding.full
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: separator
|
||||
|
||||
anchors.top: indicator.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: indicator.bottom
|
||||
color: Colors.palette.m3outlineVariant
|
||||
|
||||
implicitHeight: 1
|
||||
color: Colors.palette.m3outlineVariant
|
||||
}
|
||||
|
||||
component Tab: TabButton {
|
||||
id: tab
|
||||
|
||||
readonly property bool current: TabBar.tabBar.currentItem === this
|
||||
required property string iconName
|
||||
readonly property bool current: TabBar.tabBar.currentItem === this
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 1 // Uniform width across all tabs
|
||||
implicitWidth: implicitContentWidth
|
||||
implicitHeight: implicitContentHeight
|
||||
background: null
|
||||
|
||||
contentItem: CustomMouseArea {
|
||||
@@ -111,117 +126,47 @@ Item {
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
if (event.angleDelta.y < 0)
|
||||
root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1);
|
||||
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, bar.count - 1);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
|
||||
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
|
||||
}
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
implicitHeight: icon.height + label.height
|
||||
implicitWidth: Math.max(icon.width, label.width)
|
||||
implicitHeight: icon.height + label.height
|
||||
|
||||
onPressed: event => {
|
||||
root.state.currentTab = tab.TabBar.index;
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
const stateY = stateWrapper.y;
|
||||
rippleAnim.x = event.x;
|
||||
rippleAnim.y = event.y - stateY;
|
||||
onPressed: root.screenState.dashboardTab = tab.TabBar.index
|
||||
|
||||
const dist = (ox, oy) => ox * ox + oy * oy;
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y)));
|
||||
|
||||
rippleAnim.restart();
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
property real radius
|
||||
property real x
|
||||
property real y
|
||||
|
||||
PropertyAction {
|
||||
property: "x"
|
||||
target: ripple
|
||||
value: rippleAnim.x
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "y"
|
||||
target: ripple
|
||||
value: rippleAnim.y
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "opacity"
|
||||
target: ripple
|
||||
value: 0.08
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
target: ripple
|
||||
to: rippleAnim.radius * 2
|
||||
}
|
||||
|
||||
Anim {
|
||||
easing.type: Easing.BezierSpline
|
||||
property: "opacity"
|
||||
target: ripple
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: stateWrapper
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
anchors.fill: undefined
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
implicitHeight: parent.height + 8 * 2
|
||||
radius: Tokens.rounding.small
|
||||
implicitHeight: parent.height + root.tabIndicatorSpacing * 2
|
||||
|
||||
CustomRect {
|
||||
id: stateLayer
|
||||
|
||||
anchors.fill: parent
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: ripple
|
||||
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
opacity: 0
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
radius: Tokens.rounding.medium
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
onClicked: root.screenState.dashboardTab = tab.TabBar.index
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.bottom: label.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: label.top
|
||||
|
||||
text: tab.iconName
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
fill: tab.current ? 1 : 0
|
||||
font.pointSize: 18
|
||||
text: tab.iconName
|
||||
font.pointSize: Tokens.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,10 +174,11 @@ Item {
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: tab.text
|
||||
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
readonly property list<var> forecastToday: Weather.hourlyForecast.slice(0, 25)
|
||||
|
||||
property var now: new Date()
|
||||
property bool useTwelveHourClock: Config.services.useTwelveHourClock
|
||||
property bool curveSmoothing: true
|
||||
property real smoothing: 2
|
||||
property real markerGap: Math.min(Math.max(Tokens.spacing.medium / 2, 0), 6)
|
||||
|
||||
readonly property real markerX: {
|
||||
const d = root.now;
|
||||
return d.getHours() + d.getMinutes() / 60;
|
||||
}
|
||||
|
||||
function formatHour(h) {
|
||||
if (root.useTwelveHourClock) {
|
||||
const hr = h % 12 === 0 ? 12 : h % 12;
|
||||
return hr + (h < 12 ? " AM" : " PM");
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.width / 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
MaterialIcon {
|
||||
text: "device_thermostat"
|
||||
font.pointSize: Tokens.font.size.large
|
||||
color: Colors.palette.m3secondary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Temperature")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: temperature
|
||||
|
||||
implicitHeight: 150
|
||||
Layout.fillWidth: true
|
||||
|
||||
LineGraph {
|
||||
id: tempGraph
|
||||
|
||||
anchors.fill: parent
|
||||
values: root.forecastToday.map(e => e.tempC)
|
||||
verticalLineStep: 3
|
||||
xSteps: 24
|
||||
horizontalLineStep: 10
|
||||
minHorizontalLines: 4
|
||||
yPaddingTop: horizontalLineStep
|
||||
yPaddingBottom: horizontalLineStep
|
||||
xLabel: i => root.formatHour(i)
|
||||
yLabel: v => Math.round(v) + "°"
|
||||
|
||||
markerX: root.markerX
|
||||
smoothing: root.smoothing
|
||||
curveSmoothing: root.curveSmoothing
|
||||
markerGap: root.markerGap
|
||||
|
||||
lineColor: Colors.palette.m3secondary
|
||||
gridColor: Colors.palette.m3onSurface
|
||||
markerColor: Colors.palette.m3secondary
|
||||
labelColor: Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.width / 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
MaterialIcon {
|
||||
text: "water_drop"
|
||||
font.pointSize: Tokens.font.size.large
|
||||
color: Colors.palette.m3tertiary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Chance of precipitation")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: precipitation
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 150
|
||||
|
||||
LineGraph {
|
||||
id: graph
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
values: root.forecastToday.map(e => e?.precipChance ?? null)
|
||||
xSteps: 24
|
||||
yMin: 0
|
||||
yMax: 100
|
||||
yPaddingTop: 20
|
||||
verticalLineStep: 3
|
||||
lastXLine: false
|
||||
horizontalLineStep: 20
|
||||
xLabel: i => root.formatHour(i)
|
||||
yLabel: v => v + "%"
|
||||
|
||||
markerX: root.markerX
|
||||
smoothing: root.smoothing
|
||||
curveSmoothing: root.curveSmoothing
|
||||
markerGap: root.markerGap
|
||||
|
||||
lineColor: Colors.palette.m3tertiary
|
||||
gridColor: Colors.palette.m3onSurface
|
||||
markerColor: Colors.palette.m3tertiary
|
||||
labelColor: Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules.Dashboard.Weather
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null
|
||||
|
||||
implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840
|
||||
implicitHeight: layout.implicitHeight
|
||||
Component.onCompleted: Weather.reload()
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
RowLayout {
|
||||
Layout.leftMargin: Tokens.padding.large
|
||||
Layout.rightMargin: Tokens.padding.large
|
||||
Layout.fillWidth: true
|
||||
|
||||
Column {
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
text: Weather.city || qsTr("Loading...")
|
||||
font.pointSize: Tokens.font.size.large
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
|
||||
CustomText {
|
||||
text: new Date().toLocaleDateString(Qt.locale("en_US"), "dddd, MMMM d")
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Tokens.spacing.largeIncreased
|
||||
|
||||
WeatherStat {
|
||||
icon: "wb_twilight"
|
||||
label: qsTr("Sunrise")
|
||||
value: Weather.sunrise
|
||||
colour: Colors.palette.m3tertiary
|
||||
}
|
||||
|
||||
WeatherStat {
|
||||
icon: "bedtime"
|
||||
label: qsTr("Sunset")
|
||||
value: Weather.sunset
|
||||
colour: Colors.palette.m3tertiary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: bigInfoRow.implicitHeight + bigInfoRow.anchors.margins * 2
|
||||
|
||||
radius: Tokens.rounding.large
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
|
||||
Graphs {
|
||||
id: bigInfoRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.medium
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
DetailCard {
|
||||
icon: Weather.icon
|
||||
label: qsTr("Temperature")
|
||||
value: Weather.temp
|
||||
colour: Colors.palette.m3primary
|
||||
}
|
||||
DetailCard {
|
||||
icon: "thermostat"
|
||||
label: qsTr("Feels like", "apparent temperature")
|
||||
value: Weather.feelsLike
|
||||
colour: Colors.palette.m3primary
|
||||
}
|
||||
DetailCard {
|
||||
icon: "water_drop"
|
||||
label: qsTr("Humidity")
|
||||
value: Strings.percent(Weather.humidity)
|
||||
colour: Colors.palette.m3secondary
|
||||
}
|
||||
DetailCard {
|
||||
icon: "air"
|
||||
label: qsTr("Wind")
|
||||
value: Weather.windSpeed ? qsTr("%1 km/h").arg(Weather.windSpeed) : "--"
|
||||
colour: Colors.palette.m3tertiary
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.topMargin: Tokens.spacing.medium
|
||||
Layout.leftMargin: Tokens.padding.medium
|
||||
visible: forecastRepeater.count > 0
|
||||
text: qsTr("7-day forecast")
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
Repeater {
|
||||
id: forecastRepeater
|
||||
|
||||
model: Weather.forecast
|
||||
|
||||
CustomRect {
|
||||
id: forecastItem
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
|
||||
|
||||
radius: Tokens.rounding.large
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastItemColumn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: forecastItem.index === 0 ? qsTr("Today", "forecast column") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "ddd")
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3primary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.topMargin: -Tokens.spacing.extraSmall
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale("en_US"), "MMM d")
|
||||
font.pointSize: Tokens.font.size.small
|
||||
opacity: 0.7
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: forecastItem.modelData.icon
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
color: Colors.palette.m3secondary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
const min = Weather.formatTemp(forecastItem.modelData.minTempC, true);
|
||||
const max = Weather.formatTemp(forecastItem.modelData.maxTempC, true);
|
||||
return qsTr("%1 / %2", "min/max temperature").arg(min).arg(max);
|
||||
}
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3tertiary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component DetailCard: CustomRect {
|
||||
id: detailRoot
|
||||
|
||||
property string icon
|
||||
property string label
|
||||
property string value
|
||||
property color colour
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 60
|
||||
radius: Tokens.rounding.medium
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
MaterialIcon {
|
||||
text: detailRoot.icon
|
||||
color: detailRoot.colour
|
||||
font.pointSize: Tokens.font.size.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
text: detailRoot.label
|
||||
font.pointSize: Tokens.font.size.small
|
||||
opacity: 0.7
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
CustomText {
|
||||
text: detailRoot.value
|
||||
font.pointSize: Tokens.font.size.small
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WeatherStat: Row {
|
||||
id: weatherStat
|
||||
|
||||
property string icon
|
||||
property string label
|
||||
property string value
|
||||
property color colour
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
text: weatherStat.icon
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
color: weatherStat.colour
|
||||
}
|
||||
|
||||
Column {
|
||||
CustomText {
|
||||
text: weatherStat.label
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
}
|
||||
CustomText {
|
||||
text: weatherStat.value
|
||||
font.pointSize: Tokens.font.size.small
|
||||
color: Colors.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,13 @@ import ZShell.Config
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property PersistentProperties dashState: PersistentProperties {
|
||||
property date currentDate: new Date()
|
||||
property int currentTab
|
||||
|
||||
reloadableId: "dashboardState"
|
||||
}
|
||||
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
|
||||
required property real offsetScale
|
||||
required property string position
|
||||
readonly property bool shouldBeActive: root.visibilities.dashboard && Config.dashboard.enabled
|
||||
required property PersistentProperties visibilities
|
||||
readonly property bool shouldBeActive: root.screenState.dashboard && Config.dashboard.enabled
|
||||
required property ScreenState screenState
|
||||
|
||||
anchors.bottom: position === "bottom" ? parent.bottom : undefined
|
||||
anchors.bottomMargin: position === "bottom" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
anchors.left: position === "left" ? parent.left : undefined
|
||||
anchors.leftMargin: position === "left" ? (-implicitWidth - 5) * offsetScale : 0
|
||||
anchors.top: position === "top" ? parent.top : undefined
|
||||
anchors.topMargin: position === "top" ? (-implicitHeight - 5) * offsetScale : 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (-implicitHeight - 5) * offsetScale
|
||||
implicitHeight: content.implicitHeight
|
||||
implicitWidth: content.implicitWidth || 854
|
||||
opacity: 1 - offsetScale
|
||||
@@ -35,15 +24,11 @@ Item {
|
||||
id: content
|
||||
|
||||
active: root.shouldBeActive || root.visible
|
||||
anchors.bottom: root.position === "top" ? parent.bottom : undefined
|
||||
anchors.horizontalCenter: root.position !== "left" ? parent.horizontalCenter : undefined
|
||||
anchors.right: root.position !== "left" ? undefined : parent.right
|
||||
anchors.top: root.position === "bottom" ? parent.top : undefined
|
||||
anchors.verticalCenter: root.position === "left" ? undefined : parent.verticalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
sourceComponent: Content {
|
||||
dashState: root.dashState
|
||||
visibilities: root.visibilities
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user