diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index 93defb5..60c648c 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -79,6 +79,7 @@ JsonObject { property int normal: 17 * scale property real scale: 1 property int small: 12 * scale + property int smallest: 8 * scale } component Spacing: JsonObject { property int large: 20 * scale diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 8c1fcef..5def5b9 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -48,7 +48,8 @@ Shape { Modules.Background { invertBottomRounding: wrapper.x <= 0 - startX: wrapper.x - 8 + rounding: root.panels.popouts.currentName.startsWith("updates") ? Appearance.rounding.normal : Appearance.rounding.smallest + startX: wrapper.x - rounding startY: wrapper.y wrapper: root.panels.popouts } diff --git a/Helpers/Updates.qml b/Helpers/Updates.qml new file mode 100644 index 0000000..fc4b517 --- /dev/null +++ b/Helpers/Updates.qml @@ -0,0 +1,74 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property int availableUpdates: 0 + property double now: Date.now() + property var updates: ({}) + + function formatUpdateTime(timestamp) { + const diffMs = root.now - timestamp; + const minuteMs = 60 * 1000; + const hourMs = 60 * minuteMs; + const dayMs = 24 * hourMs; + + if (diffMs < minuteMs) + return "just now"; + + if (diffMs < hourMs) + return Math.floor(diffMs / minuteMs) + " min ago"; + + if (diffMs < 48 * hourMs) + return Math.floor(diffMs / hourMs) + " hr ago"; + + return Qt.formatDateTime(new Date(timestamp), "dd hh:mm"); + } + + Timer { + interval: 1 + repeat: true + running: true + + onTriggered: { + updatesProc.running = true; + interval = 5000; + } + } + + Timer { + interval: 60000 + repeat: true + running: true + + onTriggered: root.now = Date.now() + } + + Process { + id: updatesProc + + command: ["checkupdates"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const output = this.text; + const lines = output.trim().split("\n").filter(line => line.length > 0); + + const oldMap = root.updates; + const now = Date.now(); + + root.updates = lines.reduce((acc, pkg) => { + acc[pkg] = oldMap[pkg] ?? now; + return acc; + }, {}); + root.availableUpdates = lines.length; + } + } + } +} diff --git a/Modules/Background.qml b/Modules/Background.qml index 483ce94..a726d17 100644 --- a/Modules/Background.qml +++ b/Modules/Background.qml @@ -9,7 +9,7 @@ ShapePath { readonly property bool flatten: wrapper.height < rounding * 2 property real ibr: invertBottomRounding ? -1 : 1 required property bool invertBottomRounding - readonly property real rounding: 8 + property real rounding: Appearance.rounding.smallest readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 72dfbb8..c801947 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -4,16 +4,17 @@ import Quickshell import QtQuick import QtQuick.Layouts import qs.Components -import qs.Modules as Bar +import qs.Modules import qs.Config import qs.Helpers import qs.Modules.UPower import qs.Modules.Network +import qs.Modules.Updates RowLayout { id: root - required property Bar.Wrapper popouts + required property Wrapper popouts required property ShellScreen screen readonly property int vPadding: 6 required property PersistentProperties visibilities @@ -47,6 +48,10 @@ RowLayout { popouts.currentName = "upower"; popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.hasCurrent = true; + } else if (id === "updates") { + popouts.currentName = "updates"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; } } @@ -73,7 +78,7 @@ RowLayout { roleValue: "workspaces" delegate: WrappedLoader { - sourceComponent: Bar.Workspaces { + sourceComponent: Workspaces { screen: root.screen } } @@ -83,7 +88,7 @@ RowLayout { roleValue: "audio" delegate: WrappedLoader { - sourceComponent: Bar.AudioWidget { + sourceComponent: AudioWidget { } } } @@ -92,7 +97,7 @@ RowLayout { roleValue: "tray" delegate: WrappedLoader { - sourceComponent: Bar.TrayWidget { + sourceComponent: TrayWidget { loader: root popouts: root.popouts } @@ -103,7 +108,7 @@ RowLayout { roleValue: "resources" delegate: WrappedLoader { - sourceComponent: Bar.Resources { + sourceComponent: Resources { visibilities: root.visibilities } } @@ -113,7 +118,7 @@ RowLayout { roleValue: "updates" delegate: WrappedLoader { - sourceComponent: Bar.UpdatesWidget { + sourceComponent: UpdatesWidget { } } } @@ -122,7 +127,7 @@ RowLayout { roleValue: "notifBell" delegate: WrappedLoader { - sourceComponent: Bar.NotifBell { + sourceComponent: NotifBell { popouts: root.popouts visibilities: root.visibilities } @@ -133,7 +138,7 @@ RowLayout { roleValue: "clock" delegate: WrappedLoader { - sourceComponent: Bar.Clock { + sourceComponent: Clock { loader: root popouts: root.popouts visibilities: root.visibilities @@ -145,7 +150,7 @@ RowLayout { roleValue: "activeWindow" delegate: WrappedLoader { - sourceComponent: Bar.WindowTitle { + sourceComponent: WindowTitle { bar: root } } @@ -173,7 +178,7 @@ RowLayout { roleValue: "media" delegate: WrappedLoader { - sourceComponent: Bar.MediaWidget { + sourceComponent: MediaWidget { } } } diff --git a/Modules/Content.qml b/Modules/Content.qml index 54a1f45..0040b7a 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -8,6 +8,7 @@ import qs.Components import qs.Modules.WSOverview import qs.Modules.Network import qs.Modules.UPower +import qs.Modules.Updates Item { id: root @@ -92,6 +93,14 @@ Item { wrapper: root.wrapper } } + + Popout { + name: "updates" + + sourceComponent: UpdatesPopout { + wrapper: root.wrapper + } + } } component Popout: Loader { diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml deleted file mode 100644 index f10ed9c..0000000 --- a/Modules/TrayMenu.qml +++ /dev/null @@ -1,392 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Hyprland -import QtQml -import qs.Effects -import qs.Config - -PanelWindow { - id: root - - property color backgroundColor: DynamicColors.tPalette.m3surface - required property PanelWindow bar - property int biggestWidth: 0 - property color disabledHighlightColor: DynamicColors.layer(DynamicColors.palette.m3primaryContainer, 0) - property color disabledTextColor: DynamicColors.layer(DynamicColors.palette.m3onSurface, 0) - property int entryHeight: 30 - property alias focusGrab: grab.active - property color highlightColor: DynamicColors.tPalette.m3primaryContainer - property int menuItemCount: menuOpener.children.values.length - property var menuStack: [] - property real scaleValue: 0 - property color textColor: DynamicColors.palette.m3onSurface - required property point trayItemRect - required property QsMenuHandle trayMenu - - signal finishedLoading - signal menuActionTriggered - - function goBack() { - if (root.menuStack.length > 0) { - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = root.menuStack.pop(); - listLayout.positionViewAtBeginning(); - backEntry.visible = false; - } - } - - function updateMask() { - root.mask.changed(); - } - - color: "transparent" - - // onTrayMenuChanged: { - // listLayout.forceLayout(); - // } - - visible: false - - mask: Region { - id: mask - - item: menuRect - } - - onMenuActionTriggered: { - if (root.menuStack.length > 0) { - backEntry.visible = true; - } - } - onVisibleChanged: { - if (!visible) - root.menuStack.pop(); - backEntry.visible = false; - - openAnim.start(); - } - - QsMenuOpener { - id: menuOpener - - menu: root.trayMenu - } - - anchors { - bottom: true - left: true - right: true - top: true - } - - HyprlandFocusGrab { - id: grab - - active: false - windows: [root] - - onCleared: { - closeAnim.start(); - } - } - - SequentialAnimation { - id: menuChangeAnimation - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "x" - target: translateAnim - to: -listLayout.width / 2 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 1 - property: "opacity" - target: columnLayout - to: 0 - } - } - - PropertyAction { - property: "menu" - target: columnLayout - } - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 0 - property: "opacity" - target: columnLayout - to: 1 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: listLayout.width / 2 - property: "x" - target: translateAnim - to: 0 - } - } - } - - ParallelAnimation { - id: closeAnim - - onFinished: { - root.visible = false; - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - property: "implicitHeight" - target: menuRect - to: 0 - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 1 - property: "opacity" - targets: [menuRect, shadowRect] - to: 0 - } - } - - ParallelAnimation { - id: openAnim - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "implicitHeight" - target: menuRect - to: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "opacity" - targets: [menuRect, shadowRect] - to: 1 - } - } - - ShadowRect { - id: shadowRect - - anchors.fill: menuRect - radius: menuRect.radius - } - - Rectangle { - id: menuRect - - clip: true - color: root.backgroundColor - implicitHeight: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - implicitWidth: listLayout.contentWidth + 10 - radius: 8 - x: Math.round(root.trayItemRect.x - (menuRect.implicitWidth / 2) + 11) - y: Math.round(root.trayItemRect.y - 5) - - Behavior on implicitHeight { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - ColumnLayout { - id: columnLayout - - anchors.fill: parent - anchors.margins: 5 - spacing: 0 - - transform: [ - Translate { - id: translateAnim - - x: 0 - y: 0 - } - ] - - ListView { - id: listLayout - - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - contentHeight: contentItem.childrenRect.height - contentWidth: root.biggestWidth - model: menuOpener.children - spacing: 0 - - delegate: Rectangle { - id: menuItem - - property var child: QsMenuOpener { - menu: menuItem.modelData - } - property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled - property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled - required property int index - required property QsMenuEntry modelData - - anchors.left: parent.left - anchors.right: parent.right - color: menuItem.modelData.isSeparator ? "#20FFFFFF" : containsMouseAndEnabled ? root.highlightColor : containsMouseAndNotEnabled ? root.disabledHighlightColor : "transparent" - height: menuItem.modelData.isSeparator ? 1 : root.entryHeight - radius: 4 - visible: true - width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20 - - Behavior on color { - CAnim { - duration: 150 - } - } - - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20; - if (currentWidth > biggestWidth) { - root.biggestWidth = currentWidth; - } - } - - TextMetrics { - id: widthMetrics - - text: menuItem.modelData.text - } - - MouseArea { - id: mouseArea - - acceptedButtons: Qt.LeftButton - anchors.fill: parent - hoverEnabled: true - preventStealing: true - propagateComposedEvents: true - - onClicked: { - if (!menuItem.modelData.hasChildren) { - if (menuItem.modelData.enabled) { - menuItem.modelData.triggered(); - closeAnim.start(); - } - } else { - root.menuStack.push(root.trayMenu); - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = menuItem.modelData; - listLayout.positionViewAtBeginning(); - root.menuActionTriggered(); - } - } - } - - RowLayout { - anchors.fill: parent - - Text { - id: menuText - - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.leftMargin: 10 - color: menuItem.modelData.enabled ? root.textColor : root.disabledTextColor - text: menuItem.modelData.text - } - - Image { - id: iconImage - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - fillMode: Image.PreserveAspectFit - layer.enabled: true - source: menuItem.modelData.icon - sourceSize.height: height - sourceSize.width: width - - layer.effect: ColorOverlay { - color: menuItem.modelData.enabled ? "white" : "gray" - } - } - - Text { - id: textArrow - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.bottomMargin: 5 - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - color: menuItem.modelData.enabled ? "white" : "gray" - text: "" - visible: menuItem.modelData.hasChildren ?? false - } - } - } - } - - Rectangle { - id: backEntry - - Layout.fillWidth: true - Layout.preferredHeight: root.entryHeight - color: mouseAreaBack.containsMouse ? "#15FFFFFF" : "transparent" - radius: 4 - visible: false - - MouseArea { - id: mouseAreaBack - - anchors.fill: parent - hoverEnabled: true - - onClicked: { - root.goBack(); - } - } - - Text { - anchors.fill: parent - anchors.leftMargin: 10 - color: "white" - text: "Back " - verticalAlignment: Text.AlignVCenter - } - } - } - } -} diff --git a/Modules/Updates.qml b/Modules/Updates.qml deleted file mode 100644 index b84aeee..0000000 --- a/Modules/Updates.qml +++ /dev/null @@ -1,37 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Modules - -Singleton { - property int availableUpdates: 0 - - Timer { - interval: 1 - repeat: true - running: true - - onTriggered: { - updatesProc.running = true; - interval = 5000; - } - } - - Process { - id: updatesProc - - command: ["checkupdates"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const output = this.text; - const lines = output.trim().split("\n").filter(line => line.length > 0); - availableUpdates = lines.length; - } - } - } -} diff --git a/Modules/Updates/UpdatesPopout.qml b/Modules/Updates/UpdatesPopout.qml new file mode 100644 index 0000000..11f5d9d --- /dev/null +++ b/Modules/Updates/UpdatesPopout.qml @@ -0,0 +1,130 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Config +import qs.Components +import qs.Modules +import qs.Helpers + +Item { + id: root + + required property var wrapper + + implicitHeight: profiles.implicitHeight + Appearance.padding.small + implicitWidth: profiles.implicitWidth + Appearance.padding.small * 2 + + CustomRect { + id: profiles + + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: updatesList.contentHeight + Appearance.padding.small * 2 + implicitWidth: updatesList.contentWidth + Appearance.padding.small * 2 + radius: Appearance.rounding.small + + CustomListView { + id: updatesList + + anchors.centerIn: parent + contentHeight: childrenRect.height + contentWidth: 600 + implicitHeight: contentHeight + implicitWidth: contentWidth + spacing: Appearance.spacing.normal + + delegate: CustomRect { + id: update + + required property var modelData + readonly property list sections: modelData.update.split(" ") + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: 50 + Appearance.padding.smaller * 2 + radius: Appearance.rounding.small - Appearance.padding.small + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.smaller + + MaterialIcon { + font.pointSize: Appearance.font.size.large * 2 + text: "package_2" + } + + ColumnLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + Layout.preferredHeight: 25 + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + text: update.sections[0] + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + text: Updates.formatUpdateTime(update.modelData.timestamp) + } + } + + RowLayout { + Layout.fillHeight: true + Layout.preferredWidth: 300 + + CustomText { + id: versionFrom + + Layout.fillHeight: true + Layout.preferredWidth: 125 + color: DynamicColors.palette.m3tertiary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[1] + verticalAlignment: Text.AlignVCenter + } + + MaterialIcon { + Layout.fillHeight: true + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge + horizontalAlignment: Text.AlignHCenter + text: "arrow_right_alt" + verticalAlignment: Text.AlignVCenter + } + + CustomText { + id: versionTo + + Layout.fillHeight: true + Layout.preferredWidth: 120 + color: DynamicColors.palette.m3primary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[3] + verticalAlignment: Text.AlignVCenter + } + } + } + } + model: ScriptModel { + id: script + + objectProp: "update" + values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ + update, + timestamp + })) + } + } + } +} diff --git a/Modules/UpdatesWidget.qml b/Modules/Updates/UpdatesWidget.qml similarity index 97% rename from Modules/UpdatesWidget.qml rename to Modules/Updates/UpdatesWidget.qml index 36cdc67..0eaa63a 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/Updates/UpdatesWidget.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.Components import qs.Modules +import qs.Helpers import qs.Config CustomRect {