quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,66 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: Config.border.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: Colours.palette.m3surface
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
Behavior on fillColor {
CAnim {}
}
}

View File

@@ -0,0 +1,152 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.filedialog
import qs.config
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties visibilities
required property PersistentProperties state
required property FileDialog facePicker
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.padding.normal
anchors.margins: Appearance.padding.large
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2
state: root.state
}
ClippingRectangle {
id: viewWrapper
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
radius: Appearance.rounding.normal
color: "transparent"
Flickable {
id: view
readonly property int currentIndex: root.state.currentTab
readonly property Item currentItem: row.children[currentIndex]
anchors.fill: parent
flickableDirection: Flickable.HorizontalFlick
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
contentX: currentItem.x
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
onContentXChanged: {
if (!moving)
return;
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
}
onDragEnded: {
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 10)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 10)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
else
contentX = Qt.binding(() => currentItem.x);
}
RowLayout {
id: row
Pane {
index: 0
sourceComponent: Dash {
visibilities: root.visibilities
state: root.state
facePicker: root.facePicker
}
}
Pane {
index: 1
sourceComponent: Media {
visibilities: root.visibilities
}
}
Pane {
index: 2
sourceComponent: Performance {}
}
Pane {
index: 3
sourceComponent: Weather {}
}
}
Behavior on contentX {
Anim {}
}
}
}
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
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);
})
}
}

View File

@@ -0,0 +1,105 @@
import qs.components
import qs.components.filedialog
import qs.services
import qs.config
import "dash"
import Quickshell
import QtQuick.Layouts
GridLayout {
id: root
required property PersistentProperties visibilities
required property PersistentProperties state
required property FileDialog facePicker
rowSpacing: Appearance.spacing.normal
columnSpacing: Appearance.spacing.normal
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredWidth: user.implicitWidth
Layout.preferredHeight: user.implicitHeight
radius: Appearance.rounding.large
User {
id: user
visibilities: root.visibilities
state: root.state
facePicker: root.facePicker
}
}
Rect {
Layout.row: 0
Layout.columnSpan: 2
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth
Layout.fillHeight: true
radius: Appearance.rounding.large * 1.5
Weather {}
}
Rect {
Layout.row: 1
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.normal
DateTime {
id: dateTime
}
}
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
radius: Appearance.rounding.large
Calendar {
id: calendar
state: root.state
}
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.normal
Resources {
id: resources
}
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.large * 2
Media {
id: media
}
}
component Rect: StyledRect {
color: Colours.tPalette.m3surfaceContainer
}
}

View File

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

View File

@@ -0,0 +1,967 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.components
import qs.components.misc
import qs.config
import qs.services
Item {
id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
function displayTemp(temp: real): string {
return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`;
}
implicitWidth: Math.max(minWidth, content.implicitWidth)
implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight
StyledRect {
id: placeholder
anchors.centerIn: parent
width: 400
height: 350
radius: Appearance.rounding.large
color: Colours.tPalette.m3surfaceContainer
visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)
ColumnLayout {
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "tune"
font.pointSize: Appearance.font.size.extraLarge * 2
color: Colours.palette.m3onSurfaceVariant
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("No widgets enabled")
font.pointSize: Appearance.font.size.large
color: Colours.palette.m3onSurface
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Enable widgets in dashboard settings")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
}
}
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
visible: !placeholder.visible
Ref {
service: SystemUsage
}
ColumnLayout {
id: mainColumn
Layout.fillWidth: true
spacing: Appearance.spacing.normal
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
visible: Config.dashboard.performance.showCpu
icon: "memory"
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
mainLabel: qsTr("Usage")
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
secondaryLabel: qsTr("Temp")
usage: SystemUsage.cpuPerc
temperature: SystemUsage.cpuTemp
accentColor: Colours.palette.m3primary
}
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
icon: "desktop_windows"
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
mainLabel: qsTr("Usage")
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
secondaryLabel: qsTr("Temp")
usage: SystemUsage.gpuPerc
temperature: SystemUsage.gpuTemp
accentColor: Colours.palette.m3secondary
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
GaugeCard {
Layout.minimumWidth: 250
Layout.preferredHeight: 220
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
icon: "memory_alt"
title: qsTr("Memory")
percentage: SystemUsage.memPerc
subtitle: {
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
accentColor: Colours.palette.m3tertiary
visible: Config.dashboard.performance.showMemory
}
StorageGaugeCard {
Layout.minimumWidth: 250
Layout.preferredHeight: 220
Layout.fillWidth: !Config.dashboard.performance.showNetwork
visible: Config.dashboard.performance.showStorage
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
}
}
}
BatteryTank {
Layout.preferredWidth: 120
Layout.preferredHeight: mainColumn.implicitHeight
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
}
}
component BatteryTank: StyledClippingRect {
id: batteryTank
property real percentage: UPower.displayDevice.percentage
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
property color accentColor: Colours.palette.m3primary
property real animatedPercentage: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
// Background Fill
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height * batteryTank.animatedPercentage
color: Qt.alpha(batteryTank.accentColor, 0.15)
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
// Header Section
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return "battery_full";
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc >= 0.99)
return "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
font.pointSize: Appearance.font.size.large
color: batteryTank.accentColor
}
StyledText {
Layout.fillWidth: true
text: qsTr("Battery")
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurface
}
}
Item {
Layout.fillHeight: true
}
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
StyledText {
Layout.alignment: Qt.AlignRight
text: `${Math.round(batteryTank.percentage * 100)}%`
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: batteryTank.accentColor
}
StyledText {
Layout.alignment: Qt.AlignRight
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (batteryTank.isCharging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component CardHeader: RowLayout {
property string icon
property string title
property color accentColor: Colours.palette.m3primary
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
text: parent.icon
fill: 1
color: parent.accentColor
font.pointSize: Appearance.spacing.large
}
StyledText {
Layout.fillWidth: true
text: parent.title
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
}
component ProgressBar: StyledRect {
id: progressBar
property real value: 0
property color fgColor: Colours.palette.m3primary
property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
property real animatedValue: 0
color: bgColor
radius: Appearance.rounding.full
Component.onCompleted: animatedValue = value
onValueChanged: animatedValue = value
StyledRect {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * progressBar.animatedValue
color: progressBar.fgColor
radius: Appearance.rounding.full
}
Behavior on animatedValue {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component HeroCard: StyledClippingRect {
id: heroCard
property string icon
property string title
property string mainValue
property string mainLabel
property string secondaryValue
property string secondaryLabel
property real usage: 0
property real temperature: 0
property color accentColor: Colours.palette.m3primary
readonly property real maxTemp: 100
readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
property real animatedUsage: 0
property real animatedTemp: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
Component.onCompleted: {
animatedUsage = usage;
animatedTemp = tempProgress;
}
onUsageChanged: animatedUsage = usage
onTempProgressChanged: animatedTemp = tempProgress
StyledRect {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * heroCard.animatedUsage
color: Qt.alpha(heroCard.accentColor, 0.15)
}
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
anchors.topMargin: Appearance.padding.normal
anchors.bottomMargin: Appearance.padding.normal
spacing: Appearance.spacing.small
CardHeader {
icon: heroCard.icon
title: heroCard.title
accentColor: heroCard.accentColor
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Appearance.spacing.normal
Column {
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
spacing: Appearance.spacing.small
Row {
spacing: Appearance.spacing.small
StyledText {
text: heroCard.secondaryValue
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
}
StyledText {
text: heroCard.secondaryLabel
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
anchors.baseline: parent.children[0].baseline
}
}
ProgressBar {
width: parent.width * 0.5
height: 6
value: heroCard.tempProgress
fgColor: heroCard.accentColor
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
}
}
Item {
Layout.fillWidth: true
}
}
}
Column {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
anchors.rightMargin: 32
spacing: 0
StyledText {
anchors.right: parent.right
text: heroCard.mainLabel
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurfaceVariant
}
StyledText {
anchors.right: parent.right
text: heroCard.mainValue
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: heroCard.accentColor
}
}
Behavior on animatedUsage {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on animatedTemp {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component GaugeCard: StyledRect {
id: gaugeCard
property string icon
property string title
property real percentage: 0
property string subtitle
property color accentColor: Colours.palette.m3primary
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property real animatedPercentage: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
icon: gaugeCard.icon
title: gaugeCard.title
accentColor: gaugeCard.accentColor
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: gaugeCanvas
anchors.centerIn: parent
width: Math.min(parent.width, parent.height)
height: width
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (gaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = gaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: gaugeCard
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: Colours
}
}
StyledText {
anchors.centerIn: parent
text: `${Math.round(gaugeCard.percentage * 100)}%`
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: gaugeCard.accentColor
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: gaugeCard.subtitle
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component StorageGaugeCard: StyledRect {
id: storageGaugeCard
property int currentDiskIndex: 0
readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
property int diskCount: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property real animatedPercentage: 0
property color accentColor: Colours.palette.m3secondary
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Component.onCompleted: {
diskCount = SystemUsage.disks.length;
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
onCurrentDiskChanged: {
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
// Update diskCount and animatedPercentage when disks data changes
Connections {
function onDisksChanged() {
if (SystemUsage.disks.length !== storageGaugeCard.diskCount)
storageGaugeCard.diskCount = SystemUsage.disks.length;
// Update animated percentage when disk data refreshes
if (storageGaugeCard.currentDisk)
storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
}
target: SystemUsage
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;
else if (wheel.angleDelta.y < 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
icon: "hard_disk"
title: {
const base = qsTr("Storage");
if (!storageGaugeCard.currentDisk)
return base;
return `${base} - ${storageGaugeCard.currentDisk.mount}`;
}
accentColor: storageGaugeCard.accentColor
// Scroll hint icon
MaterialIcon {
text: "unfold_more"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
visible: storageGaugeCard.diskCount > 1
opacity: 0.7
ToolTip.visible: hintHover.hovered
ToolTip.text: qsTr("Scroll to switch disks")
ToolTip.delay: 500
HoverHandler {
id: hintHover
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: storageGaugeCanvas
anchors.centerIn: parent
width: Math.min(parent.width, parent.height)
height: width
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (storageGaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = storageGaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
storageGaugeCanvas.requestPaint();
}
target: storageGaugeCard
}
Connections {
function onPaletteChanged() {
storageGaugeCanvas.requestPaint();
}
target: Colours
}
}
StyledText {
anchors.centerIn: parent
text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: storageGaugeCard.accentColor
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: {
if (!storageGaugeCard.currentDisk)
return "—";
const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);
const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component NetworkCard: StyledRect {
id: networkCard
property color accentColor: Colours.palette.m3primary
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Ref {
service: NetworkUsage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
CardHeader {
icon: "swap_vert"
title: qsTr("Network")
accentColor: networkCard.accentColor
}
// Sparkline graph
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: sparklineCanvas
property var downHistory: NetworkUsage.downloadHistory
property var upHistory: NetworkUsage.uploadHistory
property real targetMax: 1024
property real smoothMax: targetMax
property real slideProgress: 0
property int _tickCount: 0
property int _lastTickCount: -1
function checkAndAnimate(): void {
const currentLength = (downHistory || []).length;
if (currentLength > 0 && _tickCount !== _lastTickCount) {
_lastTickCount = _tickCount;
updateMax();
}
}
function updateMax(): void {
const downHist = downHistory || [];
const upHist = upHistory || [];
const allValues = downHist.concat(upHist);
targetMax = Math.max(...allValues, 1024);
requestPaint();
}
anchors.fill: parent
onDownHistoryChanged: checkAndAnimate()
onUpHistoryChanged: checkAndAnimate()
onSmoothMaxChanged: requestPaint()
onSlideProgressChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const w = width;
const h = height;
const downHist = downHistory || [];
const upHist = upHistory || [];
if (downHist.length < 2 && upHist.length < 2)
return;
const maxVal = smoothMax;
const drawLine = (history, color, fillAlpha) => {
if (history.length < 2)
return;
const len = history.length;
const stepX = w / (NetworkUsage.historyLength - 1);
const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX;
ctx.beginPath();
ctx.moveTo(startX, h - (history[0] / maxVal) * h);
for (let i = 1; i < len; i++) {
const x = startX + i * stepX;
const y = h - (history[i] / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
ctx.lineTo(startX + (len - 1) * stepX, h);
ctx.lineTo(startX, h);
ctx.closePath();
ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha);
ctx.fill();
};
drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15);
drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2);
}
Component.onCompleted: updateMax()
Connections {
function onPaletteChanged() {
sparklineCanvas.requestPaint();
}
target: Colours
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
running: true
repeat: true
onTriggered: sparklineCanvas._tickCount++
}
NumberAnimation on slideProgress {
from: 0
to: 1
duration: Config.dashboard.resourceUpdateInterval
loops: Animation.Infinite
running: true
}
Behavior on smoothMax {
Anim {
duration: Appearance.anim.durations.large
}
}
}
// "No data" placeholder
StyledText {
anchors.centerIn: parent
text: qsTr("Collecting data...")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
visible: NetworkUsage.downloadHistory.length < 2
opacity: 0.6
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "download"
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Download")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
color: Colours.palette.m3tertiary
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "upload"
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Upload")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
color: Colours.palette.m3secondary
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "history"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Total")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
}

View File

@@ -0,0 +1,247 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
Item {
id: root
required property real nonAnimWidth
required property PersistentProperties state
readonly property alias count: bar.count
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
currentIndex: root.state.currentTab
background: null
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
Tab {
iconName: "cloud"
text: qsTr("Weather")
}
// Tab {
// iconName: "workspaces"
// text: qsTr("Workspaces")
// }
}
Item {
id: indicator
anchors.top: bar.bottom
anchors.topMargin: 5
implicitWidth: bar.currentItem.implicitWidth
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;
}
clip: true
StyledRect {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight * 2
color: Colours.palette.m3primary
radius: Appearance.rounding.full
}
Behavior on x {
Anim {}
}
Behavior on implicitWidth {
Anim {}
}
}
StyledRect {
id: separator
anchors.top: indicator.bottom
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 1
color: Colours.palette.m3outlineVariant
}
component Tab: TabButton {
id: tab
required property string iconName
readonly property bool current: TabBar.tabBar.currentItem === this
background: null
contentItem: CustomMouseArea {
id: mouse
implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
cursorShape: Qt.PointingHandCursor
onPressed: event => {
root.state.currentTab = tab.TabBar.index;
const stateY = stateWrapper.y;
rippleAnim.x = event.x;
rippleAnim.y = event.y - stateY;
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();
}
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y < 0)
root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1);
else if (event.angleDelta.y > 0)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
}
SequentialAnimation {
id: rippleAnim
property real x
property real y
property real radius
PropertyAction {
target: ripple
property: "x"
value: rippleAnim.x
}
PropertyAction {
target: ripple
property: "y"
value: rippleAnim.y
}
PropertyAction {
target: ripple
property: "opacity"
value: 0.08
}
Anim {
target: ripple
properties: "implicitWidth,implicitHeight"
from: 0
to: rippleAnim.radius * 2
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: ripple
property: "opacity"
to: 0
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
ClippingRectangle {
id: stateWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2
color: "transparent"
radius: Appearance.rounding.small
StyledRect {
id: stateLayer
anchors.fill: parent
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
Behavior on opacity {
Anim {}
}
}
StyledRect {
id: ripple
radius: Appearance.rounding.full
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
opacity: 0
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
}
MaterialIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
font.pointSize: Appearance.font.size.large
Behavior on fill {
Anim {}
}
}
StyledText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
text: tab.text
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
}
}
}
}

View File

@@ -0,0 +1,280 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
Item {
id: root
implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840
implicitHeight: layout.implicitHeight
readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null
Component.onCompleted: Weather.reload()
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Appearance.spacing.smaller
RowLayout {
Layout.leftMargin: Appearance.padding.large
Layout.rightMargin: Appearance.padding.large
Layout.fillWidth: true
Column {
spacing: Appearance.spacing.small / 2
StyledText {
text: Weather.city || qsTr("Loading...")
font.pointSize: Appearance.font.size.extraLarge
font.weight: 600
color: Colours.palette.m3onSurface
}
StyledText {
text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
}
Item {
Layout.fillWidth: true
}
Row {
spacing: Appearance.spacing.large
WeatherStat {
icon: "wb_twilight"
label: "Sunrise"
value: Weather.sunrise
colour: Colours.palette.m3tertiary
}
WeatherStat {
icon: "bedtime"
label: "Sunset"
value: Weather.sunset
colour: Colours.palette.m3tertiary
}
}
}
StyledRect {
Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.large * 2
color: Colours.tPalette.m3surfaceContainer
RowLayout {
id: bigInfoRow
anchors.centerIn: parent
spacing: Appearance.spacing.large
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
text: Weather.icon
font.pointSize: Appearance.font.size.extraLarge * 3
color: Colours.palette.m3secondary
animate: true
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
spacing: -Appearance.spacing.small
StyledText {
text: Weather.temp
font.pointSize: Appearance.font.size.extraLarge * 2
font.weight: 500
color: Colours.palette.m3primary
}
StyledText {
Layout.leftMargin: Appearance.padding.small
text: Weather.description
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
DetailCard {
icon: "water_drop"
label: "Humidity"
value: Weather.humidity + "%"
colour: Colours.palette.m3secondary
}
DetailCard {
icon: "thermostat"
label: "Feels Like"
value: Weather.feelsLike
colour: Colours.palette.m3primary
}
DetailCard {
icon: "air"
label: "Wind"
value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--"
colour: Colours.palette.m3tertiary
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
Layout.leftMargin: Appearance.padding.normal
visible: forecastRepeater.count > 0
text: qsTr("7-Day Forecast")
font.pointSize: Appearance.font.size.normal
font.weight: 600
color: Colours.palette.m3onSurface
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
Repeater {
id: forecastRepeater
model: Weather.forecast
StyledRect {
id: forecastItem
required property int index
required property var modelData
Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
id: forecastItemColumn
anchors.centerIn: parent
spacing: Appearance.spacing.small
StyledText {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd")
font.pointSize: Appearance.font.size.normal
font.weight: 600
color: Colours.palette.m3primary
}
StyledText {
Layout.topMargin: -Appearance.spacing.small / 2
Layout.alignment: Qt.AlignHCenter
text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d")
font.pointSize: Appearance.font.size.small
opacity: 0.7
color: Colours.palette.m3onSurfaceVariant
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.modelData.icon
font.pointSize: Appearance.font.size.extraLarge
color: Colours.palette.m3secondary
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°"
font.weight: 600
color: Colours.palette.m3tertiary
}
}
}
}
}
}
component DetailCard: StyledRect {
id: detailRoot
property string icon
property string label
property string value
property color colour
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Appearance.rounding.small
color: Colours.tPalette.m3surfaceContainer
Row {
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
text: detailRoot.icon
color: detailRoot.colour
font.pointSize: Appearance.font.size.large
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
text: detailRoot.label
font.pointSize: Appearance.font.size.smaller
opacity: 0.7
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: detailRoot.value
font.weight: 600
horizontalAlignment: Text.AlignLeft
}
}
}
}
component WeatherStat: Row {
id: weatherStat
property string icon
property string label
property string value
property color colour
spacing: Appearance.spacing.small
MaterialIcon {
text: weatherStat.icon
font.pointSize: Appearance.font.size.extraLarge
color: weatherStat.colour
}
Column {
StyledText {
text: weatherStat.label
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
StyledText {
text: weatherStat.value
font.pointSize: Appearance.font.size.small
font.weight: 600
color: Colours.palette.m3onSurface
}
}
}
}

View File

@@ -0,0 +1,105 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.filedialog
import qs.config
import qs.utils
import Caelestia
import Quickshell
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
readonly property PersistentProperties dashState: PersistentProperties {
property int currentTab
property date currentDate: new Date()
reloadableId: "dashboardState"
}
readonly property FileDialog facePicker: FileDialog {
title: qsTr("Select a profile picture")
filterLabel: qsTr("Image files")
filters: Images.validImageExtensions
onAccepted: path => {
if (CUtils.copyFile(Qt.resolvedUrl(path), Qt.resolvedUrl(`${Paths.home}/.face`)))
Quickshell.execDetached(["notify-send", "-a", "caelestia-shell", "-u", "low", "-h", `STRING:image-path:${path}`, "Profile picture changed", `Profile picture changed to ${Paths.shortenHome(path)}`]);
else
Quickshell.execDetached(["notify-send", "-a", "caelestia-shell", "-u", "critical", "Unable to change profile picture", `Failed to change profile picture to ${Paths.shortenHome(path)}`]);
}
}
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
states: State {
name: "visible"
when: root.visibilities.dashboard && Config.dashboard.enabled
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitHeight"
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Timer {
id: timer
running: true
interval: Appearance.anim.durations.extraLarge
onTriggered: {
content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible);
content.visible = true;
}
}
Loader {
id: content
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
visible: false
active: true
sourceComponent: Content {
visibilities: root.visibilities
state: root.dashState
facePicker: root.facePicker
}
}
}

View File

@@ -0,0 +1,253 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.effects
import qs.components.controls
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
CustomMouseArea {
id: root
required property var state
readonly property int currMonth: state.currentDate.getMonth()
readonly property int currYear: state.currentDate.getFullYear()
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
acceptedButtons: Qt.MiddleButton
onClicked: root.state.currentDate = new Date()
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0)
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
else if (event.angleDelta.y < 0)
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
ColumnLayout {
id: inner
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
RowLayout {
id: monthNavigationRow
Layout.fillWidth: true
spacing: Appearance.spacing.small
Item {
implicitWidth: implicitHeight
implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2
StateLayer {
id: prevMonthStateLayer
radius: Appearance.rounding.full
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
}
}
MaterialIcon {
id: prevMonthText
anchors.centerIn: parent
text: "chevron_left"
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
}
}
Item {
Layout.fillWidth: true
implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2
implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2
StateLayer {
anchors.fill: monthYearDisplay
anchors.margins: -Appearance.padding.small
anchors.leftMargin: -Appearance.padding.normal
anchors.rightMargin: -Appearance.padding.normal
radius: Appearance.rounding.full
disabled: {
const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();
}
function onClicked(): void {
root.state.currentDate = new Date();
}
}
StyledText {
id: monthYearDisplay
anchors.centerIn: parent
text: grid.title
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.normal
font.weight: 500
font.capitalization: Font.Capitalize
}
}
Item {
implicitWidth: implicitHeight
implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2
StateLayer {
id: nextMonthStateLayer
radius: Appearance.rounding.full
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
}
MaterialIcon {
id: nextMonthText
anchors.centerIn: parent
text: "chevron_right"
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
}
}
}
DayOfWeekRow {
id: daysRow
Layout.fillWidth: true
locale: grid.locale
delegate: StyledText {
required property var model
horizontalAlignment: Text.AlignHCenter
text: model.shortName
font.weight: 500
color: (model.day === 0 || model.day === 6) ? Colours.palette.m3secondary : Colours.palette.m3onSurfaceVariant
}
}
Item {
Layout.fillWidth: true
implicitHeight: grid.implicitHeight
MonthGrid {
id: grid
month: root.currMonth
year: root.currYear
anchors.fill: parent
spacing: 3
locale: Qt.locale()
delegate: Item {
id: dayItem
required property var model
implicitWidth: implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
StyledText {
id: text
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: grid.locale.toString(dayItem.model.day)
color: {
const dayOfWeek = dayItem.model.date.getUTCDay();
if (dayOfWeek === 0 || dayOfWeek === 6)
return Colours.palette.m3secondary;
return Colours.palette.m3onSurfaceVariant;
}
opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}
}
StyledRect {
id: todayIndicator
readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null
property Item today
onTodayItemChanged: {
if (todayItem)
today = todayItem;
}
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today?.y ?? 0
implicitWidth: today?.implicitWidth ?? 0
implicitHeight: today?.implicitHeight ?? 0
clip: true
radius: Appearance.rounding.full
color: Colours.palette.m3primary
opacity: todayItem ? 1 : 0
scale: todayItem ? 1 : 0.7
Colouriser {
x: -todayIndicator.x
y: -todayIndicator.y
implicitWidth: grid.width
implicitHeight: grid.height
source: grid
sourceColor: Colours.palette.m3onSurface
colorizationColor: Colours.palette.m3onPrimary
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on x {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
Item {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: Config.dashboard.sizes.dateTimeWidth
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
StyledText {
Layout.bottomMargin: -(font.pointSize * 0.4)
Layout.alignment: Qt.AlignHCenter
text: Time.hourStr
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.family: Appearance.font.family.clock
font.weight: 600
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: "•••"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge * 0.9
font.family: Appearance.font.family.clock
}
StyledText {
Layout.topMargin: -(font.pointSize * 0.4)
Layout.alignment: Qt.AlignHCenter
text: Time.minuteStr
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.family: Appearance.font.family.clock
font.weight: 600
}
Loader {
Layout.alignment: Qt.AlignHCenter
active: Config.services.useTwelveHourClock
visible: active
sourceComponent: StyledText {
text: Time.amPmStr
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.large
font.family: Appearance.font.family.clock
font.weight: 600
}
}
}
}

View File

@@ -0,0 +1,254 @@
import qs.components
import qs.services
import qs.config
import qs.utils
import Caelestia.Services
import QtQuick
import QtQuick.Shapes
Item {
id: root
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: Config.dashboard.sizes.mediaWidth
Behavior on playerProgress {
Anim {
duration: Appearance.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.beatTracker
}
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep
}
Behavior on strokeColor {
CAnim {}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: Colours.palette.m3primary
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress
}
Behavior on strokeColor {
CAnim {}
}
}
}
StyledClippingRect {
id: cover
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
implicitHeight: width
color: Colours.tPalette.m3surfaceContainerHigh
radius: Infinity
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
StyledText {
id: title
anchors.top: cover.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.normal
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.normal
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
StyledText {
id: album
anchors.top: title.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
StyledText {
id: artist
anchors.top: album.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
color: Colours.palette.m3secondary
width: parent.implicitWidth - Appearance.padding.large * 2
elide: Text.ElideRight
}
Row {
id: controls
anchors.top: artist.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Appearance.spacing.smaller
spacing: Appearance.spacing.small
Control {
icon: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
Control {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
canUse: Players.active?.canTogglePlaying ?? false
function onClicked(): void {
Players.active?.togglePlaying();
}
}
Control {
icon: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
AnimatedImage {
id: bongocat
anchors.top: controls.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.spacing.small
anchors.bottomMargin: Appearance.padding.large
anchors.margins: Appearance.padding.large * 2
playing: Players.active?.isPlaying ?? false
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment
source: Paths.absolutePath(Config.paths.mediaGif)
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
component Control: StyledRect {
id: control
required property string icon
required property bool canUse
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
implicitHeight: implicitWidth
StateLayer {
disabled: !control.canUse
radius: Appearance.rounding.full
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
text: control.icon
color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline
font.pointSize: Appearance.font.size.large
}
}
}

View File

@@ -0,0 +1,87 @@
import qs.components
import qs.components.misc
import qs.services
import qs.config
import QtQuick
Row {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
padding: Appearance.padding.large
spacing: Appearance.spacing.normal
Ref {
service: SystemUsage
}
Resource {
icon: "memory"
value: SystemUsage.cpuPerc
colour: Colours.palette.m3primary
}
Resource {
icon: "memory_alt"
value: SystemUsage.memPerc
colour: Colours.palette.m3secondary
}
Resource {
icon: "hard_disk"
value: SystemUsage.storagePerc
colour: Colours.palette.m3tertiary
}
component Resource: Item {
id: res
required property string icon
required property real value
required property color colour
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
implicitWidth: icon.implicitWidth
StyledRect {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
radius: Appearance.rounding.full
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: res.value * parent.height
color: res.colour
radius: Appearance.rounding.full
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
text: res.icon
color: res.colour
}
Behavior on value {
Anim {
duration: Appearance.anim.durations.large
}
}
}
}

View File

@@ -0,0 +1,195 @@
import qs.components
import qs.components.effects
import qs.components.images
import qs.components.filedialog
import qs.services
import qs.config
import qs.utils
import Quickshell
import QtQuick
Row {
id: root
required property PersistentProperties visibilities
required property PersistentProperties state
required property FileDialog facePicker
padding: Appearance.padding.large
spacing: Appearance.spacing.normal
StyledClippingRect {
implicitWidth: info.implicitHeight
implicitHeight: info.implicitHeight
radius: Appearance.rounding.large
color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
MaterialIcon {
anchors.centerIn: parent
text: "person"
fill: 1
grade: 200
font.pointSize: Math.floor(info.implicitHeight / 2) || 1
}
CachingImage {
id: pfp
anchors.fill: parent
path: `${Paths.home}/.face`
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
StyledRect {
anchors.fill: parent
color: Qt.alpha(Colours.palette.m3scrim, 0.5)
opacity: parent.containsMouse ? 1 : 0
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
}
}
}
StyledRect {
anchors.centerIn: parent
implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2
implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.normal
color: Colours.palette.m3primary
scale: parent.containsMouse ? 1 : 0.5
opacity: parent.containsMouse ? 1 : 0
StateLayer {
color: Colours.palette.m3onPrimary
function onClicked(): void {
root.visibilities.launcher = false;
root.facePicker.open();
}
}
MaterialIcon {
id: selectIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -font.pointSize * 0.02
text: "frame_person"
color: Colours.palette.m3onPrimary
font.pointSize: Appearance.font.size.extraLarge
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
}
}
}
}
}
Column {
id: info
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
Item {
id: line
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
ColouredIcon {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
source: SysInfo.osLogo
implicitSize: Math.floor(Appearance.font.size.normal * 1.34)
colour: Colours.palette.m3primary
}
StyledText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin
text: `: ${SysInfo.osPrettyName || SysInfo.osName}`
font.pointSize: Appearance.font.size.normal
width: Config.dashboard.sizes.infoWidth
elide: Text.ElideRight
}
}
InfoLine {
icon: "select_window_2"
text: SysInfo.wm
colour: Colours.palette.m3secondary
}
InfoLine {
id: uptime
icon: "timer"
text: qsTr("up %1").arg(SysInfo.uptime)
colour: Colours.palette.m3tertiary
}
}
component InfoLine: Item {
id: line
required property string icon
required property string text
required property color colour
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2
fill: 1
text: line.icon
color: line.colour
font.pointSize: Appearance.font.size.normal
}
StyledText {
id: text
anchors.verticalCenter: icon.verticalCenter
anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin
text: `: ${line.text}`
font.pointSize: Appearance.font.size.normal
width: Config.dashboard.sizes.infoWidth
elide: Text.ElideRight
}
}
}

View File

@@ -0,0 +1,57 @@
import qs.components
import qs.services
import qs.config
import qs.utils
import QtQuick
Item {
id: root
anchors.centerIn: parent
implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin
Component.onCompleted: Weather.reload()
MaterialIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
animate: true
text: Weather.icon
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge * 2
}
Column {
id: info
anchors.verticalCenter: parent.verticalCenter
anchors.left: icon.right
anchors.leftMargin: Appearance.spacing.large
spacing: Appearance.spacing.small
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.temp
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.description
elide: Text.ElideRight
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2)
}
}
}