Merge pull request #19 from Zacharias-Brohn/settingsWindow2

Settings window2
This commit was merged in pull request #19.
This commit is contained in:
Zach
2026-03-01 18:13:23 +01:00
committed by GitHub
48 changed files with 3056 additions and 396 deletions
+169
View File
@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Config
ComboBox {
id: root
property int cornerRadius: Appearance.rounding.normal
property int fieldHeight: 42
property bool filled: true
property real focusRingOpacity: 0.70
property int hPadding: 16
property int menuCornerRadius: 16
property int menuRowHeight: 46
property int menuVisibleRows: 7
property bool preferPopupWindow: false
hoverEnabled: true
implicitHeight: fieldHeight
implicitWidth: 240
spacing: 8
// ---------- Field background (filled/outlined + state layers + focus ring) ----------
background: Item {
anchors.fill: parent
CustomRect {
id: container
anchors.fill: parent
color: DynamicColors.palette.m3surfaceVariant
radius: root.cornerRadius
StateLayer {
}
}
}
// ---------- Content ----------
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: root.hPadding
anchors.rightMargin: root.hPadding
spacing: 12
// Display text
CustomText {
Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pixelSize: 16
font.weight: Font.Medium
text: root.currentText
verticalAlignment: Text.AlignVCenter
}
// Indicator chevron (simple, replace with your icon system)
CustomText {
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurfaceVariant
rotation: root.popup.visible ? 180 : 0
text: "▾"
transformOrigin: Item.Center
verticalAlignment: Text.AlignVCenter
Behavior on rotation {
NumberAnimation {
duration: 140
easing.type: Easing.OutCubic
}
}
}
}
popup: Popup {
id: p
implicitHeight: list.contentItem.height + Appearance.padding.small * 2
implicitWidth: root.width
modal: true
popupType: root.preferPopupWindow ? Popup.Window : Popup.Item
y: -list.currentIndex * (root.menuRowHeight + Appearance.spacing.small) - Appearance.padding.small
background: CustomRect {
color: DynamicColors.palette.m3surface
radius: root.menuCornerRadius
}
contentItem: ListView {
id: list
anchors.bottomMargin: Appearance.padding.small
anchors.fill: parent
anchors.topMargin: Appearance.padding.small
clip: true
currentIndex: root.currentIndex
model: root.delegateModel
spacing: Appearance.spacing.small
delegate: CustomRect {
required property int index
required property var modelData
anchors.horizontalCenter: parent.horizontalCenter
color: (index === root.currentIndex) ? DynamicColors.palette.m3primary : "transparent"
implicitHeight: root.menuRowHeight
implicitWidth: p.implicitWidth - Appearance.padding.small * 2
radius: Appearance.rounding.normal - Appearance.padding.small
RowLayout {
anchors.fill: parent
spacing: 10
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurface
elide: Text.ElideRight
font.pixelSize: 15
text: modelData
verticalAlignment: Text.AlignVCenter
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: "✓"
verticalAlignment: Text.AlignVCenter
visible: index === root.currentIndex
}
}
StateLayer {
onClicked: {
root.currentIndex = index;
p.close();
}
}
}
}
// Expressive-ish open/close motion: subtle scale+fade (tune to taste). :contentReference[oaicite:5]{index=5}
enter: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
from: 0.98
property: "scale"
to: 1.0
}
}
exit: Transition {
Anim {
from: 1
property: "opacity"
to: 0
}
}
Elevation {
anchors.fill: parent
level: 2
radius: root.menuCornerRadius
z: -1
}
}
}
+159
View File
@@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
Row {
id: root
enum Type {
Filled,
Tonal
}
property alias active: menu.active
property color color: type == CustomSplitButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
property bool disabled
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledTextColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property alias expanded: menu.expanded
property string fallbackIcon
property string fallbackText
property real horizontalPadding: Appearance.padding.normal
property alias iconLabel: iconLabel
property alias label: label
property alias menu: menu
property alias menuItems: menu.items
property bool menuOnTop
property alias stateLayer: stateLayer
property color textColor: type == CustomSplitButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller
spacing: Math.floor(Appearance.spacing.small / 2)
CustomRect {
bottomRightRadius: Appearance.rounding.small / 2
color: root.disabled ? root.disabledColor : root.color
implicitHeight: expandBtn.implicitHeight
implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topRightRadius: Appearance.rounding.small / 2
StateLayer {
id: stateLayer
function onClicked(): void {
root.active?.clicked();
}
color: root.textColor
disabled: root.disabled
rect.bottomRightRadius: parent.bottomRightRadius
rect.topRightRadius: parent.topRightRadius
}
RowLayout {
id: textRow
anchors.centerIn: parent
anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4)
spacing: Appearance.spacing.small
MaterialIcon {
id: iconLabel
Layout.alignment: Qt.AlignVCenter
animate: true
color: root.disabled ? root.disabledTextColor : root.textColor
fill: 1
text: root.active?.activeIcon ?? root.fallbackIcon
}
CustomText {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: implicitWidth
animate: true
clip: true
color: root.disabled ? root.disabledTextColor : root.textColor
text: root.active?.activeText ?? root.fallbackText
Behavior on Layout.preferredWidth {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
}
}
CustomRect {
id: expandBtn
property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2
bottomLeftRadius: rad
color: root.disabled ? root.disabledColor : root.color
implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2
implicitWidth: implicitHeight
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topLeftRadius: rad
Behavior on rad {
Anim {
}
}
StateLayer {
id: expandStateLayer
function onClicked(): void {
root.expanded = !root.expanded;
}
color: root.textColor
disabled: root.disabled
rect.bottomLeftRadius: parent.bottomLeftRadius
rect.topLeftRadius: parent.topLeftRadius
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
color: root.disabled ? root.disabledTextColor : root.textColor
rotation: root.expanded ? 180 : 0
text: "expand_more"
Behavior on anchors.horizontalCenterOffset {
Anim {
}
}
Behavior on rotation {
Anim {
}
}
}
Menu {
id: menu
anchors.bottomMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.bottom
anchors.topMargin: Appearance.spacing.small
states: State {
when: root.menuOnTop
AnchorChanges {
anchors.bottom: expandBtn.top
anchors.top: undefined
target: menu
}
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Config
CustomRect {
id: root
property alias active: splitButton.active
property bool enabled: true
property alias expanded: splitButton.expanded
property int expandedZ: 100
required property string label
property alias menuItems: splitButton.menuItems
property alias type: splitButton.type
signal selected(item: MenuItem)
Layout.fillWidth: true
clip: false
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
opacity: enabled ? 1.0 : 0.5
radius: Appearance.rounding.normal
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
RowLayout {
id: row
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
CustomText {
Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
text: root.label
}
CustomSplitButton {
id: splitButton
enabled: root.enabled
menu.z: 1
type: CustomSplitButton.Filled
menu.onItemSelected: item => {
root.selected(item);
}
stateLayer.onClicked: {
splitButton.expanded = !splitButton.expanded;
}
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ Text {
color: DynamicColors.palette.m3onSurface color: DynamicColors.palette.m3onSurface
font.family: Appearance.font.family.sans font.family: Appearance.font.family.sans
font.pointSize: 12 font.pointSize: Appearance.font.size.normal
renderType: Text.NativeRendering renderType: Text.NativeRendering
textFormat: Text.PlainText textFormat: Text.PlainText
+38 -17
View File
@@ -4,6 +4,8 @@ import qs.Config
Item { Item {
id: root id: root
property alias anim: marqueeAnim
property bool animate: false
property color color: DynamicColors.palette.m3onSurface property color color: DynamicColors.palette.m3onSurface
property int fadeStrengthAnimMs: 180 property int fadeStrengthAnimMs: 180
property real fadeStrengthIdle: 0.0 property real fadeStrengthIdle: 0.0
@@ -27,6 +29,17 @@ Item {
return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000)); return Math.max(1, Math.round(Math.abs(px) / root.pixelsPerSecond * 1000));
} }
function resetMarquee() {
marqueeAnim.stop();
strip.x = 0;
root.sliding = false;
root.leftFadeEnabled = false;
if (root.marqueeEnabled && root.overflowing && root.visible) {
marqueeAnim.restart();
}
}
clip: true clip: true
implicitHeight: elideText.implicitHeight implicitHeight: elideText.implicitHeight
@@ -39,10 +52,10 @@ Item {
} }
} }
onTextChanged: strip.x = 0 onTextChanged: resetMarquee()
onVisibleChanged: if (!visible) onVisibleChanged: if (!visible)
strip.x = 0 resetMarquee()
onWidthChanged: strip.x = 0 onWidthChanged: resetMarquee()
TextMetrics { TextMetrics {
id: metrics id: metrics
@@ -55,8 +68,10 @@ Item {
id: elideText id: elideText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
animate: root.animate
animateProp: "scale,opacity"
color: root.color color: root.color
elide: Text.ElideRight elide: Text.ElideNone
visible: !root.overflowing visible: !root.overflowing
width: root.width width: root.width
} }
@@ -84,6 +99,8 @@ Item {
CustomText { CustomText {
id: t1 id: t1
animate: root.animate
animateProp: "opacity"
color: root.color color: root.color
text: elideText.text text: elideText.text
} }
@@ -91,6 +108,8 @@ Item {
CustomText { CustomText {
id: t2 id: t2
animate: root.animate
animateProp: "opacity"
color: root.color color: root.color
text: t1.text text: t1.text
x: t1.width + root.gap x: t1.width + root.gap
@@ -100,20 +119,9 @@ Item {
SequentialAnimation { SequentialAnimation {
id: marqueeAnim id: marqueeAnim
loops: Animation.Infinite running: false
running: root.marqueeEnabled && root.overflowing && root.visible
ScriptAction { onFinished: pauseTimer.restart()
script: {
strip.x = 0;
root.sliding = false;
root.leftFadeEnabled = false;
}
}
PauseAnimation {
duration: root.pauseMs
}
ScriptAction { ScriptAction {
script: { script: {
@@ -155,6 +163,19 @@ Item {
} }
} }
} }
Timer {
id: pauseTimer
interval: root.pauseMs
repeat: false
running: true
onTriggered: {
if (root.marqueeEnabled)
marqueeAnim.start();
}
}
} }
Rectangle { Rectangle {
+12 -4
View File
@@ -13,11 +13,11 @@ Elevation {
signal itemSelected(item: MenuItem) signal itemSelected(item: MenuItem)
implicitHeight: root.expanded ? column.implicitHeight : 0 implicitHeight: root.expanded ? column.implicitHeight + Appearance.padding.small * 2 : 0
implicitWidth: Math.max(200, column.implicitWidth) implicitWidth: Math.max(200, column.implicitWidth)
level: 2 level: 2
opacity: root.expanded ? 1 : 0 opacity: root.expanded ? 1 : 0
radius: Appearance.rounding.small / 2 radius: Appearance.rounding.normal
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {
@@ -41,7 +41,8 @@ Elevation {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
spacing: 0 anchors.verticalCenter: parent.verticalCenter
spacing: 5
Repeater { Repeater {
model: root.items model: root.items
@@ -54,10 +55,16 @@ Elevation {
required property MenuItem modelData required property MenuItem modelData
Layout.fillWidth: true Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2
CustomRect {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
radius: Appearance.rounding.normal - Appearance.padding.small
StateLayer { StateLayer {
function onClicked(): void { function onClicked(): void {
root.itemSelected(item.modelData); root.itemSelected(item.modelData);
@@ -68,6 +75,7 @@ Elevation {
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
disabled: !root.expanded disabled: !root.expanded
} }
}
RowLayout { RowLayout {
id: menuOptionRow id: menuOptionRow
+4
View File
@@ -11,6 +11,10 @@ JsonObject {
id: "audio", id: "audio",
enabled: true enabled: true
}, },
{
id: "media",
enabled: true
},
{ {
id: "resources", id: "resources",
enabled: true enabled: true
+10
View File
@@ -121,7 +121,16 @@ Singleton {
return { return {
enabled: dashboard.enabled, enabled: dashboard.enabled,
mediaUpdateInterval: dashboard.mediaUpdateInterval, mediaUpdateInterval: dashboard.mediaUpdateInterval,
resourceUpdateInterval: dashboard.resourceUpdateInterval,
dragThreshold: dashboard.dragThreshold, dragThreshold: dashboard.dragThreshold,
performance: {
showBattery: dashboard.performance.showBattery,
showGpu: dashboard.performance.showGpu,
showCpu: dashboard.performance.showCpu,
showMemory: dashboard.performance.showMemory,
showStorage: dashboard.performance.showStorage,
showNetwork: dashboard.performance.showNetwork
},
sizes: { sizes: {
tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight,
tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing,
@@ -236,6 +245,7 @@ Singleton {
return { return {
weatherLocation: services.weatherLocation, weatherLocation: services.weatherLocation,
useFahrenheit: services.useFahrenheit, useFahrenheit: services.useFahrenheit,
ddcutilService: services.ddcutilService,
useTwelveHourClock: services.useTwelveHourClock, useTwelveHourClock: services.useTwelveHourClock,
gpuType: services.gpuType, gpuType: services.gpuType,
audioIncrement: services.audioIncrement, audioIncrement: services.audioIncrement,
+11
View File
@@ -4,9 +4,20 @@ JsonObject {
property int dragThreshold: 50 property int dragThreshold: 50
property bool enabled: true property bool enabled: true
property int mediaUpdateInterval: 500 property int mediaUpdateInterval: 500
property Performance performance: Performance {
}
property int resourceUpdateInterval: 1000
property Sizes sizes: Sizes { property Sizes sizes: Sizes {
} }
component Performance: JsonObject {
property bool showBattery: true
property bool showCpu: true
property bool showGpu: true
property bool showMemory: true
property bool showNetwork: true
property bool showStorage: true
}
component Sizes: JsonObject { component Sizes: JsonObject {
readonly property int dateTimeWidth: 110 readonly property int dateTimeWidth: 110
readonly property int infoIconSize: 25 readonly property int infoIconSize: 25
+1
View File
@@ -4,6 +4,7 @@ import QtQuick
JsonObject { JsonObject {
property real audioIncrement: 0.1 property real audioIncrement: 0.1
property real brightnessIncrement: 0.1 property real brightnessIncrement: 0.1
property bool ddcutilService: false
property string defaultPlayer: "Spotify" property string defaultPlayer: "Spotify"
property string gpuType: "" property string gpuType: ""
property real maxVolume: 1.0 property real maxVolume: 1.0
+6
View File
@@ -10,6 +10,7 @@ import qs.Config
Singleton { Singleton {
id: root id: root
readonly property alias beatTracker: beatTracker
readonly property alias cava: cava readonly property alias cava: cava
readonly property bool muted: !!sink?.audio?.muted readonly property bool muted: !!sink?.audio?.muted
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
@@ -138,6 +139,11 @@ Singleton {
bars: Config.services.visualizerBars bars: Config.services.visualizerBars
} }
BeatTracker {
id: beatTracker
}
PwObjectTracker { PwObjectTracker {
objects: [...root.sinks, ...root.sources, ...root.streams] objects: [...root.sinks, ...root.sources, ...root.streams]
} }
+23 -9
View File
@@ -1,6 +1,8 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.Components
import qs.Config
import qs.Modules as Modules import qs.Modules as Modules
import qs.Modules.Notifications as Notifications import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar import qs.Modules.Notifications.Sidebar as Sidebar
@@ -8,8 +10,9 @@ import qs.Modules.Notifications.Sidebar.Utils as Utils
import qs.Modules.Dashboard as Dashboard import qs.Modules.Dashboard as Dashboard
import qs.Modules.Osd as Osd import qs.Modules.Osd as Osd
import qs.Modules.Launcher as Launcher import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources
// import qs.Modules.Settings as Settings import qs.Modules.Settings as Settings
Shape { Shape {
id: root id: root
@@ -20,9 +23,20 @@ Shape {
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 // anchors.margins: 8
anchors.topMargin: bar.implicitHeight anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
Behavior on anchors.topMargin {
Anim {
}
}
Resources.Background {
startX: 0 - rounding
startY: 0
wrapper: root.panels.resources
}
Osd.Background { Osd.Background {
startX: root.width - root.panels.sidebar.width startX: root.width - root.panels.sidebar.width
startY: (root.height - wrapper.height) / 2 - rounding startY: (root.height - wrapper.height) / 2 - rounding
@@ -71,11 +85,11 @@ Shape {
wrapper: root.panels.sidebar wrapper: root.panels.sidebar
} }
// Settings.Background { Settings.Background {
// id: settings id: settings
//
// startX: (root.width - wrapper.width) / 2 - rounding startX: (root.width - wrapper.width) / 2 - rounding
// startY: 0 startY: 0
// wrapper: root.panels.settings wrapper: root.panels.settings
// } }
} }
+6 -3
View File
@@ -28,7 +28,7 @@ Variants {
property bool trayMenuVisible: false property bool trayMenuVisible: false
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.namespace: "ZShell-Bar" WlrLayershell.namespace: "ZShell-Bar"
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
@@ -52,6 +52,7 @@ Variants {
visibilities.dashboard = false; visibilities.dashboard = false;
visibilities.osd = false; visibilities.osd = false;
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false;
} }
PanelWindow { PanelWindow {
@@ -97,7 +98,7 @@ Variants {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
active: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [bar] windows: [bar]
onCleared: { onCleared: {
@@ -106,6 +107,7 @@ Variants {
visibilities.dashboard = false; visibilities.dashboard = false;
visibilities.osd = false; visibilities.osd = false;
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
} }
@@ -118,6 +120,7 @@ Variants {
property bool launcher property bool launcher
property bool notif: NotifServer.popups.length > 0 property bool notif: NotifServer.popups.length > 0
property bool osd property bool osd
property bool resources
property bool settings property bool settings
property bool sidebar property bool sidebar
@@ -127,7 +130,7 @@ Variants {
Binding { Binding {
property: "bar" property: "bar"
target: visibilities target: visibilities
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources
when: Config.barConfig.autoHide when: Config.barConfig.autoHide
} }
+21 -12
View File
@@ -9,7 +9,8 @@ import qs.Modules.Dashboard as Dashboard
import qs.Modules.Osd as Osd import qs.Modules.Osd as Osd
import qs.Components.Toast as Toasts import qs.Components.Toast as Toasts
import qs.Modules.Launcher as Launcher import qs.Modules.Launcher as Launcher
// import qs.Modules.Settings as Settings import qs.Modules.Resources as Resources
import qs.Modules.Settings as Settings
import qs.Config import qs.Config
Item { Item {
@@ -21,8 +22,9 @@ Item {
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias osd: osd readonly property alias osd: osd
readonly property alias popouts: popouts readonly property alias popouts: popouts
readonly property alias resources: resources
required property ShellScreen screen required property ShellScreen screen
// readonly property alias settings: settings readonly property alias settings: settings
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias utilities: utilities readonly property alias utilities: utilities
@@ -30,14 +32,21 @@ Item {
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 // anchors.margins: 8
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight
bar.implicitHeight
Behavior on anchors.topMargin { Behavior on anchors.topMargin {
Anim { Anim {
} }
} }
Resources.Wrapper {
id: resources
anchors.left: parent.left
anchors.top: parent.top
visibilities: root.visibilities
}
Osd.Wrapper { Osd.Wrapper {
id: osd id: osd
@@ -118,12 +127,12 @@ Item {
visibilities: root.visibilities visibilities: root.visibilities
} }
// Settings.Wrapper { Settings.Wrapper {
// id: settings id: settings
//
// anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
// anchors.top: parent.top anchors.top: parent.top
// panels: root panels: root
// visibilities: root.visibilities visibilities: root.visibilities
// } }
} }
+40 -6
View File
@@ -12,6 +12,7 @@ Singleton {
property bool appleDisplayPresent: false property bool appleDisplayPresent: false
property list<var> ddcMonitors: [] property list<var> ddcMonitors: []
property list<var> ddcServiceMon: []
readonly property list<Monitor> monitors: variants.instances readonly property list<Monitor> monitors: variants.instances
function decreaseBrightness(): void { function decreaseBrightness(): void {
@@ -55,6 +56,8 @@ Singleton {
onMonitorsChanged: { onMonitorsChanged: {
ddcMonitors = []; ddcMonitors = [];
ddcServiceMon = [];
ddcServiceProc.running = true;
ddcProc.running = true; ddcProc.running = true;
} }
@@ -68,7 +71,7 @@ Singleton {
} }
Process { Process {
command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed command: ["sh", "-c", "asdbctl get"]
running: true running: true
stdout: StdioCollector { stdout: StdioCollector {
@@ -89,6 +92,26 @@ Singleton {
} }
} }
Process {
id: ddcServiceProc
command: ["ddcutil-client", "detect"]
// running: true
stdout: StdioCollector {
onStreamFinished: {
const t = text.replace(/\r\n/g, "\n").trim();
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
})).filter(d => d.display > 0);
root.ddcServiceMon = output;
}
}
}
CustomShortcut { CustomShortcut {
description: "Increase brightness" description: "Increase brightness"
name: "brightnessUp" name: "brightnessUp"
@@ -161,10 +184,15 @@ Singleton {
property real brightness property real brightness
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
readonly property Process initProc: Process { readonly property Process initProc: Process {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (monitor.isAppleDisplay) { if (monitor.isDdcService) {
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
const val = parseInt(output.trim());
monitor.brightness = val / 100;
} else if (monitor.isAppleDisplay) {
const val = parseInt(text.trim()); const val = parseInt(text.trim());
monitor.brightness = val / 101; monitor.brightness = val / 101;
} else { } else {
@@ -176,6 +204,7 @@ Singleton {
} }
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
readonly property bool isDdcService: Config.services.ddcutilService
required property ShellScreen modelData required property ShellScreen modelData
property real queuedBrightness: NaN property real queuedBrightness: NaN
readonly property Timer timer: Timer { readonly property Timer timer: Timer {
@@ -190,7 +219,9 @@ Singleton {
} }
function initBrightness(): void { function initBrightness(): void {
if (isAppleDisplay) if (isDdcService)
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
else if (isAppleDisplay)
initProc.command = ["asdbctl", "get"]; initProc.command = ["asdbctl", "get"];
else if (isDdc) else if (isDdc)
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
@@ -206,25 +237,28 @@ Singleton {
if (Math.round(brightness * 100) === rounded) if (Math.round(brightness * 100) === rounded)
return; return;
if (isDdc && timer.running) { if ((isDdc || isDdcService) && timer.running) {
queuedBrightness = value; queuedBrightness = value;
return; return;
} }
brightness = value; brightness = value;
if (isAppleDisplay) if (isDdcService)
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
else if (isAppleDisplay)
Quickshell.execDetached(["asdbctl", "set", rounded]); Quickshell.execDetached(["asdbctl", "set", rounded]);
else if (isDdc) else if (isDdc)
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]); Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
else else
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
if (isDdc) if (isDdc || isDdcService)
timer.restart(); timer.restart();
} }
Component.onCompleted: initBrightness() Component.onCompleted: initBrightness()
onBusNumChanged: initBrightness() onBusNumChanged: initBrightness()
onDisplayNumChanged: initBrightness()
} }
} }
+1
View File
@@ -28,6 +28,7 @@ Singleton {
enabled: props.enabled enabled: props.enabled
window: PanelWindow { window: PanelWindow {
WlrLayershell.namespace: "ZShell-IdleInhibitor"
color: "transparent" color: "transparent"
implicitHeight: 0 implicitHeight: 0
implicitWidth: 0 implicitWidth: 0
+234
View File
@@ -0,0 +1,234 @@
pragma Singleton
import qs.Config
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property var _downloadHistory: []
// Private properties
property real _downloadSpeed: 0
property real _downloadTotal: 0
// Initial readings for calculating totals
property real _initialRxBytes: 0
property real _initialTxBytes: 0
property bool _initialized: false
// Previous readings for calculating speed
property real _prevRxBytes: 0
property real _prevTimestamp: 0
property real _prevTxBytes: 0
property var _uploadHistory: []
property real _uploadSpeed: 0
property real _uploadTotal: 0
// History of speeds for sparkline (most recent at end)
readonly property var downloadHistory: _downloadHistory
// Current speeds in bytes per second
readonly property real downloadSpeed: _downloadSpeed
// Total bytes transferred since tracking started
readonly property real downloadTotal: _downloadTotal
readonly property int historyLength: 30
property int refCount: 0
readonly property var uploadHistory: _uploadHistory
readonly property real uploadSpeed: _uploadSpeed
readonly property real uploadTotal: _uploadTotal
function formatBytes(bytes: real): var {
// Handle negative or invalid values
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
return {
value: 0,
unit: "B/s"
};
}
if (bytes < 1024) {
return {
value: bytes,
unit: "B/s"
};
} else if (bytes < 1024 * 1024) {
return {
value: bytes / 1024,
unit: "KB/s"
};
} else if (bytes < 1024 * 1024 * 1024) {
return {
value: bytes / (1024 * 1024),
unit: "MB/s"
};
} else {
return {
value: bytes / (1024 * 1024 * 1024),
unit: "GB/s"
};
}
}
function formatBytesTotal(bytes: real): var {
// Handle negative or invalid values
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
return {
value: 0,
unit: "B"
};
}
if (bytes < 1024) {
return {
value: bytes,
unit: "B"
};
} else if (bytes < 1024 * 1024) {
return {
value: bytes / 1024,
unit: "KB"
};
} else if (bytes < 1024 * 1024 * 1024) {
return {
value: bytes / (1024 * 1024),
unit: "MB"
};
} else {
return {
value: bytes / (1024 * 1024 * 1024),
unit: "GB"
};
}
}
function parseNetDev(content: string): var {
const lines = content.split("\n");
let totalRx = 0;
let totalTx = 0;
for (let i = 2; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(/\s+/);
if (parts.length < 10)
continue;
const iface = parts[0].replace(":", "");
// Skip loopback interface
if (iface === "lo")
continue;
const rxBytes = parseFloat(parts[1]) || 0;
const txBytes = parseFloat(parts[9]) || 0;
totalRx += rxBytes;
totalTx += txBytes;
}
return {
rx: totalRx,
tx: totalTx
};
}
FileView {
id: netDevFile
path: "/proc/net/dev"
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
netDevFile.reload();
const content = netDevFile.text();
if (!content)
return;
const data = root.parseNetDev(content);
const now = Date.now();
if (!root._initialized) {
root._initialRxBytes = data.rx;
root._initialTxBytes = data.tx;
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
root._initialized = true;
return;
}
const timeDelta = (now - root._prevTimestamp) / 1000; // seconds
if (timeDelta > 0) {
// Calculate byte deltas
let rxDelta = data.rx - root._prevRxBytes;
let txDelta = data.tx - root._prevTxBytes;
// Handle counter overflow (when counters wrap around from max to 0)
// This happens when counters exceed 32-bit or 64-bit limits
if (rxDelta < 0) {
// Counter wrapped around - assume 64-bit counter
rxDelta += Math.pow(2, 64);
}
if (txDelta < 0) {
txDelta += Math.pow(2, 64);
}
// Calculate speeds
root._downloadSpeed = rxDelta / timeDelta;
root._uploadSpeed = txDelta / timeDelta;
const maxHistory = root.historyLength + 1;
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) {
let newDownHist = root._downloadHistory.slice();
newDownHist.push(root._downloadSpeed);
if (newDownHist.length > maxHistory) {
newDownHist.shift();
}
root._downloadHistory = newDownHist;
}
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) {
let newUpHist = root._uploadHistory.slice();
newUpHist.push(root._uploadSpeed);
if (newUpHist.length > maxHistory) {
newUpHist.shift();
}
root._uploadHistory = newUpHist;
}
}
// Calculate totals with overflow handling
let downTotal = data.rx - root._initialRxBytes;
let upTotal = data.tx - root._initialTxBytes;
// Handle counter overflow for totals
if (downTotal < 0) {
downTotal += Math.pow(2, 64);
}
if (upTotal < 0) {
upTotal += Math.pow(2, 64);
}
root._downloadTotal = downTotal;
root._uploadTotal = upTotal;
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
}
}
}
+2 -5
View File
@@ -69,15 +69,12 @@ Singleton {
onLoaded: { onLoaded: {
const up = parseInt(text().split(" ")[0] ?? 0); const up = parseInt(text().split(" ")[0] ?? 0);
const days = Math.floor(up / 86400); const hours = Math.floor(up / 3600);
const hours = Math.floor((up % 86400) / 3600);
const minutes = Math.floor((up % 3600) / 60); const minutes = Math.floor((up % 3600) / 60);
let str = ""; let str = "";
if (days > 0)
str += `${days} day${days === 1 ? "" : "s"}`;
if (hours > 0) if (hours > 0)
str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`; str += `${hours} hour${hours === 1 ? "" : "s"}`;
if (minutes > 0 || !str) if (minutes > 0 || !str)
str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`; str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`;
root.uptime = str; root.uptime = str;
+129 -24
View File
@@ -9,10 +9,15 @@ Singleton {
id: root id: root
property string autoGpuType: "NONE" property string autoGpuType: "NONE"
property string cpuName: ""
property real cpuPerc property real cpuPerc
property real cpuTemp property real cpuTemp
// Individual disks: Array of { mount, used, total, free, perc }
property var disks: []
property real gpuMemTotal: 0 property real gpuMemTotal: 0
property real gpuMemUsed property real gpuMemUsed
property string gpuName: ""
property real gpuPerc property real gpuPerc
property real gpuTemp property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
@@ -22,9 +27,23 @@ Singleton {
property real memTotal property real memTotal
property real memUsed property real memUsed
property int refCount property int refCount
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 readonly property real storagePerc: {
property real storageTotal let totalUsed = 0;
property real storageUsed let totalSize = 0;
for (const disk of disks) {
totalUsed += disk.used;
totalSize += disk.total;
}
return totalSize > 0 ? totalUsed / totalSize : 0;
}
function cleanCpuName(name: string): string {
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
}
function cleanGpuName(name: string): string {
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
}
function formatKib(kib: real): var { function formatKib(kib: real): var {
const mib = 1024; const mib = 1024;
@@ -53,7 +72,7 @@ Singleton {
} }
Timer { Timer {
interval: 3000 interval: Config.dashboard.resourceUpdateInterval
repeat: true repeat: true
running: root.refCount > 0 running: root.refCount > 0
triggeredOnStart: true triggeredOnStart: true
@@ -67,6 +86,18 @@ Singleton {
} }
} }
FileView {
id: cpuinfoInit
path: "/proc/cpuinfo"
onLoaded: {
const nameMatch = text().match(/model name\s*:\s*(.+)/);
if (nameMatch)
root.cpuName = root.cleanCpuName(nameMatch[1]);
}
}
FileView { FileView {
id: stat id: stat
@@ -104,42 +135,116 @@ Singleton {
Process { Process {
id: storage id: storage
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const deviceMap = new Map(); const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
const lines = text.trim().split("\n");
for (const line of text.trim().split("\n")) { for (const line of lines) {
if (line.trim() === "") if (line.trim() === "")
continue; continue;
const nameMatch = line.match(/NAME="([^"]+)"/);
const sizeMatch = line.match(/SIZE="([^"]+)"/);
const typeMatch = line.match(/TYPE="([^"]+)"/);
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
const parts = line.trim().split(/\s+/); if (!nameMatch || !typeMatch)
if (parts.length >= 3) { continue;
const device = parts[0];
const used = parseInt(parts[1], 10) || 0;
const avail = parseInt(parts[2], 10) || 0;
// Only keep the entry with the largest total space for each device const name = nameMatch[1];
if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { const type = typeMatch[1];
deviceMap.set(device, { const size = parseInt(sizeMatch?.[1] || "0", 10);
used: used, const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
avail: avail const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
});
if (type === "disk") {
// Skip zram (swap) devices
if (name.startsWith("zram"))
continue;
// Initialize disk entry
if (!diskMap[name]) {
diskMap[name] = {
name: name,
totalSize: size,
used: 0,
fsTotal: 0
};
}
} else if (type === "part") {
// Find parent disk (remove trailing numbers/p+numbers)
let parentDisk = name.replace(/p?\d+$/, "");
// For nvme devices like nvme0n1p1, parent is nvme0n1
if (name.match(/nvme\d+n\d+p\d+/))
parentDisk = name.replace(/p\d+$/, "");
// Aggregate partition usage to parent disk
if (diskMap[parentDisk]) {
diskMap[parentDisk].used += fsused;
diskMap[parentDisk].fsTotal += fssize;
} }
} }
} }
const diskList = [];
let totalUsed = 0; let totalUsed = 0;
let totalAvail = 0; let totalSize = 0;
for (const [device, stats] of deviceMap) { for (const diskName of Object.keys(diskMap).sort()) {
totalUsed += stats.used; const disk = diskMap[diskName];
totalAvail += stats.avail; // Use filesystem total if available, otherwise use disk size
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
const used = disk.used;
const perc = total > 0 ? used / total : 0;
// Convert bytes to KiB for consistency with formatKib
diskList.push({
mount: disk.name // Using 'mount' property for compatibility
,
used: used / 1024,
total: total / 1024,
free: (total - used) / 1024,
perc: perc
});
totalUsed += used;
totalSize += total;
} }
root.storageUsed = totalUsed; root.disks = diskList;
root.storageTotal = totalUsed + totalAvail; }
}
}
Process {
id: gpuNameDetect
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const output = text.trim();
if (!output)
return;
// Check if it's from nvidia-smi (clean GPU name)
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
root.gpuName = root.cleanGpuName(output);
} else {
// Parse lspci output: extract name from brackets or after colon
const bracketMatch = output.match(/\[([^\]]+)\]/);
if (bracketMatch) {
root.gpuName = root.cleanGpuName(bracketMatch[1]);
} else {
const colonMatch = output.match(/:\s*(.+)/);
if (colonMatch)
root.gpuName = root.cleanGpuName(colonMatch[1]);
}
}
} }
} }
} }
+8 -32
View File
@@ -24,63 +24,44 @@ Item {
} }
} }
Rectangle { CustomRect {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
height: 22 height: 22
radius: height / 2 radius: height / 2
Behavior on color {
CAnim {
}
}
Rectangle {
anchors.centerIn: parent
border.color: "#30ffffff"
border.width: 0
color: "transparent"
height: parent.height
radius: width / 2
width: parent.width
} }
RowLayout { RowLayout {
id: layout id: layout
anchors.left: parent.left anchors.fill: parent
anchors.leftMargin: Appearance.padding.small anchors.leftMargin: Appearance.padding.small
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.small * 2 anchors.rightMargin: Appearance.padding.small * 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true
color: Audio.muted ? DynamicColors.palette.m3error : root.textColor color: Audio.muted ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14 font.pointSize: 14
text: Audio.muted ? "volume_off" : "volume_up" text: Audio.muted ? "volume_off" : "volume_up"
} }
Rectangle { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
color: "#50ffffff" color: "#50ffffff"
implicitHeight: 4 implicitHeight: 4
radius: 20 radius: 20
Rectangle { CustomRect {
id: sinkVolumeBar id: sinkVolumeBar
color: Audio.muted ? DynamicColors.palette.m3error : root.barColor color: Audio.muted ? DynamicColors.palette.m3error : root.barColor
implicitWidth: parent.width * (Audio.volume ?? 0) implicitWidth: parent.width * (Audio.volume ?? 0)
radius: parent.radius radius: parent.radius
Behavior on color {
CAnim {
}
}
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
left: parent.left left: parent.left
@@ -91,29 +72,25 @@ Item {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14 font.pointSize: 14
text: Audio.sourceMuted ? "mic_off" : "mic" text: Audio.sourceMuted ? "mic_off" : "mic"
} }
Rectangle { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
color: "#50ffffff" color: "#50ffffff"
implicitHeight: 4 implicitHeight: 4
radius: 20 radius: 20
Rectangle { CustomRect {
id: sourceVolumeBar id: sourceVolumeBar
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.barColor color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.barColor
implicitWidth: parent.width * (Audio.sourceVolume ?? 0) implicitWidth: parent.width * (Audio.sourceVolume ?? 0)
radius: parent.radius radius: parent.radius
Behavior on color {
CAnim {
}
}
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
left: parent.left left: parent.left
@@ -123,4 +100,3 @@ Item {
} }
} }
} }
}
+14 -31
View File
@@ -23,13 +23,13 @@ RowLayout {
function checkPopout(x: real): void { function checkPopout(x: real): void {
const ch = childAt(x, 2) as WrappedLoader; const ch = childAt(x, 2) as WrappedLoader;
if (!ch) { if (!ch || ch?.id === "spacer") {
if (!popouts.currentName.includes("traymenu")) if (!popouts.currentName.startsWith("traymenu"))
popouts.hasCurrent = false; popouts.hasCurrent = false;
return; return;
} }
if (visibilities.sidebar || visibilities.dashboard) if (visibilities.sidebar || visibilities.dashboard || visibilities.resources)
return; return;
const id = ch.id; const id = ch.id;
@@ -41,26 +41,6 @@ RowLayout {
popouts.currentName = "audio"; popouts.currentName = "audio";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true; popouts.hasCurrent = true;
} else if (id === "resources" && Config.barConfig.popouts.resources) {
popouts.currentName = "resources";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true;
} else if (id === "tray" && Config.barConfig.popouts.tray) {
const index = Math.floor(((x - top) / item.implicitWidth) * item.items.count);
const trayItem = item.items.itemAt(index);
if (trayItem) {
// popouts.currentName = `traymenu${ index }`;
// popouts.currentCenter = Qt.binding( () => trayItem.mapToItem( root, trayItem.implicitWidth / 2, 0 ).x );
// popouts.hasCurrent = true;
} else {
// popouts.hasCurrent = false;
}
} else if (id === "clock" && Config.barConfig.popouts.clock) {
// Calendar.displayYear = new Date().getFullYear();
// Calendar.displayMonth = new Date().getMonth();
// popouts.currentName = "calendar";
// popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x );
// popouts.hasCurrent = true;
} else if (id === "network" && Config.barConfig.popouts.network) { } else if (id === "network" && Config.barConfig.popouts.network) {
popouts.currentName = "network"; popouts.currentName = "network";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
@@ -93,6 +73,7 @@ RowLayout {
Repeater { Repeater {
id: repeater id: repeater
// model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index)
model: Config.barConfig.entries model: Config.barConfig.entries
DelegateChooser { DelegateChooser {
@@ -142,6 +123,7 @@ RowLayout {
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Resources { sourceComponent: Resources {
visibilities: root.visibilities
} }
} }
} }
@@ -206,14 +188,15 @@ RowLayout {
} }
} }
} }
// DelegateChoice {
// roleValue: "dash" DelegateChoice {
// delegate: WrappedLoader { roleValue: "media"
// sourceComponent: DashWidget {
// visibilities: root.visibilities delegate: WrappedLoader {
// } sourceComponent: MediaWidget {
// } }
// } }
}
} }
} }
-8
View File
@@ -33,14 +33,6 @@ Item {
} }
} }
Popout {
name: "resources"
sourceComponent: ResourcePopout {
wrapper: root.wrapper
}
}
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: [...SystemTray.items.values] values: [...SystemTray.items.values]
+2 -2
View File
@@ -49,12 +49,12 @@ ShapePath {
radiusX: root.rounding radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height) radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding relativeX: root.rounding
relativeY: -root.roundingY relativeY: root.roundingY
} }
PathLine { PathLine {
relativeX: 0 relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2) relativeY: -(root.wrapper.height)
} }
PathArc { PathArc {
+82 -51
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import QtQuick.Shapes
import ZShell.Services
import qs.Daemons import qs.Daemons
import qs.Components import qs.Components
import qs.Config import qs.Config
@@ -37,54 +38,55 @@ Item {
onTriggered: Players.active?.positionChanged() onTriggered: Players.active?.positionChanged()
} }
// Shape { ServiceRef {
// id: visualizer service: Audio.cava
// }
// readonly property real centerX: width / 2
// readonly property real centerY: height / 2 Shape {
// property color colour: DynamicColors.palette.m3primary id: visualizer
// readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small
// readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small readonly property real barW: Math.max(0, (width - gap * (bars - 1)) / bars)
// readonly property int bars: Config.services.visualizerBars
// anchors.fill: cover property color color: DynamicColors.palette.m3primary
// anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize readonly property real gap: Appearance.spacing.small
// asynchronous: true
// data: visualizerBars.instances anchors.fill: layout
// preferredRendererType: Shape.CurveRenderer asynchronous: true
// } data: visualizerBars.instances
// preferredRendererType: Shape.CurveRenderer
// Variants { }
// id: visualizerBars
// Variants {
// model: Array.from({ id: visualizerBars
// length: Config.services.visualizerBars
// }, (_, i) => i) model: Array.from({
// length: Config.services.visualizerBars
// ShapePath { }, (_, i) => i)
// id: visualizerBar
// ShapePath {
// readonly property real angle: modelData * 2 * Math.PI / Config.services.visualizerBars id: visualizerBar
// readonly property real cos: Math.cos(angle)
// readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize
// required property int modelData required property int modelData
// readonly property real sin: Math.sin(angle) readonly property real value: Math.max(1e-3, Audio.cava.values[modelData])
// readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData]))
// capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
// capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap startX: (visualizer.barW / 2) + modelData * (visualizer.barW + visualizer.gap)
// startX: visualizer.centerX + (visualizer.innerX + strokeWidth / 2) * cos startY: layout.y + layout.height
// strokeColor: DynamicColors.palette.m3primary strokeColor: visualizer.color
// strokeWidth: 360 / Config.services.visualizerBars - Appearance.spacing.small / 4 strokeWidth: visualizer.barW
//
// startY: PathLine { Behavior on strokeColor {
// x: visualizer.centerX + (visualizer.innerX + visualizerBar.strokeWidth / 2 + visualizerBar.magnitude) * visualizerBar.cos CAnim {
// y: visualizer.centerY + (visualizer.innerY + visualizerBar.strokeWidth / 2 + visualizerBar.magnitude) * visualizerBar.sin }
// } }
// Behavior on strokeColor {
// CAnim { PathLine {
// } relativeX: 0
// } relativeY: -visualizerBar.magnitude
// } }
// } }
}
Shape { Shape {
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
@@ -215,7 +217,7 @@ Item {
width: parent.width - Appearance.padding.large * 4 width: parent.width - Appearance.padding.large * 4
} }
Row { RowLayout {
id: controls id: controls
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -258,20 +260,48 @@ Item {
required property bool canUse required property bool canUse
required property string icon required property string icon
property int level: 1
property string set_color: "Secondary"
function onClicked(): void { function onClicked(): void {
} }
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : 0)
color: canUse ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
implicitHeight: implicitWidth implicitHeight: implicitWidth
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
radius: Appearance.rounding.full
Behavior on Layout.preferredWidth {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
Behavior on radius {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
Elevation {
anchors.fill: parent
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
radius: parent.radius
z: -1
}
StateLayer { StateLayer {
id: controlState
function onClicked(): void { function onClicked(): void {
control.onClicked(); control.onClicked();
} }
color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
disabled: !control.canUse disabled: !control.canUse
radius: Appearance.rounding.full // radius: Appearance.rounding.full
} }
MaterialIcon { MaterialIcon {
@@ -280,7 +310,8 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05 anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true animate: true
color: control.canUse ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
fill: control.canUse ? 1 : 0
font.pointSize: Appearance.font.size.large font.pointSize: Appearance.font.size.large
text: control.icon text: control.icon
} }
+2 -2
View File
@@ -82,7 +82,7 @@ Row {
colour: DynamicColors.palette.m3tertiary colour: DynamicColors.palette.m3tertiary
icon: "timer" icon: "timer"
text: qsTr("up %1").arg(SystemInfo.uptime) text: qsTr("%1").arg(SystemInfo.uptime)
} }
} }
@@ -113,7 +113,7 @@ Row {
anchors.left: icon.right anchors.left: icon.right
anchors.leftMargin: icon.anchors.leftMargin anchors.leftMargin: icon.anchors.leftMargin
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideRight elide: Text.ElideNone
font.pointSize: 13 font.pointSize: 13
text: `: ${line.text}` text: `: ${line.text}`
width: Config.dashboard.sizes.infoWidth width: Config.dashboard.sizes.infoWidth
+1 -1
View File
@@ -60,7 +60,7 @@ Item {
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: implicitWidth / 16 * 9 implicitHeight: implicitWidth / 16 * 9
implicitWidth: Config.launcher.sizes.wallpaperWidth implicitWidth: Config.launcher.sizes.wallpaperWidth
radius: Appearance.rounding.normal radius: Appearance.rounding.small
y: Appearance.padding.large y: Appearance.padding.large
MaterialIcon { MaterialIcon {
+1 -3
View File
@@ -11,7 +11,7 @@ PathView {
id: root id: root
required property var content required property var content
readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.9 + Appearance.padding.larger * 2
readonly property int numItems: { readonly property int numItems: {
const screen = QsWindow.window?.screen; const screen = QsWindow.window?.screen;
if (!screen) if (!screen)
@@ -20,8 +20,6 @@ PathView {
// Screen width - 4x outer rounding - 2x max side thickness (cause centered) // Screen width - 4x outer rounding - 2x max side thickness (cause centered)
const barMargins = panels.bar.implicitWidth; const barMargins = panels.bar.implicitWidth;
let outerMargins = 0; let outerMargins = 0;
if (panels.popouts.hasCurrent && panels.popouts.currentCenter + panels.popouts.nonAnimHeight / 2 > screen.height - content.implicitHeight)
outerMargins = panels.popouts.nonAnimWidth;
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins) if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
outerMargins = panels.utilities.implicitWidth; outerMargins = panels.utilities.implicitWidth;
const maxWidth = screen.width - Config.barConfig.rounding * 4 - (barMargins + outerMargins) * 2; const maxWidth = screen.width - Config.barConfig.rounding * 4 - (barMargins + outerMargins) * 2;
+15
View File
@@ -164,6 +164,21 @@ WlSessionLockSurface {
implicitWidth: size implicitWidth: size
scale: 0 scale: 0
// MultiEffect {
// anchors.fill: lockBg
// autoPaddingEnabled: false
// blur: 1
// blurEnabled: true
// blurMax: 64
// maskEnabled: true
// maskSource: lockBg
//
// source: ShaderEffectSource {
// sourceItem: background
// sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height)
// }
// }
CustomRect { CustomRect {
id: lockBg id: lockBg
+3 -3
View File
@@ -121,9 +121,9 @@ Item {
active: Players.active?.isPlaying ?? false active: Players.active?.isPlaying ?? false
animate: true animate: true
colour: "Primary"
icon: active ? "pause" : "play_arrow" icon: active ? "pause" : "play_arrow"
level: active ? 2 : 1 level: active ? 2 : 1
set_color: "Primary"
} }
PlayerControl { PlayerControl {
@@ -142,15 +142,15 @@ Item {
property bool active property bool active
property alias animate: controlIcon.animate property alias animate: controlIcon.animate
property string colour: "Secondary"
property alias icon: controlIcon.text property alias icon: controlIcon.text
property int level: 1 property int level: 1
property string set_color: "Secondary"
function onClicked(): void { function onClicked(): void {
} }
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
color: active ? DynamicColors.palette[`m3${colour.toLowerCase()}`] : DynamicColors.palette[`m3${colour.toLowerCase()}Container`] color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
+88
View File
@@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Daemons
import qs.Config
import qs.Helpers
Item {
id: root
readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
readonly property int textWidth: Math.min(metrics.width, 200)
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
Behavior on implicitWidth {
Anim {
}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22
radius: Appearance.rounding.full
}
TextMetrics {
id: metrics
font: mediatext.font
text: mediatext.text
}
RowLayout {
id: layout
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.normal
anchors.top: parent.top
Behavior on implicitWidth {
Anim {
}
}
MaterialIcon {
animate: true
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
font.pointSize: 14
text: Players.active?.isPlaying ? "music_note" : "music_off"
}
MarqueeText {
id: mediatext
Layout.preferredWidth: root.textWidth
animate: true
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: false
pauseMs: 4000
text: root.currentMedia
width: root.textWidth
CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
mediatext.marqueeEnabled = false;
} else {
mediatext.marqueeEnabled = true;
mediatext.anim.start();
}
}
}
}
}
}
+8 -7
View File
@@ -12,28 +12,30 @@ import qs.Components
Item { Item {
id: root id: root
required property PersistentProperties visibilities
clip: true clip: true
implicitHeight: 34 implicitHeight: 34
implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2
Rectangle { CustomRect {
id: backgroundRect id: backgroundRect
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 22 implicitHeight: 22
radius: height / 2 radius: height / 2
Behavior on color {
CAnim {
}
}
anchors { anchors {
left: parent.left left: parent.left
right: parent.right right: parent.right
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
StateLayer {
onClicked: root.visibilities.resources = !root.visibilities.resources
}
}
RowLayout { RowLayout {
id: rowLayout id: rowLayout
@@ -92,4 +94,3 @@ Item {
} }
} }
} }
}
+65
View File
@@ -0,0 +1,65 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+975
View File
@@ -0,0 +1,975 @@
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.Components
import qs.Config
import qs.Helpers
Item {
id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
readonly property real nonAnimHeight: (placeholder.visible ? placeholder.height : content.implicitHeight) + Appearance.padding.normal * 2
readonly property real nonAnimWidth: Math.max(minWidth, content.implicitWidth) + Appearance.padding.normal * 2
required property real padding
required property PersistentProperties visibilities
function displayTemp(temp: real): string {
return `${Math.ceil(temp)}°C`;
}
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
CustomRect {
id: placeholder
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
height: 350
radius: Appearance.rounding.large - 10
visible: false
width: 400
ColumnLayout {
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 2
text: "tune"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.large
text: qsTr("No widgets enabled")
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Enable widgets in dashboard settings")
}
}
}
RowLayout {
id: content
anchors.left: parent.left
anchors.leftMargin: root.padding
anchors.right: parent.right
anchors.rightMargin: root.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
visible: !placeholder.visible
Ref {
service: SystemUsage
}
ColumnLayout {
id: mainColumn
Layout.fillWidth: true
spacing: Appearance.spacing.normal
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: true
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3primary
icon: "memory"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
temperature: SystemUsage.cpuTemp
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
usage: SystemUsage.cpuPerc
visible: Config.dashboard.performance.showCpu
}
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3secondary
icon: "desktop_windows"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
temperature: SystemUsage.gpuTemp
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
usage: SystemUsage.gpuPerc
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
GaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
accentColor: DynamicColors.palette.m3tertiary
icon: "memory_alt"
percentage: SystemUsage.memPerc
subtitle: {
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
title: qsTr("Memory")
visible: Config.dashboard.performance.showMemory
}
StorageGaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showStorage
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
}
}
}
BatteryTank {
Layout.preferredHeight: mainColumn.implicitHeight
Layout.preferredWidth: 120
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
}
}
component BatteryTank: CustomClippingRect {
id: batteryTank
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
property real percentage: UPower.displayDevice.percentage
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
// Background Fill
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: Qt.alpha(batteryTank.accentColor, 0.15)
height: parent.height * batteryTank.animatedPercentage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
// Header Section
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.large
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return "battery_full";
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc >= 0.99)
return "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
text: qsTr("Battery")
}
}
Item {
Layout.fillHeight: true
}
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
CustomText {
Layout.alignment: Qt.AlignRight
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(batteryTank.percentage * 100)}%`
}
CustomText {
Layout.alignment: Qt.AlignRight
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (batteryTank.isCharging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
}
}
}
}
component CardHeader: RowLayout {
property color accentColor: DynamicColors.palette.m3primary
property string icon
property string title
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: parent.accentColor
fill: 1
font.pointSize: Appearance.spacing.large
text: parent.icon
}
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
text: parent.title
}
}
component GaugeCard: CustomRect {
id: gaugeCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property string icon
property real percentage: 0
property string subtitle
property string title
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: gaugeCard.accentColor
icon: gaugeCard.icon
title: gaugeCard.title
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Canvas {
id: gaugeCanvas
anchors.centerIn: parent
height: width
width: Math.min(parent.width, parent.height)
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (gaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = gaugeCard.accentColor;
ctx.stroke();
}
}
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: gaugeCard
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: DynamicColors
}
}
CustomText {
anchors.centerIn: parent
color: gaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(gaugeCard.percentage * 100)}%`
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: gaugeCard.subtitle
}
}
}
component HeroCard: CustomClippingRect {
id: heroCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedTemp: 0
property real animatedUsage: 0
property string icon
property string mainLabel
property string mainValue
readonly property real maxTemp: 100
property string secondaryLabel
property string secondaryValue
readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
property real temperature: 0
property string title
property real usage: 0
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10
Behavior on animatedTemp {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on animatedUsage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
animatedUsage = usage;
animatedTemp = tempProgress;
}
onTempProgressChanged: animatedTemp = tempProgress
onUsageChanged: animatedUsage = usage
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(heroCard.accentColor, 0.15)
width: parent.width * heroCard.animatedUsage
}
ColumnLayout {
anchors.bottomMargin: Appearance.padding.normal
anchors.fill: parent
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
anchors.topMargin: Appearance.padding.normal
spacing: Appearance.spacing.small
CardHeader {
accentColor: heroCard.accentColor
icon: heroCard.icon
title: heroCard.title
}
RowLayout {
Layout.fillHeight: true
Layout.fillWidth: true
spacing: Appearance.spacing.normal
Column {
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
spacing: Appearance.spacing.small
Row {
spacing: Appearance.spacing.small
CustomText {
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: heroCard.secondaryValue
}
CustomText {
anchors.baseline: parent.children[0].baseline
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: heroCard.secondaryLabel
}
}
ProgressBar {
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
fgColor: heroCard.accentColor
height: 6
value: heroCard.tempProgress
width: parent.width * 0.5
}
}
Item {
Layout.fillWidth: true
}
}
}
Column {
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: heroCard.mainLabel
}
CustomText {
anchors.right: parent.right
color: heroCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: heroCard.mainValue
}
}
}
component NetworkCard: CustomRect {
id: networkCard
property color accentColor: DynamicColors.palette.m3primary
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10
Ref {
service: NetworkUsage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
CardHeader {
accentColor: networkCard.accentColor
icon: "swap_vert"
title: qsTr("Network")
}
// Sparkline graph
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Canvas {
id: sparklineCanvas
property int _lastTickCount: -1
property int _tickCount: 0
property var downHistory: NetworkUsage.downloadHistory
property real slideProgress: 0
property real smoothMax: targetMax
property real targetMax: 1024
property var upHistory: NetworkUsage.uploadHistory
function checkAndAnimate(): void {
const currentLength = (downHistory || []).length;
if (currentLength > 0 && _tickCount !== _lastTickCount) {
_lastTickCount = _tickCount;
updateMax();
}
}
function updateMax(): void {
const downHist = downHistory || [];
const upHist = upHistory || [];
const allValues = downHist.concat(upHist);
targetMax = Math.max(...allValues, 1024);
requestPaint();
}
anchors.fill: parent
NumberAnimation on slideProgress {
duration: Config.dashboard.resourceUpdateInterval
from: 0
loops: Animation.Infinite
running: true
to: 1
}
Behavior on smoothMax {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: updateMax()
onDownHistoryChanged: checkAndAnimate()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const w = width;
const h = height;
const downHist = downHistory || [];
const upHist = upHistory || [];
if (downHist.length < 2 && upHist.length < 2)
return;
const maxVal = smoothMax;
const drawLine = (history, color, fillAlpha) => {
if (history.length < 2)
return;
const len = history.length;
const stepX = w / (NetworkUsage.historyLength - 1);
const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX;
ctx.beginPath();
ctx.moveTo(startX, h - (history[0] / maxVal) * h);
for (let i = 1; i < len; i++) {
const x = startX + i * stepX;
const y = h - (history[i] / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
ctx.lineTo(startX + (len - 1) * stepX, h);
ctx.lineTo(startX, h);
ctx.closePath();
ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha);
ctx.fill();
};
drawLine(upHist, DynamicColors.palette.m3secondary.toString(), 0.15);
drawLine(downHist, DynamicColors.palette.m3tertiary.toString(), 0.2);
}
onSlideProgressChanged: requestPaint()
onSmoothMaxChanged: requestPaint()
onUpHistoryChanged: checkAndAnimate()
Connections {
function onPaletteChanged() {
sparklineCanvas.requestPaint();
}
target: DynamicColors
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: true
onTriggered: sparklineCanvas._tickCount++
}
}
// "No data" placeholder
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
opacity: 0.6
text: qsTr("Collecting data...")
visible: NetworkUsage.downloadHistory.length < 2
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
text: "download"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Download")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
text: "upload"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Upload")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: "history"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Total")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
}
}
}
}
component ProgressBar: CustomRect {
id: progressBar
property real animatedValue: 0
property color bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
property color fgColor: DynamicColors.palette.m3primary
property real value: 0
color: bgColor
radius: Appearance.rounding.full
Behavior on animatedValue {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedValue = value
onValueChanged: animatedValue = value
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: progressBar.fgColor
radius: Appearance.rounding.full
width: parent.width * progressBar.animatedValue
}
}
component StorageGaugeCard: CustomRect {
id: storageGaugeCard
property color accentColor: DynamicColors.palette.m3secondary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
property int currentDiskIndex: 0
property int diskCount: 0
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - 10
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
diskCount = SystemUsage.disks.length;
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
onCurrentDiskChanged: {
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
// Update diskCount and animatedPercentage when disks data changes
Connections {
function onDisksChanged() {
if (SystemUsage.disks.length !== storageGaugeCard.diskCount)
storageGaugeCard.diskCount = SystemUsage.disks.length;
// Update animated percentage when disk data refreshes
if (storageGaugeCard.currentDisk)
storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
}
target: SystemUsage
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;
else if (wheel.angleDelta.y < 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: storageGaugeCard.accentColor
icon: "hard_disk"
title: {
const base = qsTr("Storage");
if (!storageGaugeCard.currentDisk)
return base;
return `${base} - ${storageGaugeCard.currentDisk.mount}`;
}
// Scroll hint icon
MaterialIcon {
ToolTip.delay: 500
ToolTip.text: qsTr("Scroll to switch disks")
ToolTip.visible: hintHover.hovered
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
opacity: 0.7
text: "unfold_more"
visible: storageGaugeCard.diskCount > 1
HoverHandler {
id: hintHover
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
Canvas {
id: storageGaugeCanvas
anchors.centerIn: parent
height: width
width: Math.min(parent.width, parent.height)
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (storageGaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = storageGaugeCard.accentColor;
ctx.stroke();
}
}
Connections {
function onAnimatedPercentageChanged() {
storageGaugeCanvas.requestPaint();
}
target: storageGaugeCard
}
Connections {
function onPaletteChanged() {
storageGaugeCanvas.requestPaint();
}
target: DynamicColors
}
}
CustomText {
anchors.centerIn: parent
color: storageGaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (!storageGaugeCard.currentDisk)
return "—";
const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);
const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Config
Item {
id: root
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0
required property PersistentProperties visibilities
implicitHeight: 0
implicitWidth: content.implicitWidth
visible: height > 0
states: State {
name: "visible"
when: root.visibilities.resources
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
},
Transition {
from: "visible"
to: ""
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
}
]
onStateChanged: {
if (state === "visible" && timer.running) {
timer.triggered();
timer.stop();
}
}
Timer {
id: timer
interval: Appearance.anim.durations.extraLarge
running: true
onTriggered: {
content.active = Qt.binding(() => (root.visibilities.resources) || root.visible);
content.visible = true;
}
}
CustomClippingRect {
anchors.fill: parent
Loader {
id: content
active: true
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
visible: false
sourceComponent: Content {
padding: Appearance.padding.normal
visibilities: root.visibilities
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.roundingY, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+178
View File
@@ -0,0 +1,178 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
Item {
id: root
required property Item content
implicitHeight: clayout.contentHeight + Appearance.padding.smaller * 2
implicitWidth: clayout.contentWidth + Appearance.padding.smaller * 2
ListModel {
id: listModel
ListElement {
icon: "settings"
name: "General"
}
ListElement {
icon: "wallpaper"
name: "Wallpaper"
}
ListElement {
icon: "settop_component"
name: "Bar"
}
ListElement {
icon: "lock"
name: "Lockscreen"
}
ListElement {
icon: "build_circle"
name: "Services"
}
ListElement {
icon: "notifications"
name: "Notifications"
}
ListElement {
icon: "view_sidebar"
name: "Sidebar"
}
ListElement {
icon: "handyman"
name: "Utilities"
}
ListElement {
icon: "dashboard"
name: "Dashboard"
}
ListElement {
icon: "colors"
name: "Appearance"
}
ListElement {
icon: "display_settings"
name: "On screen display"
}
ListElement {
icon: "rocket_launch"
name: "Launcher"
}
ListElement {
icon: "colors"
name: "Colors"
}
}
CustomRect {
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
CustomListView {
id: clayout
anchors.centerIn: parent
contentHeight: contentItem.childrenRect.height
contentWidth: contentItem.childrenRect.width
highlightFollowsCurrentItem: false
implicitHeight: contentItem.childrenRect.height
implicitWidth: contentItem.childrenRect.width
model: listModel
spacing: 5
delegate: Category {
}
highlight: CustomRect {
color: DynamicColors.palette.m3primary
implicitHeight: clayout.currentItem?.implicitHeight ?? 0
implicitWidth: clayout.width
radius: 4
y: clayout.currentItem?.y ?? 0
Behavior on y {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
}
}
}
}
}
component Category: CustomRect {
id: categoryItem
required property string icon
required property int index
required property string name
implicitHeight: 42
implicitWidth: 200
radius: 4
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
id: icon
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.preferredWidth: icon.contentWidth
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
font.pointSize: 22
text: categoryItem.icon
verticalAlignment: Text.AlignVCenter
}
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.normal
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
text: categoryItem.name
verticalAlignment: Text.AlignVCenter
}
}
StateLayer {
id: layer
onClicked: {
root.content.currentCategory = categoryItem.name.toLowerCase();
clayout.currentIndex = categoryItem.index;
}
}
}
}
@@ -0,0 +1,97 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Controls
import qs.Config
import qs.Helpers
CustomRect {
id: root
ColumnLayout {
id: clayout
anchors.left: parent.left
anchors.right: parent.right
CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: colorLayout.implicitHeight
color: DynamicColors.tPalette.m3surfaceContainer
ColumnLayout {
id: colorLayout
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: parent.right
Settings {
name: "smth"
}
SettingSwitch {
name: "wallust"
object: Config.general.color
setting: "wallust"
}
CustomSplitButtonRow {
enabled: true
label: qsTr("Scheme mode")
menuItems: [
MenuItem {
property string val: "light"
icon: "light_mode"
text: qsTr("Light")
},
MenuItem {
property string val: "dark"
icon: "dark_mode"
text: qsTr("Dark")
}
]
Component.onCompleted: {
if (Config.general.color.mode === "light")
active = menuItems[0];
else
active = menuItems[1];
}
onSelected: item => {
Config.general.color.mode = item.val;
Config.save();
}
}
}
}
}
component Settings: CustomRect {
id: settingsItem
required property string name
Layout.preferredHeight: 42
Layout.preferredWidth: 200
radius: 4
CustomText {
id: text
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
font.bold: true
font.pointSize: 32
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
}
}
}
@@ -0,0 +1,13 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
CustomRect {
id: root
}
+55
View File
@@ -0,0 +1,55 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config
import qs.Helpers
CustomRect {
id: root
ColumnLayout {
id: clayout
anchors.fill: parent
Settings {
name: "apps"
}
Item {
}
}
component Settings: CustomRect {
id: settingsItem
required property string name
implicitHeight: 42
implicitWidth: 200
radius: 4
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.normal
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
}
}
}
}
+102
View File
@@ -0,0 +1,102 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Categories as Cat
import qs.Config
import qs.Helpers
Item {
id: root
property string currentCategory: "general"
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + 500 + viewWrapper.anchors.margins * 2
required property PersistentProperties visibilities
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Connections {
function onCurrentCategoryChanged() {
stack.pop();
if (currentCategory === "general") {
stack.push(general);
} else if (currentCategory === "wallpaper") {
stack.push(background);
} else if (currentCategory === "appearance") {
stack.push(appearance);
}
}
target: root
}
ClippingRectangle {
id: viewWrapper
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
color: "transparent"
Item {
id: view
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
Categories {
id: layout
anchors.fill: parent
content: root
}
}
CustomClippingRect {
id: categoryContent
anchors.bottom: parent.bottom
anchors.left: view.right
anchors.leftMargin: Appearance.spacing.smaller
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
StackView {
id: stack
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
initialItem: general
}
}
}
Component {
id: general
Cat.General {
}
}
Component {
id: background
Cat.Background {
}
}
Component {
id: appearance
Cat.Appearance {
}
}
}
@@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
RowLayout {
id: root
required property string name
required property var object
required property string setting
Layout.fillWidth: true
Layout.preferredHeight: 42
CustomText {
id: text
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
font.pointSize: 16
text: root.name
}
CustomSwitch {
id: cswitch
Layout.alignment: Qt.AlignRight
checked: root.object[root.setting]
onToggled: {
root.object[root.setting] = checked;
Config.save();
}
}
}
+61
View File
@@ -0,0 +1,61 @@
import Quickshell
import QtQuick
import qs.Components
import qs.Config
import qs.Helpers
Item {
id: root
required property var panels
required property PersistentProperties visibilities
implicitHeight: 0
implicitWidth: content.implicitWidth
visible: height > 0
states: State {
name: "visible"
when: root.visibilities.settings
PropertyChanges {
root.implicitHeight: content.implicitHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
},
Transition {
from: "visible"
to: ""
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
}
]
Loader {
id: content
active: true
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
visible: true
sourceComponent: Content {
visibilities: root.visibilities
}
}
}
+1 -9
View File
@@ -27,25 +27,17 @@ Item {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
implicitHeight: 22
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter
font.pointSize: 14 font.pointSize: 14
text: "package_2" text: "package_2"
} }
TextMetrics {
id: textMetrics
text: root.countUpdates
}
CustomText { CustomText {
color: root.textColor color: root.textColor
font.pointSize: 12 font.pointSize: 12
text: textMetrics.text text: root.countUpdates
} }
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ Loader {
required property var modelData required property var modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Bottom WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "ZShell-Wallpaper" WlrLayershell.namespace: "ZShell-Wallpaper"
color: "transparent" color: "transparent"
screen: modelData screen: modelData
+17 -12
View File
@@ -2,6 +2,7 @@
#include "audiocollector.hpp" #include "audiocollector.hpp"
#include "audioprovider.hpp" #include "audioprovider.hpp"
#include <algorithm>
#include <cava/cavacore.h> #include <cava/cavacore.h>
#include <cstddef> #include <cstddef>
#include <qdebug.h> #include <qdebug.h>
@@ -34,20 +35,24 @@ void CavaProcessor::process() {
// Apply monstercat filter // Apply monstercat filter
QVector<double> values(m_bars); QVector<double> values(m_bars);
// Left to right pass
const double inv = 1.0 / 1.5;
double carry = 0.0;
for(int i = 0; i < m_bars; ++i) { for(int i = 0; i < m_bars; ++i) {
carry = std::max(m_out[i], carry * inv); values[i] = std::clamp(m_out[i], 0.0, 1.0);
values[i] = carry;
} }
// Right to left pass and combine // Left to right pass
carry = 0.0; // const double inv = 1.0 / 1.5;
for (int i = m_bars - 1; i >= 0; --i) { // double carry = 0.0;
carry = std::max(m_out[i], carry * inv); // for (int i = 0; i < m_bars; ++i) {
values[i] = std::max(values[i], carry); // carry = std::max(m_out[i], carry * inv);
} // values[i] = carry;
// }
//
// // Right to left pass and combine
// carry = 0.0;
// for (int i = m_bars - 1; i >= 0; --i) {
// carry = std::max(m_out[i], carry * inv);
// values[i] = std::max(values[i], carry);
// }
// Update values // Update values
if (values != m_values) { if (values != m_values) {
@@ -90,7 +95,7 @@ void CavaProcessor::initCava() {
return; return;
} }
m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000); m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.55, 50, 10000);
m_out = new double[static_cast<size_t>(m_bars)]; m_out = new double[static_cast<size_t>(m_bars)];
} }
+8 -5
View File
@@ -1,4 +1,6 @@
{ {
fftw,
libcava,
rev, rev,
lib, lib,
stdenv, stdenv,
@@ -23,7 +25,8 @@
pkg-config, pkg-config,
pythonEnv, pythonEnv,
zshell-cli, zshell-cli,
}: let }:
let
version = "1.0.0"; version = "1.0.0";
runtimeDeps = [ runtimeDeps = [
@@ -74,11 +77,12 @@
libqalculate libqalculate
pipewire pipewire
aubio aubio
libcava
fftw
]; ];
dontWrapQtApps = true; dontWrapQtApps = true;
cmakeFlags = cmakeFlags = [
[
(lib.cmakeFeature "ENABLE_MODULES" "plugin") (lib.cmakeFeature "ENABLE_MODULES" "plugin")
(lib.cmakeFeature "INSTALL_QMLDIR" qt6.qtbase.qtQmlPrefix) (lib.cmakeFeature "INSTALL_QMLDIR" qt6.qtbase.qtQmlPrefix)
] ]
@@ -104,8 +108,7 @@ in
]; ];
propagatedBuildInputs = runtimeDeps; propagatedBuildInputs = runtimeDeps;
cmakeFlags = cmakeFlags = [
[
(lib.cmakeFeature "ENABLE_MODULES" "shell") (lib.cmakeFeature "ENABLE_MODULES" "shell")
(lib.cmakeFeature "INSTALL_QSCONFDIR" "${placeholder "out"}/share/ZShell") (lib.cmakeFeature "INSTALL_QSCONFDIR" "${placeholder "out"}/share/ZShell")
] ]
+2 -2
View File
@@ -5,8 +5,6 @@
# Stupid idea's from Daivin # Stupid idea's from Daivin
- [ ] An on screen pencil to draw on your screen :). - [ ] An on screen pencil to draw on your screen :).
- [ ] Audio module + cava / audio wave ;) ( Don't make it into minecraft blocks
but aan actual wave) -- Probably not planned
- [ ] Bluetooth device battery view -- Not planned ( Don't have a bluetooth - [ ] Bluetooth device battery view -- Not planned ( Don't have a bluetooth
receiver ) receiver )
@@ -21,3 +19,5 @@
- [x] Battery icon for Laptops. Broken? - [x] Battery icon for Laptops. Broken?
- [x] Quick toggle for BT, WiFi (modules in the tray do this too) - [x] Quick toggle for BT, WiFi (modules in the tray do this too)
- [x] Update module: When there is 1 package it still looks extremely off - [x] Update module: When there is 1 package it still looks extremely off
- [x] Audio module + cava / audio wave ;) ( Don't make it into minecraft blocks
but aan actual wave) -- Probably not planned