diff --git a/Components/CustomComboBox.qml b/Components/CustomComboBox.qml new file mode 100644 index 0000000..2a0186f --- /dev/null +++ b/Components/CustomComboBox.qml @@ -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 + } + } +} diff --git a/Components/CustomSplitButton.qml b/Components/CustomSplitButton.qml new file mode 100644 index 0000000..becabf5 --- /dev/null +++ b/Components/CustomSplitButton.qml @@ -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 + } + } + } + } +} diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml new file mode 100644 index 0000000..763ca8a --- /dev/null +++ b/Components/CustomSplitButtonRow.qml @@ -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; + } + } + } +} diff --git a/Components/CustomText.qml b/Components/CustomText.qml index 74347fa..cff8b2d 100644 --- a/Components/CustomText.qml +++ b/Components/CustomText.qml @@ -14,7 +14,7 @@ Text { color: DynamicColors.palette.m3onSurface font.family: Appearance.font.family.sans - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal renderType: Text.NativeRendering textFormat: Text.PlainText diff --git a/Components/MarqueeText.qml b/Components/MarqueeText.qml index 3f8b69a..925b409 100644 --- a/Components/MarqueeText.qml +++ b/Components/MarqueeText.qml @@ -4,6 +4,8 @@ import qs.Config Item { id: root + property alias anim: marqueeAnim + property bool animate: false property color color: DynamicColors.palette.m3onSurface property int fadeStrengthAnimMs: 180 property real fadeStrengthIdle: 0.0 @@ -27,6 +29,17 @@ Item { 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 implicitHeight: elideText.implicitHeight @@ -39,10 +52,10 @@ Item { } } - onTextChanged: strip.x = 0 + onTextChanged: resetMarquee() onVisibleChanged: if (!visible) - strip.x = 0 - onWidthChanged: strip.x = 0 + resetMarquee() + onWidthChanged: resetMarquee() TextMetrics { id: metrics @@ -55,8 +68,10 @@ Item { id: elideText anchors.verticalCenter: parent.verticalCenter + animate: root.animate + animateProp: "scale,opacity" color: root.color - elide: Text.ElideRight + elide: Text.ElideNone visible: !root.overflowing width: root.width } @@ -84,6 +99,8 @@ Item { CustomText { id: t1 + animate: root.animate + animateProp: "opacity" color: root.color text: elideText.text } @@ -91,6 +108,8 @@ Item { CustomText { id: t2 + animate: root.animate + animateProp: "opacity" color: root.color text: t1.text x: t1.width + root.gap @@ -100,20 +119,9 @@ Item { SequentialAnimation { id: marqueeAnim - loops: Animation.Infinite - running: root.marqueeEnabled && root.overflowing && root.visible + running: false - ScriptAction { - script: { - strip.x = 0; - root.sliding = false; - root.leftFadeEnabled = false; - } - } - - PauseAnimation { - duration: root.pauseMs - } + onFinished: pauseTimer.restart() ScriptAction { script: { @@ -155,6 +163,19 @@ Item { } } } + + Timer { + id: pauseTimer + + interval: root.pauseMs + repeat: false + running: true + + onTriggered: { + if (root.marqueeEnabled) + marqueeAnim.start(); + } + } } Rectangle { diff --git a/Components/Menu.qml b/Components/Menu.qml index d4ffb02..8222359 100644 --- a/Components/Menu.qml +++ b/Components/Menu.qml @@ -13,11 +13,11 @@ Elevation { 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) level: 2 opacity: root.expanded ? 1 : 0 - radius: Appearance.rounding.small / 2 + radius: Appearance.rounding.normal Behavior on implicitHeight { Anim { @@ -41,7 +41,8 @@ Elevation { anchors.left: parent.left anchors.right: parent.right - spacing: 0 + anchors.verticalCenter: parent.verticalCenter + spacing: 5 Repeater { model: root.items @@ -54,19 +55,26 @@ Elevation { required property MenuItem modelData Layout.fillWidth: true - color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0) implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 - StateLayer { - function onClicked(): void { - root.itemSelected(item.modelData); - root.active = item.modelData; - root.expanded = false; - } + 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 - color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface - disabled: !root.expanded + StateLayer { + function onClicked(): void { + root.itemSelected(item.modelData); + root.active = item.modelData; + root.expanded = false; + } + + color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + disabled: !root.expanded + } } RowLayout { diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index 32292ee..4ec7578 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -11,6 +11,10 @@ JsonObject { id: "audio", enabled: true }, + { + id: "media", + enabled: true + }, { id: "resources", enabled: true diff --git a/Config/Config.qml b/Config/Config.qml index 6638e4e..a995ca9 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -121,7 +121,16 @@ Singleton { return { enabled: dashboard.enabled, mediaUpdateInterval: dashboard.mediaUpdateInterval, + resourceUpdateInterval: dashboard.resourceUpdateInterval, 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: { tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, @@ -236,6 +245,7 @@ Singleton { return { weatherLocation: services.weatherLocation, useFahrenheit: services.useFahrenheit, + ddcutilService: services.ddcutilService, useTwelveHourClock: services.useTwelveHourClock, gpuType: services.gpuType, audioIncrement: services.audioIncrement, diff --git a/Config/DashboardConfig.qml b/Config/DashboardConfig.qml index 6b55776..0b6a610 100644 --- a/Config/DashboardConfig.qml +++ b/Config/DashboardConfig.qml @@ -4,9 +4,20 @@ JsonObject { property int dragThreshold: 50 property bool enabled: true property int mediaUpdateInterval: 500 + property Performance performance: Performance { + } + property int resourceUpdateInterval: 1000 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 { readonly property int dateTimeWidth: 110 readonly property int infoIconSize: 25 diff --git a/Config/Services.qml b/Config/Services.qml index 2daf398..e091fa2 100644 --- a/Config/Services.qml +++ b/Config/Services.qml @@ -4,6 +4,7 @@ import QtQuick JsonObject { property real audioIncrement: 0.1 property real brightnessIncrement: 0.1 + property bool ddcutilService: false property string defaultPlayer: "Spotify" property string gpuType: "" property real maxVolume: 1.0 diff --git a/Daemons/Audio.qml b/Daemons/Audio.qml index 624b849..fddb283 100644 --- a/Daemons/Audio.qml +++ b/Daemons/Audio.qml @@ -10,6 +10,7 @@ import qs.Config Singleton { id: root + readonly property alias beatTracker: beatTracker readonly property alias cava: cava readonly property bool muted: !!sink?.audio?.muted readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { @@ -138,6 +139,11 @@ Singleton { bars: Config.services.visualizerBars } + BeatTracker { + id: beatTracker + + } + PwObjectTracker { objects: [...root.sinks, ...root.sources, ...root.streams] } diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 7fb4578..bcd1488 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -1,6 +1,8 @@ import Quickshell import QtQuick import QtQuick.Shapes +import qs.Components +import qs.Config import qs.Modules as Modules import qs.Modules.Notifications as Notifications 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.Osd as Osd 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 { id: root @@ -20,9 +23,20 @@ Shape { anchors.fill: parent // anchors.margins: 8 - anchors.topMargin: bar.implicitHeight + anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight preferredRendererType: Shape.CurveRenderer + Behavior on anchors.topMargin { + Anim { + } + } + + Resources.Background { + startX: 0 - rounding + startY: 0 + wrapper: root.panels.resources + } + Osd.Background { startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding @@ -71,11 +85,11 @@ Shape { wrapper: root.panels.sidebar } - // Settings.Background { - // id: settings - // - // startX: (root.width - wrapper.width) / 2 - rounding - // startY: 0 - // wrapper: root.panels.settings - // } + Settings.Background { + id: settings + + startX: (root.width - wrapper.width) / 2 - rounding + startY: 0 + wrapper: root.panels.settings + } } diff --git a/Drawers/Bar.qml b/Drawers/Bar.qml index c5bbdde..8439a91 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Bar.qml @@ -28,7 +28,7 @@ Variants { property bool trayMenuVisible: false 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" color: "transparent" contentItem.focus: true @@ -52,6 +52,7 @@ Variants { visibilities.dashboard = false; visibilities.osd = false; visibilities.settings = false; + visibilities.resources = false; } PanelWindow { @@ -97,7 +98,7 @@ Variants { HyprlandFocusGrab { 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] onCleared: { @@ -106,6 +107,7 @@ Variants { visibilities.dashboard = false; visibilities.osd = false; visibilities.settings = false; + visibilities.resources = false; panels.popouts.hasCurrent = false; } } @@ -118,6 +120,7 @@ Variants { property bool launcher property bool notif: NotifServer.popups.length > 0 property bool osd + property bool resources property bool settings property bool sidebar @@ -127,7 +130,7 @@ Variants { Binding { property: "bar" 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 } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index df0fba5..7dea587 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -9,7 +9,8 @@ import qs.Modules.Dashboard as Dashboard import qs.Modules.Osd as Osd import qs.Components.Toast as Toasts 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 Item { @@ -21,8 +22,9 @@ Item { readonly property alias notifications: notifications readonly property alias osd: osd readonly property alias popouts: popouts + readonly property alias resources: resources required property ShellScreen screen - // readonly property alias settings: settings + readonly property alias settings: settings readonly property alias sidebar: sidebar readonly property alias toasts: toasts readonly property alias utilities: utilities @@ -30,14 +32,21 @@ Item { anchors.fill: parent // anchors.margins: 8 - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : - bar.implicitHeight + anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight Behavior on anchors.topMargin { Anim { } } + Resources.Wrapper { + id: resources + + anchors.left: parent.left + anchors.top: parent.top + visibilities: root.visibilities + } + Osd.Wrapper { id: osd @@ -118,12 +127,12 @@ Item { visibilities: root.visibilities } - // Settings.Wrapper { - // id: settings - // - // anchors.horizontalCenter: parent.horizontalCenter - // anchors.top: parent.top - // panels: root - // visibilities: root.visibilities - // } + Settings.Wrapper { + id: settings + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + panels: root + visibilities: root.visibilities + } } diff --git a/Helpers/Brightness.qml b/Helpers/Brightness.qml index b1e1e15..5e2e7fb 100644 --- a/Helpers/Brightness.qml +++ b/Helpers/Brightness.qml @@ -12,6 +12,7 @@ Singleton { property bool appleDisplayPresent: false property list ddcMonitors: [] + property list ddcServiceMon: [] readonly property list monitors: variants.instances function decreaseBrightness(): void { @@ -55,6 +56,8 @@ Singleton { onMonitorsChanged: { ddcMonitors = []; + ddcServiceMon = []; + ddcServiceProc.running = true; ddcProc.running = true; } @@ -68,7 +71,7 @@ Singleton { } Process { - command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed + command: ["sh", "-c", "asdbctl get"] running: true 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 { description: "Increase brightness" name: "brightnessUp" @@ -161,10 +184,15 @@ Singleton { property real brightness 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 { stdout: StdioCollector { 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()); monitor.brightness = val / 101; } else { @@ -176,6 +204,7 @@ Singleton { } 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 isDdcService: Config.services.ddcutilService required property ShellScreen modelData property real queuedBrightness: NaN readonly property Timer timer: Timer { @@ -190,7 +219,9 @@ Singleton { } function initBrightness(): void { - if (isAppleDisplay) + if (isDdcService) + initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"]; + else if (isAppleDisplay) initProc.command = ["asdbctl", "get"]; else if (isDdc) initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; @@ -206,25 +237,28 @@ Singleton { if (Math.round(brightness * 100) === rounded) return; - if (isDdc && timer.running) { + if ((isDdc || isDdcService) && timer.running) { queuedBrightness = value; return; } brightness = value; - if (isAppleDisplay) + if (isDdcService) + Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]); + else if (isAppleDisplay) Quickshell.execDetached(["asdbctl", "set", rounded]); else if (isDdc) Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]); else Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); - if (isDdc) + if (isDdc || isDdcService) timer.restart(); } Component.onCompleted: initBrightness() onBusNumChanged: initBrightness() + onDisplayNumChanged: initBrightness() } } diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml index f3fab59..8aad68b 100644 --- a/Helpers/IdleInhibitor.qml +++ b/Helpers/IdleInhibitor.qml @@ -28,6 +28,7 @@ Singleton { enabled: props.enabled window: PanelWindow { + WlrLayershell.namespace: "ZShell-IdleInhibitor" color: "transparent" implicitHeight: 0 implicitWidth: 0 diff --git a/Helpers/NetworkUsage.qml b/Helpers/NetworkUsage.qml new file mode 100644 index 0000000..a8178ad --- /dev/null +++ b/Helpers/NetworkUsage.qml @@ -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; + } + } +} diff --git a/Helpers/SystemInfo.qml b/Helpers/SystemInfo.qml index 750d3af..c4e3565 100644 --- a/Helpers/SystemInfo.qml +++ b/Helpers/SystemInfo.qml @@ -69,15 +69,12 @@ Singleton { onLoaded: { const up = parseInt(text().split(" ")[0] ?? 0); - const days = Math.floor(up / 86400); - const hours = Math.floor((up % 86400) / 3600); + const hours = Math.floor(up / 3600); const minutes = Math.floor((up % 3600) / 60); let str = ""; - if (days > 0) - str += `${days} day${days === 1 ? "" : "s"}`; if (hours > 0) - str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`; + str += `${hours} hour${hours === 1 ? "" : "s"}`; if (minutes > 0 || !str) str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`; root.uptime = str; diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml index d7a568b..d9234e9 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -9,10 +9,15 @@ Singleton { id: root property string autoGpuType: "NONE" + property string cpuName: "" property real cpuPerc property real cpuTemp + + // Individual disks: Array of { mount, used, total, free, perc } + property var disks: [] property real gpuMemTotal: 0 property real gpuMemUsed + property string gpuName: "" property real gpuPerc property real gpuTemp readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType @@ -22,9 +27,23 @@ Singleton { property real memTotal property real memUsed property int refCount - property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 - property real storageTotal - property real storageUsed + readonly property real storagePerc: { + let totalUsed = 0; + 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 { const mib = 1024; @@ -53,7 +72,7 @@ Singleton { } Timer { - interval: 3000 + interval: Config.dashboard.resourceUpdateInterval repeat: true running: root.refCount > 0 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 { id: stat @@ -104,42 +135,116 @@ Singleton { Process { 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 { 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() === "") 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 (parts.length >= 3) { - const device = parts[0]; - const used = parseInt(parts[1], 10) || 0; - const avail = parseInt(parts[2], 10) || 0; + if (!nameMatch || !typeMatch) + continue; - // Only keep the entry with the largest total space for each device - if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { - deviceMap.set(device, { - used: used, - avail: avail - }); + const name = nameMatch[1]; + const type = typeMatch[1]; + const size = parseInt(sizeMatch?.[1] || "0", 10); + const fsused = parseInt(fsusedMatch?.[1] || "0", 10); + 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 totalAvail = 0; + let totalSize = 0; - for (const [device, stats] of deviceMap) { - totalUsed += stats.used; - totalAvail += stats.avail; + for (const diskName of Object.keys(diskMap).sort()) { + const disk = diskMap[diskName]; + // 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.storageTotal = totalUsed + totalAvail; + root.disks = diskList; + } + } + } + + 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]); + } + } } } } diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index 4e3fa41..80efd85 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -24,101 +24,77 @@ Item { } } - Rectangle { + CustomRect { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter color: DynamicColors.tPalette.m3surfaceContainer height: 22 radius: height / 2 + } - Behavior on color { - CAnim { - } + RowLayout { + id: layout + + anchors.fill: parent + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small * 2 + anchors.verticalCenter: parent.verticalCenter + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + animate: true + color: Audio.muted ? DynamicColors.palette.m3error : root.textColor + font.pointSize: 14 + text: Audio.muted ? "volume_off" : "volume_up" } - Rectangle { - anchors.centerIn: parent - border.color: "#30ffffff" - border.width: 0 - color: "transparent" - height: parent.height - radius: width / 2 - width: parent.width - } + CustomRect { + Layout.fillWidth: true + color: "#50ffffff" + implicitHeight: 4 + radius: 20 - RowLayout { - id: layout + CustomRect { + id: sinkVolumeBar - anchors.left: parent.left - anchors.leftMargin: Appearance.padding.small - anchors.right: parent.right - anchors.rightMargin: Appearance.padding.small * 2 - anchors.verticalCenter: parent.verticalCenter + color: Audio.muted ? DynamicColors.palette.m3error : root.barColor + implicitWidth: parent.width * (Audio.volume ?? 0) + radius: parent.radius - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 - text: Audio.muted ? "volume_off" : "volume_up" - } - - Rectangle { - Layout.fillWidth: true - color: "#50ffffff" - implicitHeight: 4 - radius: 20 - - Rectangle { - id: sinkVolumeBar - - color: Audio.muted ? DynamicColors.palette.m3error : root.barColor - implicitWidth: parent.width * (Audio.volume ?? 0) - radius: parent.radius - - Behavior on color { - CAnim { - } - } - - anchors { - bottom: parent.bottom - left: parent.left - top: parent.top - } + anchors { + bottom: parent.bottom + left: parent.left + top: parent.top } } + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 - text: Audio.sourceMuted ? "mic_off" : "mic" - } + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + animate: true + color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor + font.pointSize: 14 + text: Audio.sourceMuted ? "mic_off" : "mic" + } - Rectangle { - Layout.fillWidth: true - color: "#50ffffff" - implicitHeight: 4 - radius: 20 + CustomRect { + Layout.fillWidth: true + color: "#50ffffff" + implicitHeight: 4 + radius: 20 - Rectangle { - id: sourceVolumeBar + CustomRect { + id: sourceVolumeBar - color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.barColor - implicitWidth: parent.width * (Audio.sourceVolume ?? 0) - radius: parent.radius + color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.barColor + implicitWidth: parent.width * (Audio.sourceVolume ?? 0) + radius: parent.radius - Behavior on color { - CAnim { - } - } - - anchors { - bottom: parent.bottom - left: parent.left - top: parent.top - } + anchors { + bottom: parent.bottom + left: parent.left + top: parent.top } } } diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 24d9071..df21fd4 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -23,13 +23,13 @@ RowLayout { function checkPopout(x: real): void { const ch = childAt(x, 2) as WrappedLoader; - if (!ch) { - if (!popouts.currentName.includes("traymenu")) + if (!ch || ch?.id === "spacer") { + if (!popouts.currentName.startsWith("traymenu")) popouts.hasCurrent = false; return; } - if (visibilities.sidebar || visibilities.dashboard) + if (visibilities.sidebar || visibilities.dashboard || visibilities.resources) return; const id = ch.id; @@ -41,26 +41,6 @@ RowLayout { popouts.currentName = "audio"; popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); 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) { popouts.currentName = "network"; popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); @@ -93,6 +73,7 @@ RowLayout { Repeater { id: repeater + // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) model: Config.barConfig.entries DelegateChooser { @@ -142,6 +123,7 @@ RowLayout { delegate: WrappedLoader { sourceComponent: Resources { + visibilities: root.visibilities } } } @@ -206,14 +188,15 @@ RowLayout { } } } - // DelegateChoice { - // roleValue: "dash" - // delegate: WrappedLoader { - // sourceComponent: DashWidget { - // visibilities: root.visibilities - // } - // } - // } + + DelegateChoice { + roleValue: "media" + + delegate: WrappedLoader { + sourceComponent: MediaWidget { + } + } + } } } diff --git a/Modules/Content.qml b/Modules/Content.qml index 1306768..80e16d0 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -33,14 +33,6 @@ Item { } } - Popout { - name: "resources" - - sourceComponent: ResourcePopout { - wrapper: root.wrapper - } - } - Repeater { model: ScriptModel { values: [...SystemTray.items.values] diff --git a/Modules/Dashboard/Background.qml b/Modules/Dashboard/Background.qml index 5341a92..f192a11 100644 --- a/Modules/Dashboard/Background.qml +++ b/Modules/Dashboard/Background.qml @@ -49,12 +49,12 @@ ShapePath { radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) relativeX: root.rounding - relativeY: -root.roundingY + relativeY: root.roundingY } PathLine { relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) + relativeY: -(root.wrapper.height) } PathArc { diff --git a/Modules/Dashboard/Dash/Media.qml b/Modules/Dashboard/Dash/Media.qml index 1eb32f2..4b47b81 100644 --- a/Modules/Dashboard/Dash/Media.qml +++ b/Modules/Dashboard/Dash/Media.qml @@ -4,6 +4,7 @@ import Quickshell import QtQuick import QtQuick.Layouts import QtQuick.Shapes +import ZShell.Services import qs.Daemons import qs.Components import qs.Config @@ -37,54 +38,55 @@ Item { onTriggered: Players.active?.positionChanged() } - // Shape { - // id: visualizer - // - // readonly property real centerX: width / 2 - // readonly property real centerY: height / 2 - // property color colour: DynamicColors.palette.m3primary - // readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small - // readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small - // - // anchors.fill: cover - // anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize - // asynchronous: true - // data: visualizerBars.instances - // preferredRendererType: Shape.CurveRenderer - // } - // - // Variants { - // id: visualizerBars - // - // model: Array.from({ - // length: Config.services.visualizerBars - // }, (_, i) => i) - // - // ShapePath { - // id: visualizerBar - // - // readonly property real angle: modelData * 2 * Math.PI / Config.services.visualizerBars - // readonly property real cos: Math.cos(angle) - // readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize - // required property int modelData - // readonly property real sin: Math.sin(angle) - // readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - // - // capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - // startX: visualizer.centerX + (visualizer.innerX + strokeWidth / 2) * cos - // strokeColor: DynamicColors.palette.m3primary - // strokeWidth: 360 / Config.services.visualizerBars - Appearance.spacing.small / 4 - // - // startY: PathLine { - // x: visualizer.centerX + (visualizer.innerX + visualizerBar.strokeWidth / 2 + visualizerBar.magnitude) * visualizerBar.cos - // y: visualizer.centerY + (visualizer.innerY + visualizerBar.strokeWidth / 2 + visualizerBar.magnitude) * visualizerBar.sin - // } - // Behavior on strokeColor { - // CAnim { - // } - // } - // } - // } + ServiceRef { + service: Audio.cava + } + + Shape { + id: visualizer + + readonly property real barW: Math.max(0, (width - gap * (bars - 1)) / bars) + readonly property int bars: Config.services.visualizerBars + property color color: DynamicColors.palette.m3primary + readonly property real gap: Appearance.spacing.small + + anchors.fill: layout + asynchronous: true + data: visualizerBars.instances + preferredRendererType: Shape.CurveRenderer + } + + Variants { + id: visualizerBars + + model: Array.from({ + length: Config.services.visualizerBars + }, (_, i) => i) + + ShapePath { + id: visualizerBar + + readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize + required property int modelData + readonly property real value: Math.max(1e-3, Audio.cava.values[modelData]) + + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + startX: (visualizer.barW / 2) + modelData * (visualizer.barW + visualizer.gap) + startY: layout.y + layout.height + strokeColor: visualizer.color + strokeWidth: visualizer.barW + + Behavior on strokeColor { + CAnim { + } + } + + PathLine { + relativeX: 0 + relativeY: -visualizerBar.magnitude + } + } + } Shape { preferredRendererType: Shape.CurveRenderer @@ -215,7 +217,7 @@ Item { width: parent.width - Appearance.padding.large * 4 } - Row { + RowLayout { id: controls anchors.horizontalCenter: parent.horizontalCenter @@ -258,20 +260,48 @@ Item { required property bool canUse required property string icon + property int level: 1 + property string set_color: "Secondary" 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 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 { + id: controlState + function onClicked(): void { control.onClicked(); } + color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`] disabled: !control.canUse - radius: Appearance.rounding.full + // radius: Appearance.rounding.full } MaterialIcon { @@ -280,7 +310,8 @@ Item { anchors.centerIn: parent anchors.verticalCenterOffset: font.pointSize * 0.05 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 text: control.icon } diff --git a/Modules/Dashboard/Dash/User.qml b/Modules/Dashboard/Dash/User.qml index 3980721..45e8ad8 100644 --- a/Modules/Dashboard/Dash/User.qml +++ b/Modules/Dashboard/Dash/User.qml @@ -82,7 +82,7 @@ Row { colour: DynamicColors.palette.m3tertiary 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.leftMargin: icon.anchors.leftMargin anchors.verticalCenter: icon.verticalCenter - elide: Text.ElideRight + elide: Text.ElideNone font.pointSize: 13 text: `: ${line.text}` width: Config.dashboard.sizes.infoWidth diff --git a/Modules/Launcher/Items/WallpaperItem.qml b/Modules/Launcher/Items/WallpaperItem.qml index 6620cb9..65d9418 100644 --- a/Modules/Launcher/Items/WallpaperItem.qml +++ b/Modules/Launcher/Items/WallpaperItem.qml @@ -60,7 +60,7 @@ Item { color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: implicitWidth / 16 * 9 implicitWidth: Config.launcher.sizes.wallpaperWidth - radius: Appearance.rounding.normal + radius: Appearance.rounding.small y: Appearance.padding.large MaterialIcon { diff --git a/Modules/Launcher/WallpaperList.qml b/Modules/Launcher/WallpaperList.qml index b6a6a7b..94d6ab1 100644 --- a/Modules/Launcher/WallpaperList.qml +++ b/Modules/Launcher/WallpaperList.qml @@ -11,7 +11,7 @@ PathView { id: root 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: { const screen = QsWindow.window?.screen; if (!screen) @@ -20,8 +20,6 @@ PathView { // Screen width - 4x outer rounding - 2x max side thickness (cause centered) const barMargins = panels.bar.implicitWidth; 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) outerMargins = panels.utilities.implicitWidth; const maxWidth = screen.width - Config.barConfig.rounding * 4 - (barMargins + outerMargins) * 2; diff --git a/Modules/Lock/LockSurface.qml b/Modules/Lock/LockSurface.qml index 2739713..73ee33b 100644 --- a/Modules/Lock/LockSurface.qml +++ b/Modules/Lock/LockSurface.qml @@ -164,6 +164,21 @@ WlSessionLockSurface { implicitWidth: size 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 { id: lockBg diff --git a/Modules/Lock/Media.qml b/Modules/Lock/Media.qml index 1ec301c..a241e4b 100644 --- a/Modules/Lock/Media.qml +++ b/Modules/Lock/Media.qml @@ -121,9 +121,9 @@ Item { active: Players.active?.isPlaying ?? false animate: true - colour: "Primary" icon: active ? "pause" : "play_arrow" level: active ? 2 : 1 + set_color: "Primary" } PlayerControl { @@ -142,15 +142,15 @@ Item { property bool active property alias animate: controlIcon.animate - property string colour: "Secondary" property alias icon: controlIcon.text property int level: 1 + property string set_color: "Secondary" function onClicked(): void { } 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 implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal diff --git a/Modules/MediaWidget.qml b/Modules/MediaWidget.qml new file mode 100644 index 0000000..8b47a07 --- /dev/null +++ b/Modules/MediaWidget.qml @@ -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(); + } + } + } + } + } +} diff --git a/Modules/ResourcePopout.qml b/Modules/ResourcePopout-old.qml similarity index 100% rename from Modules/ResourcePopout.qml rename to Modules/ResourcePopout-old.qml diff --git a/Modules/Resources.qml b/Modules/Resources.qml index 829251f..8707508 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -12,84 +12,85 @@ import qs.Components Item { id: root + required property PersistentProperties visibilities + clip: true implicitHeight: 34 implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 - Rectangle { + CustomRect { id: backgroundRect color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: 22 radius: height / 2 - Behavior on color { - CAnim { - } - } - anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter } - RowLayout { - id: rowLayout + StateLayer { + onClicked: root.visibilities.resources = !root.visibilities.resources + } + } - anchors.centerIn: parent - spacing: 6 + RowLayout { + id: rowLayout - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - text: "memory_alt" - } + anchors.centerIn: parent + spacing: 6 - Resource { - Layout.alignment: Qt.AlignVCenter - mainColor: DynamicColors.palette.m3primary - percentage: ResourceUsage.memoryUsedPercentage - warningThreshold: 95 - } + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3onSurface + font.pointSize: 14 + text: "memory_alt" + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - text: "memory" - } + Resource { + Layout.alignment: Qt.AlignVCenter + mainColor: DynamicColors.palette.m3primary + percentage: ResourceUsage.memoryUsedPercentage + warningThreshold: 95 + } - Resource { - mainColor: DynamicColors.palette.m3secondary - percentage: ResourceUsage.cpuUsage - warningThreshold: 80 - } + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3onSurface + font.pointSize: 14 + text: "memory" + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - text: "gamepad" - } + Resource { + mainColor: DynamicColors.palette.m3secondary + percentage: ResourceUsage.cpuUsage + warningThreshold: 80 + } - Resource { - mainColor: DynamicColors.palette.m3tertiary - percentage: ResourceUsage.gpuUsage - } + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3onSurface + font.pointSize: 14 + text: "gamepad" + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - text: "developer_board" - } + Resource { + mainColor: DynamicColors.palette.m3tertiary + percentage: ResourceUsage.gpuUsage + } - Resource { - mainColor: DynamicColors.palette.m3primary - percentage: ResourceUsage.gpuMemUsage - } + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3onSurface + font.pointSize: 14 + text: "developer_board" + } + + Resource { + mainColor: DynamicColors.palette.m3primary + percentage: ResourceUsage.gpuMemUsage } } } diff --git a/Modules/Resources/Background.qml b/Modules/Resources/Background.qml new file mode 100644 index 0000000..48a7eb8 --- /dev/null +++ b/Modules/Resources/Background.qml @@ -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 + } +} diff --git a/Modules/Resources/Content.qml b/Modules/Resources/Content.qml new file mode 100644 index 0000000..432edc6 --- /dev/null +++ b/Modules/Resources/Content.qml @@ -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}`; + } + } + } + } +} diff --git a/Modules/Resources/Wrapper.qml b/Modules/Resources/Wrapper.qml new file mode 100644 index 0000000..a1f750e --- /dev/null +++ b/Modules/Resources/Wrapper.qml @@ -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 + } + } + } +} diff --git a/Modules/Settings/Background.qml b/Modules/Settings/Background.qml new file mode 100644 index 0000000..cc242f7 --- /dev/null +++ b/Modules/Settings/Background.qml @@ -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 + } +} diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml new file mode 100644 index 0000000..08bcacc --- /dev/null +++ b/Modules/Settings/Categories.qml @@ -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; + } + } + } +} diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml new file mode 100644 index 0000000..78c18a0 --- /dev/null +++ b/Modules/Settings/Categories/Appearance.qml @@ -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 + } + } +} diff --git a/Modules/Settings/Categories/Background.qml b/Modules/Settings/Categories/Background.qml new file mode 100644 index 0000000..2d24d68 --- /dev/null +++ b/Modules/Settings/Categories/Background.qml @@ -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 + +} diff --git a/Modules/Settings/Categories/General.qml b/Modules/Settings/Categories/General.qml new file mode 100644 index 0000000..3e8335c --- /dev/null +++ b/Modules/Settings/Categories/General.qml @@ -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 + } + } + } +} diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml new file mode 100644 index 0000000..1037a21 --- /dev/null +++ b/Modules/Settings/Content.qml @@ -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 { + } + } +} diff --git a/Modules/Settings/Controls/SettingSwitch.qml b/Modules/Settings/Controls/SettingSwitch.qml new file mode 100644 index 0000000..524e740 --- /dev/null +++ b/Modules/Settings/Controls/SettingSwitch.qml @@ -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(); + } + } +} diff --git a/Modules/Settings/Wrapper.qml b/Modules/Settings/Wrapper.qml new file mode 100644 index 0000000..dec6264 --- /dev/null +++ b/Modules/Settings/Wrapper.qml @@ -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 + } + } +} diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index 7bb4b51..c0141a8 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -27,25 +27,17 @@ Item { id: contentRow anchors.centerIn: parent - implicitHeight: 22 spacing: Appearance.spacing.small MaterialIcon { - Layout.alignment: Qt.AlignVCenter font.pointSize: 14 text: "package_2" } - TextMetrics { - id: textMetrics - - text: root.countUpdates - } - CustomText { color: root.textColor font.pointSize: 12 - text: textMetrics.text + text: root.countUpdates } } } diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index 705e6d0..fa77580 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -16,7 +16,7 @@ Loader { required property var modelData WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Bottom + WlrLayershell.layer: WlrLayer.Background WlrLayershell.namespace: "ZShell-Wallpaper" color: "transparent" screen: modelData diff --git a/Plugins/ZShell/Services/cavaprovider.cpp b/Plugins/ZShell/Services/cavaprovider.cpp index fdff3b3..8715f39 100644 --- a/Plugins/ZShell/Services/cavaprovider.cpp +++ b/Plugins/ZShell/Services/cavaprovider.cpp @@ -2,6 +2,7 @@ #include "audiocollector.hpp" #include "audioprovider.hpp" +#include #include #include #include @@ -34,20 +35,24 @@ void CavaProcessor::process() { // Apply monstercat filter QVector 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) { - carry = std::max(m_out[i], carry * inv); - values[i] = carry; + for(int i = 0; i < m_bars; ++i) { + values[i] = std::clamp(m_out[i], 0.0, 1.0); } - // 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); - } + // Left to right pass + // const double inv = 1.0 / 1.5; + // double carry = 0.0; + // for (int i = 0; i < m_bars; ++i) { + // 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 if (values != m_values) { @@ -90,7 +95,7 @@ void CavaProcessor::initCava() { 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(m_bars)]; } diff --git a/nix/default.nix b/nix/default.nix index 15f4d8d..83edc51 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,4 +1,6 @@ { + fftw, + libcava, rev, lib, stdenv, @@ -23,7 +25,8 @@ pkg-config, pythonEnv, zshell-cli, -}: let +}: +let version = "1.0.0"; runtimeDeps = [ @@ -74,66 +77,66 @@ libqalculate pipewire aubio + libcava + fftw ]; dontWrapQtApps = true; - cmakeFlags = - [ - (lib.cmakeFeature "ENABLE_MODULES" "plugin") - (lib.cmakeFeature "INSTALL_QMLDIR" qt6.qtbase.qtQmlPrefix) - ] - ++ cmakeVersionFlags; + cmakeFlags = [ + (lib.cmakeFeature "ENABLE_MODULES" "plugin") + (lib.cmakeFeature "INSTALL_QMLDIR" qt6.qtbase.qtQmlPrefix) + ] + ++ cmakeVersionFlags; }; in - stdenv.mkDerivation { - inherit version cmakeBuildType; - pname = "zshell"; - src = ./..; +stdenv.mkDerivation { + inherit version cmakeBuildType; + pname = "zshell"; + src = ./..; - nativeBuildInputs = [ - cmake - ninja - makeWrapper - qt6.wrapQtAppsHook - ]; - buildInputs = [ - quickshell - plugin - qt6.qtbase - qt6.qtwayland - ]; - propagatedBuildInputs = runtimeDeps; + nativeBuildInputs = [ + cmake + ninja + makeWrapper + qt6.wrapQtAppsHook + ]; + buildInputs = [ + quickshell + plugin + qt6.qtbase + qt6.qtwayland + ]; + propagatedBuildInputs = runtimeDeps; - cmakeFlags = - [ - (lib.cmakeFeature "ENABLE_MODULES" "shell") - (lib.cmakeFeature "INSTALL_QSCONFDIR" "${placeholder "out"}/share/ZShell") - ] - ++ cmakeVersionFlags; + cmakeFlags = [ + (lib.cmakeFeature "ENABLE_MODULES" "shell") + (lib.cmakeFeature "INSTALL_QSCONFDIR" "${placeholder "out"}/share/ZShell") + ] + ++ cmakeVersionFlags; - prePatch = '' - substituteInPlace shell.qml \ - --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' - ''; + prePatch = '' + substituteInPlace shell.qml \ + --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' + ''; - postInstall = '' - makeWrapper ${quickshell}/bin/qs $out/bin/zshell \ - --prefix PATH : "${lib.makeBinPath runtimeDeps}" \ - --set FONTCONFIG_FILE "${fontconfig}" \ - --add-flags "-p $out/share/ZShell" + postInstall = '' + makeWrapper ${quickshell}/bin/qs $out/bin/zshell \ + --prefix PATH : "${lib.makeBinPath runtimeDeps}" \ + --set FONTCONFIG_FILE "${fontconfig}" \ + --add-flags "-p $out/share/ZShell" - echo "$out" - mkdir -p $out/lib - ''; + echo "$out" + mkdir -p $out/lib + ''; - passthru = { - inherit plugin; - }; + passthru = { + inherit plugin; + }; - meta = { - description = "A very segsy desktop shell"; - homepage = "https://github.com/Zacharias-Brohn/z-bar-qt"; - license = lib.licenses.gpl3Only; - mainProgram = "zshell"; - }; - } + meta = { + description = "A very segsy desktop shell"; + homepage = "https://github.com/Zacharias-Brohn/z-bar-qt"; + license = lib.licenses.gpl3Only; + mainProgram = "zshell"; + }; +} diff --git a/plans/ideas.md b/plans/ideas.md index 085bc59..941e513 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -5,8 +5,6 @@ # Stupid idea's from Daivin - [ ] 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 receiver ) @@ -21,3 +19,5 @@ - [x] Battery icon for Laptops. Broken? - [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] Audio module + cava / audio wave ;) ( Don't make it into minecraft blocks + but aan actual wave) -- Probably not planned