init: working version

This commit is contained in:
Kiana Sheibani 2025-10-07 19:43:46 -04:00
commit 7d8d7dacae
Signed by: toki
GPG key ID: 6CB106C25E86A9F7
109 changed files with 15066 additions and 0 deletions

View file

@ -0,0 +1,59 @@
import qs.config
import qs.services
import QtQuick
import QtQuick.Shapes
Shape {
id: root
required property Item wrapper
readonly property real rounding: Config.border.rounding
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
ShapePath {
startX: -0.5
startY: -root.rounding
strokeWidth: -1
fillColor: Config.colors.bg
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX * 2)
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: PathArc.Counterclockwise
}
}
}

View file

@ -0,0 +1,124 @@
pragma ComponentBehavior: Bound
import qs.modules.bar.popouts as BarPopouts
import qs.config
import qs.custom
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties uiState
required property BarPopouts.Wrapper popouts
readonly property color color: tabs.color
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
implicitWidth: tabs.implicitWidth + tabs.anchors.leftMargin + view.implicitWidth + viewWrapper.anchors.margins * 2
implicitHeight: nonAnimHeight
Tabs {
id: tabs
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 10
anchors.margins: 15
nonAnimHeight: root.nonAnimHeight - anchors.margins * 2
uiState: root.uiState
}
CustomClippingRect {
id: viewWrapper
anchors.left: tabs.right
anchors.top: parent.top
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 15
radius: 17
color: "transparent"
Item {
id: view
readonly property int currentIndex: root.uiState.dashboardTab
readonly property Item currentItem: column.children[currentIndex]
anchors.fill: parent
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
ColumnLayout {
id: column
y: -view.currentItem.y
spacing: 8
Pane {
sourceComponent: Dash {
uiState: root.uiState
}
}
Pane {
sourceComponent: Mixer {
uiState: root.uiState
index: 1
}
}
Pane {
sourceComponent: Media {
uiState: root.uiState
}
}
Pane {
source: "Performance.qml"
}
Pane {
sourceComponent: Workspaces {
uiState: root.uiState
popouts: root.popouts
}
}
Behavior on y {
Anim {}
}
}
}
}
Behavior on implicitWidth {
Anim {
duration: Config.anim.durations.large
easing.bezierCurve: Config.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
duration: Config.anim.durations.large
easing.bezierCurve: Config.anim.curves.emphasized
}
}
component Pane: Loader {
Layout.alignment: Qt.AlignLeft
property real bufferY: 5
Component.onCompleted: active = Qt.binding(() => {
const vy = Math.floor(-column.y);
const vey = Math.floor(vy + view.height);
return (vy >= y - bufferY && vy <= y + implicitHeight) || (vey >= y + bufferY && vey <= y + implicitHeight);
})
}
}

View file

@ -0,0 +1,65 @@
import qs.config
import qs.custom
import qs.services
import Quickshell
import QtQuick.Layouts
import "dash"
GridLayout {
id: root
required property PersistentProperties uiState
rowSpacing: 10
columnSpacing: 10
Rect {
Layout.fillWidth: true
Layout.preferredWidth: Config.dashboard.timeWidth
Layout.preferredHeight: Config.dashboard.timeHeight
DateTime {}
}
Rect {
Layout.row: 1
Layout.fillWidth: true
Layout.preferredHeight: 70
User {}
}
Rect {
Layout.row: 2
Layout.fillWidth: true
Layout.preferredHeight: 110
Weather {}
}
Rect {
Layout.row: 0
Layout.column: 1
Layout.rowSpan: 3
Layout.preferredWidth: 200
Layout.preferredHeight: 300
Layout.fillWidth: true
Media {}
}
Rect {
Layout.row: 3
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: 300
Notifs {}
}
component Rect: CustomRect {
radius: 12
color: Config.colors.containerDash
}
}

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

@ -0,0 +1,532 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
Item {
id: root
required property PersistentProperties uiState
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
implicitWidth: slider.width + 32
implicitHeight: childrenRect.height
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Item {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: Config.dashboard.mediaCoverArtHeight
CustomClippingRect {
id: cover
anchors.horizontalCenter: parent.horizontalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.verticalCenter: parent.top
anchors.verticalCenterOffset: Config.dashboard.mediaCoverArtHeight / 2
implicitWidth: image.paintedWidth > 0 ? image.paintedWidth : Config.dashboard.mediaCoverArtHeight
implicitHeight: image.paintedHeight > 0 ? image.paintedHeight : Config.dashboard.mediaCoverArtHeight
color: Config.colors.containerDash
radius: 12
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: Config.colors.tertiary
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.centerIn: parent
width: Config.dashboard.mediaCoverArtWidth
height: Config.dashboard.mediaCoverArtHeight
source: Players.active?.trackArtUrl ?? ""
asynchronous: true
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
}
}
ColumnLayout {
id: details
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 12 + Config.dashboard.mediaCoverArtHeight
spacing: 8
CustomText {
id: title
Layout.fillWidth: true
Layout.maximumWidth: parent.implicitWidth
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Config.colors.secondary
font.pointSize: Config.font.size.normal
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
CustomText {
id: artist
Layout.fillWidth: true
visible: Players.active
animate: true
horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackArtist || qsTr("Unknown artist")
opacity: Players.active ? 1 : 0
color: Config.colors.primary
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
CustomText {
id: album
Layout.fillWidth: true
Layout.topMargin: -5
visible: text !== ""
animate: true
horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackAlbum ?? ""
color: Config.colors.tertiary
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
id: controls
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 12
Layout.bottomMargin: 10
spacing: 7
PlayerControl {
icon.text: "skip_previous"
canUse: Players.active?.canGoPrevious ?? false
function onClicked(): void {
Players.active?.previous();
}
}
CustomRect {
id: playBtn
implicitWidth: Math.max(playIcon.implicitWidth, playIcon.implicitHeight)
implicitHeight: implicitWidth
radius: Players.active?.isPlaying ? 12 : implicitHeight / 2
color: {
if (!Players.active?.canTogglePlaying)
return "transparent";
return Players.active?.isPlaying ? Config.colors.media : Config.colors.container;
}
StateLayer {
anchors.fill: parent
disabled: !Players.active?.canTogglePlaying
color: Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.primary
radius: parent.radius
function onClicked(): void {
Players.active?.togglePlaying();
}
}
MaterialIcon {
id: playIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -font.pointSize * 0.02
anchors.verticalCenterOffset: font.pointSize * 0.02
animate: true
fill: 1
text: Players.active?.isPlaying ? "pause" : "play_arrow"
color: {
if (!Players.active?.canTogglePlaying)
return Config.colors.inactive;
return Players.active?.isPlaying ? Config.colors.primaryDark : Config.colors.media;
}
font.pointSize: Config.font.size.largest
}
Behavior on radius {
Anim {}
}
}
PlayerControl {
icon.text: "skip_next"
canUse: Players.active?.canGoNext ?? false
function onClicked(): void {
Players.active?.next();
}
}
}
CustomSlider {
id: slider
enabled: !!Players.active
progressColor: Config.colors.media
implicitWidth: controls.implicitWidth * 1.5
implicitHeight: 20
onMoved: {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: slider
property: "value"
value: root.playerProgress
when: !slider.pressed
}
CustomMouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
function onWheel(event: WheelEvent) {
const active = Players.active;
if (!active?.canSeek || !active?.positionSupported)
return;
event.accepted = true;
const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds
Qt.callLater(() => {
active.position = Math.max(0, Math.min(active.length, active.position + delta));
});
}
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: -6
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
opacity: Players.active ? 1 : 0
visible: opacity > 0
CustomText {
id: position
anchors.left: parent.left
text: root.lengthStr(Players.active?.position ?? 0)
color: Config.colors.primary
font.pointSize: Config.font.size.small
}
CustomText {
id: length
anchors.right: parent.right
text: root.lengthStr(Players.active?.length ?? 0)
color: Config.colors.primary
font.pointSize: Config.font.size.small
}
Behavior on opacity {
Anim { duration: Config.anim.durations.small }
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 12
spacing: 7
PlayerControl {
icon.text: "flip_to_front"
canUse: Players.active?.canRaise ?? false
fontSize: Config.font.size.larger
size: playerSelector.height
fill: 0
color: Config.colors.container
activeColor: Config.colors.secondary
function onClicked(): void {
Players.active?.raise();
root.uiState.dashboard = false;
}
}
CustomRect {
id: playerSelector
property bool expanded
Layout.alignment: Qt.AlignVCenter
implicitWidth: slider.implicitWidth * 0.6
implicitHeight: currentPlayer.implicitHeight + 14
radius: 17
color: Config.colors.container
z: 1
StateLayer {
anchors.fill: parent
disabled: Players.list.length <= 1
function onClicked(): void {
playerSelector.expanded = !playerSelector.expanded;
}
}
RowLayout {
id: currentPlayer
anchors.centerIn: parent
spacing: 7
PlayerIcon {
player: Players.active
}
CustomText {
Layout.fillWidth: true
Layout.maximumWidth: playerSelector.implicitWidth - implicitHeight - parent.spacing - 20
text: Players.active ? Players.getName(Players.active) : qsTr("No players")
color: Players.active ? Config.colors.primary : Config.colors.tertiary
elide: Text.ElideRight
}
}
Elevation {
anchors.fill: playerSelectorBg
radius: playerSelectorBg.radius
opacity: playerSelector.expanded ? 1 : 0
level: 2
Behavior on opacity {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
}
}
}
CustomClippingRect {
id: playerSelectorBg
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: playerSelector.expanded ? playerList.implicitWidth : playerSelector.implicitWidth
implicitHeight: playerSelector.expanded ? playerList.implicitHeight : playerSelector.implicitHeight
color: Config.colors.containerAlt
radius: 17
opacity: playerSelector.expanded ? 1 : 0
ColumnLayout {
id: playerList
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: 0
Repeater {
model: [...Players.list].sort((a, b) => (a === Players.active) - (b === Players.active))
Item {
id: player
required property MprisPlayer modelData
Layout.fillWidth: true
Layout.minimumWidth: playerSelector.implicitWidth
implicitWidth: playerInner.implicitWidth + 20
implicitHeight: playerInner.implicitHeight + 14
StateLayer {
disabled: !playerSelector.expanded
function onClicked(): void {
playerSelector.expanded = false;
Players.manualActive = player.modelData;
}
}
RowLayout {
id: playerInner
anchors.centerIn: parent
spacing: 7
PlayerIcon {
player: player.modelData
}
CustomText {
text: Players.getName(player.modelData)
color: Config.colors.primary
}
}
}
}
}
Behavior on opacity {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
}
}
Behavior on implicitWidth {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}
Behavior on implicitHeight {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}
}
}
PlayerControl {
icon.text: "close"
icon.anchors.horizontalCenterOffset: 0
canUse: Players.active?.canQuit ?? false
size: playerSelector.height
fontSize: Config.font.size.larger
fill: 1
color: Config.colors.container
activeColor: Config.colors.error
function onClicked(): void {
Players.active?.quit();
}
}
}
}
component PlayerIcon: Loader {
id: loader
required property MprisPlayer player
readonly property string icon: player ? Icons.getAppIcon(Players.getIdentity(player)) : ""
Layout.fillHeight: true
sourceComponent: !player || icon === "image://icon/" ? fallbackIcon : playerImage
Component {
id: playerImage
IconImage {
implicitWidth: height
source: loader.icon
}
}
Component {
id: fallbackIcon
MaterialIcon {
text: loader.player ? "animated_images" : "music_off"
}
}
}
component PlayerControl: CustomRect {
id: control
required property bool canUse
property alias icon: icon
property int fontSize: Config.font.size.largest
property int size: Math.max(icon.implicitWidth, icon.implicitHeight)
property real fill: 1
property color activeColor: Config.colors.media
function onClicked() {}
implicitWidth: size
implicitHeight: implicitWidth
radius: 1000
StateLayer {
anchors.fill: parent
disabled: !control.canUse
function onClicked(): void {
control.onClicked();
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -font.pointSize * 0.02
anchors.verticalCenterOffset: font.pointSize * 0.02
animate: true
fill: control.fill
text: control.icon
color: control.canUse ? control.activeColor : Config.colors.inactive
font.pointSize: control.fontSize
}
}
}

208
modules/dashboard/Mixer.qml Normal file
View file

@ -0,0 +1,208 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Pipewire
Item {
id: root
required property PersistentProperties uiState
required property int index
readonly property list<PwNode> nodes: Pipewire.nodes.values.filter(node => node.isSink && node.isStream)
width: Config.dashboard.mixerWidth
height: Config.dashboard.mixerHeight
PwObjectTracker {
objects: root.nodes
}
Binding {
target: root.uiState
when: root.uiState.dashboardTab === root.index
property: "osdVolumeReact"
value: false
}
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 7
CustomText {
Layout.topMargin: 6
text: qsTr("Master Volume")
color: Config.colors.secondary
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: Config.osd.sliderWidth
function onWheel(event: WheelEvent) {
if (event.angleDelta.y > 0)
Audio.increaseVolume();
else if (event.angleDelta.y < 0)
Audio.decreaseVolume();
}
acceptedButtons: Qt.RightButton
onClicked: Audio.sink.audio.muted = !Audio.muted
CustomFilledSlider {
anchors.fill: parent
orientation: Qt.Horizontal
color: Audio.muted ? Config.colors.error : Config.colors.volume
icon: Icons.getVolumeIcon(value, Audio.muted)
value: Audio.volume
onMoved: Audio.setVolume(value)
Behavior on color {
CAnim {
duration: Config.anim.durations.small
}
}
}
}
CustomRect {
Layout.fillWidth: true
Layout.topMargin: 5
Layout.bottomMargin: 5
height: 1
color: Config.colors.inactive
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
CustomListView {
id: list
anchors.fill: parent
spacing: 12
model: ScriptModel {
values: [...root.nodes]
objectProp: "id"
}
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
delegate: RowLayout {
id: entry
required property PwNode modelData
spacing: 6
width: root.width
IconImage {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: source != ""
implicitSize: slider.height * 1.4
source: {
const icon = entry.modelData.properties["application.icon-name"];
if (icon)
return Icons.getAppIcon(icon, "image-missing");
Icons.getAppIcon(entry.modelData.name, "image-missing")
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = entry.modelData.properties["application.name"]
?? (entry.modelData.description != "" ? entry.modelData.description : entry.modelData.name);
const media = entry.modelData.properties["media.name"];
return media != undefined ? `${app} 🞄 ${media}` : app;
}
color: Config.colors.secondary
}
CustomMouseArea {
id: slider
Layout.fillWidth: true
implicitHeight: Config.osd.sliderWidth
acceptedButtons: Qt.RightButton
onClicked: entry.modelData.audio.muted = !entry.modelData.audio.muted
CustomFilledSlider {
anchors.fill: parent
orientation: Qt.Horizontal
color: entry.modelData.audio.muted ? Config.colors.error : Config.colors.volume
icon: Icons.getVolumeIcon(value, entry.modelData.audio.muted)
value: entry.modelData.audio.volume
onMoved: {
if (entry.modelData.ready)
entry.modelData.audio.volume = Math.max(0, Math.min(1, value));
}
Behavior on color {
CAnim {
duration: Config.anim.durations.small
}
}
}
}
}
}
}
CustomText {
anchors.centerIn: parent
opacity: list.count === 0 ? 1 : 0
visible: opacity > 0
text: qsTr("No audio sources")
color: Config.colors.tertiary
font.pointSize: Config.font.size.normal
Behavior on opacity {
Anim {}
}
}
}
/* RowLayout { */
/* id: deviceSelectorRowLayout */
/* Layout.fillWidth: true */
/* uniformCellSizes: true */
/* AudioDeviceSelectorButton { */
/* Layout.fillWidth: true */
/* input: false */
/* onClicked: root.showDeviceSelectorDialog(input) */
/* } */
/* AudioDeviceSelectorButton { */
/* Layout.fillWidth: true */
/* input: true */
/* onClicked: root.showDeviceSelectorDialog(input) */
/* } */
/* } */
}
}

View file

@ -0,0 +1,234 @@
import qs.config
import qs.custom
import qs.services
import qs.util
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
function displayTemp(temp: real): string {
return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`;
}
readonly property int padding: 20
spacing: 28
Component.onCompleted: SystemUsage.refCount++;
Component.onDestruction: SystemUsage.refCount--;
Resource {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: root.padding
Layout.leftMargin: root.padding
Layout.rightMargin: root.padding
value1: SystemUsage.gpuPerc
label1: `${Math.round(SystemUsage.gpuPerc * 100)}%`
sublabel1: qsTr("GPU Usage")
value2: Math.min(1, SystemUsage.gpuTemp / 90)
label2: root.displayTemp(SystemUsage.gpuTemp)
sublabel2: qsTr("Temp")
warning2: value2 > 0.75
}
Resource {
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: root.padding
Layout.rightMargin: root.padding
primary: true
value1: SystemUsage.cpuPerc
label1: `${Math.round(SystemUsage.cpuPerc * 100)}%`
sublabel1: qsTr("CPU Usage")
value2: Math.min(1, SystemUsage.cpuTemp / 90)
label2: root.displayTemp(SystemUsage.cpuTemp)
sublabel2: qsTr("Temp")
warning2: value2 > 0.75
}
Resource {
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: root.padding
Layout.rightMargin: root.padding
Layout.bottomMargin: root.padding
value1: SystemUsage.memPerc
label1: {
const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
return `${+fmt.value.toFixed(1)}${fmt.unit}`;
}
sublabel1: qsTr("Memory")
value2: SystemUsage.storagePerc
label2: {
const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
return `${Math.floor(fmt.value)}${fmt.unit}`;
}
sublabel2: qsTr("Storage")
}
component Resource: Item {
id: res
required property real value1
required property string label1
required property string sublabel1
property bool warning1: value1 > 0.9
required property real value2
required property string label2
required property string sublabel2
property bool warning2: value2 > 0.9
property bool primary
readonly property real primaryMult: primary ? 1.2 : 1
readonly property real thickness: 10 * primaryMult
property color fg1: warning1 ? Config.colors.error : Color.mute(Config.colors.performance)
property color fg2: warning2 ? Config.colors.error : Color.mute(Config.colors.performance, 1.5, 1.6)
property color bg1: Qt.alpha(warning1 ? Config.colors.error : Config.colors.performance, 0.1)
property color bg2: Qt.alpha(warning2 ? Config.colors.error : Config.colors.performance, 0.05)
implicitWidth: implicitHeight
implicitHeight: 175 * primaryMult
onValue1Changed: canvas.requestPaint()
onValue2Changed: canvas.requestPaint()
onFg1Changed: canvas.requestPaint()
onFg2Changed: canvas.requestPaint()
onBg1Changed: canvas.requestPaint()
onBg2Changed: canvas.requestPaint()
Column {
anchors.centerIn: parent
readonly property color color: res.warning1 ? Config.colors.error :
res.value1 === 0 ? Config.colors.inactive : Config.colors.primary
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label1
color: parent.color
font.pointSize: Config.font.size.largest * res.primaryMult
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel1
color: parent.color
font.pointSize: Config.font.size.smaller * res.primaryMult
}
}
Column {
anchors.horizontalCenter: parent.right
anchors.top: parent.verticalCenter
anchors.horizontalCenterOffset: -res.thickness / 2
anchors.topMargin: res.thickness / 2 + 7
readonly property color color: res.warning2 ? Config.colors.error :
res.value2 === 0 ? Config.colors.inactive : Config.colors.primary
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.label2
color: parent.color
font.pointSize: Config.font.size.smaller * res.primaryMult
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
text: res.sublabel2
color: parent.color
font.pointSize: Config.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(270)
readonly property real arc2Start: degToRad(360)
readonly property real arc2End: degToRad(280)
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, true);
ctx.strokeStyle = res.bg2;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, true);
ctx.strokeStyle = res.fg2;
ctx.stroke();
}
}
Behavior on value1 {
Anim {}
}
Behavior on value2 {
Anim {}
}
Behavior on fg1 {
CAnim {}
}
Behavior on fg2 {
CAnim {}
}
Behavior on bg1 {
CAnim {}
}
Behavior on bg2 {
CAnim {}
}
}
}

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

@ -0,0 +1,269 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property real nonAnimHeight
required property PersistentProperties uiState
readonly property int count: repeater.model.count
readonly property color color: indicator.currentItem.color
implicitWidth: childrenRect.width
ColumnLayout {
id: bar
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 16
width: 100
Repeater {
id: repeater
model: ListModel {}
Component.onCompleted: {
model.append({
text: qsTr("Dashboard"),
iconName: "dashboard",
color: Config.colors.dashboard
});
model.append({
text: qsTr("Mixer"),
iconName: "tune",
color: Config.colors.mixer
});
model.append({
text: qsTr("Media"),
iconName: "queue_music",
color: Config.colors.media
});
model.append({
text: qsTr("Performance"),
iconName: "speed",
color: Config.colors.performance
});
model.append({
text: qsTr("Workspaces"),
iconName: "workspaces",
color: Config.colors.workspaces
});
}
delegate: Tab {}
}
}
Item {
id: indicator
anchors.left: bar.right
anchors.leftMargin: 8
property int currentIndex: root.uiState.dashboardTab
property Item currentItem: {
repeater.count;
repeater.itemAt(currentIndex)
}
implicitWidth: 2
implicitHeight: currentItem.implicitHeight
y: currentItem ? currentItem.y + bar.y + (currentItem.height - implicitHeight) / 2 : 0
clip: true
CustomRect {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: parent.implicitWidth * 2
color: indicator.currentItem?.color ?? "transparent"
radius: 1000
}
Behavior on currentIndex {
SequentialAnimation {
Anim {
target: indicator
property: "implicitHeight"
to: 0
duration: Config.anim.durations.small / 2
}
PropertyAction {}
Anim {
target: indicator
property: "implicitHeight"
from: 0
to: bar.children[root.uiState.dashboardTab].height
duration: Config.anim.durations.small / 2
}
}
}
}
CustomRect {
id: separator
anchors.left: indicator.right
anchors.top: parent.top
anchors.bottom: parent.bottom
implicitWidth: 1
color: Config.colors.inactive
}
component Tab: CustomMouseArea {
id: tab
required property int index
required property string text
required property string iconName
required property color color
readonly property bool isCurrentItem: root.uiState.dashboardTab === index
implicitHeight: icon.height + label.height + 8
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: event => {
root.uiState.dashboardTab = tab.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.uiState.dashboardTab = Math.min(root.uiState.dashboardTab + 1, root.count - 1);
else if (event.angleDelta.y > 0)
root.uiState.dashboardTab = Math.max(root.uiState.dashboardTab - 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
easing.bezierCurve: Config.anim.curves.standardDecel
}
Anim {
target: ripple
property: "opacity"
to: 0
}
}
ClippingRectangle {
id: stateWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height
color: "transparent"
radius: 12
CustomRect {
id: stateLayer
anchors.fill: parent
color: tab.isCurrentItem ? tab.color : Config.colors.primary
opacity: tab.pressed ? 0.1 : tab.containsMouse ? 0.08 : 0
Behavior on opacity {
Anim {}
}
}
CustomRect {
id: ripple
radius: 1000
color: tab.isCurrentItem ? tab.color : Config.colors.primary
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.isCurrentItem ? tab.color : Config.colors.primary
fill: tab.isCurrentItem ? 1 : 0
font.pointSize: Config.font.size.large
Behavior on fill {
Anim {
duration: Config.anim.durations.small
}
}
}
CustomText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
text: tab.text
color: tab.isCurrentItem ? tab.color : Config.colors.primary
}
}
}

View file

@ -0,0 +1,496 @@
import qs.modules.bar.popouts as BarPopouts
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties uiState
required property BarPopouts.Wrapper popouts
width: Config.dashboard.workspaceWidth + 32
height: 750
property HyprlandWorkspace dragSourceWorkspace: null
property HyprlandWorkspace dragTargetWorkspace: null
// Controls scrolling at edge of panel while dragging
property real dragScrollVelocity: 0
CustomListView {
id: list
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 0
spacing: 12
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
model: Hypr.monitors
delegate: Item {
id: monitor
required property int index
required property HyprlandMonitor modelData
readonly property ListModel workspaces: States.screens.get(modelData).workspaces
width: list.width - 20
height: childrenRect.height
Item {
id: monitorHeader
anchors.left: parent.left
anchors.right: parent.right
height: monIcon.height
MaterialIcon {
id: monIcon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: "desktop_windows"
color: Config.colors.tertiary
}
CustomText {
id: monText
anchors.verticalCenter: parent.verticalCenter
anchors.left: monIcon.right
anchors.leftMargin: 4
text: monitor.modelData.name
color: Config.colors.tertiary
font.family: Config.font.family.mono
font.pointSize: Config.font.size.smaller
}
CustomRect {
anchors.verticalCenter: parent.verticalCenter
anchors.left: monText.right
anchors.right: parent.right
anchors.leftMargin: 8
anchors.rightMargin: 18
height: 1
color: Config.colors.inactive
}
}
ListView {
id: workspaceList
anchors.top: monitorHeader.bottom
anchors.topMargin: 8
width: list.width - 20
spacing: 22
height: contentHeight
acceptedButtons: Qt.NoButton
boundsBehavior: Flickable.StopAtBounds
model: monitor.workspaces
delegate: Column {
id: entry
required property int index
required property HyprlandWorkspace workspace
width: list.width - 20
spacing: 5
property bool hoveredWhileDragging: false
z: root.dragSourceWorkspace === workspace ? 10 : 0
Item {
id: header
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: labelState.height
property color nonAnimColor: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
entry.workspace?.active ? Config.colors.workspaces : Config.colors.primary
property color color: nonAnimColor
Behavior on color {
CAnim {}
}
StateLayer {
id: labelState
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
implicitWidth: label.width + 20
implicitHeight: label.height + 8
color: Config.colors.secondary
function onClicked(): void {
Hypr.dispatch(`workspace ${entry.workspace.id}`)
}
Row {
id: label
anchors.centerIn: parent
spacing: 7
MaterialIcon {
text: Icons.getWorkspaceIcon(entry.workspace)
color: header.color
font.pointSize: Config.font.size.larger
animate: true
animateDuration: Config.anim.durations.small
}
CustomText {
anchors.top: parent.top
anchors.topMargin: 1
text: qsTr("Workspace %1").arg(index + 1)
font.family: Config.font.family.mono
font.pointSize: Config.font.size.normal
color: header.color
}
}
}
StateLayer {
anchors.verticalCenter: parent.verticalCenter
anchors.right: downArrow.left
anchors.rightMargin: 2
implicitWidth: implicitHeight
implicitHeight: upIcon.height + 6
disabled: entry.index === 0 && monitor.index === 0
function onClicked(): void {
if (entry.index !== 0) {
monitor.workspaces.move(entry.index, entry.index - 1, 1)
} else {
const workspace = entry.workspace;
monitor.workspaces.remove(entry.index, 1);
const otherWorkspaces = list.itemAtIndex(monitor.index - 1).workspaces;
otherWorkspaces.insert(0, {"workspace": workspace});
}
}
MaterialIcon {
id: upIcon
anchors.centerIn: parent
text: "keyboard_arrow_up"
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
font.pointSize: Config.font.size.larger
Behavior on color {
CAnim {}
}
}
}
StateLayer {
id: downArrow
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
implicitWidth: implicitHeight
implicitHeight: downIcon.height + 6
disabled: entry.index === monitor.workspaces.count - 1 && monitor.index === list.count - 1
function onClicked(): void {
if (entry.index !== monitor.workspaces.count - 1) {
root.uiState.workspaces.move(entry.index, entry.index + 1, 1)
} else {
const workspace = entry.workspace;
monitor.workspaces.remove(entry.index, 1);
const otherWorkspaces = list.itemAtIndex(monitor.index + 1).workspaces;
otherWorkspaces.append({"workspace": workspace});
}
}
MaterialIcon {
id: downIcon
anchors.centerIn: parent
text: "keyboard_arrow_down"
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
font.pointSize: Config.font.size.larger
Behavior on color {
CAnim {}
}
}
}
}
CustomRect {
id: preview
anchors.left: parent.left
anchors.right: parent.right
readonly property HyprlandMonitor mon: entry.workspace.monitor
// Exclude UI border and apply monitor scale
readonly property real monX: Config.border.thickness
readonly property real monY: Config.bar.height
readonly property real monWidth: (mon.width / mon.scale) - 2 * Config.border.thickness
readonly property real monHeight: (mon.height / mon.scale) - Config.bar.height - Config.border.thickness
readonly property real aspectRatio: monHeight / monWidth
readonly property real sizeRatio: Config.dashboard.workspaceWidth / monWidth
width: Config.dashboard.workspaceWidth
height: aspectRatio * width
radius: 7
color: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
entry.workspace?.active ? Color.mute(Config.colors.workspaces, 1.5, 1.1) : Config.colors.container
Behavior on color {
CAnim {}
}
DropArea {
anchors.fill: parent
onEntered: {
root.dragTargetWorkspace = entry.workspace;
entry.hoveredWhileDragging = true;
}
onExited: {
if (root.draggingTargetWorkspace === entry.workspace)
root.draggingTargetWorkspace = null;
entry.hoveredWhileDragging = false;
}
}
Repeater {
anchors.fill: parent
model: entry.workspace?.toplevels
delegate: Item {
id: window
required property HyprlandToplevel modelData
property var ipc: modelData.lastIpcObject
opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0
property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0
property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0
property real nonAnimWidth: ipc?.size ? ipc.size[0] * preview.sizeRatio : 0
property real nonAnimHeight: ipc?.size ? ipc.size[1] * preview.sizeRatio : 0
x: nonAnimX
y: nonAnimY
width: nonAnimWidth
height: nonAnimHeight
z: nonAnimX !== x || nonAnimY !== y ? 10 : 0
Behavior on x {
enabled: window.x !== 0
Anim {}
}
Behavior on y {
enabled: window.y !== 0
Anim {}
}
Behavior on width {
enabled: window.width !== 0
Anim {}
}
Behavior on height {
enabled: window.height !== 0
Anim {}
}
Behavior on opacity { Anim {} }
ScreencopyView {
id: view
visible: false
anchors.left: parent.left
anchors.top: parent.top
layer.enabled: true
// NOTE: Simulates cropping fill (which ScreencopyView does not natively support)
readonly property real aspectRatio: sourceSize.height / sourceSize.width
width: Math.max(window.nonAnimWidth, window.nonAnimHeight / aspectRatio)
height: Math.max(window.nonAnimHeight, window.nonAnimWidth * aspectRatio)
captureSource: window.modelData.wayland
live: true
CustomRect {
anchors.fill: parent
color: mouse.drag.active ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaceMove, 0.2)) :
mouse.pressed ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaces, 0.2)) :
mouse.containsMouse ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.5), Qt.alpha(Config.colors.workspaces, 0.1)) :
Qt.alpha(Config.colors.overlay, 0.5)
Behavior on color {
CAnim { duration: Config.anim.durations.small }
}
}
}
Item {
id: mask
visible: false
anchors.fill: view
layer.enabled: true
Rectangle {
width: window.width
height: window.height
radius: 8
}
}
MultiEffect {
anchors.fill: view
source: view
maskEnabled: true
maskSource: mask
}
IconImage {
id: icon
anchors.centerIn: parent
implicitSize: Math.min(48, window.width * 0.5, window.height * 0.5)
source: Icons.getAppIcon(window.ipc?.class ?? "", "")
}
// Interactions
Drag.active: mouse.drag.active
Drag.source: window
Drag.hotSpot.x: nonAnimWidth / 2
Drag.hotSpot.y: nonAnimHeight / 2
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.AllButtons
drag.target: parent
onPressed: event => {
root.dragSourceWorkspace = entry.workspace;
window.Drag.hotSpot.x = event.x;
window.Drag.hotSpot.y = event.y;
}
onReleased: event => {
const targetWorkspace = root.dragTargetWorkspace;
root.dragSourceWorkspace = null;
root.dragScrollVelocity = 0;
if (targetWorkspace !== null && targetWorkspace !== entry.workspace) {
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace.id}, address:0x${window.modelData.address}`)
} else {
window.x = Qt.binding(() => window.nonAnimX);
window.y = Qt.binding(() => window.nonAnimY);
}
}
onClicked: event => {
if (event.button === Qt.LeftButton) {
root.uiState.dashboard = false;
Hypr.dispatch(`focuswindow address:0x${window.modelData.address}`);
} else if (event.button === Qt.MiddleButton) {
Hypr.dispatch(`closewindow address:0x${window.modelData.address}`);
} else if (event.button === Qt.RightButton) {
root.uiState.dashboard = false;
popouts.currentName = "activewindow";
popouts.currentCenter = QsWindow.window.width / 2;
popouts.window = window.modelData;
popouts.hasCurrent = true;
}
}
onPositionChanged: event => {
if (!drag.active) return;
const y = root.mapFromItem(this, 0, event.y).y;
if (y < 100) {
root.dragScrollVelocity = (y - 100) / 25;
} else if (y > root.height - 100) {
root.dragScrollVelocity = (y - root.height + 100) / 25;
} else {
root.dragScrollVelocity = 0;
}
}
}
}
}
}
}
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
}
}
remove: Transition {
Anim {
property: "opacity"
from: 1
to: 0
}
}
move: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity"
to: 1
}
}
displaced: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity"
to: 1
}
}
}
}
}
Timer {
running: root.dragScrollVelocity !== 0
repeat: true
interval: 10
onTriggered: {
list.contentY += root.dragScrollVelocity;
list.returnToBounds();
}
}
}

View file

@ -0,0 +1,78 @@
pragma ComponentBehavior: Bound
import qs.modules.bar.popouts as BarPopouts
import qs.config
import qs.custom
import qs.util
import Quickshell
import Quickshell.Hyprland
import QtQuick
Item {
id: root
required property PersistentProperties uiState
required property BarPopouts.Wrapper popouts
visible: width > 0
implicitWidth: 0
implicitHeight: content.implicitHeight
states: State {
name: "visible"
when: root.uiState.dashboard
PropertyChanges {
root.implicitWidth: content.implicitWidth
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitWidth"
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitWidth"
easing.bezierCurve: Config.anim.curves.emphasized
}
}
]
Background {
id: background
visible: false
wrapper: root
}
GlowEffect {
source: background
glowColor: content.active ? content.item.color : "transparent"
}
Loader {
id: content
Component.onCompleted: active = Qt.binding(() => root.uiState.dashboard || root.visible)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Content {
uiState: root.uiState
popouts: root.popouts
}
}
}

View file

@ -0,0 +1,29 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.config
import qs.custom
import QtQuick
import QtQuick.Layouts
ColumnLayout {
anchors.centerIn: parent
anchors.verticalCenterOffset: -2
spacing: -6
CustomText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("hh:mm:ss")
color: Config.colors.secondary
font.family: Config.font.family.mono
font.pointSize: Config.font.size.largest
font.weight: 600
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("dddd, yyyy-MM-dd")
color: Config.colors.tertiary
font.pointSize: Config.font.size.normal
}
}

View file

@ -0,0 +1,243 @@
import qs.config
import qs.custom
import qs.services
import qs.util
import QtQuick
import QtQuick.Shapes
Item {
id: root
anchors.fill: parent
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
Behavior on playerProgress {
Anim {
duration: Config.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: 400
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
Shape {
id: progress
preferredRendererType: Shape.CurveRenderer
readonly property int thickness: 8
readonly property int angle: 300
ShapePath {
id: path
fillColor: "transparent"
strokeColor: Qt.alpha(Config.colors.media, 0.2)
strokeWidth: progress.thickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + progress.thickness) / 2 + 7
radiusY: (cover.height + progress.thickness) / 2 + 7
startAngle: -90 - progress.angle / 2
sweepAngle: progress.angle
}
Behavior on strokeColor {
CAnim {}
}
}
ShapePath {
fillColor: "transparent"
strokeColor: Config.colors.media
strokeWidth: progress.thickness
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + progress.thickness) / 2 + 7
radiusY: (cover.height + progress.thickness) / 2 + 7
startAngle: -90 - progress.angle / 2
// NOTE: Cap progress angle to account for bad MPRIS players
sweepAngle: progress.angle * Math.min(root.playerProgress, 1)
}
Behavior on strokeColor {
CAnim {}
}
}
}
CustomClippingRect {
id: cover
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 18 + progress.thickness
implicitHeight: width
color: Config.colors.inactive
radius: Infinity
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: Config.colors.tertiary
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
}
}
CustomText {
id: title
anchors.top: cover.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: album.text === "" ? 24 : 18
anchors.leftMargin: 15
anchors.rightMargin: 15
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Config.colors.secondary
font.pointSize: Config.font.size.normal
elide: Text.ElideRight
}
CustomText {
id: artist
anchors.top: title.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 15
anchors.rightMargin: 15
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
opacity: Players.active ? 1 : 0
color: Config.colors.primary
elide: Text.ElideRight
}
CustomText {
id: album
anchors.top: artist.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 15
anchors.rightMargin: 15
animate: true
horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackAlbum ?? ""
opacity: Players.active ? 1 : 0
color: Config.colors.tertiary
font.pointSize: Config.font.size.small
elide: Text.ElideRight
}
Row {
id: controls
anchors.top: album.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: album.text === "" ? -4 : 2
spacing: 7
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();
}
}
}
component Control: CustomRect {
id: control
required property string icon
required property bool canUse
function onClicked(): void {
}
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + 12
implicitHeight: implicitWidth
StateLayer {
anchors.fill: parent
disabled: !control.canUse
radius: 1000
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 ? Config.colors.media : Config.colors.inactive
font.pointSize: Config.font.size.large
}
}
}

View file

@ -0,0 +1,264 @@
pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
import qs.modules.notifications as N
ColumnLayout {
id: root
anchors.fill: parent
spacing: 7
readonly property int notifCount:
(Notifs.list && Notifs.list.length !== undefined) ? Notifs.list.length :
((Notifs.list && Notifs.list.count !== undefined) ? Notifs.list.count : 0)
function notifAt(i) {
if (!Notifs.list)
return undefined;
if (typeof Notifs.list.get === 'function')
return Notifs.list.get(i);
return Notifs.list[i];
}
function scrollToTop(): void {
if (notifScroll && notifScroll.contentItem && notifScroll.contentItem.contentY !== undefined) {
notifScroll.contentItem.contentY = 0;
}
}
RowLayout {
Layout.alignment: Qt.AlignTop
Layout.margins: 10
Layout.fillWidth: true
spacing: 7
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
id: icon
text: {
if (Notifs.dnd)
return "notifications_off";
if (notifCount > 0)
return "notifications_active";
return "notifications";
}
fill: {
if (Notifs.dnd)
return 0;
if (notifCount > 0)
return 1;
return 0;
}
color: Notifs.dnd ? Config.colors.error : Config.colors.notification
font.pointSize: Config.font.size.larger
animate: true
Behavior on color {
SequentialAnimation {
PauseAnimation {
duration: icon.animateDuration / 2
}
PropertyAction {}
}
}
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: notifCount > 0 ? qsTr("%1 notifications").arg(notifCount) : qsTr("No notifications")
font.weight: 600
font.pointSize: Config.font.size.normal
animate: true
}
CustomText {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Do Not Disturb")
}
CustomSwitch {
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 5
Layout.rightMargin: 5
bg: Config.colors.containerAlt
accent: Color.mute(Config.colors.notification)
checked: Notifs.dnd
onToggled: Notifs.toggleDnd()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: 10
CustomListView {
id: notifScroll
anchors.fill: parent
anchors.margins: 10
anchors.rightMargin: 5
spacing: 12
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: notifScroll
}
model: ScriptModel {
values: [...Notifs.list].reverse()
}
delegate: Item {
id: wrapper
required property int index
required property var modelData
readonly property alias nonAnimHeight: notif.nonAnimHeight
width: 405
height: notif.implicitHeight
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: true
}
PropertyAction {
target: wrapper
property: "enabled"
value: false
}
PropertyAction {
target: wrapper
property: "implicitHeight"
value: 0
}
PropertyAction {
target: wrapper
property: "z"
value: 1
}
Anim {
target: notif
property: "x"
to: (notif.x >= 0 ? Config.notifs.width : -Config.notifs.width) * 2
easing.bezierCurve: Config.anim.curves.emphasized
}
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: false
}
}
N.Notification {
id: notif
width: parent.width
notif: wrapper.modelData
color: wrapper.modelData.urgency === NotificationUrgency.Critical ? Config.colors.errorBg : Config.colors.containerAlt
inPopup: false
}
}
add: Transition {
Anim {
property: "x"
from: Config.notifs.width
to: 0
easing.bezierCurve: Config.anim.curves.emphasizedDecel
}
}
move: Transition {
NotifAnim {
property: "y"
}
}
displaced: Transition {
NotifAnim {
property: "y"
}
}
}
}
Item {
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
height: 0
CustomRect {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: bottomMargin
property real bottomMargin: notifCount > 0 ? 12 : -4
opacity: notifCount > 0 ? 1 : 0
visible: opacity > 0
implicitWidth: clearBtn.implicitWidth + 32
implicitHeight: clearBtn.implicitHeight + 20
radius: 25
color: Config.colors.inactive
Behavior on opacity {
Anim {}
}
Behavior on bottomMargin {
Anim {}
}
StateLayer {
anchors.fill: parent
function onClicked(): void {
for (let i = root.notifCount - 1; i >= 0; i--) {
const n = root.notifAt(i);
n?.notification?.dismiss();
}
}
}
RowLayout {
id: clearBtn
anchors.centerIn: parent
spacing: 7
MaterialIcon {
id: clearIcon
text: "clear_all"
color: Config.colors.secondary
}
CustomText {
text: qsTr("Clear all")
color: Config.colors.secondary
}
}
}
}
component NotifAnim: Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}

View file

@ -0,0 +1,73 @@
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import QtQuick
Column {
id: root
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 24
Item {
id: userLine
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
MaterialIcon {
id: userIcon
anchors.left: parent.left
text: "account_circle"
fill: 1
color: Config.colors.primary
font.pointSize: Config.font.size.larger
}
CustomText {
id: userText
anchors.verticalCenter: userIcon.verticalCenter
anchors.left: userIcon.right
anchors.leftMargin: 7
text: qsTr("User: %1").arg(User.user)
color: Config.colors.secondary
}
}
Item {
id: uptimeLine
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
MaterialIcon {
id: uptimeIcon
anchors.left: parent.left
text: "timer"
fill: 1
color: Config.colors.primary
font.pointSize: Config.font.size.larger
}
CustomText {
id: uptimeText
anchors.verticalCenter: uptimeIcon.verticalCenter
anchors.left: uptimeIcon.right
anchors.verticalCenterOffset: 1
anchors.leftMargin: 7
text: qsTr("Uptime: %1").arg(User.uptime)
color: Config.colors.secondary
}
}
}

View file

@ -0,0 +1,129 @@
import qs.config
import qs.custom
import qs.services
import qs.util
import QtQuick
import QtQuick.Layouts
Item {
id: root
anchors.fill: parent
anchors.margins: 16
anchors.topMargin: 6
Component.onCompleted: Weather.reload()
CustomText {
id: city
anchors.top: parent.top
anchors.left: parent.left
text: Weather.city
color: Config.colors.tertiary
}
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: font.pointSize * 0.25
animate: true
animateProp: "opacity"
text: Weather.icon
color: Weather.iconColor
font.pointSize: Config.font.size.largest * 1.8
Behavior on color {
SequentialAnimation {
PauseAnimation {
duration: icon.animateDuration / 2
}
PropertyAction {}
}
}
}
Column {
id: info
anchors.left: icon.right
anchors.verticalCenter: icon.verticalCenter
anchors.verticalCenterOffset: -3
anchors.leftMargin: 14
spacing: 1
opacity: Weather.available ? 1 : 0
Behavior on opacity {
SequentialAnimation {
PauseAnimation {
duration: temp.animateDuration / 2
}
PropertyAction {}
}
}
CustomText {
id: temp
animate: true
text: Weather.temp
color: Config.colors.primary
font.pointSize: Config.font.size.large
font.weight: 500
// Reduce padding at bottom of text
height: implicitHeight * 0.9
CustomText {
anchors.left: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 12
anchors.bottomMargin: 1
animate: true
text: Weather.feelsLike
color: Config.colors.tertiary
font.pointSize: Config.font.size.larger
}
}
CustomText {
animate: true
text: Weather.description
color: Config.colors.secondary
elide: Text.ElideRight
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - 30)
}
Item {
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
MaterialIcon {
id: humidityIcon
animate: true
text: Weather.humidityIcon
color: Config.colors.primary
font.pointSize: Config.font.size.normal
}
CustomText {
anchors.left: humidityIcon.right
anchors.verticalCenter: humidityIcon.verticalCenter
anchors.leftMargin: 2
animate: true
text: `${Math.round(Weather.humidity * 100)}% Humidity`
color: Config.colors.primary
}
}
}
}