Merge settings window to main #23

Merged
Zacharias-Brohn merged 48 commits from settingsWindow into main 2026-03-18 16:27:50 +01:00
10 changed files with 234 additions and 442 deletions
Showing only changes of commit 152b363da2 - Show all commits
+1
View File
@@ -79,6 +79,7 @@ JsonObject {
property int normal: 17 * scale property int normal: 17 * scale
property real scale: 1 property real scale: 1
property int small: 12 * scale property int small: 12 * scale
property int smallest: 8 * scale
} }
component Spacing: JsonObject { component Spacing: JsonObject {
property int large: 20 * scale property int large: 20 * scale
+2 -1
View File
@@ -48,7 +48,8 @@ Shape {
Modules.Background { Modules.Background {
invertBottomRounding: wrapper.x <= 0 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 startY: wrapper.y
wrapper: root.panels.popouts wrapper: root.panels.popouts
} }
+74
View File
@@ -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;
}
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ ShapePath {
readonly property bool flatten: wrapper.height < rounding * 2 readonly property bool flatten: wrapper.height < rounding * 2
property real ibr: invertBottomRounding ? -1 : 1 property real ibr: invertBottomRounding ? -1 : 1
required property bool invertBottomRounding required property bool invertBottomRounding
readonly property real rounding: 8 property real rounding: Appearance.rounding.smallest
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper required property Wrapper wrapper
+16 -11
View File
@@ -4,16 +4,17 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Modules as Bar import qs.Modules
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers
import qs.Modules.UPower import qs.Modules.UPower
import qs.Modules.Network import qs.Modules.Network
import qs.Modules.Updates
RowLayout { RowLayout {
id: root id: root
required property Bar.Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -47,6 +48,10 @@ RowLayout {
popouts.currentName = "upower"; popouts.currentName = "upower";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true; popouts.hasCurrent = true;
} else if (id === "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" roleValue: "workspaces"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.Workspaces { sourceComponent: Workspaces {
screen: root.screen screen: root.screen
} }
} }
@@ -83,7 +88,7 @@ RowLayout {
roleValue: "audio" roleValue: "audio"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.AudioWidget { sourceComponent: AudioWidget {
} }
} }
} }
@@ -92,7 +97,7 @@ RowLayout {
roleValue: "tray" roleValue: "tray"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.TrayWidget { sourceComponent: TrayWidget {
loader: root loader: root
popouts: root.popouts popouts: root.popouts
} }
@@ -103,7 +108,7 @@ RowLayout {
roleValue: "resources" roleValue: "resources"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.Resources { sourceComponent: Resources {
visibilities: root.visibilities visibilities: root.visibilities
} }
} }
@@ -113,7 +118,7 @@ RowLayout {
roleValue: "updates" roleValue: "updates"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.UpdatesWidget { sourceComponent: UpdatesWidget {
} }
} }
} }
@@ -122,7 +127,7 @@ RowLayout {
roleValue: "notifBell" roleValue: "notifBell"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.NotifBell { sourceComponent: NotifBell {
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
} }
@@ -133,7 +138,7 @@ RowLayout {
roleValue: "clock" roleValue: "clock"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.Clock { sourceComponent: Clock {
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
@@ -145,7 +150,7 @@ RowLayout {
roleValue: "activeWindow" roleValue: "activeWindow"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.WindowTitle { sourceComponent: WindowTitle {
bar: root bar: root
} }
} }
@@ -173,7 +178,7 @@ RowLayout {
roleValue: "media" roleValue: "media"
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Bar.MediaWidget { sourceComponent: MediaWidget {
} }
} }
} }
+9
View File
@@ -8,6 +8,7 @@ import qs.Components
import qs.Modules.WSOverview import qs.Modules.WSOverview
import qs.Modules.Network import qs.Modules.Network
import qs.Modules.UPower import qs.Modules.UPower
import qs.Modules.Updates
Item { Item {
id: root id: root
@@ -92,6 +93,14 @@ Item {
wrapper: root.wrapper wrapper: root.wrapper
} }
} }
Popout {
name: "updates"
sourceComponent: UpdatesPopout {
wrapper: root.wrapper
}
}
} }
component Popout: Loader { component Popout: Loader {
-392
View File
@@ -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
}
}
}
}
}
-37
View File
@@ -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;
}
}
}
}
+130
View File
@@ -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<string> 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
}))
}
}
}
}
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import qs.Helpers
import qs.Config import qs.Config
CustomRect { CustomRect {