This commit is contained in:
pika 2025-06-10 15:34:15 +02:00
parent 32edcff102
commit 0296117901
110 changed files with 9713 additions and 5 deletions

View file

@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Shapes
import "root:/config"
import "root:/services"
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: BorderConfig.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: BorderConfig.colour
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 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View file

@ -0,0 +1,123 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: nonAnimWidth
implicitHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
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
currentIndex: view.currentIndex
}
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: tabs.currentIndex
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)
tabs.bar.incrementCurrentIndex();
else if (x < -currentItem.implicitWidth / 2)
tabs.bar.decrementCurrentIndex();
}
onDragEnded: {
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 10)
tabs.bar.incrementCurrentIndex();
else if (x < -currentItem.implicitWidth / 10)
tabs.bar.decrementCurrentIndex();
else
contentX = Qt.binding(() => currentItem.x);
}
Row {
id: row
Dash {
shouldUpdate: visible && this === view.currentItem
}
Media {
shouldUpdate: visible && this === view.currentItem
visibilities: root.visibilities
}
Performance {}
}
Behavior on contentX {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}

View file

@ -0,0 +1,86 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "dash"
import QtQuick.Layouts
GridLayout {
id: root
required property bool shouldUpdate
rowSpacing: Appearance.spacing.normal
columnSpacing: Appearance.spacing.normal
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredWidth: user.implicitWidth
Layout.preferredHeight: user.implicitHeight
User {
id: user
}
}
Rect {
Layout.row: 0
Layout.columnSpan: 2
Layout.preferredWidth: DashboardConfig.sizes.weatherWidth
Layout.fillHeight: true
Weather {}
}
Rect {
Layout.row: 1
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
DateTime {
id: dateTime
}
}
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
Calendar {
id: calendar
}
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
Resources {
id: resources
}
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
Media {
id: media
shouldUpdate: root.shouldUpdate
}
}
component Rect: StyledRect {
radius: Appearance.rounding.small
color: Colours.palette.m3surfaceContainer
}
}

594
modules/dashboard/Media.qml Normal file
View file

@ -0,0 +1,594 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/utils"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
Item {
id: root
required property bool shouldUpdate
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";
return `${Math.floor(length / 60)}:${Math.floor(length % 60).toString().padStart(2, "0")}`;
}
implicitWidth: cover.implicitWidth + DashboardConfig.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2
implicitHeight: Math.max(cover.implicitHeight + DashboardConfig.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2
Behavior on playerProgress {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Timer {
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
interval: DashboardConfig.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Connections {
target: Cava
function onValuesChanged(): void {
if (root.shouldUpdate)
visualiser.requestPaint();
}
}
Canvas {
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: -DashboardConfig.sizes.mediaVisualiserSize
onColourChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const values = Cava.values;
const len = values.length;
ctx.strokeStyle = colour;
ctx.lineWidth = 360 / len - Appearance.spacing.small / 4;
ctx.lineCap = "round";
const size = DashboardConfig.sizes.mediaVisualiserSize;
const cx = centerX;
const cy = centerY;
const rx = innerX + ctx.lineWidth / 2;
const ry = innerY + ctx.lineWidth / 2;
for (let i = 0; i < len; i++) {
const v = Math.max(1, Math.min(100, values[i]));
const angle = i * 2 * Math.PI / len;
const magnitude = v / 100 * size;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
ctx.moveTo(cx + rx * cos, cy + ry * sin);
ctx.lineTo(cx + (rx + magnitude) * cos, cy + (ry + magnitude) * sin);
}
ctx.stroke();
}
Behavior on colour {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledClippingRect {
id: cover
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large + DashboardConfig.sizes.mediaVisualiserSize
implicitWidth: DashboardConfig.sizes.mediaCoverArtSize
implicitHeight: DashboardConfig.sizes.mediaCoverArtSize
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
Column {
id: details
anchors.verticalCenter: parent.verticalCenter
anchors.left: visualiser.right
anchors.leftMargin: Appearance.spacing.normal
spacing: Appearance.spacing.small
StyledText {
id: title
anchors.horizontalCenter: parent.horizontalCenter
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
elide: Text.ElideRight
}
StyledText {
id: album
anchors.horizontalCenter: parent.horizontalCenter
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
elide: Text.ElideRight
}
StyledText {
id: artist
anchors.horizontalCenter: parent.horizontalCenter
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
color: Colours.palette.m3secondary
width: parent.implicitWidth
elide: Text.ElideRight
}
Row {
id: controls
anchors.horizontalCenter: parent.horizontalCenter
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
primary: true
function onClicked(): void {
Players.active?.togglePlaying();
}
}
Control {
icon: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
Slider {
id: slider
implicitWidth: controls.implicitWidth * 1.5
implicitHeight: Appearance.padding.normal * 3
value: root.playerProgress
onMoved: {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
background: Item {
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: slider.handle.x - slider.implicitHeight / 6
color: Colours.palette.m3primary
radius: Appearance.rounding.full
topRightRadius: slider.implicitHeight / 15
bottomRightRadius: slider.implicitHeight / 15
}
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: slider.implicitHeight / 3
anchors.bottomMargin: slider.implicitHeight / 3
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.full
topLeftRadius: slider.implicitHeight / 15
bottomLeftRadius: slider.implicitHeight / 15
}
}
handle: StyledRect {
id: rect
x: slider.visualPosition * slider.availableWidth
implicitWidth: slider.implicitHeight / 4.5
implicitHeight: slider.implicitHeight
color: Colours.palette.m3primary
radius: Appearance.rounding.full
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: event => event.accepted = false
}
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
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
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
Control {
icon: "flip_to_front"
canUse: Players.active?.canRaise ?? false
fontSize: Appearance.font.size.larger
padding: Appearance.padding.small
fill: false
color: Colours.palette.m3surfaceContainer
function onClicked(): void {
Players.active?.raise();
root.visibilities.dashboard = false;
}
}
MouseArea {
id: playerSelector
property bool expanded
anchors.verticalCenter: parent.verticalCenter
implicitWidth: slider.implicitWidth / 2
implicitHeight: currentPlayer.implicitHeight + Appearance.padding.small * 2
cursorShape: Qt.PointingHandCursor
onClicked: expanded = !expanded
RectangularShadow {
anchors.fill: playerSelectorBg
opacity: playerSelector.expanded ? 1 : 0
radius: playerSelectorBg.radius
color: Colours.palette.m3shadow
blur: 5
spread: 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledRect {
id: playerSelectorBg
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: playersWrapper.implicitHeight + Appearance.padding.small * 2
color: Colours.palette.m3secondaryContainer
radius: Appearance.rounding.normal
Item {
id: playersWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.small
clip: true
implicitHeight: playerSelector.expanded && Players.list.length > 1 ? players.implicitHeight : currentPlayer.implicitHeight
Column {
id: players
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: Appearance.spacing.small
Repeater {
model: Players.list.filter(p => p !== Players.active)
Row {
id: player
required property MprisPlayer modelData
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
IconImage {
id: playerIcon
source: Icons.getAppIcon(player.modelData.identity, "image-missing")
implicitSize: Math.round(identity.implicitHeight * 0.9)
}
StyledText {
id: identity
text: identityMetrics.elidedText
color: Colours.palette.m3onSecondaryContainer
TextMetrics {
id: identityMetrics
text: player.modelData.identity
font.family: identity.font.family
font.pointSize: identity.font.pointSize
elide: Text.ElideRight
elideWidth: playerSelector.implicitWidth - playerIcon.implicitWidth - player.spacing - Appearance.padding.smaller * 2
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Players.manualActive = player.modelData;
playerSelector.expanded = false;
}
}
}
}
}
Item {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 1
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: -Appearance.padding.normal
color: Colours.palette.m3secondary
implicitHeight: 1
}
}
Row {
id: currentPlayer
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
IconImage {
id: currentIcon
source: Icons.getAppIcon(Players.active?.identity ?? "", "multimedia-player")
implicitSize: Math.round(currentIdentity.implicitHeight * 0.9)
}
StyledText {
id: currentIdentity
animate: true
text: currentIdentityMetrics.elidedText
color: Colours.palette.m3onSecondaryContainer
TextMetrics {
id: currentIdentityMetrics
text: Players.active?.identity ?? "No players"
font.family: currentIdentity.font.family
font.pointSize: currentIdentity.font.pointSize
elide: Text.ElideRight
elideWidth: playerSelector.implicitWidth - currentIcon.implicitWidth - currentPlayer.spacing - Appearance.padding.smaller * 2
}
}
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
}
}
Control {
icon: "delete"
canUse: Players.active?.canQuit ?? false
fontSize: Appearance.font.size.larger
padding: Appearance.padding.small
fill: false
color: Colours.palette.m3surfaceContainer
function onClicked(): void {
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: root.shouldUpdate && (Players.active?.isPlaying ?? false)
speed: BeatDetector.bpm / 300
source: "root:/assets/bongocat.gif"
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
}
component Control: StyledRect {
id: control
required property string icon
required property bool canUse
property int fontSize: Appearance.font.size.extraLarge
property int padding
property bool fill: true
property bool primary
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitWidth, icon.implicitHeight) + padding * 2
implicitHeight: implicitWidth
radius: Appearance.rounding.full
color: primary && canUse ? Colours.palette.m3primary : "transparent"
StateLayer {
disabled: !control.canUse
radius: parent.radius
color: control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
fill: control.fill ? 1 : 0
text: control.icon
color: control.canUse ? control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface : Colours.palette.m3outline
font.pointSize: control.fontSize
}
}
}

View file

@ -0,0 +1,230 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Row {
id: root
spacing: Appearance.spacing.large * 3
padding: Appearance.padding.large
leftPadding: padding * 2
rightPadding: padding * 3
Resource {
value1: Math.min(1, SystemUsage.gpuTemp / 90)
value2: SystemUsage.gpuPerc
label1: `${Math.ceil(SystemUsage.gpuTemp)}°C`
label2: `${Math.round(SystemUsage.gpuPerc * 100)}%`
sublabel1: qsTr("GPU temp")
sublabel2: qsTr("Usage")
}
Resource {
primary: true
value1: Math.min(1, SystemUsage.cpuTemp / 90)
value2: SystemUsage.cpuPerc
label1: `${Math.ceil(SystemUsage.cpuTemp)}°C`
label2: `${Math.round(SystemUsage.cpuPerc * 100)}%`
sublabel1: qsTr("CPU temp")
sublabel2: qsTr("Usage")
}
Resource {
value1: SystemUsage.memPerc
value2: SystemUsage.storagePerc
label1: {
const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
return `${+fmt.value.toFixed(1)}${fmt.unit}`;
}
label2: {
const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
return `${Math.floor(fmt.value)}${fmt.unit}`;
}
sublabel1: qsTr("Memory")
sublabel2: qsTr("Storage")
}
component Resource: Item {
id: res
required property real value1
required property real value2
required property string sublabel1
required property string sublabel2
required property string label1
required property string label2
property bool primary
readonly property real primaryMult: primary ? 1.2 : 1
readonly property real thickness: DashboardConfig.sizes.resourceProgessThickness * primaryMult
property color fg1: Colours.palette.m3primary
property color fg2: Colours.palette.m3secondary
property color bg1: Colours.palette.m3primaryContainer
property color bg2: Colours.palette.m3secondaryContainer
anchors.verticalCenter: parent.verticalCenter
implicitWidth: DashboardConfig.sizes.resourceSize * primaryMult
implicitHeight: DashboardConfig.sizes.resourceSize * primaryMult
onValue1Changed: canvas.requestPaint()
onValue2Changed: canvas.requestPaint()
onFg1Changed: canvas.requestPaint()
onFg2Changed: canvas.requestPaint()
onBg1Changed: canvas.requestPaint()
onBg2Changed: canvas.requestPaint()
Column {
anchors.centerIn: parent
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label1
font.pointSize: Appearance.font.size.extraLarge * res.primaryMult
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel1
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller * res.primaryMult
}
}
Column {
anchors.horizontalCenter: parent.right
anchors.top: parent.verticalCenter
anchors.horizontalCenterOffset: -res.thickness / 2
anchors.topMargin: res.thickness / 2 + Appearance.spacing.small
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label2
font.pointSize: Appearance.font.size.smaller * res.primaryMult
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel2
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small * res.primaryMult
}
}
Canvas {
id: canvas
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real arc1Start: degToRad(45)
readonly property real arc1End: degToRad(220)
readonly property real arc2Start: degToRad(230)
readonly property real arc2End: degToRad(360)
function degToRad(deg: int): real {
return deg * Math.PI / 180;
}
anchors.fill: parent
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.lineWidth = res.thickness;
ctx.lineCap = "round";
const radius = (Math.min(width, height) - ctx.lineWidth) / 2;
const cx = centerX;
const cy = centerY;
const a1s = arc1Start;
const a1e = arc1End;
const a2s = arc2Start;
const a2e = arc2End;
ctx.beginPath();
ctx.arc(cx, cy, radius, a1s, a1e, false);
ctx.strokeStyle = res.bg1;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false);
ctx.strokeStyle = res.fg1;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a2s, a2e, false);
ctx.strokeStyle = res.bg2;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false);
ctx.strokeStyle = res.fg2;
ctx.stroke();
}
}
Behavior on value1 {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on value2 {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on fg1 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on fg2 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on bg1 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on bg2 {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

249
modules/dashboard/Tabs.qml Normal file
View file

@ -0,0 +1,249 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
Item {
id: root
required property real nonAnimWidth
property alias currentIndex: bar.currentIndex
readonly property TabBar bar: bar
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
background: null
Tab {
iconName: "dashboard"
text: qsTr("Dashboard")
}
Tab {
iconName: "queue_music"
text: qsTr("Media")
}
Tab {
iconName: "speed"
text: qsTr("Performance")
}
Tab {
iconName: "workspaces"
text: qsTr("Workspaces")
}
}
Item {
id: indicator
anchors.top: bar.bottom
anchors.topMargin: DashboardConfig.sizes.tabIndicatorSpacing
implicitWidth: bar.currentItem.implicitWidth
implicitHeight: DashboardConfig.sizes.tabIndicatorHeight
x: {
const tab = bar.currentItem;
const width = (root.nonAnimWidth - DashboardConfig.sizes.tabIndicatorSpacing * (bar.count - 1) * 2) / 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: MouseArea {
id: mouse
implicitWidth: Math.max(icon.width, label.width)
implicitHeight: icon.height + label.height
cursorShape: Qt.PointingHandCursor
onPressed: ({x,y}) => {
tab.TabBar.tabBar.setCurrentIndex(tab.TabBar.index);
const stateY = stateWrapper.y;
rippleAnim.x = x;
rippleAnim.y = y - stateY;
const dist = (ox,oy) => ox * ox + oy * oy;
const stateEndY = stateY + stateWrapper.height;
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)));
rippleAnim.restart();
}
onWheel: event => {
if (event.angleDelta.y < 0)
tab.TabBar.tabBar.incrementCurrentIndex();
else if (event.angleDelta.y > 0)
tab.TabBar.tabBar.decrementCurrentIndex();
}
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.1
}
ParallelAnimation {
Anim {
target: ripple
properties: "implicitWidth,implicitHeight"
from: 0
to: rippleAnim.radius * 2
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
target: ripple
property: "opacity"
to: 0
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
ClippingRectangle {
id: stateWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + DashboardConfig.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 {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
StyledText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
text: tab.text
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}

View file

@ -0,0 +1,55 @@
import QtQuick
import Quickshell
import "root:/config"
Item {
id: root
required property PersistentProperties visibilities
visible: height > 0
implicitHeight: 0
implicitWidth: content.implicitWidth
states: State {
name: "visible"
when: root.visibilities.dashboard
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
NumberAnimation {
target: root
property: "implicitHeight"
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Content {
id: content
visibilities: root.visibilities
}
}

View file

@ -0,0 +1,70 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Controls
Column {
id: root
anchors.left: parent.left
anchors.right: parent.right
padding: Appearance.padding.large
spacing: Appearance.spacing.small
DayOfWeekRow {
id: days
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: parent.padding
delegate: StyledText {
required property var model
horizontalAlignment: Text.AlignHCenter
text: model.shortName
font.family: Appearance.font.family.sans
font.weight: 500
}
}
MonthGrid {
id: grid
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: parent.padding
spacing: 3
delegate: Item {
id: day
required property var model
implicitWidth: implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
StyledRect {
anchors.centerIn: parent
implicitWidth: parent.implicitHeight
implicitHeight: parent.implicitHeight
radius: Appearance.rounding.full
color: model.today ? Colours.palette.m3primary : "transparent"
StyledText {
id: text
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: grid.locale.toString(day.model.date, "d")
color: day.model.today ? Colours.palette.m3onPrimary : day.model.month === grid.month ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3outline
}
}
}
}
}

View file

@ -0,0 +1,71 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
Item {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: DashboardConfig.sizes.dateTimeWidth
StyledText {
id: hours
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: (root.height - (hours.implicitHeight + sep.implicitHeight + sep.anchors.topMargin + mins.implicitHeight + mins.anchors.topMargin + date.implicitHeight + date.anchors.topMargin)) / 2
horizontalAlignment: Text.AlignHCenter
text: Time.format("HH")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
id: sep
anchors.left: parent.left
anchors.right: parent.right
anchors.top: hours.bottom
anchors.topMargin: -font.pointSize * 0.5
horizontalAlignment: Text.AlignHCenter
text: "•••"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge * 0.9
}
StyledText {
id: mins
anchors.left: parent.left
anchors.right: parent.right
anchors.top: sep.bottom
anchors.topMargin: -sep.font.pointSize * 0.45
horizontalAlignment: Text.AlignHCenter
text: Time.format("MM")
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
id: date
anchors.left: parent.left
anchors.right: parent.right
anchors.top: mins.bottom
anchors.topMargin: Appearance.spacing.normal
horizontalAlignment: Text.AlignHCenter
text: Time.format("ddd, d")
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 500
}
}

View file

@ -0,0 +1,262 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import QtQuick
import QtQuick.Shapes
Item {
id: root
required property bool shouldUpdate
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: DashboardConfig.sizes.mediaWidth
Behavior on playerProgress {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Timer {
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
interval: DashboardConfig.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "transparent"
strokeColor: Colours.palette.m3surfaceContainerHigh
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
sweepAngle: DashboardConfig.sizes.mediaProgressSweep
}
Behavior on strokeColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: Colours.palette.m3primary
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
sweepAngle: DashboardConfig.sizes.mediaProgressSweep * root.playerProgress
}
Behavior on strokeColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
StyledClippingRect {
id: cover
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Appearance.padding.large + DashboardConfig.sizes.mediaProgressThickness + Appearance.spacing.small
implicitHeight: width
color: Colours.palette.m3surfaceContainerHigh
radius: Appearance.rounding.full
MaterialIcon {
anchors.centerIn: parent
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? ""
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: root.shouldUpdate && (Players.active?.isPlaying ?? false)
speed: BeatDetector.bpm / 300
source: "root:/assets/bongocat.gif"
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,85 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import QtQuick
import QtQuick.Controls
Row {
id: root
anchors.top: parent.top
anchors.bottom: parent.bottom
padding: Appearance.padding.large
spacing: Appearance.spacing.normal
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: DashboardConfig.sizes.resourceProgessThickness
color: Colours.palette.m3surfaceContainerHigh
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 {
NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}

View file

@ -0,0 +1,116 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Io
import QtQuick
Row {
id: root
padding: Appearance.padding.large
spacing: Appearance.spacing.large
StyledClippingRect {
implicitWidth: info.implicitHeight
implicitHeight: info.implicitHeight
radius: Appearance.rounding.full
color: Colours.palette.m3surfaceContainerHigh
MaterialIcon {
anchors.centerIn: parent
text: "person"
fill: 1
font.pointSize: (info.implicitHeight / 2) || 1
}
CachingImage {
anchors.fill: parent
path: `${Paths.home}/.face`
}
}
Column {
id: info
spacing: Appearance.spacing.normal
InfoLine {
icon: Icons.osIcon
text: Icons.osName
colour: Colours.palette.m3primary
}
InfoLine {
icon: "select_window_2"
text: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
colour: Colours.palette.m3secondary
}
InfoLine {
icon: "timer"
text: uptimeProc.uptime
colour: Colours.palette.m3tertiary
Timer {
running: true
repeat: true
interval: 15000
onTriggered: uptimeProc.running = true
}
Process {
id: uptimeProc
property string uptime
running: true
command: ["uptime", "-p"]
stdout: SplitParser {
onRead: data => uptimeProc.uptime = data
}
}
}
}
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: (DashboardConfig.sizes.infoIconSize - implicitWidth) / 2
text: line.icon
color: line.colour
font.pointSize: Appearance.font.size.normal
font.variableAxes: ({
FILL: 1
})
}
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: DashboardConfig.sizes.infoWidth
elide: Text.ElideRight
}
}
}

View file

@ -0,0 +1,63 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import QtQuick
Item {
id: root
anchors.centerIn: parent
implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin
onVisibleChanged: {
if (visible)
Weather.reload();
}
MaterialIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
animate: true
text: Weather.icon || "cloud_alert"
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge * 2
font.variableAxes: ({
opsz: Appearance.font.size.extraLarge * 1.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.temperature}°C`
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.extraLarge
font.weight: 500
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Weather.description || qsTr("No weather")
elide: Text.ElideRight
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2)
}
}
}