diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 0000000..405a8d1 --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,10 @@ +[General] +FunctionsSpacing=true +IndentWidth=4 +MaxColumnWidth=-1 +NewlineType=native +NormalizeOrder=true +ObjectsSpacing=true +SemicolonRule=always +SortImports=false +UseTabs=true diff --git a/Bar.qml b/Bar.qml deleted file mode 100644 index a15860e..0000000 --- a/Bar.qml +++ /dev/null @@ -1,197 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import qs.Daemons -import qs.Components -import qs.Modules -import qs.Modules.Bar -import qs.Config -import qs.Helpers -import qs.Drawers - -Variants { - model: Quickshell.screens - Scope { - id: scope - required property var modelData - PanelWindow { - id: bar - property bool trayMenuVisible: false - screen: scope.modelData - color: "transparent" - property var root: Quickshell.shellDir - - WlrLayershell.namespace: "ZShell-Bar" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - contentItem.focus: true - - contentItem.Keys.onEscapePressed: { - if ( Config.barConfig.autoHide ) - visibilities.bar = false; - visibilities.sidebar = false; - visibilities.dashboard = false; - visibilities.osd = false; - } - - PanelWindow { - id: exclusionZone - WlrLayershell.namespace: "ZShell-Bar-Exclusion" - screen: bar.screen - WlrLayershell.layer: WlrLayer.Bottom - WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto - anchors { - left: true - right: true - top: true - } - color: "transparent" - implicitHeight: 34 - } - - anchors { - top: true - left: true - right: true - bottom: true - } - - mask: Region { - id: region - x: 0 - y: Config.barConfig.autoHide && !visibilities.bar ? 4 : 34 - - property list nullRegions: [] - - width: bar.width - height: bar.screen.height - backgroundRect.implicitHeight - intersection: Intersection.Xor - - regions: popoutRegions.instances - } - - Variants { - id: popoutRegions - model: panels.children - - Region { - required property Item modelData - - x: modelData.x - y: modelData.y + backgroundRect.implicitHeight - width: modelData.width - height: modelData.height - intersection: Intersection.Subtract - } - } - - HyprlandFocusGrab { - id: focusGrab - - active: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || ( panels.popouts.hasCurrent && panels.popouts.currentName.startsWith( "traymenu" )) - windows: [bar] - onCleared: { - visibilities.launcher = false; - visibilities.sidebar = false; - visibilities.dashboard = false; - visibilities.osd = false; - panels.popouts.hasCurrent = false; - } - } - - PersistentProperties { - id: visibilities - - property bool sidebar - property bool dashboard - property bool bar - property bool osd - property bool launcher - property bool notif: NotifServer.popups.length > 0 - - Component.onCompleted: Visibilities.load(scope.modelData, this) - } - - Binding { - target: visibilities - property: "bar" - value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif - when: Config.barConfig.autoHide - } - - Item { - anchors.fill: parent - opacity: Appearance.transparency.enabled ? DynamicColors.transparency.base : 1 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 32 - shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 1) - } - - Border { - bar: backgroundRect - visibilities: visibilities - } - - Backgrounds { - visibilities: visibilities - panels: panels - bar: backgroundRect - } - } - - Interactions { - id: mouseArea - screen: scope.modelData - popouts: panels.popouts - visibilities: visibilities - panels: panels - bar: barLoader - anchors.fill: parent - - Panels { - id: panels - screen: scope.modelData - bar: backgroundRect - visibilities: visibilities - } - - CustomRect { - id: backgroundRect - property Wrapper popouts: panels.popouts - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: 34 - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0 - color: "transparent" - radius: 0 - - - Behavior on color { - CAnim {} - } - - Behavior on anchors.topMargin { - Anim {} - } - - BarLoader { - id: barLoader - anchors.fill: parent - popouts: panels.popouts - bar: bar - visibilities: visibilities - screen: scope.modelData - } - } - } - } - } -} diff --git a/CMakeLists.txt b/CMakeLists.txt index 302c0df..cbc124a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,5 +30,5 @@ if("shell" IN_LIST ENABLE_MODULES) foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths) install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") endforeach() - install(FILES shell.qml Bar.qml Wallpaper.qml DESTINATION "${INSTALL_QSCONFDIR}") + install(FILES shell.qml DESTINATION "${INSTALL_QSCONFDIR}") endif() diff --git a/Components/Anim.qml b/Components/Anim.qml new file mode 100644 index 0000000..242354f --- /dev/null +++ b/Components/Anim.qml @@ -0,0 +1,8 @@ +import QtQuick +import qs.Config + +NumberAnimation { + duration: MaterialEasing.standardTime + easing.bezierCurve: MaterialEasing.standard + easing.type: Easing.BezierSpline +} diff --git a/Components/AnimatedTabIndexPair.qml b/Components/AnimatedTabIndexPair.qml new file mode 100644 index 0000000..d8fb656 --- /dev/null +++ b/Components/AnimatedTabIndexPair.qml @@ -0,0 +1,24 @@ +import QtQuick + +QtObject { + id: root + + property real idx1: index + property int idx1Duration: 100 + property real idx2: index + property int idx2Duration: 300 + required property int index + + Behavior on idx1 { + NumberAnimation { + duration: root.idx1Duration + easing.type: Easing.OutSine + } + } + Behavior on idx2 { + NumberAnimation { + duration: root.idx2Duration + easing.type: Easing.OutSine + } + } +} diff --git a/Components/CAnim.qml b/Components/CAnim.qml new file mode 100644 index 0000000..d5be385 --- /dev/null +++ b/Components/CAnim.qml @@ -0,0 +1,8 @@ +import QtQuick +import qs.Config + +ColorAnimation { + duration: MaterialEasing.standardTime + easing.bezierCurve: MaterialEasing.standard + easing.type: Easing.BezierSpline +} diff --git a/Components/CircularIndicator.qml b/Components/CircularIndicator.qml index c398d48..662c571 100644 --- a/Components/CircularIndicator.qml +++ b/Components/CircularIndicator.qml @@ -1,108 +1,102 @@ -import qs.Helpers import qs.Config -import qs.Modules import ZShell.Internal import QtQuick import QtQuick.Templates BusyIndicator { - id: root + id: root - enum AnimType { - Advance = 0, - Retreat - } + enum AnimState { + Stopped, + Running, + Completing + } + enum AnimType { + Advance = 0, + Retreat + } - enum AnimState { - Stopped, - Running, - Completing - } + property int animState + property color bgColour: DynamicColors.palette.m3secondaryContainer + property color fgColour: DynamicColors.palette.m3primary + property real implicitSize: Appearance.font.size.normal * 3 + property real internalStrokeWidth: strokeWidth + readonly property alias progress: manager.progress + property real strokeWidth: Appearance.padding.small * 0.8 + property alias type: manager.indeterminateAnimationType - property real implicitSize: Appearance.font.size.normal * 3 - property real strokeWidth: Appearance.padding.small * 0.8 - property color fgColour: DynamicColors.palette.m3primary - property color bgColour: DynamicColors.palette.m3secondaryContainer + implicitHeight: implicitSize + implicitWidth: implicitSize + padding: 0 - property alias type: manager.indeterminateAnimationType - readonly property alias progress: manager.progress + contentItem: CircularProgress { + anchors.fill: parent + bgColour: root.bgColour + fgColour: root.fgColour + padding: root.padding + rotation: manager.rotation + startAngle: manager.startFraction * 360 + strokeWidth: root.internalStrokeWidth + value: manager.endFraction - manager.startFraction + } + states: State { + name: "stopped" + when: !root.running - property real internalStrokeWidth: strokeWidth - property int animState + PropertyChanges { + root.internalStrokeWidth: root.strokeWidth / 3 + root.opacity: 0 + } + } + transitions: Transition { + Anim { + duration: manager.completeEndDuration * Appearance.anim.durations.scale + properties: "opacity,internalStrokeWidth" + } + } - padding: 0 - implicitWidth: implicitSize - implicitHeight: implicitSize + Component.onCompleted: { + if (running) { + running = false; + running = true; + } + } + onRunningChanged: { + if (running) { + manager.completeEndProgress = 0; + animState = CircularIndicator.Running; + } else { + if (animState == CircularIndicator.Running) + animState = CircularIndicator.Completing; + } + } - Component.onCompleted: { - if (running) { - running = false; - running = true; - } - } + CircularIndicatorManager { + id: manager - onRunningChanged: { - if (running) { - manager.completeEndProgress = 0; - animState = CircularIndicator.Running; - } else { - if (animState == CircularIndicator.Running) - animState = CircularIndicator.Completing; - } - } + } - states: State { - name: "stopped" - when: !root.running + NumberAnimation { + duration: manager.duration * Appearance.anim.durations.scale + from: 0 + loops: Animation.Infinite + property: "progress" + running: root.animState !== CircularIndicator.Stopped + target: manager + to: 1 + } - PropertyChanges { - root.opacity: 0 - root.internalStrokeWidth: root.strokeWidth / 3 - } - } + NumberAnimation { + duration: manager.completeEndDuration * Appearance.anim.durations.scale + from: 0 + property: "completeEndProgress" + running: root.animState === CircularIndicator.Completing + target: manager + to: 1 - transitions: Transition { - Anim { - properties: "opacity,internalStrokeWidth" - duration: manager.completeEndDuration * Appearance.anim.durations.scale - } - } - - contentItem: CircularProgress { - anchors.fill: parent - strokeWidth: root.internalStrokeWidth - fgColour: root.fgColour - bgColour: root.bgColour - padding: root.padding - rotation: manager.rotation - startAngle: manager.startFraction * 360 - value: manager.endFraction - manager.startFraction - } - - CircularIndicatorManager { - id: manager - } - - NumberAnimation { - running: root.animState !== CircularIndicator.Stopped - loops: Animation.Infinite - target: manager - property: "progress" - from: 0 - to: 1 - duration: manager.duration * Appearance.anim.durations.scale - } - - NumberAnimation { - running: root.animState === CircularIndicator.Completing - target: manager - property: "completeEndProgress" - from: 0 - to: 1 - duration: manager.completeEndDuration * Appearance.anim.durations.scale - onFinished: { - if (root.animState === CircularIndicator.Completing) - root.animState = CircularIndicator.Stopped; - } - } + onFinished: { + if (root.animState === CircularIndicator.Completing) + root.animState = CircularIndicator.Stopped; + } + } } diff --git a/Components/CircularProgress.qml b/Components/CircularProgress.qml index ef98fd6..fa1011c 100644 --- a/Components/CircularProgress.qml +++ b/Components/CircularProgress.qml @@ -1,69 +1,66 @@ import QtQuick import QtQuick.Shapes -import qs.Helpers import qs.Config -import qs.Modules Shape { - id: root + id: root - property real value - property int startAngle: -90 - property int strokeWidth: Appearance.padding.smaller - property int padding: 0 - property int spacing: Appearance.spacing.small - property color fgColour: DynamicColors.palette.m3primary - property color bgColour: DynamicColors.palette.m3secondaryContainer + readonly property real arcRadius: (size - padding - strokeWidth) / 2 + property color bgColour: DynamicColors.palette.m3secondaryContainer + property color fgColour: DynamicColors.palette.m3primary + readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + property int padding: 0 + readonly property real size: Math.min(width, height) + property int spacing: Appearance.spacing.small + property int startAngle: -90 + property int strokeWidth: Appearance.padding.smaller + readonly property real vValue: value || 1 / 360 + property real value - readonly property real size: Math.min(width, height) - readonly property real arcRadius: (size - padding - strokeWidth) / 2 - readonly property real vValue: value || 1 / 360 - readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + asynchronous: true + preferredRendererType: Shape.CurveRenderer - preferredRendererType: Shape.CurveRenderer - asynchronous: true + ShapePath { + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: root.strokeWidth - ShapePath { - fillColor: "transparent" - strokeColor: root.bgColour - strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } - PathAngleArc { - startAngle: root.startAngle + 360 * root.vValue + root.gapAngle - sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 - } + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.arcRadius + radiusY: root.arcRadius + startAngle: root.startAngle + 360 * root.vValue + root.gapAngle + sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) + } + } - Behavior on strokeColor { - CAnim { - duration: Appearance.anim.durations.large - } - } - } + ShapePath { + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + fillColor: "transparent" + strokeColor: root.fgColour + strokeWidth: root.strokeWidth - ShapePath { - fillColor: "transparent" - strokeColor: root.fgColour - strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } - PathAngleArc { - startAngle: root.startAngle - sweepAngle: 360 * root.vValue - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 - } - - Behavior on strokeColor { - CAnim { - duration: Appearance.anim.durations.large - } - } - } + PathAngleArc { + centerX: root.size / 2 + centerY: root.size / 2 + radiusX: root.arcRadius + radiusY: root.arcRadius + startAngle: root.startAngle + sweepAngle: 360 * root.vValue + } + } } diff --git a/Components/CollapsibleSection.qml b/Components/CollapsibleSection.qml new file mode 100644 index 0000000..6d6fc3e --- /dev/null +++ b/Components/CollapsibleSection.qml @@ -0,0 +1,135 @@ +import QtQuick +import QtQuick.Layouts +import qs.Config + +ColumnLayout { + id: root + + default property alias content: contentColumn.data + property string description: "" + property bool expanded: false + property bool nested: false + property bool showBackground: false + required property string title + + signal toggleRequested + + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Item { + id: sectionHeaderItem + + Layout.fillWidth: true + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) + + RowLayout { + id: titleRow + + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.normal + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal + + CustomText { + font.pointSize: Appearance.font.size.larger + font.weight: 500 + text: root.title + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + rotation: root.expanded ? 180 : 0 + text: "expand_more" + + Behavior on rotation { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + + StateLayer { + function onClicked(): void { + root.toggleRequested(); + root.expanded = !root.expanded; + } + + anchors.fill: parent + color: DynamicColors.palette.m3onSurface + radius: Appearance.rounding.normal + showHoverBackground: false + } + } + + Item { + id: contentWrapper + + Layout.fillWidth: true + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + CustomRect { + id: backgroundRect + + anchors.fill: parent + color: DynamicColors.transparency.enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3surfaceContainer) + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + radius: Appearance.rounding.normal + visible: root.showBackground + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + ColumnLayout { + id: contentColumn + + anchors.bottomMargin: Appearance.spacing.small + anchors.left: parent.left + anchors.leftMargin: Appearance.padding.normal + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + opacity: root.expanded ? 1.0 : 0.0 + spacing: Appearance.spacing.small + y: Appearance.spacing.small + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + CustomText { + id: descriptionText + + Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + Layout.fillWidth: true + Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + text: root.description + visible: root.description !== "" + wrapMode: Text.Wrap + } + } + } +} diff --git a/Components/ColoredIcon.qml b/Components/ColoredIcon.qml index fb38a08..a4711c6 100644 --- a/Components/ColoredIcon.qml +++ b/Components/ColoredIcon.qml @@ -5,31 +5,30 @@ import Quickshell.Widgets import QtQuick IconImage { - id: root + id: root - required property color color + required property color color - asynchronous: true + asynchronous: true + layer.enabled: true - layer.enabled: true - layer.effect: Coloriser { - sourceColor: analyser.dominantColour - colorizationColor: root.color - } + layer.effect: Coloriser { + colorizationColor: root.color + sourceColor: analyser.dominantColour + } - layer.onEnabledChanged: { - if (layer.enabled && status === Image.Ready) - analyser.requestUpdate(); - } + layer.onEnabledChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } + onStatusChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } - onStatusChanged: { - if (layer.enabled && status === Image.Ready) - analyser.requestUpdate(); - } + ImageAnalyser { + id: analyser - ImageAnalyser { - id: analyser - - sourceItem: root - } + sourceItem: root + } } diff --git a/Components/Coloriser.qml b/Components/Coloriser.qml index f0212f1..77bdf8d 100644 --- a/Components/Coloriser.qml +++ b/Components/Coloriser.qml @@ -1,14 +1,14 @@ import QtQuick import QtQuick.Effects -import qs.Modules MultiEffect { - property color sourceColor: "black" + property color sourceColor: "black" - colorization: 1 - brightness: 1 - sourceColor.hslLightness + brightness: 1 - sourceColor.hslLightness + colorization: 1 - Behavior on colorizationColor { - CAnim {} - } + Behavior on colorizationColor { + CAnim { + } + } } diff --git a/Components/CustomAudioSlider.qml b/Components/CustomAudioSlider.qml index 8a74a29..f7d35af 100644 --- a/Components/CustomAudioSlider.qml +++ b/Components/CustomAudioSlider.qml @@ -1,83 +1,70 @@ import QtQuick import QtQuick.Templates import qs.Config -import qs.Modules Slider { - id: root + id: root - required property real peak property color nonPeakColor: DynamicColors.tPalette.m3primary + required property real peak property color peakColor: DynamicColors.palette.m3primary - background: Item { - CustomRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 - - implicitWidth: root.handle.x - root.implicitHeight - - color: root.nonPeakColor - radius: 1000 - topRightRadius: root.implicitHeight / 15 - bottomRightRadius: root.implicitHeight / 15 + background: Item { + CustomRect { + anchors.bottom: parent.bottom + anchors.bottomMargin: root.implicitHeight / 3 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: root.implicitHeight / 3 + bottomRightRadius: root.implicitHeight / 15 + color: root.nonPeakColor + implicitWidth: root.handle.x - root.implicitHeight + radius: 1000 + topRightRadius: root.implicitHeight / 15 CustomRect { - anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left - + anchors.top: parent.top + bottomRightRadius: root.implicitHeight / 15 + color: root.peakColor implicitWidth: parent.width * root.peak radius: 1000 topRightRadius: root.implicitHeight / 15 - bottomRightRadius: root.implicitHeight / 15 - - color: root.peakColor Behavior on implicitWidth { - Anim { duration: 50 } + Anim { + duration: 50 + } } } - } + } - CustomRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.topMargin: root.implicitHeight / 3 - anchors.bottomMargin: root.implicitHeight / 3 + CustomRect { + anchors.bottom: parent.bottom + anchors.bottomMargin: root.implicitHeight / 3 + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: root.implicitHeight / 3 + bottomLeftRadius: root.implicitHeight / 15 + color: DynamicColors.tPalette.m3surfaceContainer + implicitWidth: root.implicitWidth - root.handle.x - root.handle.implicitWidth - root.implicitHeight + radius: 1000 + topLeftRadius: root.implicitHeight / 15 + } + } + handle: CustomRect { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + implicitHeight: 15 + implicitWidth: 5 + radius: 1000 + x: root.visualPosition * root.availableWidth - implicitWidth / 2 - implicitWidth: root.implicitWidth - root.handle.x - root.handle.implicitWidth - root.implicitHeight - - Component.onCompleted: { - console.log(root.handle.x, implicitWidth) - } - - - color: DynamicColors.tPalette.m3surfaceContainer - radius: 1000 - topLeftRadius: root.implicitHeight / 15 - bottomLeftRadius: root.implicitHeight / 15 - } - } - - handle: CustomRect { - x: root.visualPosition * root.availableWidth - implicitWidth / 2 - - implicitWidth: 5 - implicitHeight: 15 - anchors.verticalCenter: parent.verticalCenter - - color: DynamicColors.palette.m3primary - radius: 1000 - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: Qt.PointingHandCursor - } - } + MouseArea { + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } } diff --git a/Components/CustomBusyIndicator.qml b/Components/CustomBusyIndicator.qml index 223989e..877db30 100644 --- a/Components/CustomBusyIndicator.qml +++ b/Components/CustomBusyIndicator.qml @@ -2,64 +2,68 @@ import QtQuick import QtQuick.Controls.Basic BusyIndicator { - id: control - property color color: delegate.color + id: control + property int busySize: 64 + property color color: delegate.color - contentItem: Item { - implicitWidth: control.busySize - implicitHeight: control.busySize + contentItem: Item { + implicitHeight: control.busySize + implicitWidth: control.busySize - Item { - id: item - x: parent.width / 2 - (control.busySize / 2) - y: parent.height / 2 - (control.busySize / 2) - width: control.busySize - height: control.busySize - opacity: control.running ? 1 : 0 + Item { + id: item - Behavior on opacity { - OpacityAnimator { - duration: 250 - } - } + height: control.busySize + opacity: control.running ? 1 : 0 + width: control.busySize + x: parent.width / 2 - (control.busySize / 2) + y: parent.height / 2 - (control.busySize / 2) - RotationAnimator { - target: item - running: control.visible && control.running - from: 0 - to: 360 - loops: Animation.Infinite - duration: 1250 - } + Behavior on opacity { + OpacityAnimator { + duration: 250 + } + } - Repeater { - id: repeater - model: 6 + RotationAnimator { + duration: 1250 + from: 0 + loops: Animation.Infinite + running: control.visible && control.running + target: item + to: 360 + } - CustomRect { - id: delegate - x: item.width / 2 - width / 2 - y: item.height / 2 - height / 2 - implicitWidth: 10 - implicitHeight: 10 - radius: 5 - color: control.color + Repeater { + id: repeater - required property int index + model: 6 - transform: [ - Translate { - y: -Math.min(item.width, item.height) * 0.5 + 5 - }, - Rotation { - angle: delegate.index / repeater.count * 360 - origin.x: 5 - origin.y: 5 - } - ] - } - } - } - } + CustomRect { + id: delegate + + required property int index + + color: control.color + implicitHeight: 10 + implicitWidth: 10 + radius: 5 + x: item.width / 2 - width / 2 + y: item.height / 2 - height / 2 + + transform: [ + Translate { + y: -Math.min(item.width, item.height) * 0.5 + 5 + }, + Rotation { + angle: delegate.index / repeater.count * 360 + origin.x: 5 + origin.y: 5 + } + ] + } + } + } + } } diff --git a/Components/CustomButton.qml b/Components/CustomButton.qml index 62586bc..79580f7 100644 --- a/Components/CustomButton.qml +++ b/Components/CustomButton.qml @@ -4,30 +4,28 @@ import QtQuick.Controls Button { id: control - required property color textColor required property color bgColor property int radius: 4 + required property color textColor - contentItem: CustomText { - text: control.text - + background: CustomRect { + color: control.bgColor opacity: control.enabled ? 1.0 : 0.5 + radius: control.radius + } + contentItem: CustomText { color: control.textColor horizontalAlignment: Text.AlignHCenter + opacity: control.enabled ? 1.0 : 0.5 + text: control.text verticalAlignment: Text.AlignVCenter } - background: CustomRect { - opacity: control.enabled ? 1.0 : 0.5 - - radius: control.radius - color: control.bgColor - } - StateLayer { - radius: control.radius function onClicked(): void { control.clicked(); } + + radius: control.radius } } diff --git a/Components/CustomCheckbox.qml b/Components/CustomCheckbox.qml index 2749781..55a728f 100644 --- a/Components/CustomCheckbox.qml +++ b/Components/CustomCheckbox.qml @@ -5,35 +5,33 @@ import qs.Config CheckBox { id: control - property int checkWidth: 20 property int checkHeight: 20 + property int checkWidth: 20 + contentItem: CustomText { + anchors.left: parent.left + anchors.leftMargin: control.checkWidth + control.leftPadding + 8 + anchors.verticalCenter: parent.verticalCenter + font.pointSize: control.font.pointSize + text: control.text + } indicator: CustomRect { - implicitWidth: control.checkWidth - implicitHeight: control.checkHeight // x: control.leftPadding // y: parent.implicitHeight / 2 - implicitHeight / 2 border.color: control.checked ? DynamicColors.palette.m3primary : "transparent" color: DynamicColors.palette.m3surfaceVariant - + implicitHeight: control.checkHeight + implicitWidth: control.checkWidth radius: 4 CustomRect { - implicitWidth: control.checkWidth - (x * 2) + color: DynamicColors.palette.m3primary implicitHeight: control.checkHeight - (y * 2) + implicitWidth: control.checkWidth - (x * 2) + radius: 3 + visible: control.checked x: 4 y: 4 - radius: 3 - color: DynamicColors.palette.m3primary - visible: control.checked } } - - contentItem: CustomText { - text: control.text - font.pointSize: control.font.pointSize - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: control.checkWidth + control.leftPadding + 8 - } } diff --git a/Components/CustomClippingRect.qml b/Components/CustomClippingRect.qml index 120b117..9498a06 100644 --- a/Components/CustomClippingRect.qml +++ b/Components/CustomClippingRect.qml @@ -1,13 +1,13 @@ import Quickshell.Widgets import QtQuick -import qs.Modules ClippingRectangle { - id: root + id: root - color: "transparent" + color: "transparent" - Behavior on color { - CAnim {} - } + Behavior on color { + CAnim { + } + } } 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/CustomFlickable.qml b/Components/CustomFlickable.qml index 05ba9a8..01a9652 100644 --- a/Components/CustomFlickable.qml +++ b/Components/CustomFlickable.qml @@ -1,14 +1,13 @@ import QtQuick -import qs.Modules Flickable { - id: root + id: root - maximumFlickVelocity: 3000 + maximumFlickVelocity: 3000 - rebound: Transition { - Anim { - properties: "x,y" - } - } + rebound: Transition { + Anim { + properties: "x,y" + } + } } diff --git a/Components/CustomIcon.qml b/Components/CustomIcon.qml index 930c852..8201305 100644 --- a/Components/CustomIcon.qml +++ b/Components/CustomIcon.qml @@ -4,7 +4,7 @@ import Quickshell.Widgets import QtQuick IconImage { - id: root + id: root - asynchronous: true + asynchronous: true } diff --git a/Components/CustomListView.qml b/Components/CustomListView.qml index e570513..51c7110 100644 --- a/Components/CustomListView.qml +++ b/Components/CustomListView.qml @@ -1,15 +1,13 @@ import QtQuick -import qs.Config -import qs.Modules ListView { - id: root + id: root - maximumFlickVelocity: 3000 + maximumFlickVelocity: 3000 - rebound: Transition { - Anim { - properties: "x,y" - } - } + rebound: Transition { + Anim { + properties: "x,y" + } + } } diff --git a/Components/CustomMouseArea.qml b/Components/CustomMouseArea.qml index a17a74e..6d52d16 100644 --- a/Components/CustomMouseArea.qml +++ b/Components/CustomMouseArea.qml @@ -1,19 +1,19 @@ import QtQuick MouseArea { - property int scrollAccumulatedY: 0 + property int scrollAccumulatedY: 0 - function onWheel(event: WheelEvent): void { - } + function onWheel(event: WheelEvent): void { + } - onWheel: event => { - if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) - scrollAccumulatedY = 0; - scrollAccumulatedY += event.angleDelta.y; + onWheel: event => { + if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) + scrollAccumulatedY = 0; + scrollAccumulatedY += event.angleDelta.y; - if (Math.abs(scrollAccumulatedY) >= 120) { - onWheel(event); - scrollAccumulatedY = 0; - } - } + if (Math.abs(scrollAccumulatedY) >= 120) { + onWheel(event); + scrollAccumulatedY = 0; + } + } } diff --git a/Components/CustomRadioButton.qml b/Components/CustomRadioButton.qml index cfaa64c..f719261 100644 --- a/Components/CustomRadioButton.qml +++ b/Components/CustomRadioButton.qml @@ -1,56 +1,53 @@ import QtQuick import QtQuick.Templates import qs.Config -import qs.Modules RadioButton { - id: root + id: root - font.pointSize: 12 + font.pointSize: 12 + implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) + implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin - implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin - implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) + contentItem: CustomText { + anchors.left: outerCircle.right + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + font.pointSize: root.font.pointSize + text: root.text + } + indicator: Rectangle { + id: outerCircle - indicator: Rectangle { - id: outerCircle + anchors.verticalCenter: parent.verticalCenter + border.color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + border.width: 2 + color: "transparent" + implicitHeight: 16 + implicitWidth: 16 + radius: 1000 - implicitWidth: 16 - implicitHeight: 16 - radius: 1000 - color: "transparent" - border.color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - border.width: 2 - anchors.verticalCenter: parent.verticalCenter + Behavior on border.color { + CAnim { + } + } - StateLayer { - anchors.margins: -7 - color: root.checked ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3primary - z: -1 + StateLayer { + function onClicked(): void { + root.click(); + } - function onClicked(): void { - root.click(); - } - } + anchors.margins: -7 + color: root.checked ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3primary + z: -1 + } - CustomRect { - anchors.centerIn: parent - implicitWidth: 8 - implicitHeight: 8 - - radius: 1000 - color: Qt.alpha(DynamicColors.palette.m3primary, root.checked ? 1 : 0) - } - - Behavior on border.color { - CAnim {} - } - } - - contentItem: CustomText { - text: root.text - font.pointSize: root.font.pointSize - anchors.verticalCenter: parent.verticalCenter - anchors.left: outerCircle.right - anchors.leftMargin: 10 - } + CustomRect { + anchors.centerIn: parent + color: Qt.alpha(DynamicColors.palette.m3primary, root.checked ? 1 : 0) + implicitHeight: 8 + implicitWidth: 8 + radius: 1000 + } + } } diff --git a/Components/CustomRect.qml b/Components/CustomRect.qml index 0aba620..2f5da64 100644 --- a/Components/CustomRect.qml +++ b/Components/CustomRect.qml @@ -1,12 +1,12 @@ import QtQuick -import qs.Modules Rectangle { - id: root + id: root - color: "transparent" + color: "transparent" - Behavior on color { - CAnim {} - } + Behavior on color { + CAnim { + } + } } diff --git a/Components/CustomScrollBar.qml b/Components/CustomScrollBar.qml index d57cc43..1f0acbc 100644 --- a/Components/CustomScrollBar.qml +++ b/Components/CustomScrollBar.qml @@ -1,189 +1,189 @@ import qs.Config -import qs.Modules import QtQuick import QtQuick.Templates ScrollBar { - id: root + id: root - required property Flickable flickable - property bool shouldBeActive - property real nonAnimPosition - property bool animating + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false + property bool animating + required property Flickable flickable + property real nonAnimPosition + property bool shouldBeActive - onHoveredChanged: { - if (hovered) - shouldBeActive = true; - else - shouldBeActive = flickable.moving; - } + implicitWidth: 8 - property bool _updatingFromFlickable: false - property bool _updatingFromUser: false + contentItem: CustomRect { + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.palette.m3secondary + opacity: { + if (root.size === 1) + return 0; + if (fullMouse.pressed) + return 1; + if (mouse.containsMouse) + return 0.8; + if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive) + return 0.6; + return 0; + } + radius: 1000 - // Sync nonAnimPosition with Qt's automatic position binding - onPositionChanged: { - if (_updatingFromUser) { - _updatingFromUser = false; - return; - } - if (position === nonAnimPosition) { - animating = false; - return; - } - if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { - nonAnimPosition = position; - } - } + Behavior on opacity { + Anim { + } + } - // Sync nonAnimPosition with flickable when not animating - Connections { - target: flickable - function onContentYChanged() { - if (!animating && !fullMouse.pressed) { - _updatingFromFlickable = true; - const contentHeight = flickable.contentHeight; - const height = flickable.height; - if (contentHeight > height) { - nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); - } else { - nonAnimPosition = 0; - } - _updatingFromFlickable = false; - } - } - } + MouseArea { + id: mouse - Component.onCompleted: { - if (flickable) { - const contentHeight = flickable.contentHeight; - const height = flickable.height; - if (contentHeight > height) { - nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); - } - } - } - implicitWidth: 8 + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } + } + Behavior on position { + enabled: !fullMouse.pressed - contentItem: CustomRect { - anchors.left: parent.left - anchors.right: parent.right - opacity: { - if (root.size === 1) - return 0; - if (fullMouse.pressed) - return 1; - if (mouse.containsMouse) - return 0.8; - if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive) - return 0.6; - return 0; - } - radius: 1000 - color: DynamicColors.palette.m3secondary + Anim { + } + } - MouseArea { - id: mouse + Component.onCompleted: { + if (flickable) { + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } + } + } + onHoveredChanged: { + if (hovered) + shouldBeActive = true; + else + shouldBeActive = flickable.moving; + } - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - acceptedButtons: Qt.NoButton - } + // Sync nonAnimPosition with Qt's automatic position binding + onPositionChanged: { + if (_updatingFromUser) { + _updatingFromUser = false; + return; + } + if (position === nonAnimPosition) { + animating = false; + return; + } + if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { + nonAnimPosition = position; + } + } - Behavior on opacity { - Anim {} - } - } + // Sync nonAnimPosition with flickable when not animating + Connections { + function onContentYChanged() { + if (!animating && !fullMouse.pressed) { + _updatingFromFlickable = true; + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } else { + nonAnimPosition = 0; + } + _updatingFromFlickable = false; + } + } - Connections { - target: root.flickable + target: flickable + } - function onMovingChanged(): void { - if (root.flickable.moving) - root.shouldBeActive = true; - else - hideDelay.restart(); - } - } + Connections { + function onMovingChanged(): void { + if (root.flickable.moving) + root.shouldBeActive = true; + else + hideDelay.restart(); + } - Timer { - id: hideDelay + target: root.flickable + } - interval: 600 - onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered - } + Timer { + id: hideDelay - CustomMouseArea { - id: fullMouse + interval: 600 - anchors.fill: parent - preventStealing: true + onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered + } - onPressed: event => { - root.animating = true; - root._updatingFromUser = true; - const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); - root.nonAnimPosition = newPos; - // Update flickable position - // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] - if (root.flickable) { - const contentHeight = root.flickable.contentHeight; - const height = root.flickable.height; - if (contentHeight > height) { - const maxContentY = contentHeight - height; - const maxPos = 1 - root.size; - const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; - root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); - } - } - } + CustomMouseArea { + id: fullMouse - onPositionChanged: event => { - root._updatingFromUser = true; - const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); - root.nonAnimPosition = newPos; - // Update flickable position - // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] - if (root.flickable) { - const contentHeight = root.flickable.contentHeight; - const height = root.flickable.height; - if (contentHeight > height) { - const maxContentY = contentHeight - height; - const maxPos = 1 - root.size; - const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; - root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); - } - } - } + function onWheel(event: WheelEvent): void { + root.animating = true; + root._updatingFromUser = true; + let newPos = root.nonAnimPosition; + if (event.angleDelta.y > 0) + newPos = Math.max(0, root.nonAnimPosition - 0.1); + else if (event.angleDelta.y < 0) + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } - function onWheel(event: WheelEvent): void { - root.animating = true; - root._updatingFromUser = true; - let newPos = root.nonAnimPosition; - if (event.angleDelta.y > 0) - newPos = Math.max(0, root.nonAnimPosition - 0.1); - else if (event.angleDelta.y < 0) - newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); - root.nonAnimPosition = newPos; - // Update flickable position - // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] - if (root.flickable) { - const contentHeight = root.flickable.contentHeight; - const height = root.flickable.height; - if (contentHeight > height) { - const maxContentY = contentHeight - height; - const maxPos = 1 - root.size; - const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; - root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); - } - } - } - } + anchors.fill: parent + preventStealing: true - Behavior on position { - enabled: !fullMouse.pressed - - Anim {} - } + onPositionChanged: event => { + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + onPressed: event => { + root.animating = true; + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + } } diff --git a/Components/CustomSlider.qml b/Components/CustomSlider.qml index 5f21e71..8a375aa 100644 --- a/Components/CustomSlider.qml +++ b/Components/CustomSlider.qml @@ -1,53 +1,45 @@ import QtQuick import QtQuick.Templates import qs.Config -import qs.Modules Slider { - id: root + id: root - background: Item { - CustomRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left + background: Item { + CustomRect { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + bottomRightRadius: root.implicitHeight / 6 + color: DynamicColors.palette.m3primary + implicitWidth: root.handle.x - root.implicitHeight / 2 + radius: 1000 + topRightRadius: root.implicitHeight / 6 + } - implicitWidth: root.handle.x - root.implicitHeight / 2 + CustomRect { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + bottomLeftRadius: root.implicitHeight / 6 + color: DynamicColors.tPalette.m3surfaceContainer + implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 2 + radius: 1000 + topLeftRadius: root.implicitHeight / 6 + } + } + handle: CustomRect { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + implicitHeight: 15 + implicitWidth: 5 + radius: 1000 + x: root.visualPosition * root.availableWidth - implicitWidth / 2 - color: DynamicColors.palette.m3primary - radius: 1000 - topRightRadius: root.implicitHeight / 6 - bottomRightRadius: root.implicitHeight / 6 - } - - CustomRect { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - - implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 2 - - color: DynamicColors.tPalette.m3surfaceContainer - radius: 1000 - topLeftRadius: root.implicitHeight / 6 - bottomLeftRadius: root.implicitHeight / 6 - } - } - - handle: CustomRect { - x: root.visualPosition * root.availableWidth - implicitWidth / 2 - - implicitWidth: 5 - implicitHeight: 15 - anchors.verticalCenter: parent.verticalCenter - - color: DynamicColors.palette.m3primary - radius: 1000 - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - cursorShape: Qt.PointingHandCursor - } - } + MouseArea { + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } } diff --git a/Components/CustomSpinBox.qml b/Components/CustomSpinBox.qml new file mode 100644 index 0000000..fe98e72 --- /dev/null +++ b/Components/CustomSpinBox.qml @@ -0,0 +1,166 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config + +RowLayout { + id: root + + property string displayText: root.value.toString() + property bool isEditing: false + property real max: Infinity + property real min: -Infinity + property alias repeatRate: timer.interval + property real step: 1 + property real value + + signal valueModified(value: real) + + spacing: Appearance.spacing.small + + onValueChanged: { + if (!root.isEditing) { + root.displayText = root.value.toString(); + } + } + + CustomTextField { + id: textField + + inputMethodHints: Qt.ImhFormattedNumbersOnly + leftPadding: Appearance.padding.normal + padding: Appearance.padding.small + rightPadding: Appearance.padding.normal + text: root.isEditing ? text : root.displayText + + background: CustomRect { + color: DynamicColors.tPalette.m3surfaceContainerHigh + implicitWidth: 100 + radius: Appearance.rounding.small + } + validator: DoubleValidator { + bottom: root.min + decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0 + top: root.max + } + + onAccepted: { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + root.isEditing = false; + } + onActiveFocusChanged: { + if (activeFocus) { + root.isEditing = true; + } else { + root.isEditing = false; + root.displayText = root.value.toString(); + } + } + onEditingFinished: { + if (text !== root.displayText) { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + } + root.isEditing = false; + } + } + + CustomRect { + color: DynamicColors.palette.m3primary + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.small + + StateLayer { + id: upState + + function onClicked(): void { + let newValue = Math.min(root.max, root.value + root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + + color: DynamicColors.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + text: "keyboard_arrow_up" + } + } + + CustomRect { + color: DynamicColors.palette.m3primary + implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.small + + StateLayer { + id: downState + + function onClicked(): void { + let newValue = Math.max(root.min, root.value - root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + + color: DynamicColors.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + } + + MaterialIcon { + id: downIcon + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + text: "keyboard_arrow_down" + } + } + + Timer { + id: timer + + interval: 100 + repeat: true + triggeredOnStart: true + + onTriggered: { + if (upState.pressed) + upState.onClicked(); + else if (downState.pressed) + downState.onClicked(); + } + } +} 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/CustomSwitch.qml b/Components/CustomSwitch.qml index 83de2dd..15a54c2 100644 --- a/Components/CustomSwitch.qml +++ b/Components/CustomSwitch.qml @@ -1,66 +1,155 @@ -import qs.Config -import qs.Modules import QtQuick import QtQuick.Templates import QtQuick.Shapes +import qs.Config Switch { - id: root + id: root - property int cLayer: 1 + property int cLayer: 1 - implicitWidth: implicitIndicatorWidth - implicitHeight: implicitIndicatorHeight + implicitHeight: implicitIndicatorHeight + implicitWidth: implicitIndicatorWidth - indicator: CustomRect { - radius: 1000 - color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) + indicator: CustomRect { + color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) + implicitHeight: 13 + 7 * 2 + implicitWidth: implicitHeight * 1.7 + radius: 1000 - implicitWidth: implicitHeight * 1.7 - implicitHeight: 13 + 7 * 2 + CustomRect { + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight - CustomRect { - readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + anchors.verticalCenter: parent.verticalCenter + color: root.checked ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1) + implicitHeight: parent.implicitHeight - 10 + implicitWidth: nonAnimWidth + radius: 1000 + x: root.checked ? parent.implicitWidth - nonAnimWidth - 10 / 2 : 10 / 2 - radius: 1000 - color: root.checked ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1) + Behavior on implicitWidth { + Anim { + } + } + Behavior on x { + Anim { + } + } - x: root.checked ? parent.implicitWidth - nonAnimWidth - 10 / 2 : 10 / 2 - implicitWidth: nonAnimWidth - implicitHeight: parent.implicitHeight - 10 - anchors.verticalCenter: parent.verticalCenter + CustomRect { + anchors.fill: parent + color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 + radius: parent.radius - CustomRect { - anchors.fill: parent - radius: parent.radius + Behavior on opacity { + Anim { + } + } + } - color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 + Shape { + id: icon - Behavior on opacity { - Anim {} - } - } + property point end1: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.8, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.85, height * 0.85); + } + property point end2: { + if (root.pressed) + return Qt.point(width, height / 2); + if (root.checked) + return Qt.point(width * 0.85, height * 0.2); + return Qt.point(width * 0.85, height * 0.15); + } + property point start1: { + if (root.pressed) + return Qt.point(width * 0.1, height / 2); + if (root.checked) + return Qt.point(width * 0.15, height / 2); + return Qt.point(width * 0.15, height * 0.15); + } + property point start2: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.2, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.15, height * 0.85); + } - Behavior on x { - Anim {} - } + anchors.centerIn: parent + asynchronous: true + height: parent.implicitHeight - Appearance.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + width: height - Behavior on implicitWidth { - Anim {} - } - } - } + Behavior on end1 { + PropAnim { + } + } + Behavior on end2 { + PropAnim { + } + } + Behavior on start1 { + PropAnim { + } + } + Behavior on start2 { + PropAnim { + } + } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: false - } + ShapePath { + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + fillColor: "transparent" + startX: icon.start1.x + startY: icon.start1.y + strokeColor: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3surfaceContainerHighest + strokeWidth: Appearance.font.size.larger * 0.15 - component PropAnim: PropertyAnimation { + Behavior on strokeColor { + CAnim { + } + } + + PathLine { + x: icon.end1.x + y: icon.end1.y + } + + PathMove { + x: icon.start2.x + y: icon.start2.y + } + + PathLine { + x: icon.end2.x + y: icon.end2.y + } + } + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + + component PropAnim: PropertyAnimation { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - easing.type: Easing.BezierSpline - } + easing.type: Easing.BezierSpline + } } diff --git a/Components/CustomText.qml b/Components/CustomText.qml index 4b30529..cff8b2d 100644 --- a/Components/CustomText.qml +++ b/Components/CustomText.qml @@ -2,48 +2,50 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Config -import qs.Modules Text { - id: root + id: root - property bool animate: false - property string animateProp: "scale" - property real animateFrom: 0 - property real animateTo: 1 - property int animateDuration: 400 + property bool animate: false + property int animateDuration: 400 + property real animateFrom: 0 + property string animateProp: "scale" + property real animateTo: 1 - renderType: Text.NativeRendering - textFormat: Text.PlainText - color: DynamicColors.palette.m3onSurface - font.family: Appearance.font.family.sans - font.pointSize: 12 + color: DynamicColors.palette.m3onSurface + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.normal + renderType: Text.NativeRendering + textFormat: Text.PlainText - Behavior on color { - CAnim {} - } + Behavior on color { + CAnim { + } + } + Behavior on text { + enabled: root.animate - Behavior on text { - enabled: root.animate + SequentialAnimation { + Anim { + easing.bezierCurve: MaterialEasing.standardAccel + to: root.animateFrom + } - SequentialAnimation { - Anim { - to: root.animateFrom - easing.bezierCurve: MaterialEasing.standardAccel - } - PropertyAction {} - Anim { - to: root.animateTo - easing.bezierCurve: MaterialEasing.standardDecel - } - } - } + PropertyAction { + } - component Anim: NumberAnimation { - target: root - property: root.animateProp.split(",").length === 1 ? root.animateProp : "" - properties: root.animateProp.split(",").length > 1 ? root.animateProp : "" - duration: root.animateDuration / 2 - easing.type: Easing.BezierSpline - } + Anim { + easing.bezierCurve: MaterialEasing.standardDecel + to: root.animateTo + } + } + } + + component Anim: NumberAnimation { + duration: root.animateDuration / 2 + easing.type: Easing.BezierSpline + properties: root.animateProp.split(",").length > 1 ? root.animateProp : "" + property: root.animateProp.split(",").length === 1 ? root.animateProp : "" + target: root + } } diff --git a/Components/CustomTextField.qml b/Components/CustomTextField.qml index b739901..16c39a6 100644 --- a/Components/CustomTextField.qml +++ b/Components/CustomTextField.qml @@ -2,75 +2,74 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls -import qs.Helpers import qs.Config -import qs.Modules TextField { - id: root + id: root - color: DynamicColors.palette.m3onSurface - placeholderTextColor: DynamicColors.palette.m3outline - font.family: Appearance.font.family.sans - font.pointSize: Appearance.font.size.smaller - renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering - cursorVisible: !readOnly + background: null + color: DynamicColors.palette.m3onSurface + cursorVisible: !readOnly + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + placeholderTextColor: DynamicColors.palette.m3outline + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering - background: null + Behavior on color { + CAnim { + } + } + cursorDelegate: CustomRect { + id: cursor - cursorDelegate: CustomRect { - id: cursor + property bool disableBlink - property bool disableBlink + color: DynamicColors.palette.m3primary + implicitWidth: 2 + radius: Appearance.rounding.normal - implicitWidth: 2 - color: DynamicColors.palette.m3primary - radius: Appearance.rounding.normal + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } - Connections { - target: root + Connections { + function onCursorPositionChanged(): void { + if (root.activeFocus && root.cursorVisible) { + cursor.opacity = 1; + cursor.disableBlink = true; + enableBlink.restart(); + } + } - function onCursorPositionChanged(): void { - if (root.activeFocus && root.cursorVisible) { - cursor.opacity = 1; - cursor.disableBlink = true; - enableBlink.restart(); - } - } - } + target: root + } - Timer { - id: enableBlink + Timer { + id: enableBlink - interval: 100 - onTriggered: cursor.disableBlink = false - } + interval: 100 - Timer { - running: root.activeFocus && root.cursorVisible && !cursor.disableBlink - repeat: true - triggeredOnStart: true - interval: 500 - onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 - } + onTriggered: cursor.disableBlink = false + } - Binding { - when: !root.activeFocus || !root.cursorVisible - cursor.opacity: 0 - } + Timer { + interval: 500 + repeat: true + running: root.activeFocus && root.cursorVisible && !cursor.disableBlink + triggeredOnStart: true - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.small - } - } - } + onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 + } - Behavior on color { - CAnim {} - } - - Behavior on placeholderTextColor { - CAnim {} - } + Binding { + cursor.opacity: 0 + when: !root.activeFocus || !root.cursorVisible + } + } + Behavior on placeholderTextColor { + CAnim { + } + } } diff --git a/Components/CustomTooltip.qml b/Components/CustomTooltip.qml index 2211849..59cb1ad 100644 --- a/Components/CustomTooltip.qml +++ b/Components/CustomTooltip.qml @@ -3,21 +3,23 @@ import QtQuick.Controls import qs.Components ToolTip { - id: root - property bool extraVisibleCondition: true - property bool alternativeVisibleCondition: false - readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition - verticalPadding: 5 - horizontalPadding: 10 - background: null + id: root - visible: internalVisibleCondition - - contentItem: CustomTooltipContent { - id: contentItem - text: root.text - shown: root.internalVisibleCondition - horizontalPadding: root.horizontalPadding - verticalPadding: root.verticalPadding - } + property bool alternativeVisibleCondition: false + property bool extraVisibleCondition: true + readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition + + background: null + horizontalPadding: 10 + verticalPadding: 5 + visible: internalVisibleCondition + + contentItem: CustomTooltipContent { + id: contentItem + + horizontalPadding: root.horizontalPadding + shown: root.internalVisibleCondition + text: root.text + verticalPadding: root.verticalPadding + } } diff --git a/Components/CustomTooltipContent.qml b/Components/CustomTooltipContent.qml index 6a399cb..8c70d19 100644 --- a/Components/CustomTooltipContent.qml +++ b/Components/CustomTooltipContent.qml @@ -1,48 +1,54 @@ import QtQuick import qs.Components -import qs.Modules import qs.Config Item { - id: root - required property string text - property bool shown: false - property real horizontalPadding: 10 - property real verticalPadding: 5 - implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding - implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding + id: root - property bool isVisible: backgroundRectangle.implicitHeight > 0 + property real horizontalPadding: 10 + property bool isVisible: backgroundRectangle.implicitHeight > 0 + property bool shown: false + required property string text + property real verticalPadding: 5 - Rectangle { - id: backgroundRectangle - anchors { - bottom: root.bottom - horizontalCenter: root.horizontalCenter - } - color: DynamicColors.tPalette.m3inverseSurface ?? "#3C4043" - radius: 8 - opacity: shown ? 1 : 0 - implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0 - implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0 - clip: true + implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding + implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding - Behavior on implicitWidth { - Anim {} - } - Behavior on implicitHeight { - Anim {} - } - Behavior on opacity { - Anim {} - } + Rectangle { + id: backgroundRectangle - CustomText { - id: tooltipTextObject - anchors.centerIn: parent - text: root.text - color: DynamicColors.palette.m3inverseOnSurface ?? "#FFFFFF" - wrapMode: Text.Wrap - } - } + clip: true + color: DynamicColors.tPalette.m3inverseSurface ?? "#3C4043" + implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0 + implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0 + opacity: shown ? 1 : 0 + radius: 8 + + Behavior on implicitHeight { + Anim { + } + } + Behavior on implicitWidth { + Anim { + } + } + Behavior on opacity { + Anim { + } + } + + anchors { + bottom: root.bottom + horizontalCenter: root.horizontalCenter + } + + CustomText { + id: tooltipTextObject + + anchors.centerIn: parent + color: DynamicColors.palette.m3inverseOnSurface ?? "#FFFFFF" + text: root.text + wrapMode: Text.Wrap + } + } } diff --git a/Components/Elevation.qml b/Components/Elevation.qml index 83f9639..26b8fe6 100644 --- a/Components/Elevation.qml +++ b/Components/Elevation.qml @@ -1,18 +1,18 @@ import qs.Config -import qs.Modules import QtQuick import QtQuick.Effects RectangularShadow { - property int level - property real dp: [0, 1, 3, 6, 8, 12][level] + property real dp: [0, 1, 3, 6, 8, 12][level] + property int level - color: Qt.alpha(DynamicColors.palette.m3shadow, 0.7) - blur: (dp * 5) ** 0.7 - spread: -dp * 0.3 + (dp * 0.1) ** 2 - offset.y: dp / 2 + blur: (dp * 5) ** 0.7 + color: Qt.alpha(DynamicColors.palette.m3shadow, 0.7) + offset.y: dp / 2 + spread: -dp * 0.3 + (dp * 0.1) ** 2 - Behavior on dp { - Anim {} - } + Behavior on dp { + Anim { + } + } } diff --git a/Components/ExtraIndicator.qml b/Components/ExtraIndicator.qml index e67c284..da0302c 100644 --- a/Components/ExtraIndicator.qml +++ b/Components/ExtraIndicator.qml @@ -1,49 +1,44 @@ import qs.Config -import qs.Modules import QtQuick CustomRect { - required property int extra + required property int extra - anchors.right: parent.right - anchors.margins: 8 + anchors.margins: 8 + anchors.right: parent.right + color: DynamicColors.palette.m3tertiary + implicitHeight: count.implicitHeight + 4 * 2 + implicitWidth: count.implicitWidth + 8 * 2 + opacity: extra > 0 ? 1 : 0 + radius: 8 + scale: extra > 0 ? 1 : 0.5 - color: DynamicColors.palette.m3tertiary - radius: 8 - - implicitWidth: count.implicitWidth + 8 * 2 - implicitHeight: count.implicitHeight + 4 * 2 - - opacity: extra > 0 ? 1 : 0 - scale: extra > 0 ? 1 : 0.5 - - Elevation { - anchors.fill: parent - radius: parent.radius - opacity: parent.opacity - z: -1 - level: 2 - } - - CustomText { - id: count - - anchors.centerIn: parent - animate: parent.opacity > 0 - text: qsTr("+%1").arg(parent.extra) - color: DynamicColors.palette.m3onTertiary - } - - Behavior on opacity { - Anim { + Behavior on opacity { + Anim { duration: MaterialEasing.expressiveEffectsTime - } - } - - Behavior on scale { - Anim { + } + } + Behavior on scale { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } + + Elevation { + anchors.fill: parent + level: 2 + opacity: parent.opacity + radius: parent.radius + z: -1 + } + + CustomText { + id: count + + anchors.centerIn: parent + animate: parent.opacity > 0 + color: DynamicColors.palette.m3onTertiary + text: qsTr("+%1").arg(parent.extra) + } } diff --git a/Components/FilledSlider.qml b/Components/FilledSlider.qml index f5d3615..bab8051 100644 --- a/Components/FilledSlider.qml +++ b/Components/FilledSlider.qml @@ -1,146 +1,141 @@ import QtQuick import QtQuick.Templates -import qs.Helpers import qs.Config -import qs.Modules Slider { - id: root + id: root - required property string icon - property real oldValue - property bool initialized property color color: DynamicColors.palette.m3secondary + required property string icon + property bool initialized + property real oldValue - orientation: Qt.Vertical + orientation: Qt.Vertical - background: CustomRect { - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + background: CustomRect { + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.full - CustomRect { - anchors.left: parent.left - anchors.right: parent.right + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + color: root.color + implicitHeight: parent.height - y + radius: parent.radius + y: root.handle.y + } + } + handle: Item { + id: handle - y: root.handle.y - implicitHeight: parent.height - y + property alias moving: icon.moving - color: root.color - radius: parent.radius - } - } + implicitHeight: root.width + implicitWidth: root.width + y: root.visualPosition * (root.availableHeight - height) - handle: Item { - id: handle + Elevation { + anchors.fill: parent + level: handleInteraction.containsMouse ? 2 : 1 + radius: rect.radius + } - property alias moving: icon.moving + CustomRect { + id: rect - y: root.visualPosition * (root.availableHeight - height) - implicitWidth: root.width - implicitHeight: root.width + anchors.fill: parent + color: DynamicColors.palette.m3inverseSurface + radius: Appearance.rounding.full - Elevation { - anchors.fill: parent - radius: rect.radius - level: handleInteraction.containsMouse ? 2 : 1 - } + MouseArea { + id: handleInteraction - CustomRect { - id: rect + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } - anchors.fill: parent + MaterialIcon { + id: icon - color: DynamicColors.palette.m3inverseSurface - radius: Appearance.rounding.full + property bool moving - MouseArea { - id: handleInteraction + function update(): void { + animate = !moving; + binding.when = moving; + font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; + font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + } - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.NoButton - } + anchors.centerIn: parent + color: DynamicColors.palette.m3inverseOnSurface + text: root.icon - MaterialIcon { - id: icon + onMovingChanged: anim.restart() - property bool moving + Binding { + id: binding - function update(): void { - animate = !moving; - binding.when = moving; - font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; - font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; - } + property: "text" + target: icon + value: Math.round(root.value * 100) + when: false + } - text: root.icon - color: DynamicColors.palette.m3inverseOnSurface - anchors.centerIn: parent + SequentialAnimation { + id: anim - onMovingChanged: anim.restart() + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "scale" + target: icon + to: 0 + } - Binding { - id: binding + ScriptAction { + script: icon.update() + } - target: icon - property: "text" - value: Math.round(root.value * 100) - when: false - } + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + property: "scale" + target: icon + to: 1 + } + } + } + } + } + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } - SequentialAnimation { - id: anim + onPressedChanged: handle.moving = pressed + onValueChanged: { + if (!initialized) { + initialized = true; + return; + } + if (Math.abs(value - oldValue) < 0.01) + return; + oldValue = value; + handle.moving = true; + stateChangeDelay.restart(); + } - Anim { - target: icon - property: "scale" - to: 0 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - ScriptAction { - script: icon.update() - } - Anim { - target: icon - property: "scale" - to: 1 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - } - } - } + Timer { + id: stateChangeDelay - onPressedChanged: handle.moving = pressed + interval: 500 - onValueChanged: { - if (!initialized) { - initialized = true; - return; - } - if (Math.abs(value - oldValue) < 0.01) - return; - oldValue = value; - handle.moving = true; - stateChangeDelay.restart(); - } - - Timer { - id: stateChangeDelay - - interval: 500 - onTriggered: { - if (!root.pressed) - handle.moving = false; - } - } - - Behavior on value { - Anim { - duration: Appearance.anim.durations.large - } - } + onTriggered: { + if (!root.pressed) + handle.moving = false; + } + } } diff --git a/Components/IconButton.qml b/Components/IconButton.qml index 6bbc64c..e55289a 100644 --- a/Components/IconButton.qml +++ b/Components/IconButton.qml @@ -1,82 +1,80 @@ import qs.Config -import qs.Modules import QtQuick CustomRect { - id: root + id: root - enum Type { - Filled, - Tonal, - Text - } + enum Type { + Filled, + Tonal, + Text + } - property alias icon: label.text - property bool checked - property bool toggle - property real padding: type === IconButton.Text ? 10 / 2 : 7 - property alias font: label.font - property int type: IconButton.Filled - property bool disabled + property color activeColour: type === IconButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary + property color activeOnColour: type === IconButton.Filled ? DynamicColors.palette.m3onPrimary : type === IconButton.Tonal ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3primary + property bool checked + property bool disabled + property color disabledColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1) + property color disabledOnColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38) + property alias font: label.font + property alias icon: label.text + property color inactiveColour: { + if (!toggle && type === IconButton.Filled) + return DynamicColors.palette.m3primary; + return type === IconButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer; + } + property color inactiveOnColour: { + if (!toggle && type === IconButton.Filled) + return DynamicColors.palette.m3onPrimary; + return type === IconButton.Tonal ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant; + } + property bool internalChecked + property alias label: label + property real padding: type === IconButton.Text ? 10 / 2 : 7 + property alias radiusAnim: radiusAnim + property alias stateLayer: stateLayer + property bool toggle + property int type: IconButton.Filled - property alias stateLayer: stateLayer - property alias label: label - property alias radiusAnim: radiusAnim + signal clicked - property bool internalChecked - property color activeColour: type === IconButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary - property color inactiveColour: { - if (!toggle && type === IconButton.Filled) - return DynamicColors.palette.m3primary; - return type === IconButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer; - } - property color activeOnColour: type === IconButton.Filled ? DynamicColors.palette.m3onPrimary : type === IconButton.Tonal ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3primary - property color inactiveOnColour: { - if (!toggle && type === IconButton.Filled) - return DynamicColors.palette.m3onPrimary; - return type === IconButton.Tonal ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant; - } - property color disabledColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1) - property color disabledOnColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38) + color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + implicitHeight: label.implicitHeight + padding * 2 + implicitWidth: implicitHeight + radius: internalChecked ? 6 : implicitHeight / 2 * Math.min(1, 1) - signal clicked + Behavior on radius { + Anim { + id: radiusAnim - onCheckedChanged: internalChecked = checked + } + } - radius: internalChecked ? 6 : implicitHeight / 2 * Math.min(1, 1) - color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + onCheckedChanged: internalChecked = checked - implicitWidth: implicitHeight - implicitHeight: label.implicitHeight + padding * 2 + StateLayer { + id: stateLayer - StateLayer { - id: stateLayer + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + } - function onClicked(): void { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); - } - } + MaterialIcon { + id: label - MaterialIcon { - id: label + anchors.centerIn: parent + color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour + fill: !root.toggle || root.internalChecked ? 1 : 0 - anchors.centerIn: parent - color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour - fill: !root.toggle || root.internalChecked ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - Behavior on radius { - Anim { - id: radiusAnim - } - } + Behavior on fill { + Anim { + } + } + } } diff --git a/Components/MarqueeText.qml b/Components/MarqueeText.qml new file mode 100644 index 0000000..925b409 --- /dev/null +++ b/Components/MarqueeText.qml @@ -0,0 +1,221 @@ +import QtQuick +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 + property real fadeStrengthMoving: 1.0 + property alias font: elideText.font + property int gap: 40 + property alias horizontalAlignment: elideText.horizontalAlignment + property bool leftFadeEnabled: false + property real leftFadeStrength: overflowing && leftFadeEnabled ? fadeStrengthMoving : fadeStrengthIdle + property int leftFadeWidth: 28 + property bool marqueeEnabled: true + readonly property bool overflowing: metrics.width > root.width + property int pauseMs: 1200 + property real pixelsPerSecond: 40 + property real rightFadeStrength: overflowing ? fadeStrengthMoving : fadeStrengthIdle + property int rightFadeWidth: 28 + property bool sliding: false + property alias text: elideText.text + + function durationForDistance(px): int { + 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 + + Behavior on leftFadeStrength { + Anim { + } + } + Behavior on rightFadeStrength { + Anim { + } + } + + onTextChanged: resetMarquee() + onVisibleChanged: if (!visible) + resetMarquee() + onWidthChanged: resetMarquee() + + TextMetrics { + id: metrics + + font: elideText.font + text: elideText.text + } + + CustomText { + id: elideText + + anchors.verticalCenter: parent.verticalCenter + animate: root.animate + animateProp: "scale,opacity" + color: root.color + elide: Text.ElideNone + visible: !root.overflowing + width: root.width + } + + Item { + id: marqueeViewport + + anchors.fill: parent + clip: true + layer.enabled: true + visible: root.overflowing + + layer.effect: OpacityMask { + maskSource: rightFadeMask + } + + Item { + id: strip + + anchors.verticalCenter: parent.verticalCenter + height: t1.implicitHeight + width: t1.width + root.gap + t2.width + x: 0 + + CustomText { + id: t1 + + animate: root.animate + animateProp: "opacity" + color: root.color + text: elideText.text + } + + CustomText { + id: t2 + + animate: root.animate + animateProp: "opacity" + color: root.color + text: t1.text + x: t1.width + root.gap + } + } + + SequentialAnimation { + id: marqueeAnim + + running: false + + onFinished: pauseTimer.restart() + + ScriptAction { + script: { + root.sliding = true; + root.leftFadeEnabled = true; + } + } + + Anim { + duration: root.durationForDistance(t1.width) + easing.bezierCurve: Easing.Linear + easing.type: Easing.Linear + from: 0 + property: "x" + target: strip + to: -t1.width + } + + ScriptAction { + script: { + root.leftFadeEnabled = false; + } + } + + Anim { + duration: root.durationForDistance(root.gap) + easing.bezierCurve: Easing.Linear + easing.type: Easing.Linear + from: -t1.width + property: "x" + target: strip + to: -(t1.width + root.gap) + } + + ScriptAction { + script: { + root.sliding = false; + strip.x = 0; + } + } + } + + Timer { + id: pauseTimer + + interval: root.pauseMs + repeat: false + running: true + + onTriggered: { + if (root.marqueeEnabled) + marqueeAnim.start(); + } + } + } + + Rectangle { + id: rightFadeMask + + readonly property real fadeStartPos: { + const w = Math.max(1, width); + return Math.max(0, Math.min(1, (w - root.rightFadeWidth) / w)); + } + readonly property real leftFadeEndPos: { + const w = Math.max(1, width); + return Math.max(0, Math.min(1, root.leftFadeWidth / w)); + } + + anchors.fill: marqueeViewport + layer.enabled: true + visible: false + + gradient: Gradient { + orientation: Gradient.Horizontal + + GradientStop { + color: Qt.rgba(1, 1, 1, 1.0 - root.leftFadeStrength) + position: 0.0 + } + + GradientStop { + color: Qt.rgba(1, 1, 1, 1.0) + position: rightFadeMask.leftFadeEndPos + } + + GradientStop { + color: Qt.rgba(1, 1, 1, 1.0) + position: rightFadeMask.fadeStartPos + } + + GradientStop { + color: Qt.rgba(1, 1, 1, 1.0 - root.rightFadeStrength) + position: 1.0 + } + } + } +} diff --git a/Components/MaterialIcon.qml b/Components/MaterialIcon.qml index 08a0792..031c6a7 100644 --- a/Components/MaterialIcon.qml +++ b/Components/MaterialIcon.qml @@ -1,15 +1,15 @@ import qs.Config CustomText { - property real fill - property int grade: DynamicColors.light ? 0 : -25 + property real fill + property int grade: DynamicColors.light ? 0 : -25 - font.family: "Material Symbols Rounded" - font.pointSize: 15 - font.variableAxes: ({ - FILL: fill.toFixed(1), - GRAD: grade, - opsz: fontInfo.pixelSize, - wght: fontInfo.weight - }) + font.family: "Material Symbols Rounded" + font.pointSize: 15 + font.variableAxes: ({ + FILL: fill.toFixed(1), + GRAD: grade, + opsz: fontInfo.pixelSize, + wght: fontInfo.weight + }) } diff --git a/Components/Menu.qml b/Components/Menu.qml new file mode 100644 index 0000000..8222359 --- /dev/null +++ b/Components/Menu.qml @@ -0,0 +1,115 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config + +Elevation { + id: root + + property MenuItem active: items[0] ?? null + property bool expanded + property list items + + signal itemSelected(item: MenuItem) + + 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.normal + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } + + CustomClippingRect { + anchors.fill: parent + color: DynamicColors.palette.m3surfaceContainer + radius: parent.radius + + ColumnLayout { + id: column + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 5 + + Repeater { + model: root.items + + CustomRect { + id: item + + readonly property bool active: modelData === root.active + required property int index + required property MenuItem modelData + + Layout.fillWidth: true + implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 + + CustomRect { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small + color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0) + radius: Appearance.rounding.normal - Appearance.padding.small + + StateLayer { + 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 { + id: menuOptionRow + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant + text: item.modelData.icon + } + + CustomText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + text: item.modelData.text + } + + Loader { + Layout.alignment: Qt.AlignVCenter + active: item.modelData.trailingIcon.length > 0 + visible: active + + sourceComponent: MaterialIcon { + color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + text: item.modelData.trailingIcon + } + } + } + } + } + } + } +} diff --git a/Components/MenuItem.qml b/Components/MenuItem.qml new file mode 100644 index 0000000..2378dd9 --- /dev/null +++ b/Components/MenuItem.qml @@ -0,0 +1,12 @@ +import QtQuick + +QtObject { + property string activeIcon: icon + property string activeText: text + property string icon + required property string text + property string trailingIcon + property var value + + signal clicked +} diff --git a/Components/OpacityMask.qml b/Components/OpacityMask.qml index 22e4249..8203f9f 100644 --- a/Components/OpacityMask.qml +++ b/Components/OpacityMask.qml @@ -2,8 +2,8 @@ import Quickshell import QtQuick ShaderEffect { - required property Item source - required property Item maskSource + required property Item maskSource + required property Item source - fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) + fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) } diff --git a/Components/Ref.qml b/Components/Ref.qml index 0a694a4..14f03d6 100644 --- a/Components/Ref.qml +++ b/Components/Ref.qml @@ -1,8 +1,8 @@ import QtQuick QtObject { - required property var service + required property var service - Component.onCompleted: service.refCount++ - Component.onDestruction: service.refCount-- + Component.onCompleted: service.refCount++ + Component.onDestruction: service.refCount-- } diff --git a/Components/SpinBoxRow.qml b/Components/SpinBoxRow.qml new file mode 100644 index 0000000..6686ec7 --- /dev/null +++ b/Components/SpinBoxRow.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Layouts +import qs.Config + +CustomRect { + id: root + + required property string label + required property real max + required property real min + property var onValueModified: function (value) {} + property real step: 1 + required property real value + + Layout.fillWidth: true + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + + Behavior on implicitHeight { + Anim { + } + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal + + CustomText { + Layout.fillWidth: true + text: root.label + } + + CustomSpinBox { + max: root.max + min: root.min + step: root.step + value: root.value + + onValueModified: value => { + root.onValueModified(value); + } + } + } +} diff --git a/Components/StateLayer.qml b/Components/StateLayer.qml index 6b58449..bde8ce3 100644 --- a/Components/StateLayer.qml +++ b/Components/StateLayer.qml @@ -1,96 +1,96 @@ import qs.Config -import qs.Modules import QtQuick MouseArea { - id: root + id: root - property bool disabled - property color color: DynamicColors.palette.m3onSurface - property real radius: parent?.radius ?? 0 - property alias rect: hoverLayer + property color color: DynamicColors.palette.m3onSurface + property bool disabled + property real radius: parent?.radius ?? 0 + property alias rect: hoverLayer - function onClicked(): void { - } + function onClicked(): void { + } - anchors.fill: parent + anchors.fill: parent + cursorShape: disabled ? undefined : Qt.PointingHandCursor + enabled: !disabled + hoverEnabled: true - enabled: !disabled - cursorShape: disabled ? undefined : Qt.PointingHandCursor - hoverEnabled: true + onClicked: event => !disabled && onClicked(event) + onPressed: event => { + if (disabled) + return; - onPressed: event => { - if (disabled) - return; + rippleAnim.x = event.x; + rippleAnim.y = event.y; - rippleAnim.x = event.x; - rippleAnim.y = event.y; + const dist = (ox, oy) => ox * ox + oy * oy; + rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); + rippleAnim.restart(); + } - rippleAnim.restart(); - } + SequentialAnimation { + id: rippleAnim - onClicked: event => !disabled && onClicked(event) + property real radius + property real x + property real y - SequentialAnimation { - id: rippleAnim + PropertyAction { + property: "x" + target: ripple + value: rippleAnim.x + } - property real x - property real y - property real radius + PropertyAction { + property: "y" + target: ripple + value: rippleAnim.y + } - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 - easing.bezierCurve: MaterialEasing.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 - } - } + PropertyAction { + property: "opacity" + target: ripple + value: 0.08 + } - CustomClippingRect { - id: hoverLayer + Anim { + easing.bezierCurve: MaterialEasing.standardDecel + from: 0 + properties: "implicitWidth,implicitHeight" + target: ripple + to: rippleAnim.radius * 2 + } - anchors.fill: parent + Anim { + property: "opacity" + target: ripple + to: 0 + } + } + + CustomClippingRect { + id: hoverLayer + + anchors.fill: parent border.pixelAligned: false + color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0) + radius: root.radius - color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0) - radius: root.radius + CustomRect { + id: ripple - CustomRect { - id: ripple - - radius: 1000 - color: root.color - opacity: 0 border.pixelAligned: false + color: root.color + opacity: 0 + radius: 1000 - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 - } - } - } + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } } diff --git a/Components/Toast/ToastItem.qml b/Components/Toast/ToastItem.qml index a245813..a726087 100644 --- a/Components/Toast/ToastItem.qml +++ b/Components/Toast/ToastItem.qml @@ -1,135 +1,131 @@ import ZShell import QtQuick import QtQuick.Layouts -import qs.Modules import qs.Components -import qs.Helpers import qs.Config CustomRect { - id: root + id: root - required property Toast modelData + required property Toast modelData - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + anchors.left: parent.left + anchors.right: parent.right + border.color: { + let colour = DynamicColors.palette.m3outlineVariant; + if (root.modelData.type === Toast.Success) + colour = DynamicColors.palette.m3success; + if (root.modelData.type === Toast.Warning) + colour = DynamicColors.palette.m3secondaryContainer; + if (root.modelData.type === Toast.Error) + colour = DynamicColors.palette.m3error; + return Qt.alpha(colour, 0.3); + } + border.width: 1 + color: { + if (root.modelData.type === Toast.Success) + return DynamicColors.palette.m3successContainer; + if (root.modelData.type === Toast.Warning) + return DynamicColors.palette.m3secondary; + if (root.modelData.type === Toast.Error) + return DynamicColors.palette.m3errorContainer; + return DynamicColors.palette.m3surface; + } + implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + radius: Appearance.rounding.normal - radius: Appearance.rounding.normal - color: { - if (root.modelData.type === Toast.Success) - return DynamicColors.palette.m3successContainer; - if (root.modelData.type === Toast.Warning) - return DynamicColors.palette.m3secondary; - if (root.modelData.type === Toast.Error) - return DynamicColors.palette.m3errorContainer; - return DynamicColors.palette.m3surface; - } + Behavior on border.color { + CAnim { + } + } - border.width: 1 - border.color: { - let colour = DynamicColors.palette.m3outlineVariant; - if (root.modelData.type === Toast.Success) - colour = DynamicColors.palette.m3success; - if (root.modelData.type === Toast.Warning) - colour = DynamicColors.palette.m3secondaryContainer; - if (root.modelData.type === Toast.Error) - colour = DynamicColors.palette.m3error; - return Qt.alpha(colour, 0.3); - } + Elevation { + anchors.fill: parent + level: 3 + opacity: parent.opacity + radius: parent.radius + z: -1 + } - Elevation { - anchors.fill: parent - radius: parent.radius - opacity: parent.opacity - z: -1 - level: 3 - } + RowLayout { + id: layout - RowLayout { - id: layout + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.margins: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal - anchors.fill: parent - anchors.margins: Appearance.padding.smaller - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal + CustomRect { + color: { + if (root.modelData.type === Toast.Success) + return DynamicColors.palette.m3success; + if (root.modelData.type === Toast.Warning) + return DynamicColors.palette.m3secondaryContainer; + if (root.modelData.type === Toast.Error) + return DynamicColors.palette.m3error; + return DynamicColors.palette.m3surfaceContainerHigh; + } + implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.normal - CustomRect { - radius: Appearance.rounding.normal - color: { - if (root.modelData.type === Toast.Success) - return DynamicColors.palette.m3success; - if (root.modelData.type === Toast.Warning) - return DynamicColors.palette.m3secondaryContainer; - if (root.modelData.type === Toast.Error) - return DynamicColors.palette.m3error; - return DynamicColors.palette.m3surfaceContainerHigh; - } + MaterialIcon { + id: icon - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + anchors.centerIn: parent + color: { + if (root.modelData.type === Toast.Success) + return DynamicColors.palette.m3onSuccess; + if (root.modelData.type === Toast.Warning) + return DynamicColors.palette.m3onSecondaryContainer; + if (root.modelData.type === Toast.Error) + return DynamicColors.palette.m3onError; + return DynamicColors.palette.m3onSurfaceVariant; + } + font.pointSize: Math.round(Appearance.font.size.large * 1.2) + text: root.modelData.icon + } + } - MaterialIcon { - id: icon + ColumnLayout { + Layout.fillWidth: true + spacing: 0 - anchors.centerIn: parent - text: root.modelData.icon - color: { - if (root.modelData.type === Toast.Success) - return DynamicColors.palette.m3onSuccess; - if (root.modelData.type === Toast.Warning) - return DynamicColors.palette.m3onSecondaryContainer; - if (root.modelData.type === Toast.Error) - return DynamicColors.palette.m3onError; - return DynamicColors.palette.m3onSurfaceVariant; - } - font.pointSize: Math.round(Appearance.font.size.large * 1.2) - } - } + CustomText { + id: title - ColumnLayout { - Layout.fillWidth: true - spacing: 0 + Layout.fillWidth: true + color: { + if (root.modelData.type === Toast.Success) + return DynamicColors.palette.m3onSuccessContainer; + if (root.modelData.type === Toast.Warning) + return DynamicColors.palette.m3onSecondary; + if (root.modelData.type === Toast.Error) + return DynamicColors.palette.m3onErrorContainer; + return DynamicColors.palette.m3onSurface; + } + elide: Text.ElideRight + font.pointSize: Appearance.font.size.normal + text: root.modelData.title + } - CustomText { - id: title - - Layout.fillWidth: true - text: root.modelData.title - color: { - if (root.modelData.type === Toast.Success) - return DynamicColors.palette.m3onSuccessContainer; - if (root.modelData.type === Toast.Warning) - return DynamicColors.palette.m3onSecondary; - if (root.modelData.type === Toast.Error) - return DynamicColors.palette.m3onErrorContainer; - return DynamicColors.palette.m3onSurface; - } - font.pointSize: Appearance.font.size.normal - elide: Text.ElideRight - } - - CustomText { - Layout.fillWidth: true - textFormat: Text.StyledText - text: root.modelData.message - color: { - if (root.modelData.type === Toast.Success) - return DynamicColors.palette.m3onSuccessContainer; - if (root.modelData.type === Toast.Warning) - return DynamicColors.palette.m3onSecondary; - if (root.modelData.type === Toast.Error) - return DynamicColors.palette.m3onErrorContainer; - return DynamicColors.palette.m3onSurface; - } - opacity: 0.8 - elide: Text.ElideRight - } - } - } - - Behavior on border.color { - CAnim {} - } + CustomText { + Layout.fillWidth: true + color: { + if (root.modelData.type === Toast.Success) + return DynamicColors.palette.m3onSuccessContainer; + if (root.modelData.type === Toast.Warning) + return DynamicColors.palette.m3onSecondary; + if (root.modelData.type === Toast.Error) + return DynamicColors.palette.m3onErrorContainer; + return DynamicColors.palette.m3onSurface; + } + elide: Text.ElideRight + opacity: 0.8 + text: root.modelData.message + textFormat: Text.StyledText + } + } + } } diff --git a/Components/Toast/Toasts.qml b/Components/Toast/Toasts.qml index 3846166..e2fbde6 100644 --- a/Components/Toast/Toasts.qml +++ b/Components/Toast/Toasts.qml @@ -1,144 +1,142 @@ pragma ComponentBehavior: Bound -import qs.Components -import qs.Config -import qs.Modules import ZShell import Quickshell import QtQuick +import qs.Components +import qs.Config Item { - id: root + id: root - readonly property int spacing: Appearance.spacing.small - property bool flag + property bool flag + readonly property int spacing: Appearance.spacing.small - implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 - implicitHeight: { - let h = -spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (!item.modelData.closed && !item.previewHidden) - h += item.implicitHeight + spacing; - } - return h; - } + implicitHeight: { + let h = -spacing; + for (let i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (!item.modelData.closed && !item.previewHidden) + h += item.implicitHeight + spacing; + } + return h; + } + implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 - Repeater { - id: repeater + Repeater { + id: repeater - model: ScriptModel { - values: { - const toasts = []; - let count = 0; - for (const toast of Toaster.toasts) { - toasts.push(toast); - if (!toast.closed) { - count++; - if (count > Config.utilities.maxToasts) - break; - } - } - return toasts; - } - onValuesChanged: root.flagChanged() - } + model: ScriptModel { + values: { + const toasts = []; + let count = 0; + for (const toast of Toaster.toasts) { + toasts.push(toast); + if (!toast.closed) { + count++; + if (count > Config.utilities.maxToasts) + break; + } + } + return toasts; + } - ToastWrapper {} - } + onValuesChanged: root.flagChanged() + } - component ToastWrapper: MouseArea { - id: toast + ToastWrapper { + } + } - required property int index - required property Toast modelData + component ToastWrapper: MouseArea { + id: toast - readonly property bool previewHidden: { - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (Toaster.toasts[i].closed) - extraHidden++; - return index >= Config.utilities.maxToasts + extraHidden; - } + required property int index + required property Toast modelData + readonly property bool previewHidden: { + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (Toaster.toasts[i].closed) + extraHidden++; + return index >= Config.utilities.maxToasts + extraHidden; + } - onPreviewHiddenChanged: { - if (initAnim.running && previewHidden) - initAnim.stop(); - } + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + anchors.bottom: parent.bottom + anchors.bottomMargin: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i) as ToastWrapper; + if (item && !item.modelData.closed && !item.previewHidden) + y += item.implicitHeight + root.spacing; + } + return y; + } + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: toastInner.implicitHeight + opacity: modelData.closed || previewHidden ? 0 : 1 + scale: modelData.closed || previewHidden ? 0.7 : 1 - opacity: modelData.closed || previewHidden ? 0 : 1 - scale: modelData.closed || previewHidden ? 0.7 : 1 + Behavior on anchors.bottomMargin { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } - anchors.bottomMargin: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as ToastWrapper; - if (item && !item.modelData.closed && !item.previewHidden) - y += item.implicitHeight + root.spacing; - } - return y; - } + Component.onCompleted: modelData.lock(this) + onClicked: modelData.close() + onPreviewHiddenChanged: { + if (initAnim.running && previewHidden) + initAnim.stop(); + } - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: toastInner.implicitHeight + Anim { + id: initAnim - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - onClicked: modelData.close() + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + from: 0 + properties: "opacity,scale" + target: toast + to: 1 - Component.onCompleted: modelData.lock(this) + Component.onCompleted: running = !toast.previewHidden + } - Anim { - id: initAnim + ParallelAnimation { + running: toast.modelData.closed - Component.onCompleted: running = !toast.previewHidden + onFinished: toast.modelData.unlock(toast) + onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin - target: toast - properties: "opacity,scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Anim { + property: "opacity" + target: toast + to: 0 + } - ParallelAnimation { - running: toast.modelData.closed - onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin - onFinished: toast.modelData.unlock(toast) + Anim { + property: "scale" + target: toast + to: 0.7 + } + } - Anim { - target: toast - property: "opacity" - to: 0 - } - Anim { - target: toast - property: "scale" - to: 0.7 - } - } + ToastItem { + id: toastInner - ToastItem { - id: toastInner - - modelData: toast.modelData - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on anchors.bottomMargin { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } + modelData: toast.modelData + } + } } diff --git a/Config/AccentColor.qml b/Config/AccentColor.qml index 2964332..82a0157 100644 --- a/Config/AccentColor.qml +++ b/Config/AccentColor.qml @@ -1,12 +1,13 @@ import Quickshell.Io JsonObject { - property Accents accents: Accents {} + property Accents accents: Accents { + } - component Accents: JsonObject { - property string primary: "#4080ff" - property string primaryAlt: "#60a0ff" - property string warning: "#ff6b6b" - property string warningAlt: "#ff8787" - } + component Accents: JsonObject { + property string primary: "#4080ff" + property string primaryAlt: "#60a0ff" + property string warning: "#ff6b6b" + property string warningAlt: "#ff8787" + } } diff --git a/Config/Appearance.qml b/Config/Appearance.qml index 00efc96..d2bf19e 100644 --- a/Config/Appearance.qml +++ b/Config/Appearance.qml @@ -3,12 +3,12 @@ pragma Singleton import Quickshell Singleton { - // Literally just here to shorten accessing stuff :woe: - // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx` - readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding - readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing - readonly property AppearanceConf.Padding padding: Config.appearance.padding - readonly property AppearanceConf.FontStuff font: Config.appearance.font - readonly property AppearanceConf.Anim anim: Config.appearance.anim - readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency + readonly property AppearanceConf.Anim anim: Config.appearance.anim + readonly property AppearanceConf.FontStuff font: Config.appearance.font + readonly property AppearanceConf.Padding padding: Config.appearance.padding + // Literally just here to shorten accessing stuff :woe: + // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx` + readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding + readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing + readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency } diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index 5ccaec5..c76b05e 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -1,94 +1,95 @@ import Quickshell.Io JsonObject { - property Rounding rounding: Rounding {} - property Spacing spacing: Spacing {} - property Padding padding: Padding {} - property FontStuff font: FontStuff {} - property Anim anim: Anim {} - property Transparency transparency: Transparency {} + property Anim anim: Anim { + } + property FontStuff font: FontStuff { + } + property Padding padding: Padding { + } + property Rounding rounding: Rounding { + } + property Spacing spacing: Spacing { + } + property Transparency transparency: Transparency { + } - component Rounding: JsonObject { - property real scale: 1 - property int small: 12 * scale - property int normal: 17 * scale - property int large: 25 * scale - property int full: 1000 * scale - } - - component Spacing: JsonObject { - property real scale: 1 - property int small: 7 * scale - property int smaller: 10 * scale - property int normal: 12 * scale - property int larger: 15 * scale - property int large: 20 * scale - } - - component Padding: JsonObject { - property real scale: 1 - property int small: 5 * scale - property int smaller: 7 * scale - property int normal: 10 * scale - property int larger: 12 * scale - property int large: 15 * scale - } - - component FontFamily: JsonObject { - property string sans: "Segoe UI Variable Text" - property string mono: "CaskaydiaCove NF" - property string material: "Material Symbols Rounded" - property string clock: "Rubik" - } - - component FontSize: JsonObject { - property real scale: 1 - property int small: 11 * scale - property int smaller: 12 * scale - property int normal: 13 * scale - property int larger: 15 * scale - property int large: 18 * scale - property int extraLarge: 28 * scale - } - - component FontStuff: JsonObject { - property FontFamily family: FontFamily {} - property FontSize size: FontSize {} - } - - component AnimCurves: JsonObject { - property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] - property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] - property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] - property list standard: [0.2, 0, 0, 1, 1, 1] - property list standardAccel: [0.3, 0, 1, 1, 1, 1] - property list standardDecel: [0, 0, 0, 1, 1, 1] - property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] - property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] - } - - component AnimDurations: JsonObject { - property real scale: 1 - property int small: 200 * scale - property int normal: 400 * scale - property int large: 600 * scale - property int extraLarge: 1000 * scale - property int expressiveFastSpatial: 350 * scale - property int expressiveDefaultSpatial: 500 * scale - property int expressiveEffects: 200 * scale - } - - component Anim: JsonObject { - property real mediaGifSpeedAdjustment: 300 - property real sessionGifSpeed: 0.7 - property AnimCurves curves: AnimCurves {} - property AnimDurations durations: AnimDurations {} - } - - component Transparency: JsonObject { - property bool enabled: false - property real base: 0.85 - property real layers: 0.4 - } + component Anim: JsonObject { + property AnimCurves curves: AnimCurves { + } + property AnimDurations durations: AnimDurations { + } + property real mediaGifSpeedAdjustment: 300 + property real sessionGifSpeed: 0.7 + } + component AnimCurves: JsonObject { + property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + property list standard: [0.2, 0, 0, 1, 1, 1] + property list standardAccel: [0.3, 0, 1, 1, 1, 1] + property list standardDecel: [0, 0, 0, 1, 1, 1] + } + component AnimDurations: JsonObject { + property int expressiveDefaultSpatial: 500 * scale + property int expressiveEffects: 200 * scale + property int expressiveFastSpatial: 350 * scale + property int extraLarge: 1000 * scale + property int large: 600 * scale + property int normal: 400 * scale + property real scale: 1 + property int small: 200 * scale + } + component FontFamily: JsonObject { + property string clock: "Rubik" + property string material: "Material Symbols Rounded" + property string mono: "CaskaydiaCove NF" + property string sans: "Segoe UI Variable Text" + } + component FontSize: JsonObject { + property int extraLarge: 28 * scale + property int large: 18 * scale + property int larger: 15 * scale + property int normal: 13 * scale + property real scale: 1 + property int small: 11 * scale + property int smaller: 12 * scale + } + component FontStuff: JsonObject { + property FontFamily family: FontFamily { + } + property FontSize size: FontSize { + } + } + component Padding: JsonObject { + property int large: 15 * scale + property int larger: 12 * scale + property int normal: 10 * scale + property real scale: 1 + property int small: 5 * scale + property int smaller: 7 * scale + } + component Rounding: JsonObject { + property int full: 1000 * scale + property int large: 25 * scale + property int normal: 17 * scale + property real scale: 1 + property int small: 12 * scale + } + component Spacing: JsonObject { + property int large: 20 * scale + property int larger: 15 * scale + property int normal: 12 * scale + property real scale: 1 + property int small: 7 * scale + property int smaller: 10 * scale + } + component Transparency: JsonObject { + property real base: 0.85 + property bool enabled: false + property real layers: 0.4 + } } diff --git a/Config/BackgroundConfig.qml b/Config/BackgroundConfig.qml index f34b832..bcae021 100644 --- a/Config/BackgroundConfig.qml +++ b/Config/BackgroundConfig.qml @@ -2,6 +2,6 @@ import Quickshell.Io import qs.Config JsonObject { - property int wallFadeDuration: MaterialEasing.standardTime - property bool enabled: true + property bool enabled: true + property int wallFadeDuration: MaterialEasing.standardTime } diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index cddf9e9..4ec7578 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -2,9 +2,6 @@ import Quickshell.Io JsonObject { property bool autoHide: false - property int rounding: 8 - property Popouts popouts: Popouts {} - property list entries: [ { id: "workspaces", @@ -14,6 +11,10 @@ JsonObject { id: "audio", enabled: true }, + { + id: "media", + enabled: true + }, { id: "resources", enabled: true @@ -59,14 +60,17 @@ JsonObject { enabled: true }, ] + property Popouts popouts: Popouts { + } + property int rounding: 8 component Popouts: JsonObject { - property bool tray: true - property bool audio: true property bool activeWindow: true - property bool resources: true + property bool audio: true property bool clock: true property bool network: true + property bool resources: true + property bool tray: true property bool upower: true } } diff --git a/Config/Config.qml b/Config/Config.qml index 6908c40..9455d7d 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -4,27 +4,28 @@ import Quickshell import Quickshell.Io import ZShell import QtQuick -import qs.Modules as Modules import qs.Helpers import qs.Paths Singleton { id: root - property alias background: adapter.background - property alias barConfig: adapter.barConfig - property alias lock: adapter.lock - property alias overview: adapter.overview - property alias services: adapter.services + property alias appearance: adapter.appearance + property alias background: adapter.background + property alias barConfig: adapter.barConfig + property alias colors: adapter.colors + property alias dashboard: adapter.dashboard + property alias dock: adapter.dock + property alias general: adapter.general + property alias launcher: adapter.launcher + property alias lock: adapter.lock property alias notifs: adapter.notifs + property alias osd: adapter.osd + property alias overview: adapter.overview + property bool recentlySaved: false + property alias services: adapter.services property alias sidebar: adapter.sidebar property alias utilities: adapter.utilities - property alias general: adapter.general - property alias dashboard: adapter.dashboard - property alias appearance: adapter.appearance - property alias osd: adapter.osd - property alias launcher: adapter.launcher - property alias colors: adapter.colors function save(): void { saveTimer.restart(); @@ -32,16 +33,290 @@ Singleton { recentSaveCooldown.restart(); } - property bool recentlySaved: false + function serializeAppearance(): var { + return { + rounding: { + scale: appearance.rounding.scale + }, + spacing: { + scale: appearance.spacing.scale + }, + padding: { + scale: appearance.padding.scale + }, + font: { + family: { + sans: appearance.font.family.sans, + mono: appearance.font.family.mono, + material: appearance.font.family.material, + clock: appearance.font.family.clock + }, + size: { + scale: appearance.font.size.scale + } + }, + anim: { + mediaGifSpeedAdjustment: 300, + sessionGifSpeed: 0.7, + durations: { + scale: appearance.anim.durations.scale + } + }, + transparency: { + enabled: appearance.transparency.enabled, + base: appearance.transparency.base, + layers: appearance.transparency.layers + } + }; + } + + function serializeBackground(): var { + return { + wallFadeDuration: background.wallFadeDuration, + enabled: background.enabled + }; + } + + function serializeBar(): var { + return { + autoHide: barConfig.autoHide, + rounding: barConfig.rounding, + popouts: { + tray: barConfig.popouts.tray, + audio: barConfig.popouts.audio, + activeWindow: barConfig.popouts.activeWindow, + resources: barConfig.popouts.resources, + clock: barConfig.popouts.clock, + network: barConfig.popouts.network, + upower: barConfig.popouts.upower + }, + entries: barConfig.entries + }; + } + + function serializeColors(): var { + return { + schemeType: colors.schemeType + }; + } + + function serializeConfig(): var { + return { + barConfig: serializeBar(), + lock: serializeLock(), + general: serializeGeneral(), + services: serializeServices(), + notifs: serializeNotifs(), + sidebar: serializeSidebar(), + utilities: serializeUtilities(), + dashboard: serializeDashboard(), + appearance: serializeAppearance(), + osd: serializeOsd(), + background: serializeBackground(), + launcher: serializeLauncher(), + colors: serializeColors(), + dock: serializeDock() + }; + } + + function serializeDashboard(): var { + 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, + infoWidth: dashboard.sizes.infoWidth, + infoIconSize: dashboard.sizes.infoIconSize, + dateTimeWidth: dashboard.sizes.dateTimeWidth, + mediaWidth: dashboard.sizes.mediaWidth, + mediaProgressSweep: dashboard.sizes.mediaProgressSweep, + mediaProgressThickness: dashboard.sizes.mediaProgressThickness, + resourceProgessThickness: dashboard.sizes.resourceProgessThickness, + weatherWidth: dashboard.sizes.weatherWidth, + mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, + mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, + resourceSize: dashboard.sizes.resourceSize + } + }; + } + + function serializeDock(): var { + return { + enable: dock.enable, + height: dock.height, + hoverRegionHeight: dock.hoverRegionHeight, + hoverToReveal: dock.hoverToReveal, + pinnedApps: dock.pinnedApps, + pinnedOnStartup: dock.pinnedOnStartup + }; + } + + function serializeGeneral(): var { + return { + logo: general.logo, + wallpaperPath: general.wallpaperPath, + color: { + wallust: general.color.wallust, + mode: general.color.mode, + schemeGeneration: general.color.schemeGeneration, + scheduleDarkStart: general.color.scheduleDarkStart, + scheduleDarkEnd: general.color.scheduleDarkEnd, + neovimColors: general.color.neovimColors + }, + apps: { + terminal: general.apps.terminal, + audio: general.apps.audio, + playback: general.apps.playback, + explorer: general.apps.explorer + }, + idle: { + timouts: general.idle.timeouts + } + }; + } + + function serializeLauncher(): var { + return { + maxAppsShown: launcher.maxAppsShown, + maxWallpapers: launcher.maxWallpapers, + actionPrefix: launcher.actionPrefix, + specialPrefix: launcher.specialPrefix, + useFuzzy: { + apps: launcher.useFuzzy.apps, + actions: launcher.useFuzzy.actions, + schemes: launcher.useFuzzy.schemes, + variants: launcher.useFuzzy.variants, + wallpapers: launcher.useFuzzy.wallpapers + }, + sizes: { + itemWidth: launcher.sizes.itemWidth, + itemHeight: launcher.sizes.itemHeight, + wallpaperWidth: launcher.sizes.wallpaperWidth, + wallpaperHeight: launcher.sizes.wallpaperHeight + }, + actions: launcher.actions + }; + } + + function serializeLock(): var { + return { + recolorLogo: lock.recolorLogo, + enableFprint: lock.enableFprint, + maxFprintTries: lock.maxFprintTries, + blurAmount: lock.blurAmount, + sizes: { + heightMult: lock.sizes.heightMult, + ratio: lock.sizes.ratio, + centerWidth: lock.sizes.centerWidth + } + }; + } + + function serializeNotifs(): var { + return { + expire: notifs.expire, + defaultExpireTimeout: notifs.defaultExpireTimeout, + appNotifCooldown: notifs.appNotifCooldown, + clearThreshold: notifs.clearThreshold, + expandThreshold: notifs.expandThreshold, + actionOnClick: notifs.actionOnClick, + groupPreviewNum: notifs.groupPreviewNum, + sizes: { + width: notifs.sizes.width, + image: notifs.sizes.image, + badge: notifs.sizes.badge + } + }; + } + + function serializeOsd(): var { + return { + enabled: osd.enabled, + hideDelay: osd.hideDelay, + enableBrightness: osd.enableBrightness, + enableMicrophone: osd.enableMicrophone, + allMonBrightness: osd.allMonBrightness, + sizes: { + sliderWidth: osd.sizes.sliderWidth, + sliderHeight: osd.sizes.sliderHeight + } + }; + } + + function serializeServices(): var { + return { + weatherLocation: services.weatherLocation, + useFahrenheit: services.useFahrenheit, + ddcutilService: services.ddcutilService, + useTwelveHourClock: services.useTwelveHourClock, + gpuType: services.gpuType, + audioIncrement: services.audioIncrement, + brightnessIncrement: services.brightnessIncrement, + maxVolume: services.maxVolume, + defaultPlayer: services.defaultPlayer, + playerAliases: services.playerAliases, + visualizerBars: services.visualizerBars + }; + } + + function serializeSidebar(): var { + return { + enabled: sidebar.enabled, + sizes: { + width: sidebar.sizes.width + } + }; + } + + function serializeUtilities(): var { + return { + enabled: utilities.enabled, + maxToasts: utilities.maxToasts, + sizes: { + width: utilities.sizes.width, + toastWidth: utilities.sizes.toastWidth + }, + toasts: { + configLoaded: utilities.toasts.configLoaded, + chargingChanged: utilities.toasts.chargingChanged, + gameModeChanged: utilities.toasts.gameModeChanged, + dndChanged: utilities.toasts.dndChanged, + audioOutputChanged: utilities.toasts.audioOutputChanged, + audioInputChanged: utilities.toasts.audioInputChanged, + capsLockChanged: utilities.toasts.capsLockChanged, + numLockChanged: utilities.toasts.numLockChanged, + kbLayoutChanged: utilities.toasts.kbLayoutChanged, + vpnChanged: utilities.toasts.vpnChanged, + nowPlaying: utilities.toasts.nowPlaying + }, + vpn: { + enabled: utilities.vpn.enabled, + provider: utilities.vpn.provider + } + }; + } ElapsedTimer { id: timer + } Timer { id: saveTimer interval: 500 + onTriggered: { timer.restart(); try { @@ -65,316 +340,80 @@ Singleton { id: recentSaveCooldown interval: 2000 + onTriggered: { root.recentlySaved = false; } } - function serializeConfig(): var { - return { - barConfig: serializeBar(), - lock: serializeLock(), - general: serializeGeneral(), - services: serializeServices(), - notifs: serializeNotifs(), - sidebar: serializeSidebar(), - utilities: serializeUtilities(), - dashboard: serializeDashboard(), - appearance: serializeAppearance(), - osd: serializeOsd(), - background: serializeBackground(), - launcher: serializeLauncher(), - colors: serializeColors() - } - } + FileView { + id: fileView - function serializeBar(): var { - return { - autoHide: barConfig.autoHide, - rounding: barConfig.rounding, - popouts: { - tray: barConfig.popouts.tray, - audio: barConfig.popouts.audio, - activeWindow: barConfig.popouts.activeWindow, - resources: barConfig.popouts.resources, - clock: barConfig.popouts.clock, - network: barConfig.popouts.network, - upower: barConfig.popouts.upower - }, - entries: barConfig.entries - } - } + path: `${Paths.config}/config.json` + watchChanges: true - function serializeLock(): var { - return { - recolorLogo: lock.recolorLogo, - enableFprint: lock.enableFprint, - maxFprintTries: lock.maxFprintTries, - blurAmount: lock.blurAmount, - sizes: { - heightMult: lock.sizes.heightMult, - ratio: lock.sizes.ratio, - centerWidth: lock.sizes.centerWidth - } - }; - } - - function serializeGeneral(): var { - return { - logo: general.logo, - wallpaperPath: general.wallpaperPath, - color: { - wallust: general.color.wallust, - mode: general.color.mode, - schemeGeneration: general.color.schemeGeneration, - scheduleDarkStart: general.color.scheduleDarkStart, - scheduleDarkEnd: general.color.scheduleDarkEnd, - neovimColors: general.color.neovimColors - }, - apps: { - terminal: general.apps.terminal, - audio: general.apps.audio, - playback: general.apps.playback, - explorer: general.apps.explorer, - }, - idle: { - timouts: general.idle.timeouts - } - } - } - - function serializeServices(): var { - return { - weatherLocation: services.weatherLocation, - useFahrenheit: services.useFahrenheit, - useTwelveHourClock: services.useTwelveHourClock, - gpuType: services.gpuType, - audioIncrement: services.audioIncrement, - brightnessIncrement: services.brightnessIncrement, - maxVolume: services.maxVolume, - defaultPlayer: services.defaultPlayer, - playerAliases: services.playerAliases - }; - } - - function serializeNotifs(): var { - return { - expire: notifs.expire, - defaultExpireTimeout: notifs.defaultExpireTimeout, - clearThreshold: notifs.clearThreshold, - expandThreshold: notifs.expandThreshold, - actionOnClick: notifs.actionOnClick, - groupPreviewNum: notifs.groupPreviewNum, - sizes: { - width: notifs.sizes.width, - image: notifs.sizes.image, - badge: notifs.sizes.badge - } - }; - } - - function serializeSidebar(): var { - return { - enabled: sidebar.enabled, - sizes: { - width: sidebar.sizes.width - } - }; - } - - function serializeUtilities(): var { - return { - enabled: utilities.enabled, - maxToasts: utilities.maxToasts, - sizes: { - width: utilities.sizes.width, - toastWidth: utilities.sizes.toastWidth - }, - toasts: { - configLoaded: utilities.toasts.configLoaded, - chargingChanged: utilities.toasts.chargingChanged, - gameModeChanged: utilities.toasts.gameModeChanged, - dndChanged: utilities.toasts.dndChanged, - audioOutputChanged: utilities.toasts.audioOutputChanged, - audioInputChanged: utilities.toasts.audioInputChanged, - capsLockChanged: utilities.toasts.capsLockChanged, - numLockChanged: utilities.toasts.numLockChanged, - kbLayoutChanged: utilities.toasts.kbLayoutChanged, - vpnChanged: utilities.toasts.vpnChanged, - nowPlaying: utilities.toasts.nowPlaying - }, - vpn: { - enabled: utilities.vpn.enabled, - provider: utilities.vpn.provider - } - }; - } - - function serializeDashboard(): var { - return { - enabled: dashboard.enabled, - mediaUpdateInterval: dashboard.mediaUpdateInterval, - dragThreshold: dashboard.dragThreshold, - sizes: { - tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, - tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, - infoWidth: dashboard.sizes.infoWidth, - infoIconSize: dashboard.sizes.infoIconSize, - dateTimeWidth: dashboard.sizes.dateTimeWidth, - mediaWidth: dashboard.sizes.mediaWidth, - mediaProgressSweep: dashboard.sizes.mediaProgressSweep, - mediaProgressThickness: dashboard.sizes.mediaProgressThickness, - resourceProgessThickness: dashboard.sizes.resourceProgessThickness, - weatherWidth: dashboard.sizes.weatherWidth, - mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, - mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, - resourceSize: dashboard.sizes.resourceSize - } - }; - } - - function serializeOsd(): var { - return { - enabled: osd.enabled, - hideDelay: osd.hideDelay, - enableBrightness: osd.enableBrightness, - enableMicrophone: osd.enableMicrophone, - allMonBrightness: osd.allMonBrightness, - sizes: { - sliderWidth: osd.sizes.sliderWidth, - sliderHeight: osd.sizes.sliderHeight - } - }; - } - - function serializeLauncher(): var { - return { - maxAppsShown: launcher.maxAppsShown, - maxWallpapers: launcher.maxWallpapers, - actionPrefix: launcher.actionPrefix, - specialPrefix: launcher.specialPrefix, - useFuzzy: { - apps: launcher.useFuzzy.apps, - actions: launcher.useFuzzy.actions, - schemes: launcher.useFuzzy.schemes, - variants: launcher.useFuzzy.variants, - wallpapers: launcher.useFuzzy.wallpapers - }, - sizes: { - itemWidth: launcher.sizes.itemWidth, - itemHeight: launcher.sizes.itemHeight, - wallpaperWidth: launcher.sizes.wallpaperWidth, - wallpaperHeight: launcher.sizes.wallpaperHeight - }, - actions: launcher.actions - } - } - - function serializeBackground(): var { - return { - wallFadeDuration: background.wallFadeDuration, - enabled: background.enabled - } - } - - function serializeAppearance(): var { - return { - rounding: { - scale: appearance.rounding.scale - }, - spacing: { - scale: appearance.spacing.scale - }, - padding: { - scale: appearance.padding.scale - }, - font: { - family: { - sans: appearance.font.family.sans, - mono: appearance.font.family.mono, - material: appearance.font.family.material, - clock: appearance.font.family.clock - }, - size: { - scale: appearance.font.size.scale - } - }, - anim: { - mediaGifSpeedAdjustment: 300, - sessionGifSpeed: 0.7, - durations: { - scale: appearance.anim.durations.scale - } - }, - transparency: { - enabled: appearance.transparency.enabled, - base: appearance.transparency.base, - layers: appearance.transparency.layers - } - }; - } - - function serializeColors(): var { - return { - schemeType: colors.schemeType, - } - } - - FileView { - id: fileView - - path: `${Paths.config}/config.json` - - watchChanges: true - - onFileChanged: { - if ( !root.recentlySaved ) { + onFileChanged: { + if (!root.recentlySaved) { timer.restart(); reload(); } else { reload(); } } - + onLoadFailed: err => { + if (err !== FileViewError.FileNotFound) + Toaster.toast(qsTr("Failed to read config"), FileViewError.toString(err), "settings_alert", Toast.Warning); + } onLoaded: { ModeScheduler.checkStartup(); try { JSON.parse(text()); const elapsed = timer.elapsedMs(); - if ( adapter.utilities.toasts.configLoaded && !root.recentlySaved ) { + if (adapter.utilities.toasts.configLoaded && !root.recentlySaved) { Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); - } else if ( adapter.utilities.toasts.configLoaded && root.recentlySaved ) { + } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved) { Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "settings_alert"); } } catch (e) { Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); } } - - onLoadFailed: err => { - if ( err !== FileViewError.FileNotFound ) - Toaster.toast(qsTr("Failed to read config"), FileViewError.toString(err), "settings_alert", Toast.Warning); - } - onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) - JsonAdapter { - id: adapter - property BackgroundConfig background: BackgroundConfig {} - property BarConfig barConfig: BarConfig {} - property LockConf lock: LockConf {} - property Overview overview: Overview {} - property Services services: Services {} - property NotifConfig notifs: NotifConfig {} - property SidebarConfig sidebar: SidebarConfig {} - property UtilConfig utilities: UtilConfig {} - property General general: General {} - property DashboardConfig dashboard: DashboardConfig {} - property AppearanceConf appearance: AppearanceConf {} - property Osd osd: Osd {} - property Launcher launcher: Launcher {} - property Colors colors: Colors {} - } - } + JsonAdapter { + id: adapter + + property AppearanceConf appearance: AppearanceConf { + } + property BackgroundConfig background: BackgroundConfig { + } + property BarConfig barConfig: BarConfig { + } + property Colors colors: Colors { + } + property DashboardConfig dashboard: DashboardConfig { + } + property DockConfig dock: DockConfig { + } + property General general: General { + } + property Launcher launcher: Launcher { + } + property LockConf lock: LockConf { + } + property NotifConfig notifs: NotifConfig { + } + property Osd osd: Osd { + } + property Overview overview: Overview { + } + property Services services: Services { + } + property SidebarConfig sidebar: SidebarConfig { + } + property UtilConfig utilities: UtilConfig { + } + } + } } diff --git a/Config/DashboardConfig.qml b/Config/DashboardConfig.qml index f1034fb..0b6a610 100644 --- a/Config/DashboardConfig.qml +++ b/Config/DashboardConfig.qml @@ -1,24 +1,36 @@ import Quickshell.Io JsonObject { - property bool enabled: true - property int mediaUpdateInterval: 500 - property int dragThreshold: 50 - property Sizes sizes: Sizes {} + 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 Sizes: JsonObject { - readonly property int tabIndicatorHeight: 3 - readonly property int tabIndicatorSpacing: 5 - readonly property int infoWidth: 200 - readonly property int infoIconSize: 25 - readonly property int dateTimeWidth: 110 - readonly property int mediaWidth: 200 - readonly property int mediaProgressSweep: 180 - readonly property int mediaProgressThickness: 8 - readonly property int resourceProgessThickness: 10 - readonly property int weatherWidth: 250 - readonly property int mediaCoverArtSize: 150 - readonly property int mediaVisualiserSize: 80 - readonly property int resourceSize: 200 - } + 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 + readonly property int infoWidth: 200 + readonly property int mediaCoverArtSize: 150 + readonly property int mediaProgressSweep: 180 + readonly property int mediaProgressThickness: 8 + readonly property int mediaVisualiserSize: 80 + readonly property int mediaWidth: 200 + readonly property int resourceProgessThickness: 10 + readonly property int resourceSize: 200 + readonly property int tabIndicatorHeight: 3 + readonly property int tabIndicatorSpacing: 5 + readonly property int weatherWidth: 250 + } } diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml new file mode 100644 index 0000000..135a3e0 --- /dev/null +++ b/Config/DockConfig.qml @@ -0,0 +1,10 @@ +import Quickshell.Io + +JsonObject { + property bool enable: false + property real height: 60 + property real hoverRegionHeight: 2 + property bool hoverToReveal: true + property list pinnedApps: ["org.kde.dolphin", "kitty",] + property bool pinnedOnStartup: false +} diff --git a/Config/DynamicColors.qml b/Config/DynamicColors.qml index 284a2db..ce13971 100644 --- a/Config/DynamicColors.qml +++ b/Config/DynamicColors.qml @@ -9,270 +9,278 @@ import qs.Helpers import qs.Paths Singleton { - id: root + id: root - property bool showPreview - property string scheme - property string flavour - readonly property bool light: showPreview ? previewLight : currentLight - property bool currentLight - property bool previewLight - readonly property M3Palette palette: showPreview ? preview : current - readonly property M3TPalette tPalette: M3TPalette {} - readonly property M3Palette current: M3Palette {} - readonly property M3Palette preview: M3Palette {} - readonly property Transparency transparency: Transparency {} - readonly property alias wallLuminance: analyser.luminance + readonly property M3Palette current: M3Palette { + } + property bool currentLight + property string flavour + readonly property bool light: showPreview ? previewLight : currentLight + readonly property M3Palette palette: showPreview ? preview : current + readonly property M3Palette preview: M3Palette { + } + property bool previewLight + property string scheme + property bool showPreview + readonly property M3TPalette tPalette: M3TPalette { + } + readonly property Transparency transparency: Transparency { + } + readonly property alias wallLuminance: analyser.luminance - function getLuminance(c: color): real { - if (c.r == 0 && c.g == 0 && c.b == 0) - return 0; - return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2)); - } + function alterColor(c: color, a: real, layer: int): color { + const luminance = getLuminance(c); - function alterColor(c: color, a: real, layer: int): color { - const luminance = getLuminance(c); + const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5)); + const scale = (luminance + offset) / luminance; + const r = Math.max(0, Math.min(1, c.r * scale)); + const g = Math.max(0, Math.min(1, c.g * scale)); + const b = Math.max(0, Math.min(1, c.b * scale)); - const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5)); - const scale = (luminance + offset) / luminance; - const r = Math.max(0, Math.min(1, c.r * scale)); - const g = Math.max(0, Math.min(1, c.g * scale)); - const b = Math.max(0, Math.min(1, c.b * scale)); + return Qt.rgba(r, g, b, a); + } - return Qt.rgba(r, g, b, a); - } + function getLuminance(c: color): real { + if (c.r == 0 && c.g == 0 && c.b == 0) + return 0; + return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2)); + } - function layer(c: color, layer: var): color { - if (!transparency.enabled) - return c; + function layer(c: color, layer: var): color { + if (!transparency.enabled) + return c; - return layer === 0 ? Qt.alpha(c, transparency.base) : alterColor(c, transparency.layers, layer ?? 1); - } + return layer === 0 ? Qt.alpha(c, transparency.base) : alterColor(c, transparency.layers, layer ?? 1); + } - function on(c: color): color { - if (c.hslLightness < 0.5) - return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1); - return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1); - } + function load(data: string, isPreview: bool): void { + const colors = isPreview ? preview : current; + const scheme = JSON.parse(data); - function load(data: string, isPreview: bool): void { - const colors = isPreview ? preview : current; - const scheme = JSON.parse(data); + if (!isPreview) { + root.scheme = scheme.name; + flavour = scheme.flavor; + currentLight = scheme.mode === "light"; + } else { + previewLight = scheme.mode === "light"; + } - if (!isPreview) { - root.scheme = scheme.name; - flavour = scheme.flavor; - currentLight = scheme.mode === "light"; - } else { - previewLight = scheme.mode === "light"; - } + for (const [name, color] of Object.entries(scheme.colors)) { + const propName = name.startsWith("term") ? name : `m3${name}`; + if (colors.hasOwnProperty(propName)) + colors[propName] = `${color}`; + } + } - for (const [name, color] of Object.entries(scheme.colors)) { - const propName = name.startsWith("term") ? name : `m3${name}`; - if (colors.hasOwnProperty(propName)) - colors[propName] = `${color}`; - } - } + function on(c: color): color { + if (c.hslLightness < 0.5) + return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1); + return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1); + } - FileView { - path: `${Paths.state}/scheme.json` - watchChanges: true - onFileChanged: reload() - onLoaded: root.load(text(), false) - } + function setMode(mode: string): void { + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); + Config.general.color.mode = mode; + Config.save(); + } - ImageAnalyser { - id: analyser + FileView { + path: `${Paths.state}/scheme.json` + watchChanges: true - source: WallpaperPath.currentWallpaperPath - } + onFileChanged: reload() + onLoaded: root.load(text(), false) + } - component Transparency: QtObject { - readonly property bool enabled: Appearance.transparency.enabled - readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) - readonly property real layers: Appearance.transparency.layers - } + ImageAnalyser { + id: analyser - component M3TPalette: QtObject { - readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor) - readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor) - readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor) - readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor) - readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor) - readonly property color m3background: root.layer(root.palette.m3background, 0) - readonly property color m3onBackground: root.layer(root.palette.m3onBackground) - readonly property color m3surface: root.layer(root.palette.m3surface, 0) - readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0) - readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0) - readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest) - readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow) - readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer) - readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh) - readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest) - readonly property color m3onSurface: root.layer(root.palette.m3onSurface) - readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0) - readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant) - readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0) - readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface) - readonly property color m3outline: root.layer(root.palette.m3outline) - readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant) - readonly property color m3shadow: root.layer(root.palette.m3shadow) - readonly property color m3scrim: root.layer(root.palette.m3scrim) - readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint) - readonly property color m3primary: root.layer(root.palette.m3primary) - readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary) - readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer) - readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer) - readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary) - readonly property color m3secondary: root.layer(root.palette.m3secondary) - readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary) - readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer) - readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer) - readonly property color m3tertiary: root.layer(root.palette.m3tertiary) - readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary) - readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer) - readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer) - readonly property color m3error: root.layer(root.palette.m3error) - readonly property color m3onError: root.layer(root.palette.m3onError) - readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer) - readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer) - readonly property color m3success: root.layer(root.palette.m3success) - readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess) - readonly property color m3successContainer: root.layer(root.palette.m3successContainer) - readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer) - readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed) - readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim) - readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed) - readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant) - readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed) - readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim) - readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed) - readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant) - readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed) - readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim) - readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed) - readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant) - } - - component M3Palette: QtObject { - property color m3primary_paletteKeyColor: "#a8627b" - property color m3secondary_paletteKeyColor: "#8e6f78" - property color m3tertiary_paletteKeyColor: "#986e4c" - property color m3neutral_paletteKeyColor: "#807477" - property color m3neutral_variant_paletteKeyColor: "#837377" - property color m3background: "#191114" - property color m3onBackground: "#efdfe2" - property color m3surface: "#191114" - property color m3surfaceDim: "#191114" - property color m3surfaceBright: "#403739" - property color m3surfaceContainerLowest: "#130c0e" - property color m3surfaceContainerLow: "#22191c" - property color m3surfaceContainer: "#261d20" - property color m3surfaceContainerHigh: "#31282a" - property color m3surfaceContainerHighest: "#3c3235" - property color m3onSurface: "#efdfe2" - property color m3surfaceVariant: "#514347" - property color m3onSurfaceVariant: "#d5c2c6" - property color m3inverseSurface: "#efdfe2" - property color m3inverseOnSurface: "#372e30" - property color m3outline: "#9e8c91" - property color m3outlineVariant: "#514347" - property color m3shadow: "#000000" - property color m3scrim: "#000000" - property color m3surfaceTint: "#ffb0ca" - property color m3primary: "#ffb0ca" - property color m3onPrimary: "#541d34" - property color m3primaryContainer: "#6f334a" - property color m3onPrimaryContainer: "#ffd9e3" - property color m3inversePrimary: "#8b4a62" - property color m3secondary: "#e2bdc7" - property color m3onSecondary: "#422932" - property color m3secondaryContainer: "#5a3f48" - property color m3onSecondaryContainer: "#ffd9e3" - property color m3tertiary: "#f0bc95" - property color m3onTertiary: "#48290c" - property color m3tertiaryContainer: "#b58763" - property color m3onTertiaryContainer: "#000000" - property color m3error: "#ffb4ab" - property color m3onError: "#690005" - property color m3errorContainer: "#93000a" - property color m3onErrorContainer: "#ffdad6" - property color m3success: "#B5CCBA" - property color m3onSuccess: "#213528" - property color m3successContainer: "#374B3E" - property color m3onSuccessContainer: "#D1E9D6" - property color m3primaryFixed: "#ffd9e3" - property color m3primaryFixedDim: "#ffb0ca" - property color m3onPrimaryFixed: "#39071f" - property color m3onPrimaryFixedVariant: "#6f334a" - property color m3secondaryFixed: "#ffd9e3" - property color m3secondaryFixedDim: "#e2bdc7" - property color m3onSecondaryFixed: "#2b151d" - property color m3onSecondaryFixedVariant: "#5a3f48" - property color m3tertiaryFixed: "#ffdcc3" - property color m3tertiaryFixedDim: "#f0bc95" - property color m3onTertiaryFixed: "#2f1500" - property color m3onTertiaryFixedVariant: "#623f21" - } + source: WallpaperPath.currentWallpaperPath + } component M3MaccchiatoPalette: QtObject { - property color m3primary_paletteKeyColor: "#6a73ac" - property color m3secondary_paletteKeyColor: "#72758e" - property color m3tertiary_paletteKeyColor: "#9b6592" + property color m3background: "#131317" + property color m3error: "#ffb4ab" + property color m3errorContainer: "#93000a" + property color m3inverseOnSurface: "#303034" + property color m3inversePrimary: "#525b92" + property color m3inverseSurface: "#e4e1e7" property color m3neutral_paletteKeyColor: "#77767b" property color m3neutral_variant_paletteKeyColor: "#767680" - property color m3background: "#131317" property color m3onBackground: "#e4e1e7" + property color m3onError: "#690005" + property color m3onErrorContainer: "#ffdad6" + property color m3onPrimary: "#232c60" + property color m3onPrimaryContainer: "#ffffff" + property color m3onPrimaryFixed: "#0b154b" + property color m3onPrimaryFixedVariant: "#3a4378" + property color m3onSecondary: "#2c2f44" + property color m3onSecondaryContainer: "#b1b3ce" + property color m3onSecondaryFixed: "#171a2e" + property color m3onSecondaryFixedVariant: "#42455c" + property color m3onSuccess: "#213528" + property color m3onSuccessContainer: "#D1E9D6" + property color m3onSurface: "#e4e1e7" + property color m3onSurfaceVariant: "#c6c5d1" + property color m3onTertiary: "#4c1f48" + property color m3onTertiaryContainer: "#000000" + property color m3onTertiaryFixed: "#340831" + property color m3onTertiaryFixedVariant: "#66365f" + property color m3outline: "#90909a" + property color m3outlineVariant: "#46464f" + property color m3primary: "#bac3ff" + property color m3primaryContainer: "#6a73ac" + property color m3primaryFixed: "#dee0ff" + property color m3primaryFixedDim: "#bac3ff" + property color m3primary_paletteKeyColor: "#6a73ac" + property color m3scrim: "#000000" + property color m3secondary: "#c3c5e0" + property color m3secondaryContainer: "#42455c" + property color m3secondaryFixed: "#dfe1fd" + property color m3secondaryFixedDim: "#c3c5e0" + property color m3secondary_paletteKeyColor: "#72758e" + property color m3shadow: "#000000" + property color m3success: "#B5CCBA" + property color m3successContainer: "#374B3E" property color m3surface: "#131317" - property color m3surfaceDim: "#131317" property color m3surfaceBright: "#39393d" - property color m3surfaceContainerLowest: "#0e0e12" - property color m3surfaceContainerLow: "#1b1b1f" property color m3surfaceContainer: "#1f1f23" property color m3surfaceContainerHigh: "#2a2a2e" property color m3surfaceContainerHighest: "#353438" - property color m3onSurface: "#e4e1e7" - property color m3surfaceVariant: "#46464f" - property color m3onSurfaceVariant: "#c6c5d1" - property color m3inverseSurface: "#e4e1e7" - property color m3inverseOnSurface: "#303034" - property color m3outline: "#90909a" - property color m3outlineVariant: "#46464f" - property color m3shadow: "#000000" - property color m3scrim: "#000000" + property color m3surfaceContainerLow: "#1b1b1f" + property color m3surfaceContainerLowest: "#0e0e12" + property color m3surfaceDim: "#131317" property color m3surfaceTint: "#bac3ff" - property color m3primary: "#bac3ff" - property color m3onPrimary: "#232c60" - property color m3primaryContainer: "#6a73ac" - property color m3onPrimaryContainer: "#ffffff" - property color m3inversePrimary: "#525b92" - property color m3secondary: "#c3c5e0" - property color m3onSecondary: "#2c2f44" - property color m3secondaryContainer: "#42455c" - property color m3onSecondaryContainer: "#b1b3ce" + property color m3surfaceVariant: "#46464f" property color m3tertiary: "#f1b3e5" - property color m3onTertiary: "#4c1f48" property color m3tertiaryContainer: "#b77ead" - property color m3onTertiaryContainer: "#000000" - property color m3error: "#ffb4ab" - property color m3onError: "#690005" - property color m3errorContainer: "#93000a" - property color m3onErrorContainer: "#ffdad6" - property color m3primaryFixed: "#dee0ff" - property color m3primaryFixedDim: "#bac3ff" - property color m3onPrimaryFixed: "#0b154b" - property color m3onPrimaryFixedVariant: "#3a4378" - property color m3secondaryFixed: "#dfe1fd" - property color m3secondaryFixedDim: "#c3c5e0" - property color m3onSecondaryFixed: "#171a2e" - property color m3onSecondaryFixedVariant: "#42455c" property color m3tertiaryFixed: "#ffd7f4" property color m3tertiaryFixedDim: "#f1b3e5" - property color m3onTertiaryFixed: "#340831" - property color m3onTertiaryFixedVariant: "#66365f" - property color m3success: "#B5CCBA" + property color m3tertiary_paletteKeyColor: "#9b6592" + } + component M3Palette: QtObject { + property color m3background: "#191114" + property color m3error: "#ffb4ab" + property color m3errorContainer: "#93000a" + property color m3inverseOnSurface: "#372e30" + property color m3inversePrimary: "#8b4a62" + property color m3inverseSurface: "#efdfe2" + property color m3neutral_paletteKeyColor: "#807477" + property color m3neutral_variant_paletteKeyColor: "#837377" + property color m3onBackground: "#efdfe2" + property color m3onError: "#690005" + property color m3onErrorContainer: "#ffdad6" + property color m3onPrimary: "#541d34" + property color m3onPrimaryContainer: "#ffd9e3" + property color m3onPrimaryFixed: "#39071f" + property color m3onPrimaryFixedVariant: "#6f334a" + property color m3onSecondary: "#422932" + property color m3onSecondaryContainer: "#ffd9e3" + property color m3onSecondaryFixed: "#2b151d" + property color m3onSecondaryFixedVariant: "#5a3f48" property color m3onSuccess: "#213528" - property color m3successContainer: "#374B3E" property color m3onSuccessContainer: "#D1E9D6" + property color m3onSurface: "#efdfe2" + property color m3onSurfaceVariant: "#d5c2c6" + property color m3onTertiary: "#48290c" + property color m3onTertiaryContainer: "#000000" + property color m3onTertiaryFixed: "#2f1500" + property color m3onTertiaryFixedVariant: "#623f21" + property color m3outline: "#9e8c91" + property color m3outlineVariant: "#514347" + property color m3primary: "#ffb0ca" + property color m3primaryContainer: "#6f334a" + property color m3primaryFixed: "#ffd9e3" + property color m3primaryFixedDim: "#ffb0ca" + property color m3primary_paletteKeyColor: "#a8627b" + property color m3scrim: "#000000" + property color m3secondary: "#e2bdc7" + property color m3secondaryContainer: "#5a3f48" + property color m3secondaryFixed: "#ffd9e3" + property color m3secondaryFixedDim: "#e2bdc7" + property color m3secondary_paletteKeyColor: "#8e6f78" + property color m3shadow: "#000000" + property color m3success: "#B5CCBA" + property color m3successContainer: "#374B3E" + property color m3surface: "#191114" + property color m3surfaceBright: "#403739" + property color m3surfaceContainer: "#261d20" + property color m3surfaceContainerHigh: "#31282a" + property color m3surfaceContainerHighest: "#3c3235" + property color m3surfaceContainerLow: "#22191c" + property color m3surfaceContainerLowest: "#130c0e" + property color m3surfaceDim: "#191114" + property color m3surfaceTint: "#ffb0ca" + property color m3surfaceVariant: "#514347" + property color m3tertiary: "#f0bc95" + property color m3tertiaryContainer: "#b58763" + property color m3tertiaryFixed: "#ffdcc3" + property color m3tertiaryFixedDim: "#f0bc95" + property color m3tertiary_paletteKeyColor: "#986e4c" + } + component M3TPalette: QtObject { + readonly property color m3background: root.layer(root.palette.m3background, 0) + readonly property color m3error: root.layer(root.palette.m3error) + readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer) + readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface) + readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary) + readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0) + readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor) + readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor) + readonly property color m3onBackground: root.layer(root.palette.m3onBackground) + readonly property color m3onError: root.layer(root.palette.m3onError) + readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer) + readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary) + readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer) + readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed) + readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant) + readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary) + readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer) + readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed) + readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant) + readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess) + readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer) + readonly property color m3onSurface: root.layer(root.palette.m3onSurface) + readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant) + readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary) + readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer) + readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed) + readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant) + readonly property color m3outline: root.layer(root.palette.m3outline) + readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant) + readonly property color m3primary: root.layer(root.palette.m3primary) + readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer) + readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed) + readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim) + readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor) + readonly property color m3scrim: root.layer(root.palette.m3scrim) + readonly property color m3secondary: root.layer(root.palette.m3secondary) + readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer) + readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed) + readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim) + readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor) + readonly property color m3shadow: root.layer(root.palette.m3shadow) + readonly property color m3success: root.layer(root.palette.m3success) + readonly property color m3successContainer: root.layer(root.palette.m3successContainer) + readonly property color m3surface: root.layer(root.palette.m3surface, 0) + readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0) + readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer) + readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh) + readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest) + readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow) + readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest) + readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0) + readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint) + readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0) + readonly property color m3tertiary: root.layer(root.palette.m3tertiary) + readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer) + readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed) + readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim) + readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor) + } + component Transparency: QtObject { + readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) + readonly property bool enabled: Appearance.transparency.enabled + readonly property real layers: Appearance.transparency.layers } } diff --git a/Config/General.qml b/Config/General.qml index 2633bf8..8b00553 100644 --- a/Config/General.qml +++ b/Config/General.qml @@ -2,28 +2,29 @@ import Quickshell.Io import Quickshell JsonObject { + property Apps apps: Apps { + } + property Color color: Color { + } + property Idle idle: Idle { + } property string logo: "" property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers" - property Color color: Color {} - property Apps apps: Apps {} - property Idle idle: Idle {} - component Color: JsonObject { - property bool wallust: false - property bool schemeGeneration: true - property string mode: "dark" - property int scheduleDarkStart: 0 - property int scheduleDarkEnd: 0 - property bool neovimColors: false + component Apps: JsonObject { + property list audio: ["pavucontrol"] + property list explorer: ["dolphin"] + property list playback: ["mpv"] + property list terminal: ["kitty"] + } + component Color: JsonObject { + property string mode: "dark" + property bool neovimColors: false + property int scheduleDarkEnd: 0 + property int scheduleDarkStart: 0 + property bool schemeGeneration: true + property bool wallust: false } - - component Apps: JsonObject { - property list terminal: ["kitty"] - property list audio: ["pavucontrol"] - property list playback: ["mpv"] - property list explorer: ["dolphin"] - } - component Idle: JsonObject { property list timeouts: [ { diff --git a/Config/Launcher.qml b/Config/Launcher.qml index 6ce97f9..a195868 100644 --- a/Config/Launcher.qml +++ b/Config/Launcher.qml @@ -1,84 +1,108 @@ import Quickshell.Io JsonObject { - property int maxAppsShown: 10 - property int maxWallpapers: 7 property string actionPrefix: ">" - property string specialPrefix: "@" - property Sizes sizes: Sizes {} - property UseFuzzy useFuzzy: UseFuzzy {} - - component UseFuzzy: JsonObject { - property bool apps: false - property bool actions: false - property bool schemes: false - property bool variants: false - property bool wallpapers: false - } - - component Sizes: JsonObject { - property int itemWidth: 600 - property int itemHeight: 50 - property int wallpaperWidth: 280 - property int wallpaperHeight: 200 - } - property list actions: [ { name: "Calculator", icon: "calculate", - description: "Do simple math equations (powered by Qalc)", + description: "Do simple math equations", command: ["autocomplete", "calc"], enabled: true, dangerous: false }, - { - name: "Wallpaper", - icon: "image", - description: "Change the current wallpaper", - command: ["autocomplete", "wallpaper"], - enabled: true, - dangerous: false - }, - { - name: "Shutdown", - icon: "power_settings_new", - description: "Shutdown the system", - command: ["systemctl", "poweroff"], - enabled: true, - dangerous: true - }, - { - name: "Reboot", - icon: "cached", - description: "Reboot the system", - command: ["systemctl", "reboot"], - enabled: true, - dangerous: true - }, - { - name: "Logout", - icon: "exit_to_app", - description: "Log out of the current session", - command: ["loginctl", "terminate-user", ""], - enabled: true, - dangerous: true - }, - { - name: "Lock", - icon: "lock", - description: "Lock the current session", - command: ["loginctl", "lock-session"], - enabled: true, - dangerous: false - }, - { - name: "Sleep", - icon: "bedtime", - description: "Suspend then hibernate", - command: ["systemctl", "suspend-then-hibernate"], - enabled: true, - dangerous: false - }, + { + name: "Light", + icon: "light_mode", + description: "Change to light mode", + command: ["setMode", "light"], + enabled: true, + dangerous: false + }, + { + name: "Dark", + icon: "dark_mode", + description: "Change to dark mode", + command: ["setMode", "dark"], + enabled: true, + dangerous: false + }, + { + name: "Wallpaper", + icon: "image", + description: "Change the current wallpaper", + command: ["autocomplete", "wallpaper"], + enabled: true, + dangerous: false + }, + { + name: "Variant", + icon: "colors", + description: "Change the current scheme variant", + command: ["autocomplete", "variant"], + enabled: true, + dangerous: false + }, + { + name: "Shutdown", + icon: "power_settings_new", + description: "Shutdown the system", + command: ["systemctl", "poweroff"], + enabled: true, + dangerous: true + }, + { + name: "Reboot", + icon: "cached", + description: "Reboot the system", + command: ["systemctl", "reboot"], + enabled: true, + dangerous: true + }, + { + name: "Logout", + icon: "logout", + description: "Log out of the current session", + command: ["loginctl", "terminate-user", ""], + enabled: true, + dangerous: true + }, + { + name: "Lock", + icon: "lock", + description: "Lock the current session", + command: ["loginctl", "lock-session"], + enabled: true, + dangerous: false + }, + { + name: "Sleep", + icon: "bedtime", + description: "Suspend then hibernate", + command: ["systemctl", "suspend-then-hibernate"], + enabled: true, + dangerous: false + }, ] + property int maxAppsShown: 10 + property int maxWallpapers: 7 + property Sizes sizes: Sizes { + } + property string specialPrefix: "@" + property UseFuzzy useFuzzy: UseFuzzy { + } + + component Sizes: JsonObject { + property int itemHeight: 50 + property int itemWidth: 600 + property int wallpaperHeight: 200 + property int wallpaperWidth: 280 + } + component UseFuzzy: JsonObject { + property bool actions: false + property bool apps: false + property bool schemes: false + property bool variants: false + property bool wallpapers: false + } } diff --git a/Config/LockConf.qml b/Config/LockConf.qml index 011dc86..e377459 100644 --- a/Config/LockConf.qml +++ b/Config/LockConf.qml @@ -1,15 +1,16 @@ import Quickshell.Io JsonObject { - property bool recolorLogo: false - property bool enableFprint: true - property int maxFprintTries: 3 - property Sizes sizes: Sizes {} property int blurAmount: 40 + property bool enableFprint: true + property int maxFprintTries: 3 + property bool recolorLogo: false + property Sizes sizes: Sizes { + } - component Sizes: JsonObject { - property real heightMult: 0.7 - property real ratio: 16 / 9 - property int centerWidth: 600 - } + component Sizes: JsonObject { + property int centerWidth: 600 + property real heightMult: 0.7 + property real ratio: 16 / 9 + } } diff --git a/Config/MaterialEasing.qml b/Config/MaterialEasing.qml index cbd4718..a43a8df 100644 --- a/Config/MaterialEasing.qml +++ b/Config/MaterialEasing.qml @@ -2,26 +2,25 @@ pragma Singleton import Quickshell Singleton { - id: root + id: root - property real scale: Appearance.anim.durations.scale - - readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] - readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] - readonly property int emphasizedAccelTime: 200 * scale - readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] - readonly property int emphasizedDecelTime: 400 * scale - readonly property int emphasizedTime: 500 * scale - readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] - readonly property int expressiveDefaultSpatialTime: 500 * scale - readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] - readonly property int expressiveEffectsTime: 200 * scale - readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] - readonly property int expressiveFastSpatialTime: 350 * scale - readonly property list standard: [0.2, 0, 0, 1, 1, 1] - readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] - readonly property int standardAccelTime: 200 * scale - readonly property list standardDecel: [0, 0, 0, 1, 1, 1] - readonly property int standardDecelTime: 250 * scale - readonly property int standardTime: 300 * scale + readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property int emphasizedAccelTime: 200 * scale + readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property int emphasizedDecelTime: 400 * scale + readonly property int emphasizedTime: 500 * scale + readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] + readonly property int expressiveDefaultSpatialTime: 500 * scale + readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] + readonly property int expressiveEffectsTime: 200 * scale + readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] + readonly property int expressiveFastSpatialTime: 350 * scale + property real scale: Appearance.anim.durations.scale + readonly property list standard: [0.2, 0, 0, 1, 1, 1] + readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property int standardAccelTime: 200 * scale + readonly property list standardDecel: [0, 0, 0, 1, 1, 1] + readonly property int standardDecelTime: 250 * scale + readonly property int standardTime: 300 * scale } diff --git a/Config/NotifConfig.qml b/Config/NotifConfig.qml index 8f68ba4..0a11d46 100644 --- a/Config/NotifConfig.qml +++ b/Config/NotifConfig.qml @@ -1,18 +1,20 @@ import Quickshell.Io JsonObject { - property bool expire: true - property int defaultExpireTimeout: 5000 - property real clearThreshold: 0.3 - property int expandThreshold: 20 - property bool actionOnClick: false - property int groupPreviewNum: 3 - property bool openExpanded: false - property Sizes sizes: Sizes {} + property bool actionOnClick: false + property int appNotifCooldown: 0 + property real clearThreshold: 0.3 + property int defaultExpireTimeout: 5000 + property int expandThreshold: 20 + property bool expire: true + property int groupPreviewNum: 3 + property bool openExpanded: false + property Sizes sizes: Sizes { + } - component Sizes: JsonObject { - property int width: 400 - property int image: 41 - property int badge: 20 - } + component Sizes: JsonObject { + property int badge: 20 + property int image: 41 + property int width: 400 + } } diff --git a/Config/Osd.qml b/Config/Osd.qml index 36f716e..79da85b 100644 --- a/Config/Osd.qml +++ b/Config/Osd.qml @@ -1,15 +1,16 @@ import Quickshell.Io JsonObject { - property bool enabled: true - property int hideDelay: 3000 - property bool enableBrightness: true - property bool enableMicrophone: true property bool allMonBrightness: false - property Sizes sizes: Sizes {} + property bool enableBrightness: true + property bool enableMicrophone: true + property bool enabled: true + property int hideDelay: 3000 + property Sizes sizes: Sizes { + } - component Sizes: JsonObject { - property int sliderWidth: 30 - property int sliderHeight: 150 - } + component Sizes: JsonObject { + property int sliderHeight: 150 + property int sliderWidth: 30 + } } diff --git a/Config/Overview.qml b/Config/Overview.qml index 6f81758..5e1a615 100644 --- a/Config/Overview.qml +++ b/Config/Overview.qml @@ -1,8 +1,8 @@ import Quickshell.Io JsonObject { - property int rows: 2 property int columns: 5 - property real scale: 0.16 property bool enable: false + property int rows: 2 + property real scale: 0.16 } diff --git a/Config/Services.qml b/Config/Services.qml index 668a9cd..e091fa2 100644 --- a/Config/Services.qml +++ b/Config/Services.qml @@ -2,18 +2,20 @@ import Quickshell.Io import QtQuick JsonObject { - property string weatherLocation: "" - property bool useFahrenheit: false - property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") - property string gpuType: "" - property real audioIncrement: 0.1 - property real brightnessIncrement: 0.1 - property real maxVolume: 1.0 - property string defaultPlayer: "Spotify" - property list playerAliases: [ - { - "from": "com.github.th_ch.youtube_music", - "to": "YT Music" - } - ] + 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 + property list playerAliases: [ + { + "from": "com.github.th_ch.youtube_music", + "to": "YT Music" + } + ] + property bool useFahrenheit: false + property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") + property int visualizerBars: 30 + property string weatherLocation: "" } diff --git a/Config/SidebarConfig.qml b/Config/SidebarConfig.qml index ba48822..80800ce 100644 --- a/Config/SidebarConfig.qml +++ b/Config/SidebarConfig.qml @@ -1,10 +1,11 @@ import Quickshell.Io JsonObject { - property bool enabled: true - property Sizes sizes: Sizes {} + property bool enabled: true + property Sizes sizes: Sizes { + } - component Sizes: JsonObject { - property int width: 430 - } + component Sizes: JsonObject { + property int width: 430 + } } diff --git a/Config/Transparency.qml b/Config/Transparency.qml index e3df2e9..e687cfb 100644 --- a/Config/Transparency.qml +++ b/Config/Transparency.qml @@ -1,7 +1,7 @@ import Quickshell.Io JsonObject { - property bool enabled: false - property real base: 0.85 - property real layers: 0.4 + property real base: 0.85 + property bool enabled: false + property real layers: 0.4 } diff --git a/Config/UtilConfig.qml b/Config/UtilConfig.qml index cf46446..c4a2087 100644 --- a/Config/UtilConfig.qml +++ b/Config/UtilConfig.qml @@ -1,35 +1,35 @@ import Quickshell.Io JsonObject { - property bool enabled: true - property int maxToasts: 4 + property bool enabled: true + property int maxToasts: 4 + property Sizes sizes: Sizes { + } + property Toasts toasts: Toasts { + } + property Vpn vpn: Vpn { + } - property Sizes sizes: Sizes {} - property Toasts toasts: Toasts {} - property Vpn vpn: Vpn {} - - component Sizes: JsonObject { - property int width: 430 - property int toastWidth: 430 - } - - component Toasts: JsonObject { - property bool configLoaded: true - property bool chargingChanged: true - property bool gameModeChanged: true - property bool dndChanged: true - property bool audioOutputChanged: true - property bool audioInputChanged: true - property bool capsLockChanged: true - property bool numLockChanged: true - property bool kbLayoutChanged: true - property bool kbLimit: true - property bool vpnChanged: true - property bool nowPlaying: false - } - - component Vpn: JsonObject { - property bool enabled: false - property list provider: ["netbird"] - } + component Sizes: JsonObject { + property int toastWidth: 430 + property int width: 430 + } + component Toasts: JsonObject { + property bool audioInputChanged: true + property bool audioOutputChanged: true + property bool capsLockChanged: true + property bool chargingChanged: true + property bool configLoaded: true + property bool dndChanged: true + property bool gameModeChanged: true + property bool kbLayoutChanged: true + property bool kbLimit: true + property bool nowPlaying: false + property bool numLockChanged: true + property bool vpnChanged: true + } + component Vpn: JsonObject { + property bool enabled: false + property list provider: ["netbird"] + } } diff --git a/Config/WorkspaceWidget.qml b/Config/WorkspaceWidget.qml index cf3e293..10a6969 100644 --- a/Config/WorkspaceWidget.qml +++ b/Config/WorkspaceWidget.qml @@ -1,6 +1,6 @@ import Quickshell.Io JsonObject { - property string textColor: "black" - property string inactiveTextColor: "white" + property string inactiveTextColor: "white" + property string textColor: "black" } diff --git a/Daemons/Audio.qml b/Daemons/Audio.qml index 51b6095..fddb283 100644 --- a/Daemons/Audio.qml +++ b/Daemons/Audio.qml @@ -1,144 +1,150 @@ pragma Singleton -import qs.Config import ZShell.Services import ZShell import Quickshell import Quickshell.Services.Pipewire import QtQuick +import qs.Config Singleton { - id: root + id: root - property string previousSinkName: "" - property string previousSourceName: "" + 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) => { + if (!node.isStream) { + if (node.isSink) + acc.sinks.push(node); + else if (node.audio) + acc.sources.push(node); + } else if (node.isStream && node.audio) { + // Application streams (output streams) + acc.streams.push(node); + } + return acc; + }, { + sources: [], + sinks: [], + streams: [] + }) + property string previousSinkName: "" + property string previousSourceName: "" + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property list sinks: nodes.sinks + readonly property PwNode source: Pipewire.defaultAudioSource + readonly property bool sourceMuted: !!source?.audio?.muted + readonly property real sourceVolume: source?.audio?.volume ?? 0 + readonly property list sources: nodes.sources + readonly property list streams: nodes.streams + readonly property real volume: sink?.audio?.volume ?? 0 - readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { - if (!node.isStream) { - if (node.isSink) - acc.sinks.push(node); - else if (node.audio) - acc.sources.push(node); - } else if (node.isStream && node.audio) { - // Application streams (output streams) - acc.streams.push(node); - } - return acc; - }, { - sources: [], - sinks: [], - streams: [] - }) + function decrementSourceVolume(amount: real): void { + setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); + } - readonly property list sinks: nodes.sinks - readonly property list sources: nodes.sources - readonly property list streams: nodes.streams + function decrementVolume(amount: real): void { + setVolume(volume - (amount || Config.services.audioIncrement)); + } - readonly property PwNode sink: Pipewire.defaultAudioSink - readonly property PwNode source: Pipewire.defaultAudioSource + function getStreamMuted(stream: PwNode): bool { + return !!stream?.audio?.muted; + } - readonly property bool muted: !!sink?.audio?.muted - readonly property real volume: sink?.audio?.volume ?? 0 + function getStreamName(stream: PwNode): string { + if (!stream) + return qsTr("Unknown"); + // Try application name first, then description, then name + return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); + } - readonly property bool sourceMuted: !!source?.audio?.muted - readonly property real sourceVolume: source?.audio?.volume ?? 0 + function getStreamVolume(stream: PwNode): real { + return stream?.audio?.volume ?? 0; + } - function setVolume(newVolume: real): void { - if (sink?.ready && sink?.audio) { - sink.audio.muted = false; - sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); - } - } + function incrementSourceVolume(amount: real): void { + setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); + } - function incrementVolume(amount: real): void { - setVolume(volume + (amount || Config.services.audioIncrement)); - } + function incrementVolume(amount: real): void { + setVolume(volume + (amount || Config.services.audioIncrement)); + } - function decrementVolume(amount: real): void { - setVolume(volume - (amount || Config.services.audioIncrement)); - } + function setAudioSink(newSink: PwNode): void { + Pipewire.preferredDefaultAudioSink = newSink; + } - function setSourceVolume(newVolume: real): void { - if (source?.ready && source?.audio) { - source.audio.muted = false; - source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); - } - } + function setAudioSource(newSource: PwNode): void { + Pipewire.preferredDefaultAudioSource = newSource; + } - function incrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); - } + function setSourceVolume(newVolume: real): void { + if (source?.ready && source?.audio) { + source.audio.muted = false; + source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + } + } - function decrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); - } + function setStreamMuted(stream: PwNode, muted: bool): void { + if (stream?.ready && stream?.audio) { + stream.audio.muted = muted; + } + } - function setAudioSink(newSink: PwNode): void { - Pipewire.preferredDefaultAudioSink = newSink; - } + function setStreamVolume(stream: PwNode, newVolume: real): void { + if (stream?.ready && stream?.audio) { + stream.audio.muted = false; + stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + } + } - function setAudioSource(newSource: PwNode): void { - Pipewire.preferredDefaultAudioSource = newSource; - } + function setVolume(newVolume: real): void { + if (sink?.ready && sink?.audio) { + sink.audio.muted = false; + sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + } + } - function setStreamVolume(stream: PwNode, newVolume: real): void { - if (stream?.ready && stream?.audio) { - stream.audio.muted = false; - stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); - } - } + Component.onCompleted: { + previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device"); + previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); + } + onSinkChanged: { + if (!sink?.ready) + return; - function setStreamMuted(stream: PwNode, muted: bool): void { - if (stream?.ready && stream?.audio) { - stream.audio.muted = muted; - } - } + const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); - function getStreamVolume(stream: PwNode): real { - return stream?.audio?.volume ?? 0; - } + if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) + Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); - function getStreamMuted(stream: PwNode): bool { - return !!stream?.audio?.muted; - } + previousSinkName = newSinkName; + } + onSourceChanged: { + if (!source?.ready) + return; - function getStreamName(stream: PwNode): string { - if (!stream) - return qsTr("Unknown"); - // Try application name first, then description, then name - return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); - } + const newSourceName = source.description || source.name || qsTr("Unknown Device"); - onSinkChanged: { - if (!sink?.ready) - return; + if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) + Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); - const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); + previousSourceName = newSourceName; + } - if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) - Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); + CavaProvider { + id: cava - previousSinkName = newSinkName; - } + bars: Config.services.visualizerBars + } - onSourceChanged: { - if (!source?.ready) - return; + BeatTracker { + id: beatTracker - const newSourceName = source.description || source.name || qsTr("Unknown Device"); + } - if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) - Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); - - previousSourceName = newSourceName; - } - - Component.onCompleted: { - previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device"); - previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); - } - - PwObjectTracker { - objects: [...root.sinks, ...root.sources, ...root.streams] - } + PwObjectTracker { + objects: [...root.sinks, ...root.sources, ...root.streams] + } } diff --git a/Daemons/Network.qml b/Daemons/Network.qml new file mode 100644 index 0000000..5604e1c --- /dev/null +++ b/Daemons/Network.qml @@ -0,0 +1,326 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property AccessPoint active: networks.find(n => n.active) ?? null + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property int ethernetDeviceCount: 0 + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + property bool ethernetProcessRunning: false + readonly property list networks: [] + property var pendingConnection: null + property list savedConnectionSsids: [] + property list savedConnections: [] + readonly property bool scanning: Nmcli.scanning + property bool wifiEnabled: true + property var wirelessDeviceDetails: null + + signal connectionFailed(string ssid) + + function cidrToSubnetMask(cidr: string): string { + // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") + const cidrNum = parseInt(cidr); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octets = [(mask >>> 24) & 0xff, (mask >>> 16) & 0xff, (mask >>> 8) & 0xff, mask & 0xff]; + + return octets.join("."); + } + + function connectEthernet(connectionName: string, interfaceName: string): void { + Nmcli.connectEthernet(connectionName, interfaceName, result => { + if (result.success) { + getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function (d) { + return d.connected; + }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }, 1000); + } + }); + } + + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + // Set up pending connection tracking if callback provided + if (callback) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback + }; + } + + Nmcli.connectToNetwork(ssid, password, bssid, result => { + if (result && result.success) { + // Connection successful + if (callback) + callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) + callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) + callback(result); + root.pendingConnection = null; + } + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + // Set up pending connection tracking + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback + }; + + Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, result => { + if (result && result.success) { + // Connection successful + if (callback) + callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) + callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) + callback(result); + root.pendingConnection = null; + } + }, bssid); + } + + function disconnectEthernet(connectionName: string): void { + Nmcli.disconnectEthernet(connectionName, result => { + if (result.success) { + getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); + } + }); + } + + function disconnectFromNetwork(): void { + // Try to disconnect - use connection name if available, otherwise use device + Nmcli.disconnectFromNetwork(); + // Refresh network list after disconnection + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + + function enableWifi(enabled: bool): void { + Nmcli.enableWifi(enabled, result => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); + } + + function forgetNetwork(ssid: string): void { + // Delete the connection profile for this network + // This will remove the saved password and connection settings + Nmcli.forgetNetwork(ssid, result => { + if (result.success) { + // Refresh network list after deletion + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + }); + } + + function getEthernetDevices(): void { + root.ethernetProcessRunning = true; + Nmcli.getEthernetInterfaces(interfaces => { + root.ethernetDevices = Nmcli.ethernetDevices; + root.ethernetDeviceCount = Nmcli.ethernetDevices.length; + root.ethernetProcessRunning = false; + }); + } + + function getWifiStatus(): void { + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + } + + function hasSavedProfile(ssid: string): bool { + // Use Nmcli's hasSavedProfile which has the same logic + return Nmcli.hasSavedProfile(ssid); + } + + function rescanWifi(): void { + Nmcli.rescanWifi(); + } + + function syncNetworksFromNmcli(): void { + const rNetworks = root.networks; + const nNetworks = Nmcli.networks; + + // Build a map of existing networks by key + const existingMap = new Map(); + for (const rn of rNetworks) { + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + existingMap.set(key, rn); + } + + // Build a map of new networks by key + const newMap = new Map(); + for (const nn of nNetworks) { + const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`; + newMap.set(key, nn); + } + + // Remove networks that no longer exist + for (const [key, network] of existingMap) { + if (!newMap.has(key)) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + } + + // Add or update networks from Nmcli + for (const [key, nNetwork] of newMap) { + const existing = existingMap.get(key); + if (existing) { + // Update existing network's lastIpcObject + existing.lastIpcObject = nNetwork.lastIpcObject; + } else { + // Create new AccessPoint from Nmcli's data + rNetworks.push(apComp.createObject(root, { + lastIpcObject: nNetwork.lastIpcObject + })); + } + } + } + + function toggleWifi(): void { + Nmcli.toggleWifi(result => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); + } + + function updateEthernetDeviceDetails(interfaceName: string): void { + Nmcli.getEthernetDeviceDetails(interfaceName, details => { + root.ethernetDeviceDetails = details; + }); + } + + function updateWirelessDeviceDetails(): void { + // Find the wireless interface by looking for wifi devices + // Pass empty string to let Nmcli find the active interface automatically + Nmcli.getWirelessDeviceDetails("", details => { + root.wirelessDeviceDetails = details; + }); + } + + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + + // Sync saved connections from Nmcli when they're updated + Connections { + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + + target: Nmcli + } + + Component { + id: apComp + + AccessPoint { + } + } + + Process { + command: ["nmcli", "m"] + running: true + + stdout: SplitParser { + onRead: { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + getEthernetDevices(); + } + } + } + + component AccessPoint: QtObject { + readonly property bool active: lastIpcObject.active + readonly property string bssid: lastIpcObject.bssid + readonly property int frequency: lastIpcObject.frequency + readonly property bool isSecure: security.length > 0 + required property var lastIpcObject + readonly property string security: lastIpcObject.security + readonly property string ssid: lastIpcObject.ssid + readonly property int strength: lastIpcObject.strength + } +} diff --git a/Daemons/Nmcli.qml b/Daemons/Nmcli.qml new file mode 100644 index 0000000..77fa771 --- /dev/null +++ b/Daemons/Nmcli.qml @@ -0,0 +1,1358 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property string activeConnection: "" + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property string activeInterface: "" + property list activeProcesses: [] + readonly property string connectionListFields: "NAME,TYPE" + readonly property string connectionParamBssid: "802-11-wireless.bssid" + readonly property string connectionParamConName: "con-name" + readonly property string connectionParamIfname: "ifname" + readonly property string connectionParamPassword: "password" + readonly property string connectionParamSsid: "ssid" + readonly property string connectionParamType: "type" + readonly property string connectionTypeWireless: "802-11-wireless" + property int currentSsidQueryIndex: 0 + property var deviceStatus: null + readonly property string deviceStatusFields: "DEVICE,TYPE,STATE,CONNECTION" + readonly property string deviceTypeEthernet: "ethernet" + + // Constants + readonly property string deviceTypeWifi: "wifi" + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + property var ethernetInterfaces: [] + property bool isConnected: false + readonly property string keyMgmtWpaPsk: "wpa-psk" + readonly property string networkDetailFields: "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY" + readonly property string networkListFields: "SSID,SIGNAL,SECURITY" + readonly property list networks: [] + readonly property string nmcliCommandConnection: "connection" + readonly property string nmcliCommandDevice: "device" + readonly property string nmcliCommandRadio: "radio" + readonly property string nmcliCommandWifi: "wifi" + property var pendingConnection: null + property list savedConnectionSsids: [] + property list savedConnections: [] + readonly property bool scanning: rescanProc.running + readonly property string securityKeyMgmt: "802-11-wireless-security.key-mgmt" + readonly property string securityPsk: "802-11-wireless-security.psk" + property var wifiConnectionQueue: [] + property bool wifiEnabled: true + property var wirelessDeviceDetails: null + property var wirelessInterfaces: [] + readonly property string wirelessSsidField: "802-11-wireless.ssid" + + signal connectionFailed(string ssid) + + function activateConnection(connectionName: string, callback: var): void { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (callback) + callback(result); + }); + } + + function bringInterfaceDown(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); + } + } + + function bringInterfaceUp(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); + } + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand([root.nmcliCommandConnection, "show", ssid], result => { + if (result.success) { + executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { + Qt.callLater(() => { + if (callback) + callback(); + }, 300); + }); + } else { + if (callback) + callback(); + } + }); + } + + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + if (!immediateCheckTimer.running) { + immediateCheckTimer.start(); + } + } + }); + } + } + + function cidrToSubnetMask(cidr: string): string { + const cidrNum = parseInt(cidr, 10); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octet1 = (mask >>> 24) & 0xff; + const octet2 = (mask >>> 16) & 0xff; + const octet3 = (mask >>> 8) & 0xff; + const octet4 = mask & 0xff; + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { + if (connectionName && connectionName.length > 0) { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + if (interfaceName && interfaceName.length > 0) { + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + } + }, 500); + } + if (callback) + callback(result); + }); + } else if (interfaceName && interfaceName.length > 0) { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + }, 500); + } + if (callback) + callback(result); + }); + } else { + if (callback) + callback({ + success: false, + output: "", + error: "No connection name or interface specified", + exitCode: -1 + }); + } + } + + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + connectWireless(ssid, password, bssid, callback); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + if (isSecure) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + connectWireless(ssid, "", bssid, result => { + if (result.success) { + if (callback) + callback({ + success: true, + usedSavedPassword: true, + output: result.output, + error: "", + exitCode: 0 + }); + } else if (result.needsPassword) { + if (callback) + callback({ + success: false, + needsPassword: true, + output: result.output, + error: result.error, + exitCode: result.exitCode + }); + } else { + if (callback) + callback(result); + } + }); + } else { + connectWireless(ssid, "", bssid, callback); + } + } + + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + const retries = retryCount !== undefined ? retryCount : 0; + const maxRetries = 2; + + if (callback) { + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback, + retryCount: retries + }; + connectionCheckTimer.start(); + immediateCheckTimer.checkCount = 0; + immediateCheckTimer.start(); + } + + if (password && password.length > 0 && hasBssid) { + const bssidUpper = bssid.toUpperCase(); + createConnectionWithPassword(ssid, bssidUpper, password, callback); + return; + } + + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; + if (password && password.length > 0) { + cmd.push(root.connectionParamPassword, password); + } + executeCommand(cmd, result => { + if (result.needsPassword && callback) { + if (callback) + callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { + console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); + } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) { + if (callback) + callback(result); + } + }); + } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { + checkAndDeleteConnection(ssid, () => { + const cmd = [root.nmcliCommandConnection, "add", root.connectionParamType, root.deviceTypeWifi, root.connectionParamConName, ssid, root.connectionParamIfname, "*", root.connectionParamSsid, ssid, root.connectionParamBssid, bssidUpper, root.securityKeyMgmt, root.keyMgmtWpaPsk, root.securityPsk, password]; + + executeCommand(cmd, result => { + if (result.success) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + const hasDuplicateWarning = result.error && (result.error.includes("another connection with the name") || result.error.includes("Reference the connection by its uuid")); + + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); + let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; + executeCommand(fallbackCmd, fallbackResult => { + if (callback) + callback(fallbackResult); + }); + } + } + }); + }); + } + + function deduplicateNetworks(networks: list): list { + if (!networks || networks.length === 0) { + return []; + } + + const networkMap = new Map(); + for (const network of networks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + return Array.from(networkMap.values()); + } + + function detectPasswordRequired(error: string): bool { + if (!error || error.length === 0) { + return false; + } + + return (error.includes("Secrets were required") || error.includes("Secrets were required, but not provided") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || error.includes("password for") || (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && !error.includes("Connection activated") && !error.includes("successfully"); + } + + function disconnect(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { + if (callback) + callback(result.success ? result.output : ""); + }); + } else { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (callback) + callback(result.success ? result.output : ""); + }); + } + } + + function disconnectEthernet(connectionName: string, callback: var): void { + if (!connectionName || connectionName.length === 0) { + if (callback) + callback({ + success: false, + output: "", + error: "No connection name specified", + exitCode: -1 + }); + return; + } + + executeCommand([root.nmcliCommandConnection, "down", connectionName], result => { + if (result.success) { + root.ethernetDeviceDetails = null; + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + }, 500); + } + if (callback) + callback(result); + }); + } + + function disconnectFromNetwork(): void { + if (active && active.ssid) { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { + if (result.success) { + getNetworks(() => {}); + } + }); + } else { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (result.success) { + getNetworks(() => {}); + } + }); + } + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => { + if (result.success) { + getWifiStatus(status => { + root.wifiEnabled = status; + if (callback) + callback(result); + }); + } else { + if (callback) + callback(result); + } + }); + } + + function executeCommand(args: list, callback: var): void { + const proc = commandProc.createObject(root); + proc.command = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); + + proc.processFinished.connect(() => { + const index = activeProcesses.indexOf(proc); + if (index >= 0) { + activeProcesses.splice(index, 1); + } + }); + + Qt.callLater(() => { + proc.exec(proc.command); + }); + } + + function forgetNetwork(ssid: string, callback: var): void { + if (!ssid || ssid.length === 0) { + if (callback) + callback({ + success: false, + output: "", + error: "No SSID specified", + exitCode: -1 + }); + return; + } + + const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid; + + executeCommand([root.nmcliCommandConnection, "delete", connectionName], result => { + if (result.success) { + Qt.callLater(() => { + loadSavedConnections(() => {}); + }, 500); + } + if (callback) + callback(result); + }); + } + + function getAllInterfaces(callback: var): void { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + const interfaces = parseDeviceStatusOutput(result.output, "both"); + if (callback) + callback(interfaces); + }); + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand([root.nmcliCommandDevice, "show", interfaceName], result => { + if (callback) + callback(result.output); + }); + } + + function getDeviceStatus(callback: var): void { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + if (callback) + callback(result.output); + }); + } + + function getEthernetDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.ethernetInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) + callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], result => { + if (!result.success || !result.output) { + root.ethernetDeviceDetails = null; + if (callback) + callback(null); + return; + } + + const details = parseDeviceDetails(result.output, true); + root.ethernetDeviceDetails = details; + if (callback) + callback(details); + }); + } + + function getEthernetInterfaces(callback: var): void { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); + const devices = []; + + for (const iface of interfaces) { + const connected = isConnectedState(iface.state); + + devices.push({ + interface: iface.device, + type: iface.type, + state: iface.state, + connection: iface.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); + } + + root.ethernetInterfaces = interfaces; + root.ethernetDevices = devices; + if (callback) + callback(interfaces); + }); + } + + function getNetworks(callback: var): void { + executeCommand(["-g", root.networkDetailFields, "d", "w"], result => { + if (!result.success) { + if (callback) + callback([]); + return; + } + + const allNetworks = parseNetworkOutput(result.output); + const networks = deduplicateNetworks(allNetworks); + const rNetworks = root.networks; + + const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); + for (const network of destroyed) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + + for (const network of networks) { + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + + if (callback) + callback(root.networks); + checkPendingConnection(); + }); + } + + function getWifiStatus(callback: var): void { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => { + if (result.success) { + const enabled = result.output.trim() === "enabled"; + root.wifiEnabled = enabled; + if (callback) + callback(enabled); + } else { + if (callback) + callback(root.wifiEnabled); + } + }); + } + + function getWirelessDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.wirelessInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) + callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], result => { + if (!result.success || !result.output) { + root.wirelessDeviceDetails = null; + if (callback) + callback(null); + return; + } + + const details = parseDeviceDetails(result.output, false); + root.wirelessDeviceDetails = details; + if (callback) + callback(details); + }); + } + + function getWirelessInterfaces(callback: var): void { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); + root.wirelessInterfaces = interfaces; + if (callback) + callback(interfaces); + }); + } + + function getWirelessSSIDs(interfaceName: string, callback: var): void { + let cmd = ["-t", "-f", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push(root.connectionParamIfname, interfaceName); + } + executeCommand(cmd, result => { + if (!result.success) { + if (callback) + callback([]); + return; + } + + const ssids = []; + const lines = result.output.trim().split("\n"); + const seenSSIDs = new Set(); + + for (const line of lines) { + if (!line || line.length === 0) + continue; + + const parts = line.split(":"); + if (parts.length >= 1) { + const ssid = parts[0].trim(); + if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { + seenSSIDs.add(ssid); + const signalStr = parts.length >= 2 ? parts[1].trim() : ""; + const signal = signalStr ? parseInt(signalStr, 10) : 0; + const security = parts.length >= 3 ? parts[2].trim() : ""; + ssids.push({ + ssid: ssid, + signal: signalStr, + signalValue: isNaN(signal) ? 0 : signal, + security: security + }); + } + } + } + + ssids.sort((a, b) => { + return b.signalValue - a.signalValue; + }); + + if (callback) + callback(ssids); + }); + } + + function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool { + if (!proc || !error || error.length === 0) { + return false; + } + + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + return false; + } + + const needsPassword = detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output || "", + error: error, + exitCode: exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return true; + } + + return false; + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; + } + } + + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower); + + if (hasSsid) { + return true; + } + + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower); + + return hasConnectionName; + } + + function isConnectedState(state: string): bool { + if (!state || state.length === 0) { + return false; + } + + return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + } + + function isConnectionCommand(command: list): bool { + if (!command || command.length === 0) { + return false; + } + + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + executeCommand([root.nmcliCommandDevice, "status"], result => { + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length >= 3 && parts[0] === interfaceName) { + const connected = isConnectedState(parts[2]); + if (callback) + callback(connected); + return; + } + } + if (callback) + callback(false); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], result => { + if (!result.success) { + root.savedConnections = []; + root.savedConnectionSsids = []; + if (callback) + callback([]); + return; + } + + parseConnectionList(result.output, callback); + }); + } + + function parseConnectionList(output: string, callback: var): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === root.connectionTypeWireless) { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + queryNextSsid(callback); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + if (callback) + callback(root.savedConnectionSsids); + } + } + + function parseDeviceDetails(output: string, isEthernet: bool): var { + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + if (!output || output.length === 0) { + return details; + } + + const lines = output.trim().split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + details.subnet = cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + if (value !== "--") { + details.gateway = value; + } + } else if (key.startsWith("IP4.DNS")) { + if (value !== "--" && value.length > 0) { + details.dns.push(value); + } + } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } else if (!isEthernet && key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + return details; + } + + function parseDeviceStatusOutput(output: string, filterType: string): list { + if (!output || output.length === 0) { + return []; + } + + const interfaces = []; + const lines = output.trim().split("\n"); + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const deviceType = parts[1]; + let shouldInclude = false; + + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { + shouldInclude = true; + } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { + shouldInclude = true; + } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { + shouldInclude = true; + } + + if (shouldInclude) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + } + + return interfaces; + } + + function parseNetworkOutput(output: string): list { + if (!output || output.length === 0) { + return []; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = output.trim().split("\n").filter(line => line && line.length > 0).map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }).filter(n => n.ssid && n.ssid.length > 0); + + return allNetworks; + } + + function processSsidOutput(output: string): void { + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + const ssidLower = ssid.toLowerCase(); + const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); + if (!exists) { + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + } + + function queryNextSsid(callback: var): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], result => { + if (result.success) { + processSsidOutput(result.output); + } + queryNextSsid(callback); + }); + } else { + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + if (callback) + callback(root.savedConnectionSsids); + } + } + + function refreshOnConnectionChange(): void { + getNetworks(networks => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 500); + } else { + root.wirelessDeviceDetails = null; + root.ethernetDeviceDetails = null; + } + + getWirelessInterfaces(() => {}); + getEthernetInterfaces(() => { + if (root.activeEthernet && root.activeEthernet.connected) { + Qt.callLater(() => { + getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); + }, 500); + } + }); + }); + } + + function refreshStatus(callback: var): void { + getDeviceStatus(output => { + const lines = output.trim().split("\n"); + let connected = false; + let activeIf = ""; + let activeConn = ""; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 4) { + const state = parts[2] || ""; + if (isConnectedState(state)) { + connected = true; + activeIf = parts[0] || ""; + activeConn = parts[3] || ""; + break; + } + } + } + + root.isConnected = connected; + root.activeInterface = activeIf; + root.activeConnection = activeConn; + + if (callback) + callback({ + connected, + interface: activeIf, + connection: activeConn + }); + }); + } + + function rescanWifi(): void { + rescanProc.running = true; + } + + function scanWirelessNetworks(interfaceName: string, callback: var): void { + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push(root.connectionParamIfname, interfaceName); + } + executeCommand(cmd, result => { + if (callback) { + callback(result); + } + }); + } + + function toggleWifi(callback: var): void { + const newState = !root.wifiEnabled; + enableWifi(newState, callback); + } + + Component.onCompleted: { + getWifiStatus(() => {}); + getNetworks(() => {}); + loadSavedConnections(() => {}); + getEthernetInterfaces(() => {}); + + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } + + Component { + id: commandProc + + CommandProcess { + } + } + + Component { + id: apComp + + AccessPoint { + } + } + + Timer { + id: connectionCheckTimer + + interval: 4000 + + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + Process { + id: rescanProc + + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + + onExited: root.getNetworks() + } + + Process { + id: monitorProc + + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + running: true + + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + + interval: 2000 + + onTriggered: { + monitorProc.running = true; + } + } + + component AccessPoint: QtObject { + readonly property bool active: lastIpcObject.active + readonly property string bssid: lastIpcObject.bssid + readonly property int frequency: lastIpcObject.frequency + readonly property bool isSecure: security.length > 0 + required property var lastIpcObject + readonly property string security: lastIpcObject.security + readonly property string ssid: lastIpcObject.ssid + readonly property int strength: lastIpcObject.strength + } + component CommandProcess: Process { + id: proc + + property var callback: null + property bool callbackCalled: false + property list command: [] + property int exitCode: 0 + + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + stdout: StdioCollector { + id: stdoutCollector + + } + + onExited: code => { + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + processFinished(); + } + }); + } + } +} diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml index 6c2a5cd..ca87549 100644 --- a/Daemons/NotifServer.qml +++ b/Daemons/NotifServer.qml @@ -14,335 +14,360 @@ import qs.Paths import qs.Config Singleton { - id: root + id: root - property list list: [] - readonly property list notClosed: list.filter( n => !n.closed ) - readonly property list popups: list.filter( n => n.popup ) - property alias dnd: props.dnd - property alias server: server + readonly property var appCooldownMap: new Map() + property alias dnd: props.dnd + property list list: [] + property bool loaded + readonly property list notClosed: list.filter(n => !n.closed) + readonly property list popups: list.filter(n => n.popup) + property alias server: server - property bool loaded + function shouldThrottle(appName: string): bool { + if (props.dnd) + return false; - onListChanged: { - if ( loaded ) { - saveTimer.restart(); - } - if ( root.list.length > 0 ) { - HasNotifications.hasNotifications = true; - } else { - HasNotifications.hasNotifications = false; - } - } + const key = (appName || "unknown").trim().toLowerCase(); + const cooldownSec = Config.notifs.appNotifCooldown; + const cooldownMs = Math.max(0, cooldownSec * 1000); - Timer { - id: saveTimer - interval: 1000 - onTriggered: storage.setText( JSON.stringify( root.notClosed.map( n => ({ - time: n.time, - id: n.id, - summary: n.summary, - body: n.body, - appIcon: n.appIcon, - appName: n.appName, - image: n.image, - expireTimeout: n.expireTimeout, - urgency: n.urgency, - resident: n.resident, - hasActionIcons: n.hasActionIcons, - actions: n.actions - })))); - } + if (cooldownMs <= 0) + return true; - PersistentProperties { - id: props + const now = Date.now(); + const until = appCooldownMap.get(key) ?? 0; - property bool dnd + if (now < until) + return false; - reloadableId: "notifs" - } + appCooldownMap.set(key, now + cooldownMs); + return true; + } - NotificationServer { - id: server + onListChanged: { + if (loaded) { + saveTimer.restart(); + } + if (root.list.length > 0) { + HasNotifications.hasNotifications = true; + } else { + HasNotifications.hasNotifications = false; + } + } - keepOnReload: false - actionsSupported: true - bodyHyperlinksSupported: true - bodyImagesSupported: true - bodyMarkupSupported: true - imageSupported: true - persistenceSupported: true + Timer { + id: saveTimer - onNotification: notif => { - notif.tracked = true; + interval: 1000 - const comp = notifComp.createObject(root, { - popup: !props.dnd, - notification: notif - }); - root.list = [comp, ...root.list]; - } - } + onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({ + time: n.time, + id: n.id, + summary: n.summary, + body: n.body, + appIcon: n.appIcon, + appName: n.appName, + image: n.image, + expireTimeout: n.expireTimeout, + urgency: n.urgency, + resident: n.resident, + hasActionIcons: n.hasActionIcons, + actions: n.actions + })))) + } - FileView { - id: storage - path: `${Paths.state}/notifs.json` + PersistentProperties { + id: props - onLoaded: { - const data = JSON.parse(text()); - for (const notif of data) - root.list.push(notifComp.createObject(root, notif)); - root.list.sort((a, b) => b.time - a.time); - root.loaded = true; - } + property bool dnd - onLoadFailed: err => { - if (err === FileViewError.FileNotFound) { - root.loaded = true; - setText("[]"); - } - } - } + reloadableId: "notifs" + } - CustomShortcut { - name: "clearnotifs" - description: "Clear all notifications" - onPressed: { - for (const notif of root.list.slice()) - notif.close(); - } - } + NotificationServer { + id: server - IpcHandler { - target: "notifs" + actionsSupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + bodyMarkupSupported: true + imageSupported: true + keepOnReload: false + persistenceSupported: true - function clear(): void { - for (const notif of root.list.slice()) - notif.close(); - } + onNotification: notif => { + notif.tracked = true; - function isDndEnabled(): bool { - return props.dnd; - } + const is_popup = root.shouldThrottle(notif.appName); - function toggleDnd(): void { - props.dnd = !props.dnd; - } + const comp = notifComp.createObject(root, { + popup: is_popup, + notification: notif + }); + root.list = [comp, ...root.list]; + } + } - function enableDnd(): void { - props.dnd = true; - } + FileView { + id: storage - function disableDnd(): void { - props.dnd = false; - } - } + path: `${Paths.state}/notifs.json` - component Notif: QtObject { - id: notif + onLoadFailed: err => { + if (err === FileViewError.FileNotFound) { + root.loaded = true; + setText("[]"); + } + } + onLoaded: { + const data = JSON.parse(text()); + for (const notif of data) + root.list.push(notifComp.createObject(root, notif)); + root.list.sort((a, b) => b.time - a.time); + root.loaded = true; + } + } - property bool popup - property bool closed - property var locks: new Set() + CustomShortcut { + description: "Clear all notifications" + name: "clearnotifs" - property date time: new Date() - readonly property string timeStr: { - const diff = Time.date.getTime() - time.getTime(); - const m = Math.floor(diff / 60000); + onPressed: { + for (const notif of root.list.slice()) + notif.close(); + } + } - if (m < 1) - return qsTr("now"); + IpcHandler { + function clear(): void { + for (const notif of root.list.slice()) + notif.close(); + } - const h = Math.floor(m / 60); - const d = Math.floor(h / 24); + function disableDnd(): void { + props.dnd = false; + } - if (d > 0) - return `${d}d`; - if (h > 0) - return `${h}h`; - return `${m}m`; - } + function enableDnd(): void { + props.dnd = true; + } - property Notification notification - property string id - property string summary - property string body - property string appIcon - property string appName - property string image - property real expireTimeout: 5 - property int urgency: NotificationUrgency.Normal - property bool resident - property bool hasActionIcons - property list actions + function isDndEnabled(): bool { + return props.dnd; + } - readonly property Timer timer: Timer { - property int totalTime: Config.notifs.defaultExpireTimeout - property int remainingTime: totalTime - property bool paused: false + function toggleDnd(): void { + props.dnd = !props.dnd; + } - running: !paused - repeat: true - interval: 50 - onTriggered: { - remainingTime -= interval; + target: "notifs" + } - if ( remainingTime <= 0 ) { - remainingTime = 0; - notif.popup = false; - stop(); + Component { + id: notifComp + + Notif { + } + } + + component Notif: QtObject { + id: notif + + property list actions + property string appIcon + property string appName + property string body + property bool closed + readonly property Connections conn: Connections { + function onActionsChanged(): void { + notif.actions = notif.notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onClosed(): void { + notif.close(); + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + if (notif.notification?.image) + notif.dummyImageLoader.active = true; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + target: notif.notification + } + readonly property LazyLoader dummyImageLoader: LazyLoader { + active: false + + PanelWindow { + color: "transparent" + implicitHeight: Config.notifs.sizes.image + implicitWidth: Config.notifs.sizes.image + + mask: Region { } - } - } - readonly property LazyLoader dummyImageLoader: LazyLoader { - active: false + Image { + function tryCache(): void { + if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) + return; - PanelWindow { - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image - color: "transparent" - mask: Region {} + const cacheKey = notif.appName + notif.summary + notif.id; + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < cacheKey.length; i++) { + ch = cacheKey.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - Image { - function tryCache(): void { - if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) - return; - - const cacheKey = notif.appName + notif.summary + notif.id; - let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; - for (let i = 0; i < cacheKey.length; i++) { - ch = cacheKey.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - - const cache = `${Paths.notifimagecache}/${hash}.png`; + const cache = `${Paths.notifimagecache}/${hash}.png`; ZShellIo.saveItem(this, Qt.resolvedUrl(cache), () => { notif.image = cache; notif.dummyImageLoader.active = false; }); - } + } - anchors.fill: parent - source: Qt.resolvedUrl(notif.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - opacity: 0 + anchors.fill: parent + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop + opacity: 0 + source: Qt.resolvedUrl(notif.image) - onStatusChanged: tryCache() - onWidthChanged: tryCache() - onHeightChanged: tryCache() - } - } - } + onHeightChanged: tryCache() + onStatusChanged: tryCache() + onWidthChanged: tryCache() + } + } + } + property real expireTimeout: 5 + property bool hasActionIcons + property string id + property string image + property var locks: new Set() + property Notification notification + property bool popup + property bool resident + property string summary + property date time: new Date() + readonly property string timeStr: { + const diff = Time.date.getTime() - time.getTime(); + const m = Math.floor(diff / 60000); - readonly property Connections conn: Connections { - target: notif.notification + if (m < 1) + return qsTr("now"); - function onClosed(): void { - notif.close(); - } + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); - function onSummaryChanged(): void { - notif.summary = notif.notification.summary; - } + if (d > 0) + return `${d}d`; + if (h > 0) + return `${h}h`; + return `${m}m`; + } + readonly property Timer timer: Timer { + property bool paused: false + property int remainingTime: totalTime + property int totalTime: Config.notifs.defaultExpireTimeout - function onBodyChanged(): void { - notif.body = notif.notification.body; - } + interval: 50 + repeat: true + running: !paused - function onAppIconChanged(): void { - notif.appIcon = notif.notification.appIcon; - } + onTriggered: { + remainingTime -= interval; - function onAppNameChanged(): void { - notif.appName = notif.notification.appName; - } + if (remainingTime <= 0) { + remainingTime = 0; + notif.popup = false; + stop(); + } + } + } + property int urgency: NotificationUrgency.Normal - function onImageChanged(): void { - notif.image = notif.notification.image; - if (notif.notification?.image) - notif.dummyImageLoader.active = true; - } + function close(): void { + closed = true; + if (locks.size === 0 && root.list.includes(this)) { + root.list = root.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } - function onExpireTimeoutChanged(): void { - notif.expireTimeout = notif.notification.expireTimeout; - } + function lock(item: Item): void { + locks.add(item); + } - function onUrgencyChanged(): void { - notif.urgency = notif.notification.urgency; - } + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } - function onResidentChanged(): void { - notif.resident = notif.notification.resident; - } + Component.onCompleted: { + if (!notification) + return; - function onHasActionIconsChanged(): void { - notif.hasActionIcons = notif.notification.hasActionIcons; - } - - function onActionsChanged(): void { - notif.actions = notif.notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } - } - - function lock(item: Item): void { - locks.add(item); - } - - function unlock(item: Item): void { - locks.delete(item); - if (closed) - close(); - } - - function close(): void { - closed = true; - if (locks.size === 0 && root.list.includes(this)) { - root.list = root.list.filter(n => n !== this); - notification?.dismiss(); - destroy(); - } - } - - Component.onCompleted: { - if (!notification) - return; - - id = notification.id; - summary = notification.summary; - body = notification.body; - appIcon = notification.appIcon; - appName = notification.appName; - image = notification.image; - if (notification?.image) - dummyImageLoader.active = true; - expireTimeout = notification.expireTimeout; - urgency = notification.urgency; - resident = notification.resident; - hasActionIcons = notification.hasActionIcons; - actions = notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } - } - - Component { - id: notifComp - - Notif {} - } + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + if (notification?.image) + dummyImageLoader.active = true; + expireTimeout = notification.expireTimeout; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + actions = notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + } } diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index 32f6b3a..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,73 +10,86 @@ 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 Shape { - id: root + id: root - required property Panels panels - required property Item bar + required property Item bar + required property Panels panels required property PersistentProperties visibilities - anchors.fill: parent - // anchors.margins: 8 - anchors.topMargin: bar.implicitHeight - preferredRendererType: Shape.CurveRenderer + anchors.fill: parent + // anchors.margins: 8 + anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight + preferredRendererType: Shape.CurveRenderer - Component.onCompleted: console.log(root.bar.implicitHeight, root.bar.anchors.topMargin) - - Osd.Background { - wrapper: root.panels.osd - - startX: root.width - root.panels.sidebar.width - startY: ( root.height - wrapper.height ) / 2 - rounding + Behavior on anchors.topMargin { + Anim { + } } - Modules.Background { - wrapper: root.panels.popouts - invertBottomRounding: wrapper.x <= 0 + Resources.Background { + startX: 0 - rounding + startY: 0 + wrapper: root.panels.resources + } - startX: wrapper.x - 8 - startY: wrapper.y - } + Osd.Background { + startX: root.width - root.panels.sidebar.width + startY: (root.height - wrapper.height) / 2 - rounding + wrapper: root.panels.osd + } + + Modules.Background { + invertBottomRounding: wrapper.x <= 0 + startX: wrapper.x - 8 + startY: wrapper.y + wrapper: root.panels.popouts + } Notifications.Background { - wrapper: root.panels.notifications sidebar: sidebar - startX: root.width startY: 0 + wrapper: root.panels.notifications } Launcher.Background { - wrapper: root.panels.launcher - - startX: ( root.width - wrapper.width ) / 2 - rounding + startX: (root.width - wrapper.width) / 2 - rounding startY: root.height + wrapper: root.panels.launcher } Dashboard.Background { - wrapper: root.panels.dashboard - startX: root.width - root.panels.dashboard.width - rounding startY: 0 + wrapper: root.panels.dashboard } Utils.Background { - wrapper: root.panels.utilities sidebar: sidebar - startX: root.width startY: root.height + wrapper: root.panels.utilities } Sidebar.Background { id: sidebar - wrapper: root.panels.sidebar panels: root.panels - startX: root.width startY: root.panels.notifications.height + wrapper: root.panels.sidebar + } + + 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 new file mode 100644 index 0000000..02d3e3c --- /dev/null +++ b/Drawers/Bar.qml @@ -0,0 +1,215 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import qs.Daemons +import qs.Components +import qs.Modules +import qs.Modules.Bar +import qs.Config +import qs.Helpers +import qs.Drawers + +Variants { + model: Quickshell.screens + + Scope { + id: scope + + required property var modelData + + PanelWindow { + id: bar + + property var root: Quickshell.shellDir + property bool trayMenuVisible: false + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + 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 + screen: scope.modelData + + mask: Region { + id: region + + height: bar.screen.height - backgroundRect.implicitHeight + intersection: Intersection.Xor + regions: popoutRegions.instances + width: bar.width + x: 0 + y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height + } + + contentItem.Keys.onEscapePressed: { + if (Config.barConfig.autoHide) + visibilities.bar = false; + visibilities.sidebar = false; + visibilities.dashboard = false; + visibilities.osd = false; + visibilities.settings = false; + visibilities.resources = false; + } + + PanelWindow { + id: exclusionZone + + WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto + WlrLayershell.layer: WlrLayer.Bottom + WlrLayershell.namespace: "ZShell-Bar-Exclusion" + color: "transparent" + implicitHeight: backgroundRect.height + screen: bar.screen + + anchors { + left: true + right: true + top: true + } + } + + anchors { + bottom: true + left: true + right: true + top: true + } + + Variants { + id: popoutRegions + + model: panels.children + + Region { + required property Item modelData + + height: modelData.height + intersection: Intersection.Subtract + width: modelData.width + x: modelData.x + y: modelData.y + backgroundRect.implicitHeight + } + } + + HyprlandFocusGrab { + id: focusGrab + + active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) + windows: [bar] + + onCleared: { + visibilities.launcher = false; + visibilities.sidebar = false; + visibilities.dashboard = false; + visibilities.osd = false; + visibilities.settings = false; + visibilities.resources = false; + panels.popouts.hasCurrent = false; + } + } + + PersistentProperties { + id: visibilities + + property bool bar + property bool dashboard + property bool launcher + property bool notif: NotifServer.popups.length > 0 + property bool osd + property bool resources + property bool settings + property bool sidebar + + Component.onCompleted: Visibilities.load(scope.modelData, this) + } + + Binding { + property: "bar" + target: visibilities + value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources + when: Config.barConfig.autoHide + } + + Item { + anchors.fill: parent + layer.enabled: true + opacity: Appearance.transparency.enabled ? DynamicColors.transparency.base : 1 + + layer.effect: MultiEffect { + blurMax: 32 + shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 1) + shadowEnabled: true + } + + Border { + bar: backgroundRect + visibilities: visibilities + } + + Backgrounds { + bar: backgroundRect + panels: panels + visibilities: visibilities + } + } + + Interactions { + id: mouseArea + + anchors.fill: parent + bar: barLoader + panels: panels + popouts: panels.popouts + screen: scope.modelData + visibilities: visibilities + + Panels { + id: panels + + bar: backgroundRect + screen: scope.modelData + visibilities: visibilities + } + + CustomRect { + id: backgroundRect + + property Wrapper popouts: panels.popouts + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0 + color: "transparent" + implicitHeight: barLoader.implicitHeight + radius: 0 + + Behavior on anchors.topMargin { + Anim { + } + } + Behavior on color { + CAnim { + } + } + + BarLoader { + id: barLoader + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + bar: bar + popouts: panels.popouts + screen: scope.modelData + visibilities: visibilities + } + } + } + } + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index a8b3cad..11f6561 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -5,53 +5,52 @@ import qs.Config import qs.Modules as BarPopouts CustomMouseArea { - id: root + id: root - required property ShellScreen screen - required property BarPopouts.Wrapper popouts - required property PersistentProperties visibilities - required property Panels panels - required property Item bar + required property Item bar + property bool dashboardShortcutActive + property point dragStart + property bool osdShortcutActive + required property Panels panels + required property BarPopouts.Wrapper popouts + required property ShellScreen screen + property bool utilitiesShortcutActive + required property PersistentProperties visibilities - property point dragStart - property bool dashboardShortcutActive - property bool osdShortcutActive - property bool utilitiesShortcutActive + function inBottomPanel(panel: Item, x: real, y: real): bool { + return y > root.height - panel.height && withinPanelWidth(panel, x, y); + } - function withinPanelHeight(panel: Item, x: real, y: real): bool { - const panelY = panel.y + bar.implicitHeight; - return y >= panelY && y <= panelY + panel.height; - } + function inLeftPanel(panel: Item, x: real, y: real): bool { + return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + } - function withinPanelWidth(panel: Item, x: real, y: real): bool { - const panelX = panel.x; - return x >= panelX && x <= panelX + panel.width; - } + function inRightPanel(panel: Item, x: real, y: real): bool { + return x > panel.x && withinPanelHeight(panel, x, y); + } - function inLeftPanel(panel: Item, x: real, y: real): bool { - return x < panel.x + panel.width && withinPanelHeight(panel, x, y); - } + function inTopPanel(panel: Item, x: real, y: real): bool { + return y < bar.implicitHeight + panel.height && withinPanelWidth(panel, x, y); + } - function inRightPanel(panel: Item, x: real, y: real): bool { - return x > panel.x && withinPanelHeight(panel, x, y); - } + function onWheel(event: WheelEvent): void { + if (event.x < bar.implicitWidth) { + bar.handleWheel(event.y, event.angleDelta); + } + } - function inTopPanel(panel: Item, x: real, y: real): bool { - return y < bar.implicitHeight + panel.height && withinPanelWidth(panel, x, y); - } + function withinPanelHeight(panel: Item, x: real, y: real): bool { + const panelY = panel.y + bar.implicitHeight; + return y >= panelY && y <= panelY + panel.height; + } - function inBottomPanel(panel: Item, x: real, y: real): bool { - return y > root.height - panel.height && withinPanelWidth(panel, x, y); - } + function withinPanelWidth(panel: Item, x: real, y: real): bool { + const panelX = panel.x; + return x >= panelX && x <= panelX + panel.width; + } - function onWheel(event: WheelEvent): void { - if (event.x < bar.implicitWidth) { - bar.handleWheel(event.y, event.angleDelta); - } - } - - anchors.fill: parent - hoverEnabled: true + anchors.fill: parent + hoverEnabled: true // onPressed: event => { // if ( root.popouts.hasCurrent && !inTopPanel( root.popouts, event.x, event.y )) { @@ -63,216 +62,215 @@ CustomMouseArea { // } // } - onContainsMouseChanged: { - if (!containsMouse) { - // Only hide if not activated by shortcut - if (!osdShortcutActive) { - visibilities.osd = false; - root.panels.osd.hovered = false; - } + onContainsMouseChanged: { + if (!containsMouse) { + // Only hide if not activated by shortcut + if (!osdShortcutActive) { + visibilities.osd = false; + root.panels.osd.hovered = false; + } - if (!popouts.currentName.startsWith("traymenu")) { - popouts.hasCurrent = false; - } + if (!popouts.currentName.startsWith("traymenu")) { + popouts.hasCurrent = false; + } - if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard) - root.visibilities.bar = false; - } - } + if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard) + root.visibilities.bar = false; + } + } + onPositionChanged: event => { + if (popouts.isDetached) + return; - onPositionChanged: event => { - if (popouts.isDetached) - return; + const x = event.x; + const y = event.y; + const dragX = x - dragStart.x; + const dragY = y - dragStart.y; - const x = event.x; - const y = event.y; - const dragX = x - dragStart.x; - const dragY = y - dragStart.y; + // Show bar in non-exclusive mode on hover + if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) + visibilities.bar = true; - // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) - visibilities.bar = true; + if (panels.sidebar.width === 0) { + // Show osd on hover + const showOsd = inRightPanel(panels.osd, x, y); - if (panels.sidebar.width === 0) { - // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); + // // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { + visibilities.osd = showOsd; + root.panels.osd.hovered = showOsd; + } else if (showOsd) { + // If hovering over OSD area while in shortcut mode, transition to hover control + osdShortcutActive = false; + root.panels.osd.hovered = true; + } - // // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control - osdShortcutActive = false; - root.panels.osd.hovered = true; - } + // const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; + // + // // Show/hide session on drag + // if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + // if (dragX < -Config.session.dragThreshold) + // visibilities.session = true; + // else if (dragX > Config.session.dragThreshold) + // visibilities.session = false; + // + // // Show sidebar on drag if in session area and session is nearly fully visible + // if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) + // visibilities.sidebar = true; + // } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { + // // Show sidebar on drag if not in session area + // visibilities.sidebar = true; + // } + } else { + const outOfSidebar = x < width - panels.sidebar.width; + // Show osd on hover + const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); - // const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; - // - // // Show/hide session on drag - // if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // - // // Show sidebar on drag if in session area and session is nearly fully visible - // if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) - // visibilities.sidebar = true; - // } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { - // // Show sidebar on drag if not in session area - // visibilities.sidebar = true; - // } - } else { - const outOfSidebar = x < width - panels.sidebar.width; - // Show osd on hover - const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { + visibilities.osd = showOsd; + root.panels.osd.hovered = showOsd; + } else if (showOsd) { + // If hovering over OSD area while in shortcut mode, transition to hover control + osdShortcutActive = false; + root.panels.osd.hovered = true; + } + // + // // Show/hide session on drag + // if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + // if (dragX < -Config.session.dragThreshold) + // visibilities.session = true; + // else if (dragX > Config.session.dragThreshold) + // visibilities.session = false; + // } + // + // // Hide sidebar on drag + // if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) + // visibilities.sidebar = false; + } - // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control - osdShortcutActive = false; - root.panels.osd.hovered = true; - } - // - // // Show/hide session on drag - // if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // } - // - // // Hide sidebar on drag - // if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) - // visibilities.sidebar = false; - } + // Show launcher on hover, or show/hide on drag if hover is disabled + // if (Config.launcher.showOnHover) { + // if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) + // visibilities.launcher = true; + // } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { + // if (dragY < -Config.launcher.dragThreshold) + // visibilities.launcher = true; + // else if (dragY > Config.launcher.dragThreshold) + // visibilities.launcher = false; + // } + // + // // Show dashboard on hover + // const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); + // + // // Always update visibility based on hover if not in shortcut mode + // if (!dashboardShortcutActive) { + // visibilities.dashboard = showDashboard; + // } else if (showDashboard) { + // // If hovering over dashboard area while in shortcut mode, transition to hover control + // dashboardShortcutActive = false; + // } + // + // // Show/hide dashboard on drag (for touchscreen devices) + // if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { + // if (dragY > Config.dashboard.dragThreshold) + // visibilities.dashboard = true; + // else if (dragY < -Config.dashboard.dragThreshold) + // visibilities.dashboard = false; + // } + // + // // Show utilities on hover + // const showUtilities = inBottomPanel(panels.utilities, x, y); + // + // // Always update visibility based on hover if not in shortcut mode + // if (!utilitiesShortcutActive) { + // visibilities.utilities = showUtilities; + // } else if (showUtilities) { + // // If hovering over utilities area while in shortcut mode, transition to hover control + // utilitiesShortcutActive = false; + // } - // Show launcher on hover, or show/hide on drag if hover is disabled - // if (Config.launcher.showOnHover) { - // if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) - // visibilities.launcher = true; - // } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { - // if (dragY < -Config.launcher.dragThreshold) - // visibilities.launcher = true; - // else if (dragY > Config.launcher.dragThreshold) - // visibilities.launcher = false; - // } - // - // // Show dashboard on hover - // const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!dashboardShortcutActive) { - // visibilities.dashboard = showDashboard; - // } else if (showDashboard) { - // // If hovering over dashboard area while in shortcut mode, transition to hover control - // dashboardShortcutActive = false; - // } - // - // // Show/hide dashboard on drag (for touchscreen devices) - // if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { - // if (dragY > Config.dashboard.dragThreshold) - // visibilities.dashboard = true; - // else if (dragY < -Config.dashboard.dragThreshold) - // visibilities.dashboard = false; - // } - // - // // Show utilities on hover - // const showUtilities = inBottomPanel(panels.utilities, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!utilitiesShortcutActive) { - // visibilities.utilities = showUtilities; - // } else if (showUtilities) { - // // If hovering over utilities area while in shortcut mode, transition to hover control - // utilitiesShortcutActive = false; - // } + // Show popouts on hover + if (y < bar.implicitHeight) { + bar.checkPopout(x); + } + } - // Show popouts on hover - if (y < bar.implicitHeight) { - bar.checkPopout(x); - } - } + // Monitor individual visibility changes + Connections { + function onDashboardChanged() { + if (root.visibilities.dashboard) { + // Dashboard became visible, immediately check if this should be shortcut mode + const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); + if (!inDashboardArea) { + root.dashboardShortcutActive = true; + } - // Monitor individual visibility changes - Connections { - target: root.visibilities + root.visibilities.sidebar = false; + root.popouts.hasCurrent = false; + } else { + // Dashboard hidden, clear shortcut flag + root.dashboardShortcutActive = false; + // root.visibilities.bar = false; + } + } - function onLauncherChanged() { - // If launcher is hidden, clear shortcut flags for dashboard and OSD - if (!root.visibilities.launcher) { - root.dashboardShortcutActive = false; - root.osdShortcutActive = false; - root.utilitiesShortcutActive = false; + function onLauncherChanged() { + // If launcher is hidden, clear shortcut flags for dashboard and OSD + if (!root.visibilities.launcher) { + root.dashboardShortcutActive = false; + root.osdShortcutActive = false; + root.utilitiesShortcutActive = false; - // Also hide dashboard and OSD if they're not being hovered - const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + // Also hide dashboard and OSD if they're not being hovered + const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); - if (!inDashboardArea) { - root.visibilities.dashboard = false; - } - if (!inOsdArea) { - root.visibilities.osd = false; - root.panels.osd.hovered = false; - } - } - } + if (!inOsdArea) { + root.visibilities.osd = false; + root.panels.osd.hovered = false; + } + } + } + + function onOsdChanged() { + if (root.visibilities.osd) { + // OSD became visible, immediately check if this should be shortcut mode + const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + if (!inOsdArea) { + root.osdShortcutActive = true; + } + } else { + // OSD hidden, clear shortcut flag + root.osdShortcutActive = false; + } + } + + function onResourcesChanged() { + if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) { + root.popouts.hasCurrent = false; + } + } function onSidebarChanged() { - if ( root.visibilities.sidebar ) { + if (root.visibilities.sidebar) { root.visibilities.dashboard = false; root.popouts.hasCurrent = false; } } - function onDashboardChanged() { - if (root.visibilities.dashboard) { - // Dashboard became visible, immediately check if this should be shortcut mode - const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); - if (!inDashboardArea) { - root.dashboardShortcutActive = true; - } + function onUtilitiesChanged() { + if (root.visibilities.utilities) { + // Utilities became visible, immediately check if this should be shortcut mode + const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); + if (!inUtilitiesArea) { + root.utilitiesShortcutActive = true; + } + } else { + // Utilities hidden, clear shortcut flag + root.utilitiesShortcutActive = false; + } + } - root.visibilities.sidebar = false; - root.popouts.hasCurrent = false; - - } else { - // Dashboard hidden, clear shortcut flag - root.dashboardShortcutActive = false; - // root.visibilities.bar = false; - } - } - - function onOsdChanged() { - if (root.visibilities.osd) { - // OSD became visible, immediately check if this should be shortcut mode - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); - console.log(inOsdArea); - if (!inOsdArea) { - root.osdShortcutActive = true; - } - } else { - // OSD hidden, clear shortcut flag - root.osdShortcutActive = false; - } - } - - function onUtilitiesChanged() { - if (root.visibilities.utilities) { - // Utilities became visible, immediately check if this should be shortcut mode - const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); - if (!inUtilitiesArea) { - root.utilitiesShortcutActive = true; - } - } else { - // Utilities hidden, clear shortcut flag - root.utilitiesShortcutActive = false; - } - } - } + target: root.visibilities + } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 4282c84..7dea587 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -1,6 +1,6 @@ import Quickshell import QtQuick -import QtQuick.Shapes +import qs.Components import qs.Modules as Modules import qs.Modules.Notifications as Notifications import qs.Modules.Notifications.Sidebar as Sidebar @@ -9,116 +9,130 @@ 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.Resources as Resources +import qs.Modules.Settings as Settings import qs.Config Item { - id: root + id: root - required property ShellScreen screen - required property Item bar + required property Item bar + readonly property alias dashboard: dashboard + readonly property alias launcher: launcher + 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 sidebar: sidebar + readonly property alias toasts: toasts + readonly property alias utilities: utilities required property PersistentProperties visibilities - readonly property alias popouts: popouts - readonly property alias sidebar: sidebar - readonly property alias notifications: notifications - readonly property alias utilities: utilities - readonly property alias dashboard: dashboard - readonly property alias osd: osd - readonly property alias toasts: toasts - readonly property alias launcher: launcher + anchors.fill: parent + // anchors.margins: 8 + anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight - anchors.fill: parent - // anchors.margins: 8 - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight Behavior on anchors.topMargin { - Modules.Anim {} + Anim { + } + } + + Resources.Wrapper { + id: resources + + anchors.left: parent.left + anchors.top: parent.top + visibilities: root.visibilities } Osd.Wrapper { id: osd + anchors.right: parent.right + anchors.rightMargin: sidebar.width + anchors.verticalCenter: parent.verticalCenter clip: sidebar.width > 0 screen: root.screen visibilities: root.visibilities - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: sidebar.width } - Modules.Wrapper { - id: popouts + Modules.Wrapper { + id: popouts - screen: root.screen - - anchors.top: parent.top - - x: { - const off = currentCenter - nonAnimWidth / 2; - const diff = root.width - Math.floor(off + nonAnimWidth); - if ( diff < 0 ) - return off + diff; - return Math.floor( Math.max( off, 0 )); - } - } + anchors.top: parent.top + screen: root.screen + x: { + const off = currentCenter - nonAnimWidth / 2; + const diff = root.width - Math.floor(off + nonAnimWidth); + if (diff < 0) + return off + diff; + return Math.floor(Math.max(off, 0)); + } + } Toasts.Toasts { id: toasts anchors.bottom: sidebar.visible ? parent.bottom : utilities.top - anchors.right: sidebar.left anchors.margins: Appearance.padding.normal + anchors.right: sidebar.left } Notifications.Wrapper { id: notifications - visibilities: root.visibilities - panels: root - - anchors.top: parent.top anchors.right: parent.right + anchors.top: parent.top + panels: root + visibilities: root.visibilities } Launcher.Wrapper { id: launcher + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + panels: root screen: root.screen visibilities: root.visibilities - panels: root - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom } Utils.Wrapper { id: utilities - visibilities: root.visibilities - sidebar: sidebar - popouts: popouts - anchors.bottom: parent.bottom anchors.right: parent.right + popouts: popouts + sidebar: sidebar + visibilities: root.visibilities } Dashboard.Wrapper { id: dashboard - visibilities: root.visibilities - anchors.right: parent.right anchors.top: parent.top + visibilities: root.visibilities } Sidebar.Wrapper { id: sidebar - visibilities: root.visibilities - panels: root - - anchors.top: notifications.bottom anchors.bottom: utilities.top anchors.right: parent.right + anchors.top: notifications.bottom + 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/Effects/ShadowRect.qml b/Effects/ShadowRect.qml index 92889c4..549546c 100644 --- a/Effects/ShadowRect.qml +++ b/Effects/ShadowRect.qml @@ -2,29 +2,32 @@ import QtQuick import QtQuick.Effects Item { - id: root - property real radius + id: root - Rectangle { - id: shadowRect - anchors.fill: root - radius: root.radius - layer.enabled: true - color: "black" - visible: false - } + property real radius - MultiEffect { - id: effects - source: shadowRect - anchors.fill: shadowRect - shadowBlur: 2.0 - shadowEnabled: true - shadowOpacity: 1 - shadowColor: "black" - maskSource: shadowRect - maskEnabled: true - maskInverted: true - autoPaddingEnabled: true - } + Rectangle { + id: shadowRect + + anchors.fill: root + color: "black" + layer.enabled: true + radius: root.radius + visible: false + } + + MultiEffect { + id: effects + + anchors.fill: shadowRect + autoPaddingEnabled: true + maskEnabled: true + maskInverted: true + maskSource: shadowRect + shadowBlur: 2.0 + shadowColor: "black" + shadowEnabled: true + shadowOpacity: 1 + source: shadowRect + } } diff --git a/Helpers/AreaPicker.qml b/Helpers/AreaPicker.qml index d329829..ce15fab 100644 --- a/Helpers/AreaPicker.qml +++ b/Helpers/AreaPicker.qml @@ -8,49 +8,49 @@ import ZShell import qs.Components Scope { - LazyLoader { - id: root + LazyLoader { + id: root - property bool freeze - property bool closing + property bool closing + property bool freeze - Variants { - model: Quickshell.screens - PanelWindow { - id: win - color: "transparent" + Variants { + model: Quickshell.screens - required property ShellScreen modelData + PanelWindow { + id: win - screen: modelData - WlrLayershell.namespace: "areapicker" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive - mask: root.closing ? empty : null + required property ShellScreen modelData - anchors { - top: true - bottom: true - left: true - right: true - } + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "areapicker" + color: "transparent" + mask: root.closing ? empty : null + screen: modelData - Region { - id: empty - } + anchors { + bottom: true + left: true + right: true + top: true + } - Picker { - loader: root - screen: win.modelData - } - } - } - } + Region { + id: empty + + } + + Picker { + loader: root + screen: win.modelData + } + } + } + } IpcHandler { - target: "picker" - function open(): void { root.freeze = false; root.closing = false; @@ -62,23 +62,27 @@ Scope { root.closing = false; root.activeAsync = true; } + + target: "picker" } - CustomShortcut { - name: "screenshot" - onPressed: { - root.freeze = false; - root.closing = false; - root.activeAsync = true; - } - } + CustomShortcut { + name: "screenshot" - CustomShortcut { - name: "screenshotFreeze" - onPressed: { - root.freeze = true; - root.closing = false; - root.activeAsync = true; - } - } + onPressed: { + root.freeze = false; + root.closing = false; + root.activeAsync = true; + } + } + + CustomShortcut { + name: "screenshotFreeze" + + onPressed: { + root.freeze = true; + root.closing = false; + root.activeAsync = true; + } + } } diff --git a/Helpers/Brightness.qml b/Helpers/Brightness.qml index becd1bc..5e2e7fb 100644 --- a/Helpers/Brightness.qml +++ b/Helpers/Brightness.qml @@ -8,219 +8,257 @@ import qs.Config import qs.Components Singleton { - id: root + id: root - property list ddcMonitors: [] - readonly property list monitors: variants.instances - property bool appleDisplayPresent: false + property bool appleDisplayPresent: false + property list ddcMonitors: [] + property list ddcServiceMon: [] + readonly property list monitors: variants.instances - function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); - } + function decreaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) + monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + } - function getMonitor(query: string): var { - if (query === "active") { - return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); - } + function getMonitor(query: string): var { + if (query === "active") { + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); + } - if (query.startsWith("model:")) { - const model = query.slice(6); - return monitors.find(m => m.modelData.model === model); - } + if (query.startsWith("model:")) { + const model = query.slice(6); + return monitors.find(m => m.modelData.model === model); + } - if (query.startsWith("serial:")) { - const serial = query.slice(7); - return monitors.find(m => m.modelData.serialNumber === serial); - } + if (query.startsWith("serial:")) { + const serial = query.slice(7); + return monitors.find(m => m.modelData.serialNumber === serial); + } - if (query.startsWith("id:")) { - const id = parseInt(query.slice(3), 10); - return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); - } + if (query.startsWith("id:")) { + const id = parseInt(query.slice(3), 10); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); + } - return monitors.find(m => m.modelData.name === query); - } + return monitors.find(m => m.modelData.name === query); + } - function increaseBrightness(): void { - const monitor = getMonitor("active"); - if (monitor) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); - } + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen); + } - function decreaseBrightness(): void { - const monitor = getMonitor("active"); - if (monitor) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); - } + function increaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) + monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + } - onMonitorsChanged: { - ddcMonitors = []; - ddcProc.running = true; - } + onMonitorsChanged: { + ddcMonitors = []; + ddcServiceMon = []; + ddcServiceProc.running = true; + ddcProc.running = true; + } - Variants { - id: variants + Variants { + id: variants - model: Quickshell.screens + model: Quickshell.screens - Monitor {} - } + Monitor { + } + } - Process { - running: true - command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed - stdout: StdioCollector { - onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 - } - } + Process { + command: ["sh", "-c", "asdbctl get"] + running: true - Process { - id: ddcProc + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } - command: ["ddcutil", "detect", "--brief"] - stdout: StdioCollector { - onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ - busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], - connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" - })) - } - } + Process { + id: ddcProc - CustomShortcut { - name: "brightnessUp" - description: "Increase brightness" - onPressed: root.increaseBrightness() - } + command: ["ddcutil", "detect", "--brief"] - CustomShortcut { - name: "brightnessDown" - description: "Decrease brightness" - onPressed: root.decreaseBrightness() - } + stdout: StdioCollector { + onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ + busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], + connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" + })) + } + } - IpcHandler { - target: "brightness" + Process { + id: ddcServiceProc - function get(): real { - return getFor("active"); - } + command: ["ddcutil-client", "detect"] - // Allows searching by active/model/serial/id/name - function getFor(query: string): real { - return root.getMonitor(query)?.brightness ?? -1; - } + // running: true - function set(value: string): string { - return setFor("active", value); - } + stdout: StdioCollector { + onStreamFinished: { + const t = text.replace(/\r\n/g, "\n").trim(); - // Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%- - function setFor(query: string, value: string): string { - const monitor = root.getMonitor(query); - if (!monitor) - return "Invalid monitor: " + query; + 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; + } + } + } - let targetBrightness; - if (value.endsWith("%-")) { - const percent = parseFloat(value.slice(0, -2)); - targetBrightness = monitor.brightness - (percent / 100); - } else if (value.startsWith("+") && value.endsWith("%")) { - const percent = parseFloat(value.slice(1, -1)); - targetBrightness = monitor.brightness + (percent / 100); - } else if (value.endsWith("%")) { - const percent = parseFloat(value.slice(0, -1)); - targetBrightness = percent / 100; - } else if (value.startsWith("+")) { - const increment = parseFloat(value.slice(1)); - targetBrightness = monitor.brightness + increment; - } else if (value.endsWith("-")) { - const decrement = parseFloat(value.slice(0, -1)); - targetBrightness = monitor.brightness - decrement; - } else if (value.includes("%") || value.includes("-") || value.includes("+")) { - return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; - } else { - targetBrightness = parseFloat(value); - } + CustomShortcut { + description: "Increase brightness" + name: "brightnessUp" - if (isNaN(targetBrightness)) - return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; + onPressed: root.increaseBrightness() + } - monitor.setBrightness(targetBrightness); + CustomShortcut { + description: "Decrease brightness" + name: "brightnessDown" - return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; - } - } + onPressed: root.decreaseBrightness() + } - component Monitor: QtObject { - id: monitor + IpcHandler { + function get(): real { + return getFor("active"); + } - required property ShellScreen modelData - readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) - readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" - readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") - property real brightness - property real queuedBrightness: NaN + // Allows searching by active/model/serial/id/name + function getFor(query: string): real { + return root.getMonitor(query)?.brightness ?? -1; + } - readonly property Process initProc: Process { - stdout: StdioCollector { - onStreamFinished: { - if (monitor.isAppleDisplay) { - const val = parseInt(text.trim()); - monitor.brightness = val / 101; - } else { - const [, , , cur, max] = text.split(" "); - monitor.brightness = parseInt(cur) / parseInt(max); - } - } - } - } + function set(value: string): string { + return setFor("active", value); + } - readonly property Timer timer: Timer { - interval: 500 - onTriggered: { - if (!isNaN(monitor.queuedBrightness)) { - monitor.setBrightness(monitor.queuedBrightness); - monitor.queuedBrightness = NaN; - } - } - } + // Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%- + function setFor(query: string, value: string): string { + const monitor = root.getMonitor(query); + if (!monitor) + return "Invalid monitor: " + query; - function setBrightness(value: real): void { - value = Math.max(0, Math.min(1, value)); - const rounded = Math.round(value * 100); - if (Math.round(brightness * 100) === rounded) - return; + let targetBrightness; + if (value.endsWith("%-")) { + const percent = parseFloat(value.slice(0, -2)); + targetBrightness = monitor.brightness - (percent / 100); + } else if (value.startsWith("+") && value.endsWith("%")) { + const percent = parseFloat(value.slice(1, -1)); + targetBrightness = monitor.brightness + (percent / 100); + } else if (value.endsWith("%")) { + const percent = parseFloat(value.slice(0, -1)); + targetBrightness = percent / 100; + } else if (value.startsWith("+")) { + const increment = parseFloat(value.slice(1)); + targetBrightness = monitor.brightness + increment; + } else if (value.endsWith("-")) { + const decrement = parseFloat(value.slice(0, -1)); + targetBrightness = monitor.brightness - decrement; + } else if (value.includes("%") || value.includes("-") || value.includes("+")) { + return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; + } else { + targetBrightness = parseFloat(value); + } - if (isDdc && timer.running) { - queuedBrightness = value; - return; - } + if (isNaN(targetBrightness)) + return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; - brightness = value; + monitor.setBrightness(targetBrightness); - 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}%`]); + return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; + } - if (isDdc) - timer.restart(); - } + target: "brightness" + } - function initBrightness(): void { - if (isAppleDisplay) - initProc.command = ["asdbctl", "get"]; - else if (isDdc) - initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; - else - initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"]; + component Monitor: QtObject { + id: monitor - initProc.running = true; - } + 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.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 { + const [, , , cur, max] = text.split(" "); + monitor.brightness = parseInt(cur) / parseInt(max); + } + } + } + } + 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 { + interval: 500 - onBusNumChanged: initBrightness() - Component.onCompleted: initBrightness() - } + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness); + monitor.queuedBrightness = NaN; + } + } + } + + function initBrightness(): void { + 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"]; + else + initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"]; + + initProc.running = true; + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)); + const rounded = Math.round(value * 100); + if (Math.round(brightness * 100) === rounded) + return; + + if ((isDdc || isDdcService) && timer.running) { + queuedBrightness = value; + return; + } + + brightness = value; + + 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 || isDdcService) + timer.restart(); + } + + Component.onCompleted: initBrightness() + onBusNumChanged: initBrightness() + onDisplayNumChanged: initBrightness() + } } diff --git a/Helpers/CachingImage.qml b/Helpers/CachingImage.qml index 905349b..649ec2c 100644 --- a/Helpers/CachingImage.qml +++ b/Helpers/CachingImage.qml @@ -4,25 +4,25 @@ import QtQuick import qs.Paths Image { - id: root + id: root - property alias path: manager.path + property alias path: manager.path - asynchronous: true - fillMode: Image.PreserveAspectCrop + asynchronous: true + fillMode: Image.PreserveAspectCrop - Connections { - target: QsWindow.window + Connections { + function onDevicePixelRatioChanged(): void { + manager.updateSource(); + } - function onDevicePixelRatioChanged(): void { - manager.updateSource(); - } - } + target: QsWindow.window + } - CachingImageManager { - id: manager + CachingImageManager { + id: manager - item: root - cacheDir: Qt.resolvedUrl(Paths.imagecache) - } + cacheDir: Qt.resolvedUrl(Paths.imagecache) + item: root + } } diff --git a/Helpers/Calendar.qml b/Helpers/Calendar.qml index 585d93a..84180a8 100644 --- a/Helpers/Calendar.qml +++ b/Helpers/Calendar.qml @@ -10,6 +10,53 @@ Singleton { property int displayYear: new Date().getFullYear() readonly property int weekStartDay: 1 // 0 = Sunday, 1 = Monday + function getISOWeekNumber(date: var): int { + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + } + + function getWeekNumbers(month: int, year: int): var { + const days = getWeeksForMonth(month, year); + const weekNumbers = []; + let lastWeekNumber = -1; + + for (let i = 0; i < days.length; i++) { + // Only add week numbers for days that belong to the current month + if (days[i].isCurrentMonth) { + const dayDate = new Date(days[i].year, days[i].month, days[i].day); + const weekNumber = getISOWeekNumber(dayDate); + + // Only push if this is a new week + if (weekNumber !== lastWeekNumber) { + weekNumbers.push(weekNumber); + lastWeekNumber = weekNumber; + } + } + } + + return weekNumbers; + } + + function getWeekStartIndex(month: int, year: int): int { + const today = new Date(); + if (today.getMonth() !== month || today.getFullYear() !== year) { + return 0; + } + + const days = getWeeksForMonth(month, year); + for (let i = 0; i < days.length; i++) { + if (days[i].isToday) { + // Return the start index of the week containing today + return Math.floor(i / 7) * 7; + } + } + + return 0; + } + function getWeeksForMonth(month: int, year: int): var { const firstDayOfMonth = new Date(year, month, 1); const lastDayOfMonth = new Date(year, month + 1, 0); @@ -43,57 +90,8 @@ Singleton { return days; } - function getWeekNumbers(month: int, year: int): var { - const days = getWeeksForMonth(month, year); - const weekNumbers = []; - let lastWeekNumber = -1; - - for (let i = 0; i < days.length; i++) { - // Only add week numbers for days that belong to the current month - if (days[i].isCurrentMonth) { - const dayDate = new Date(days[i].year, days[i].month, days[i].day); - const weekNumber = getISOWeekNumber(dayDate); - - // Only push if this is a new week - if (weekNumber !== lastWeekNumber) { - weekNumbers.push(weekNumber); - lastWeekNumber = weekNumber; - } - } - } - - return weekNumbers; - } - - function getISOWeekNumber(date: var): int { - const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); - const dayNum = d.getUTCDay() || 7; - d.setUTCDate(d.getUTCDate() + 4 - dayNum); - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); - return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); - } - function isDateToday(date: var): bool { const today = new Date(); - return date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear(); - } - - function getWeekStartIndex(month: int, year: int): int { - const today = new Date(); - if (today.getMonth() !== month || today.getFullYear() !== year) { - return 0; - } - - const days = getWeeksForMonth(month, year); - for (let i = 0; i < days.length; i++) { - if (days[i].isToday) { - // Return the start index of the week containing today - return Math.floor(i / 7) * 7; - } - } - - return 0; + return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); } } diff --git a/Helpers/DockApps.qml b/Helpers/DockApps.qml new file mode 100644 index 0000000..1bdf96a --- /dev/null +++ b/Helpers/DockApps.qml @@ -0,0 +1,70 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Components +import qs.Config + +Singleton { + id: root + + property list apps: { + var map = new Map(); + + const pinnedApps = Config.dock?.pinnedApps ?? []; + for (const appId of pinnedApps) { + if (!map.has(appId.toLowerCase())) + map.set(appId.toLowerCase(), ({ + pinned: true, + toplevels: [] + })); + } + + if (pinnedApps.length > 0) { + map.set("SEPARATOR", { + pinned: false, + toplevels: [] + }); + } + + var values = []; + + for (const [key, value] of map) { + values.push(appEntryComp.createObject(null, { + appId: key, + toplevels: value.toplevels, + pinned: value.pinned + })); + } + + return values; + } + + function isPinned(appId) { + return Config.dock.pinnedApps.indexOf(appId) !== -1; + } + + function togglePin(appId) { + if (root.isPinned(appId)) { + Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId); + } else { + Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]); + } + } + + Component { + id: appEntryComp + + TaskbarAppEntry { + } + } + + component TaskbarAppEntry: QtObject { + id: wrapper + + required property string appId + required property bool pinned + required property list toplevels + } +} diff --git a/Helpers/GetIcons.qml b/Helpers/GetIcons.qml new file mode 100644 index 0000000..e957ad3 --- /dev/null +++ b/Helpers/GetIcons.qml @@ -0,0 +1,17 @@ +pragma Singleton + +import Quickshell + +Singleton { + id: root + + function getTrayIcon(id: string, icon: string): string { + if (icon.includes("?path=")) { + const [name, path] = icon.split("?path="); + icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`); + } else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") { + icon = icon.replace("qspixmap", "icon/discord-tray"); + } + return icon; + } +} diff --git a/Helpers/HasNotifications.qml b/Helpers/HasNotifications.qml index 5479858..235dac8 100644 --- a/Helpers/HasNotifications.qml +++ b/Helpers/HasNotifications.qml @@ -4,12 +4,13 @@ import Quickshell import Quickshell.Io Singleton { - id: root + id: root - property alias hasNotifications: adapter.hasNotifications + property alias hasNotifications: adapter.hasNotifications - JsonObject { - id: adapter - property bool hasNotifications: false - } + JsonObject { + id: adapter + + property bool hasNotifications: false + } } diff --git a/Helpers/Hypr.qml b/Helpers/Hypr.qml index 0cab428..2d3eb31 100644 --- a/Helpers/Hypr.qml +++ b/Helpers/Hypr.qml @@ -9,157 +9,155 @@ import QtQuick import qs.Components Singleton { - id: root + id: root - readonly property var toplevels: Hyprland.toplevels - readonly property var workspaces: Hyprland.workspaces - readonly property var monitors: Hyprland.monitors + property string activeName + readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel + readonly property int activeWsId: focusedWorkspace?.id ?? 1 + property string applicationDir: "/usr/share/applications/" + readonly property bool capsLock: keyboard?.capsLock ?? false + readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" + property string desktopName: "" + readonly property alias devices: extras.devices + readonly property alias extras: extras + readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor + readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace + property bool hadKeyboard + readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" + readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown" + readonly property var kbMap: new Map() + readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null + readonly property var monitors: Hyprland.monitors + readonly property bool numLock: keyboard?.numLock ?? false + readonly property alias options: extras.options + readonly property var toplevels: Hyprland.toplevels + readonly property var workspaces: Hyprland.workspaces - readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel - readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace - readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor - readonly property int activeWsId: focusedWorkspace?.id ?? 1 + signal configReloaded - property string activeName - property string applicationDir: "/usr/share/applications/" - property string desktopName: "" - - readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null - readonly property bool capsLock: keyboard?.capsLock ?? false - readonly property bool numLock: keyboard?.numLock ?? false - readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" - readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown" - readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" - readonly property var kbMap: new Map() - - readonly property alias extras: extras - readonly property alias options: extras.options - readonly property alias devices: extras.devices - - property bool hadKeyboard - - signal configReloaded - - function getActiveScreen(): ShellScreen { - return Quickshell.screens.find(screen => root.monitorFor(screen) === root.focusedMonitor) + function dispatch(request: string): void { + Hyprland.dispatch(request); } - function dispatch(request: string): void { - Hyprland.dispatch(request); - } + function getActiveScreen(): ShellScreen { + return Quickshell.screens.find(screen => root.monitorFor(screen) === root.focusedMonitor); + } - function monitorFor(screen: ShellScreen): HyprlandMonitor { - return Hyprland.monitorFor(screen); - } + function monitorFor(screen: ShellScreen): HyprlandMonitor { + return Hyprland.monitorFor(screen); + } - function reloadDynamicConfs(): void { - extras.batchMessage(["keyword bindlni ,Caps_Lock,global,zshell:refreshDevices", "keyword bindlni ,Num_Lock,global,zshell:refreshDevices"]); - } + function reloadDynamicConfs(): void { + extras.batchMessage(["keyword bindlni ,Caps_Lock,global,zshell:refreshDevices", "keyword bindlni ,Num_Lock,global,zshell:refreshDevices"]); + } - Component.onCompleted: reloadDynamicConfs() + Component.onCompleted: reloadDynamicConfs() - // function updateActiveWindow(): void { - // root.desktopName = root.applicationDir + root.activeToplevel?.lastIpcObject.class + ".desktop"; - // } + // function updateActiveWindow(): void { + // root.desktopName = root.applicationDir + root.activeToplevel?.lastIpcObject.class + ".desktop"; + // } - Connections { - target: Hyprland + Connections { + function onRawEvent(event: HyprlandEvent): void { + const n = event.name; + if (n.endsWith("v2")) + return; - function onRawEvent(event: HyprlandEvent): void { - const n = event.name; - if (n.endsWith("v2")) - return; + if (n === "configreloaded") { + root.configReloaded(); + root.reloadDynamicConfs(); + } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { + Hyprland.refreshWorkspaces(); + Hyprland.refreshMonitors(); + // Qt.callLater( root.updateActiveWindow ); + } else if (["openwindow", "closewindow", "movewindow"].includes(n)) { + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("mon")) { + Hyprland.refreshMonitors(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("workspace")) { + Hyprland.refreshWorkspaces(); + // Qt.callLater( root.updateActiveWindow ); + } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { + Hyprland.refreshToplevels(); + // Qt.callLater( root.updateActiveWindow ); + } + } - if (n === "configreloaded") { - root.configReloaded(); - root.reloadDynamicConfs(); - } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { - Hyprland.refreshWorkspaces(); - Hyprland.refreshMonitors(); - // Qt.callLater( root.updateActiveWindow ); - } else if (["openwindow", "closewindow", "movewindow"].includes(n)) { - Hyprland.refreshToplevels(); - Hyprland.refreshWorkspaces(); - // Qt.callLater( root.updateActiveWindow ); - } else if (n.includes("mon")) { - Hyprland.refreshMonitors(); - // Qt.callLater( root.updateActiveWindow ); - } else if (n.includes("workspace")) { - Hyprland.refreshWorkspaces(); - // Qt.callLater( root.updateActiveWindow ); - } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { - Hyprland.refreshToplevels(); - // Qt.callLater( root.updateActiveWindow ); - } - } - } + target: Hyprland + } - FileView { - id: desktopEntryName + FileView { + id: desktopEntryName - path: root.desktopName + path: root.desktopName - onLoaded: { - const lines = text().split( "\n" ); - for ( const line of lines ) { - if ( line.startsWith( "Name=" )) { - let name = line.replace( "Name=", "" ); - let caseFix = name[ 0 ].toUpperCase() + name.slice( 1 ); - root.activeName = caseFix; - break; - } - } - } - } + onLoaded: { + const lines = text().split("\n"); + for (const line of lines) { + if (line.startsWith("Name=")) { + let name = line.replace("Name=", ""); + let caseFix = name[0].toUpperCase() + name.slice(1); + root.activeName = caseFix; + break; + } + } + } + } - FileView { - id: kbLayoutFile + FileView { + id: kbLayoutFile - path: Quickshell.env("ZSHELL_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst" - onLoaded: { - const layoutMatch = text().match(/! layout\n([\s\S]*?)\n\n/); - if (layoutMatch) { - const lines = layoutMatch[1].split("\n"); - for (const line of lines) { - if (!line.trim() || line.trim().startsWith("!")) - continue; + path: Quickshell.env("ZSHELL_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst" - const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/); - if (match) - root.kbMap.set(match[2], match[1]); - } - } + onLoaded: { + const layoutMatch = text().match(/! layout\n([\s\S]*?)\n\n/); + if (layoutMatch) { + const lines = layoutMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; - const variantMatch = text().match(/! variant\n([\s\S]*?)\n\n/); - if (variantMatch) { - const lines = variantMatch[1].split("\n"); - for (const line of lines) { - if (!line.trim() || line.trim().startsWith("!")) - continue; + const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/); + if (match) + root.kbMap.set(match[2], match[1]); + } + } - const match = line.match(/^\s*([a-zA-Z0-9_-]+)\s+([a-z]{2,}): (.+)$/); - if (match) - root.kbMap.set(match[3], match[2]); - } - } - } - } + const variantMatch = text().match(/! variant\n([\s\S]*?)\n\n/); + if (variantMatch) { + const lines = variantMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; - IpcHandler { - target: "hypr" + const match = line.match(/^\s*([a-zA-Z0-9_-]+)\s+([a-z]{2,}): (.+)$/); + if (match) + root.kbMap.set(match[3], match[2]); + } + } + } + } - function refreshDevices(): void { - extras.refreshDevices(); - } - } + IpcHandler { + function refreshDevices(): void { + extras.refreshDevices(); + } - CustomShortcut { - name: "refreshDevices" - onPressed: extras.refreshDevices() - onReleased: extras.refreshDevices() - } + target: "hypr" + } - HyprExtras { - id: extras - } + CustomShortcut { + name: "refreshDevices" + + onPressed: extras.refreshDevices() + onReleased: extras.refreshDevices() + } + + HyprExtras { + id: extras + + } } diff --git a/Helpers/Icons.qml b/Helpers/Icons.qml index 9524055..32df434 100644 --- a/Helpers/Icons.qml +++ b/Helpers/Icons.qml @@ -6,182 +6,181 @@ import Quickshell.Services.Notifications import QtQuick Singleton { - id: root + id: root - readonly property var weatherIcons: ({ - "0": "clear_day", - "1": "clear_day", - "2": "partly_cloudy_day", - "3": "cloud", - "45": "foggy", - "48": "foggy", - "51": "rainy", - "53": "rainy", - "55": "rainy", - "56": "rainy", - "57": "rainy", - "61": "rainy", - "63": "rainy", - "65": "rainy", - "66": "rainy", - "67": "rainy", - "71": "cloudy_snowing", - "73": "cloudy_snowing", - "75": "snowing_heavy", - "77": "cloudy_snowing", - "80": "rainy", - "81": "rainy", - "82": "rainy", - "85": "cloudy_snowing", - "86": "snowing_heavy", - "95": "thunderstorm", - "96": "thunderstorm", - "99": "thunderstorm" - }) + readonly property var categoryIcons: ({ + WebBrowser: "web", + Printing: "print", + Security: "security", + Network: "chat", + Archiving: "archive", + Compression: "archive", + Development: "code", + IDE: "code", + TextEditor: "edit_note", + Audio: "music_note", + Music: "music_note", + Player: "music_note", + Recorder: "mic", + Game: "sports_esports", + FileTools: "files", + FileManager: "files", + Filesystem: "files", + FileTransfer: "files", + Settings: "settings", + DesktopSettings: "settings", + HardwareSettings: "settings", + TerminalEmulator: "terminal", + ConsoleOnly: "terminal", + Utility: "build", + Monitor: "monitor_heart", + Midi: "graphic_eq", + Mixer: "graphic_eq", + AudioVideoEditing: "video_settings", + AudioVideo: "music_video", + Video: "videocam", + Building: "construction", + Graphics: "photo_library", + "2DGraphics": "photo_library", + RasterGraphics: "photo_library", + TV: "tv", + System: "host", + Office: "content_paste" + }) + readonly property var weatherIcons: ({ + "0": "clear_day", + "1": "clear_day", + "2": "partly_cloudy_day", + "3": "cloud", + "45": "foggy", + "48": "foggy", + "51": "rainy", + "53": "rainy", + "55": "rainy", + "56": "rainy", + "57": "rainy", + "61": "rainy", + "63": "rainy", + "65": "rainy", + "66": "rainy", + "67": "rainy", + "71": "cloudy_snowing", + "73": "cloudy_snowing", + "75": "snowing_heavy", + "77": "cloudy_snowing", + "80": "rainy", + "81": "rainy", + "82": "rainy", + "85": "cloudy_snowing", + "86": "snowing_heavy", + "95": "thunderstorm", + "96": "thunderstorm", + "99": "thunderstorm" + }) - readonly property var categoryIcons: ({ - WebBrowser: "web", - Printing: "print", - Security: "security", - Network: "chat", - Archiving: "archive", - Compression: "archive", - Development: "code", - IDE: "code", - TextEditor: "edit_note", - Audio: "music_note", - Music: "music_note", - Player: "music_note", - Recorder: "mic", - Game: "sports_esports", - FileTools: "files", - FileManager: "files", - Filesystem: "files", - FileTransfer: "files", - Settings: "settings", - DesktopSettings: "settings", - HardwareSettings: "settings", - TerminalEmulator: "terminal", - ConsoleOnly: "terminal", - Utility: "build", - Monitor: "monitor_heart", - Midi: "graphic_eq", - Mixer: "graphic_eq", - AudioVideoEditing: "video_settings", - AudioVideo: "music_video", - Video: "videocam", - Building: "construction", - Graphics: "photo_library", - "2DGraphics": "photo_library", - RasterGraphics: "photo_library", - TV: "tv", - System: "host", - Office: "content_paste" - }) + function getAppCategoryIcon(name: string, fallback: string): string { + const categories = DesktopEntries.heuristicLookup(name)?.categories; - function getAppIcon(name: string, fallback: string): string { - const icon = DesktopEntries.heuristicLookup(name)?.icon; - if (fallback !== "undefined") - return Quickshell.iconPath(icon, fallback); - return Quickshell.iconPath(icon); - } + if (categories) + for (const [key, value] of Object.entries(categoryIcons)) + if (categories.includes(key)) + return value; + return fallback; + } - function getAppCategoryIcon(name: string, fallback: string): string { - const categories = DesktopEntries.heuristicLookup(name)?.categories; + function getAppIcon(name: string, fallback: string): string { + const icon = DesktopEntries.heuristicLookup(name)?.icon; + if (fallback !== "undefined") + return Quickshell.iconPath(icon, fallback); + return Quickshell.iconPath(icon); + } - if (categories) - for (const [key, value] of Object.entries(categoryIcons)) - if (categories.includes(key)) - return value; - return fallback; - } + function getBluetoothIcon(icon: string): string { + if (icon.includes("headset") || icon.includes("headphones")) + return "headphones"; + if (icon.includes("audio")) + return "speaker"; + if (icon.includes("phone")) + return "smartphone"; + if (icon.includes("mouse")) + return "mouse"; + if (icon.includes("keyboard")) + return "keyboard"; + return "bluetooth"; + } - function getNetworkIcon(strength: int, isSecure = false): string { - if (isSecure) { - if (strength >= 80) - return "network_wifi_locked"; - if (strength >= 60) - return "network_wifi_3_bar_locked"; - if (strength >= 40) - return "network_wifi_2_bar_locked"; - if (strength >= 20) - return "network_wifi_1_bar_locked"; - return "signal_wifi_0_bar"; - } else { - if (strength >= 80) - return "network_wifi"; - if (strength >= 60) - return "network_wifi_3_bar"; - if (strength >= 40) - return "network_wifi_2_bar"; - if (strength >= 20) - return "network_wifi_1_bar"; - return "signal_wifi_0_bar"; - } - } + function getMicVolumeIcon(volume: real, isMuted: bool): string { + if (!isMuted && volume > 0) + return "mic"; + return "mic_off"; + } - function getBluetoothIcon(icon: string): string { - if (icon.includes("headset") || icon.includes("headphones")) - return "headphones"; - if (icon.includes("audio")) - return "speaker"; - if (icon.includes("phone")) - return "smartphone"; - if (icon.includes("mouse")) - return "mouse"; - if (icon.includes("keyboard")) - return "keyboard"; - return "bluetooth"; - } + function getNetworkIcon(strength: int, isSecure = false): string { + if (isSecure) { + if (strength >= 80) + return "network_wifi_locked"; + if (strength >= 60) + return "network_wifi_3_bar_locked"; + if (strength >= 40) + return "network_wifi_2_bar_locked"; + if (strength >= 20) + return "network_wifi_1_bar_locked"; + return "signal_wifi_0_bar"; + } else { + if (strength >= 80) + return "network_wifi"; + if (strength >= 60) + return "network_wifi_3_bar"; + if (strength >= 40) + return "network_wifi_2_bar"; + if (strength >= 20) + return "network_wifi_1_bar"; + return "signal_wifi_0_bar"; + } + } - function getWeatherIcon(code: string): string { - if (weatherIcons.hasOwnProperty(code)) - return weatherIcons[code]; - return "air"; - } + function getNotifIcon(summary: string, urgency: int): string { + summary = summary.toLowerCase(); + if (summary.includes("reboot")) + return "restart_alt"; + if (summary.includes("recording")) + return "screen_record"; + if (summary.includes("battery")) + return "power"; + if (summary.includes("screenshot")) + return "screenshot_monitor"; + if (summary.includes("welcome")) + return "waving_hand"; + if (summary.includes("time") || summary.includes("a break")) + return "schedule"; + if (summary.includes("installed")) + return "download"; + if (summary.includes("update")) + return "update"; + if (summary.includes("unable to")) + return "deployed_code_alert"; + if (summary.includes("profile")) + return "person"; + if (summary.includes("file")) + return "folder_copy"; + if (urgency === NotificationUrgency.Critical) + return "release_alert"; + return "chat"; + } - function getNotifIcon(summary: string, urgency: int): string { - summary = summary.toLowerCase(); - if (summary.includes("reboot")) - return "restart_alt"; - if (summary.includes("recording")) - return "screen_record"; - if (summary.includes("battery")) - return "power"; - if (summary.includes("screenshot")) - return "screenshot_monitor"; - if (summary.includes("welcome")) - return "waving_hand"; - if (summary.includes("time") || summary.includes("a break")) - return "schedule"; - if (summary.includes("installed")) - return "download"; - if (summary.includes("update")) - return "update"; - if (summary.includes("unable to")) - return "deployed_code_alert"; - if (summary.includes("profile")) - return "person"; - if (summary.includes("file")) - return "folder_copy"; - if (urgency === NotificationUrgency.Critical) - return "release_alert"; - return "chat"; - } + function getVolumeIcon(volume: real, isMuted: bool): string { + if (isMuted) + return "no_sound"; + if (volume >= 0.5) + return "volume_up"; + if (volume > 0) + return "volume_down"; + return "volume_mute"; + } - function getVolumeIcon(volume: real, isMuted: bool): string { - if (isMuted) - return "no_sound"; - if (volume >= 0.5) - return "volume_up"; - if (volume > 0) - return "volume_down"; - return "volume_mute"; - } - - function getMicVolumeIcon(volume: real, isMuted: bool): string { - if (!isMuted && volume > 0) - return "mic"; - return "mic_off"; - } + function getWeatherIcon(code: string): string { + if (weatherIcons.hasOwnProperty(code)) + return weatherIcons[code]; + return "air"; + } } diff --git a/Helpers/IdleInhibitor.qml b/Helpers/IdleInhibitor.qml index 29409ab..8aad68b 100644 --- a/Helpers/IdleInhibitor.qml +++ b/Helpers/IdleInhibitor.qml @@ -5,52 +5,56 @@ import Quickshell.Io import Quickshell.Wayland Singleton { - id: root + id: root - property alias enabled: props.enabled - readonly property alias enabledSince: props.enabledSince + property alias enabled: props.enabled + readonly property alias enabledSince: props.enabledSince - onEnabledChanged: { - if (enabled) - props.enabledSince = new Date(); - } + onEnabledChanged: { + if (enabled) + props.enabledSince = new Date(); + } - PersistentProperties { - id: props + PersistentProperties { + id: props - property bool enabled - property date enabledSince + property bool enabled + property date enabledSince - reloadableId: "idleInhibitor" - } + reloadableId: "idleInhibitor" + } - IdleInhibitor { - enabled: props.enabled - window: PanelWindow { - implicitWidth: 0 - implicitHeight: 0 - color: "transparent" - mask: Region {} - } - } + IdleInhibitor { + enabled: props.enabled - IpcHandler { - target: "idleInhibitor" + window: PanelWindow { + WlrLayershell.namespace: "ZShell-IdleInhibitor" + color: "transparent" + implicitHeight: 0 + implicitWidth: 0 - function isEnabled(): bool { - return props.enabled; - } + mask: Region { + } + } + } - function toggle(): void { - props.enabled = !props.enabled; - } + IpcHandler { + function disable(): void { + props.enabled = false; + } - function enable(): void { - props.enabled = true; - } + function enable(): void { + props.enabled = true; + } - function disable(): void { - props.enabled = false; - } - } + function isEnabled(): bool { + return props.enabled; + } + + function toggle(): void { + props.enabled = !props.enabled; + } + + target: "idleInhibitor" + } } diff --git a/Helpers/InitialTitle.qml b/Helpers/InitialTitle.qml index e169991..c0d832b 100644 --- a/Helpers/InitialTitle.qml +++ b/Helpers/InitialTitle.qml @@ -5,14 +5,12 @@ import Quickshell.Hyprland import qs.Helpers Singleton { - function getInitialTitle(callback) { - let activeWindow = Hypr.activeToplevel.title - let activeClass = Hypr.activeToplevel.lastIpcObject.class.toString() - let regex = new RegExp(activeClass, "i") + function getInitialTitle(callback) { + let activeWindow = Hypr.activeToplevel.title; + let activeClass = Hypr.activeToplevel.lastIpcObject.class.toString(); + let regex = new RegExp(activeClass, "i"); - console.log("ActiveWindow", activeWindow, "ActiveClass", activeClass, "Regex", regex) - - const evalTitle = activeWindow.match(regex) - callback(evalTitle) - } + const evalTitle = activeWindow.match(regex); + callback(evalTitle); + } } diff --git a/Helpers/ModeScheduler.qml b/Helpers/ModeScheduler.qml index bab9bf6..e4851f5 100644 --- a/Helpers/ModeScheduler.qml +++ b/Helpers/ModeScheduler.qml @@ -10,32 +10,11 @@ import qs.Paths Singleton { id: root - readonly property int darkStart: Config.general.color.scheduleDarkStart readonly property int darkEnd: Config.general.color.scheduleDarkEnd - - Timer { - id: darkModeTimer - - interval: 5000 - - running: true - repeat: true - onTriggered: { - if ( darkStart === darkEnd ) - return; - var now = new Date(); - if ( now.getHours() >= darkStart || now.getHours() < darkEnd ) { - if ( DynamicColors.light ) - applyDarkMode(); - } else { - if ( !DynamicColors.light ) - applyLightMode(); - } - } - } + readonly property int darkStart: Config.general.color.scheduleDarkStart function applyDarkMode() { - if ( Config.general.color.schemeGeneration ) { + if (Config.general.color.schemeGeneration) { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${WallpaperPath.currentWallpaperPath}`, "--thumbnail-path", `${Paths.cache}/imagecache/thumbnail.jpg`, "--output", `${Paths.state}/scheme.json`, "--scheme", `${Config.colors.schemeType}`, "--mode", "dark"]); } else { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "dark"]); @@ -43,18 +22,18 @@ Singleton { Config.general.color.mode = "dark"; - Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]) + Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]); - Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Light.colors/color_scheme_path=\\1Dark.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]) + Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Light.colors/color_scheme_path=\\1Dark.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]); - Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"tokyodark\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]) + Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"tokyodark\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]); - if( Config.general.color.wallust ) + if (Config.general.color.wallust) Wallust.generateColors(WallpaperPath.currentWallpaperPath); } function applyLightMode() { - if ( Config.general.color.neovimColors ) { + if (Config.general.color.neovimColors) { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${WallpaperPath.currentWallpaperPath}`, "--thumbnail-path", `${Paths.cache}/imagecache/thumbnail.jpg`, "--output", `${Paths.state}/scheme.json`, "--scheme", `${Config.colors.schemeType}`, "--mode", "light"]); } else { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]); @@ -62,25 +41,46 @@ Singleton { Config.general.color.mode = "light"; - Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]) + Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]); - Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]) + Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]); - if ( Config.general.color.neovimColors ) - Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]) + if (Config.general.color.neovimColors) + Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]); - if( Config.general.color.wallust ) + if (Config.general.color.wallust) Wallust.generateColors(WallpaperPath.currentWallpaperPath); } function checkStartup() { - if ( darkStart === darkEnd ) + if (darkStart === darkEnd) return; var now = new Date(); - if ( now.getHours() >= darkStart || now.getHours() < darkEnd ) { + if (now.getHours() >= darkStart || now.getHours() < darkEnd) { applyDarkMode(); } else { applyLightMode(); } } + + Timer { + id: darkModeTimer + + interval: 5000 + repeat: true + running: true + + onTriggered: { + if (darkStart === darkEnd) + return; + var now = new Date(); + if (now.getHours() >= darkStart || now.getHours() < darkEnd) { + if (DynamicColors.light) + applyDarkMode(); + } else { + if (!DynamicColors.light) + applyLightMode(); + } + } + } } diff --git a/Helpers/Network.qml b/Helpers/Network.qml index 7296f10..602ae76 100644 --- a/Helpers/Network.qml +++ b/Helpers/Network.qml @@ -6,6 +6,6 @@ import Quickshell.Networking Singleton { id: root - property list devices: Networking.devices.values property NetworkDevice activeDevice: devices.find(d => d.connected) + property list devices: Networking.devices.values } 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/NotifCenterSpacing.qml b/Helpers/NotifCenterSpacing.qml index e5b34ed..3e9a6d6 100644 --- a/Helpers/NotifCenterSpacing.qml +++ b/Helpers/NotifCenterSpacing.qml @@ -4,13 +4,13 @@ import Quickshell import Quickshell.Io Singleton { - id: root + id: root - property alias centerX: notifCenterSpacing.centerX + property alias centerX: notifCenterSpacing.centerX - JsonAdapter { - id: notifCenterSpacing + JsonAdapter { + id: notifCenterSpacing - property int centerX - } + property int centerX + } } diff --git a/Helpers/NotifPath.qml b/Helpers/NotifPath.qml index 3d4b3cd..5ce6db6 100644 --- a/Helpers/NotifPath.qml +++ b/Helpers/NotifPath.qml @@ -4,13 +4,13 @@ import Quickshell.Io import Quickshell Singleton { - id: root + id: root - property alias notifPath: storage.notifPath + property alias notifPath: storage.notifPath - JsonAdapter { - id: storage + JsonAdapter { + id: storage - property string notifPath: Quickshell.statePath("notifications.json") - } + property string notifPath: Quickshell.statePath("notifications.json") + } } diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml index c475f87..802b4ac 100644 --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -5,291 +5,282 @@ import Quickshell import Quickshell.Wayland import QtQuick import QtQuick.Effects -import qs.Modules +import qs.Components import qs.Config import qs.Helpers MouseArea { - id: root + id: root - required property LazyLoader loader - required property ShellScreen screen + property list clients: { + const mon = Hypr.monitorFor(screen); + if (!mon) + return []; - property bool onClient + const special = mon.lastIpcObject.specialWorkspace; + const wsId = special.name ? special.id : mon.activeWorkspace.id; - property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2 - property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0 + return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { + const ac = a.lastIpcObject; + const bc = b.lastIpcObject; + return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating); + }); + } + property real ex: screen.width + property real ey: screen.height + required property LazyLoader loader + property bool onClient + property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2 + property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0 + property real rsx: Math.min(sx, ex) + property real rsy: Math.min(sy, ey) + required property ShellScreen screen + property real sh: Math.abs(sy - ey) + property real ssx + property real ssy + property real sw: Math.abs(sx - ex) + property real sx: 0 + property real sy: 0 - property real ssx - property real ssy + function checkClientRects(x: real, y: real): void { + for (const client of clients) { + if (!client) + continue; - property real sx: 0 - property real sy: 0 - property real ex: screen.width - property real ey: screen.height + let { + at: [cx, cy], + size: [cw, ch] + } = client.lastIpcObject; + cx -= screen.x; + cy -= screen.y; + if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) { + onClient = true; + sx = cx; + sy = cy; + ex = cx + cw; + ey = cy + ch; + break; + } + } + } - property real rsx: Math.min(sx, ex) - property real rsy: Math.min(sy, ey) - property real sw: Math.abs(sx - ex) - property real sh: Math.abs(sy - ey) + function save(): void { + const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`); + ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached(["swappy", "-f", path])); + closeAnim.start(); + } - property list clients: { - const mon = Hypr.monitorFor(screen); - if (!mon) - return []; + anchors.fill: parent + cursorShape: Qt.CrossCursor + focus: true + hoverEnabled: true + opacity: 0 - const special = mon.lastIpcObject.specialWorkspace; - const wsId = special.name ? special.id : mon.activeWorkspace.id; + Behavior on opacity { + Anim { + duration: 300 + } + } + Behavior on rsx { + enabled: !root.pressed - return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { - const ac = a.lastIpcObject; - const bc = b.lastIpcObject; - return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating); - }); - } + ExAnim { + } + } + Behavior on rsy { + enabled: !root.pressed - function checkClientRects(x: real, y: real): void { - for (const client of clients) { - if (!client) - continue; + ExAnim { + } + } + Behavior on sh { + enabled: !root.pressed - let { - at: [cx, cy], - size: [cw, ch] - } = client.lastIpcObject; - cx -= screen.x; - cy -= screen.y; - if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) { - onClient = true; - sx = cx; - sy = cy; - ex = cx + cw; - ey = cy + ch; - break; - } - } - } + ExAnim { + } + } + Behavior on sw { + enabled: !root.pressed - function save(): void { - const tmpfile = Qt.resolvedUrl(`/tmp/zshell-picker-${Quickshell.processId}-${Date.now()}.png`); - ZShellIo.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => Quickshell.execDetached(["swappy", "-f", path])); - closeAnim.start(); - } + ExAnim { + } + } - onClientsChanged: checkClientRects(mouseX, mouseY) + Component.onCompleted: { + Hypr.extras.refreshOptions(); + if (loader.freeze) + clients = clients; - anchors.fill: parent - opacity: 0 - hoverEnabled: true - cursorShape: Qt.CrossCursor + opacity = 1; - Component.onCompleted: { - Hypr.extras.refreshOptions(); - if (loader.freeze) - clients = clients; + const c = clients[0]; + if (c) { + const cx = c.lastIpcObject.at[0] - screen.x; + const cy = c.lastIpcObject.at[1] - screen.y; + onClient = true; + sx = cx; + sy = cy; + ex = cx + c.lastIpcObject.size[0]; + ey = cy + c.lastIpcObject.size[1]; + } else { + sx = screen.width / 2 - 100; + sy = screen.height / 2 - 100; + ex = screen.width / 2 + 100; + ey = screen.height / 2 + 100; + } + } + Keys.onEscapePressed: closeAnim.start() + onClientsChanged: checkClientRects(mouseX, mouseY) + onPositionChanged: event => { + const x = event.x; + const y = event.y; - opacity = 1; + if (pressed) { + onClient = false; + sx = ssx; + sy = ssy; + ex = x; + ey = y; + } else { + checkClientRects(x, y); + } + } + onPressed: event => { + ssx = event.x; + ssy = event.y; + } + onReleased: { + if (closeAnim.running) + return; - const c = clients[0]; - if (c) { - const cx = c.lastIpcObject.at[0] - screen.x; - const cy = c.lastIpcObject.at[1] - screen.y; - onClient = true; - sx = cx; - sy = cy; - ex = cx + c.lastIpcObject.size[0]; - ey = cy + c.lastIpcObject.size[1]; - } else { - sx = screen.width / 2 - 100; - sy = screen.height / 2 - 100; - ex = screen.width / 2 + 100; - ey = screen.height / 2 + 100; - } - } + if (root.loader.freeze) { + save(); + } else { + overlay.visible = border.visible = false; + screencopy.visible = false; + screencopy.active = true; + } + } - onPressed: event => { - ssx = event.x; - ssy = event.y; - } + SequentialAnimation { + id: closeAnim - onReleased: { - if (closeAnim.running) - return; + PropertyAction { + property: "closing" + target: root.loader + value: true + } - if (root.loader.freeze) { - save(); - } else { - overlay.visible = border.visible = false; - screencopy.visible = false; - screencopy.active = true; - } - } + ParallelAnimation { + Anim { + duration: 300 + property: "opacity" + target: root + to: 0 + } - onPositionChanged: event => { - const x = event.x; - const y = event.y; + ExAnim { + properties: "rsx,rsy" + target: root + to: 0 + } - if (pressed) { - onClient = false; - sx = ssx; - sy = ssy; - ex = x; - ey = y; - } else { - checkClientRects(x, y); - } - } + ExAnim { + property: "sw" + target: root + to: root.screen.width + } - focus: true - Keys.onEscapePressed: closeAnim.start() + ExAnim { + property: "sh" + target: root + to: root.screen.height + } + } - SequentialAnimation { - id: closeAnim + PropertyAction { + property: "activeAsync" + target: root.loader + value: false + } + } - PropertyAction { - target: root.loader - property: "closing" - value: true - } - ParallelAnimation { - Anim { - target: root - property: "opacity" - to: 0 - duration: 300 - } - ExAnim { - target: root - properties: "rsx,rsy" - to: 0 - } - ExAnim { - target: root - property: "sw" - to: root.screen.width - } - ExAnim { - target: root - property: "sh" - to: root.screen.height - } - } - PropertyAction { - target: root.loader - property: "activeAsync" - value: false - } - } + Loader { + id: screencopy - Loader { - id: screencopy + active: root.loader.freeze + anchors.fill: parent + asynchronous: true - anchors.fill: parent + sourceComponent: ScreencopyView { + captureSource: root.screen + paintCursor: false - active: root.loader.freeze - asynchronous: true + onHasContentChanged: { + if (hasContent && !root.loader.freeze) { + overlay.visible = border.visible = true; + root.save(); + } + } + } + } - sourceComponent: ScreencopyView { - captureSource: root.screen + Rectangle { + id: overlay - paintCursor: false + anchors.fill: parent + color: "white" + layer.enabled: true + opacity: 0.3 + radius: root.realRounding - onHasContentChanged: { - if (hasContent && !root.loader.freeze) { - overlay.visible = border.visible = true; - root.save(); - } - } - } - } + layer.effect: MultiEffect { + maskEnabled: true + maskInverted: true + maskSource: selectionWrapper + maskSpreadAtMin: 1 + maskThresholdMin: 0.5 + } + } - Rectangle { - id: overlay + Item { + id: selectionWrapper - anchors.fill: parent - color: "white" - opacity: 0.3 + anchors.fill: parent + layer.enabled: true + visible: false - radius: root.realRounding + Rectangle { + id: selectionRect - layer.enabled: true - layer.effect: MultiEffect { - maskSource: selectionWrapper - maskEnabled: true - maskInverted: true - maskSpreadAtMin: 1 - maskThresholdMin: 0.5 - } - } + implicitHeight: root.sh + implicitWidth: root.sw + radius: root.realRounding + x: root.rsx + y: root.rsy + } + } - Item { - id: selectionWrapper + Rectangle { + id: border - anchors.fill: parent - layer.enabled: true - visible: false + border.color: DynamicColors.palette.m3primary + border.width: root.realBorderWidth + color: "transparent" + implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 + implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 + radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 + x: selectionRect.x - root.realBorderWidth + y: selectionRect.y - root.realBorderWidth - Rectangle { - id: selectionRect + Behavior on border.color { + Anim { + } + } + } - radius: root.realRounding - x: root.rsx - y: root.rsy - implicitWidth: root.sw - implicitHeight: root.sh - } - } - - Rectangle { - id: border - - color: "transparent" - radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 - border.width: root.realBorderWidth - border.color: DynamicColors.palette.m3primary - - x: selectionRect.x - root.realBorderWidth - y: selectionRect.y - root.realBorderWidth - implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 - implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 - - Behavior on border.color { - Anim {} - } - } - - Behavior on opacity { - Anim { - duration: 300 - } - } - - Behavior on rsx { - enabled: !root.pressed - - ExAnim {} - } - - Behavior on rsy { - enabled: !root.pressed - - ExAnim {} - } - - Behavior on sw { - enabled: !root.pressed - - ExAnim {} - } - - Behavior on sh { - enabled: !root.pressed - - ExAnim {} - } - - component ExAnim: Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } + component ExAnim: Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } } diff --git a/Helpers/Players.qml b/Helpers/Players.qml index a412849..3f72fcf 100644 --- a/Helpers/Players.qml +++ b/Helpers/Players.qml @@ -9,115 +9,119 @@ import qs.Config import qs.Components Singleton { - id: root + id: root - readonly property list list: Mpris.players.values - readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null - property alias manualActive: props.manualActive + readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null + readonly property list list: Mpris.players.values + property alias manualActive: props.manualActive - function getIdentity(player: MprisPlayer): string { - const alias = Config.services.playerAliases.find(a => a.from === player.identity); - return alias?.to ?? player.identity; - } + function getIdentity(player: MprisPlayer): string { + const alias = Config.services.playerAliases.find(a => a.from === player.identity); + return alias?.to ?? player.identity; + } - Connections { - target: active + Connections { + function onPostTrackChanged() { + if (!Config.utilities.toasts.nowPlaying) { + return; + } + } - function onPostTrackChanged() { - if (!Config.utilities.toasts.nowPlaying) { - return; - } - } - } + target: active + } - PersistentProperties { - id: props + PersistentProperties { + id: props - property MprisPlayer manualActive + property MprisPlayer manualActive - reloadableId: "players" - } + reloadableId: "players" + } - CustomShortcut { - name: "mediaToggle" - description: "Toggle media playback" - onPressed: { - const active = root.active; - if (active && active.canTogglePlaying) - active.togglePlaying(); - } - } + CustomShortcut { + description: "Toggle media playback" + name: "mediaToggle" - CustomShortcut { - name: "mediaPrev" - description: "Previous track" - onPressed: { - const active = root.active; - if (active && active.canGoPrevious) - active.previous(); - } - } + onPressed: { + const active = root.active; + if (active && active.canTogglePlaying) + active.togglePlaying(); + } + } - CustomShortcut { - name: "mediaNext" - description: "Next track" - onPressed: { - const active = root.active; - if (active && active.canGoNext) - active.next(); - } - } + CustomShortcut { + description: "Previous track" + name: "mediaPrev" - CustomShortcut { - name: "mediaStop" - description: "Stop media playback" - onPressed: root.active?.stop() - } + onPressed: { + const active = root.active; + if (active && active.canGoPrevious) + active.previous(); + } + } - IpcHandler { - target: "mpris" + CustomShortcut { + description: "Next track" + name: "mediaNext" - function getActive(prop: string): string { - const active = root.active; - return active ? active[prop] ?? "Invalid property" : "No active player"; - } + onPressed: { + const active = root.active; + if (active && active.canGoNext) + active.next(); + } + } - function list(): string { - return root.list.map(p => root.getIdentity(p)).join("\n"); - } + CustomShortcut { + description: "Stop media playback" + name: "mediaStop" - function play(): void { - const active = root.active; - if (active?.canPlay) - active.play(); - } + onPressed: root.active?.stop() + } - function pause(): void { - const active = root.active; - if (active?.canPause) - active.pause(); - } + IpcHandler { + function getActive(prop: string): string { + const active = root.active; + return active ? active[prop] ?? "Invalid property" : "No active player"; + } - function playPause(): void { - const active = root.active; - if (active?.canTogglePlaying) - active.togglePlaying(); - } + function list(): string { + return root.list.map(p => root.getIdentity(p)).join("\n"); + } - function previous(): void { - const active = root.active; - if (active?.canGoPrevious) - active.previous(); - } + function next(): void { + const active = root.active; + if (active?.canGoNext) + active.next(); + } - function next(): void { - const active = root.active; - if (active?.canGoNext) - active.next(); - } + function pause(): void { + const active = root.active; + if (active?.canPause) + active.pause(); + } - function stop(): void { - root.active?.stop(); - } - } + function play(): void { + const active = root.active; + if (active?.canPlay) + active.play(); + } + + function playPause(): void { + const active = root.active; + if (active?.canTogglePlaying) + active.togglePlaying(); + } + + function previous(): void { + const active = root.active; + if (active?.canGoPrevious) + active.previous(); + } + + function stop(): void { + root.active?.stop(); + } + + target: "mpris" + } } diff --git a/Helpers/SearchWallpapers.qml b/Helpers/SearchWallpapers.qml index 1bbca74..631b638 100644 --- a/Helpers/SearchWallpapers.qml +++ b/Helpers/SearchWallpapers.qml @@ -9,41 +9,41 @@ import qs.Helpers import qs.Paths Searcher { - id: root + id: root - property bool showPreview: false - readonly property string current: showPreview ? previewPath : actualCurrent - property string previewPath - property string actualCurrent: WallpaperPath.currentWallpaperPath + property string actualCurrent: WallpaperPath.currentWallpaperPath + readonly property string current: showPreview ? previewPath : actualCurrent + property string previewPath + property bool showPreview: false - function setWallpaper(path: string): void { - actualCurrent = path; - WallpaperPath.currentWallpaperPath = path; + function preview(path: string): void { + previewPath = path; + showPreview = true; + } + + function setWallpaper(path: string): void { + actualCurrent = path; + WallpaperPath.currentWallpaperPath = path; Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/LockScreenBg.py")} --input_image=${root.actualCurrent} --output_path=${Paths.state}/lockscreen_bg.png`]); - } + } - function preview(path: string): void { - previewPath = path; - showPreview = true; - } + function stopPreview(): void { + showPreview = false; + Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/SchemeColorGen.py")} --path=${root.actualCurrent} --thumbnail=${Paths.cache}/imagecache/thumbnail.jpg --output=${Paths.state}/scheme.json --scheme=${Config.colors.schemeType}`]); + } - function stopPreview(): void { - showPreview = false; - Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/SchemeColorGen.py")} --path=${root.actualCurrent} --thumbnail=${Paths.cache}/imagecache/thumbnail.jpg --output=${Paths.state}/scheme.json --scheme=${Config.colors.schemeType}`]); - } + extraOpts: useFuzzy ? ({}) : ({ + forward: false + }) + key: "relativePath" + list: wallpapers.entries + useFuzzy: true - list: wallpapers.entries - key: "relativePath" - useFuzzy: true - extraOpts: useFuzzy ? ({}) : ({ - forward: false - }) + FileSystemModel { + id: wallpapers - FileSystemModel { - id: wallpapers - - recursive: true - path: Config.general.wallpaperPath - filter: FileSystemModel.Images - } + filter: FileSystemModel.Images + path: Config.general.wallpaperPath + recursive: true + } } diff --git a/Helpers/Searcher.qml b/Helpers/Searcher.qml index 2a216dc..5bfdef5 100644 --- a/Helpers/Searcher.qml +++ b/Helpers/Searcher.qml @@ -4,52 +4,51 @@ import "../scripts/fuzzysort.js" as Fuzzy import QtQuick Singleton { - required property list list - property string key: "name" - property bool useFuzzy: false - property var extraOpts: ({}) + property var extraOpts: ({}) + readonly property list fuzzyPrepped: useFuzzy ? list.map(e => { + const obj = { + _item: e + }; + for (const k of keys) + obj[k] = Fuzzy.prepare(e[k]); + return obj; + }) : [] + readonly property var fzf: useFuzzy ? [] : new Fzf.Finder(list, Object.assign({ + selector + }, extraOpts)) + property string key: "name" - // Extra stuff for fuzzy - property list keys: [key] - property list weights: [1] + // Extra stuff for fuzzy + property list keys: [key] + required property list list + property bool useFuzzy: false + property list weights: [1] - readonly property var fzf: useFuzzy ? [] : new Fzf.Finder(list, Object.assign({ - selector - }, extraOpts)) - readonly property list fuzzyPrepped: useFuzzy ? list.map(e => { - const obj = { - _item: e - }; - for (const k of keys) - obj[k] = Fuzzy.prepare(e[k]); - return obj; - }) : [] + function query(search: string): list { + search = transformSearch(search); + if (!search) + return [...list]; - function transformSearch(search: string): string { - return search; - } + if (useFuzzy) + return Fuzzy.go(search, fuzzyPrepped, Object.assign({ + all: true, + keys, + scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0) + }, extraOpts)).map(r => r.obj._item); - function selector(item: var): string { - // Only for fzf - return item[key]; - } + return fzf.find(search).sort((a, b) => { + if (a.score === b.score) + return selector(a.item).trim().length - selector(b.item).trim().length; + return b.score - a.score; + }).map(r => r.item); + } - function query(search: string): list { - search = transformSearch(search); - if (!search) - return [...list]; + function selector(item: var): string { + // Only for fzf + return item[key]; + } - if (useFuzzy) - return Fuzzy.go(search, fuzzyPrepped, Object.assign({ - all: true, - keys, - scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0) - }, extraOpts)).map(r => r.obj._item); - - return fzf.find(search).sort((a, b) => { - if (a.score === b.score) - return selector(a.item).trim().length - selector(b.item).trim().length; - return b.score - a.score; - }).map(r => r.item); - } + function transformSearch(search: string): string { + return search; + } } diff --git a/Helpers/SystemInfo.qml b/Helpers/SystemInfo.qml index 550a544..c4e3565 100644 --- a/Helpers/SystemInfo.qml +++ b/Helpers/SystemInfo.qml @@ -6,79 +6,78 @@ import Quickshell.Io import QtQuick Singleton { - id: root + id: root - property string osName - property string osPrettyName - property string osId - property list osIdLike - property string osLogo - property bool isDefaultLogo: true + property bool isDefaultLogo: true + property string osId + property list osIdLike + property string osLogo + property string osName + property string osPrettyName + readonly property string shell: Quickshell.env("SHELL").split("/").pop() + property string uptime + readonly property string user: Quickshell.env("USER") + readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP") - property string uptime - readonly property string user: Quickshell.env("USER") - readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP") - readonly property string shell: Quickshell.env("SHELL").split("/").pop() + FileView { + id: osRelease - FileView { - id: osRelease + path: "/etc/os-release" - path: "/etc/os-release" - onLoaded: { - const lines = text().split("\n"); + onLoaded: { + const lines = text().split("\n"); - const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split("=")[1].replace(/"/g, "") ?? ""; + const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split("=")[1].replace(/"/g, "") ?? ""; - root.osName = fd("NAME"); - root.osPrettyName = fd("PRETTY_NAME"); - root.osId = fd("ID"); - root.osIdLike = fd("ID_LIKE").split(" "); + root.osName = fd("NAME"); + root.osPrettyName = fd("PRETTY_NAME"); + root.osId = fd("ID"); + root.osIdLike = fd("ID_LIKE").split(" "); - const logo = Quickshell.iconPath(fd("LOGO"), true); - if (Config.general.logo) { - root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); - root.isDefaultLogo = false; - } else if (logo) { - root.osLogo = logo; - root.isDefaultLogo = false; - } - } - } + const logo = Quickshell.iconPath(fd("LOGO"), true); + if (Config.general.logo) { + root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); + root.isDefaultLogo = false; + } else if (logo) { + root.osLogo = logo; + root.isDefaultLogo = false; + } + } + } - Connections { - target: Config.general + Connections { + function onLogoChanged(): void { + osRelease.reload(); + } - function onLogoChanged(): void { - osRelease.reload(); - } - } + target: Config.general + } - Timer { - running: true - repeat: true - interval: 15000 - onTriggered: fileUptime.reload() - } + Timer { + interval: 15000 + repeat: true + running: true - FileView { - id: fileUptime + onTriggered: fileUptime.reload() + } - path: "/proc/uptime" - onLoaded: { - const up = parseInt(text().split(" ")[0] ?? 0); + FileView { + id: fileUptime - const days = Math.floor(up / 86400); - const hours = Math.floor((up % 86400) / 3600); - const minutes = Math.floor((up % 3600) / 60); + path: "/proc/uptime" - let str = ""; - if (days > 0) - str += `${days} day${days === 1 ? "" : "s"}`; - if (hours > 0) - str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`; - if (minutes > 0 || !str) - str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`; - root.uptime = str; - } - } + onLoaded: { + const up = parseInt(text().split(" ")[0] ?? 0); + + const hours = Math.floor(up / 3600); + const minutes = Math.floor((up % 3600) / 60); + + let str = ""; + if (hours > 0) + 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 075545c..d9234e9 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -6,232 +6,344 @@ import QtQuick import qs.Config Singleton { - id: root + id: root - property real cpuPerc - property real cpuTemp - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType - property string autoGpuType: "NONE" - property real gpuPerc - property real gpuTemp - property real gpuMemUsed + 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 memUsed - property real memTotal - readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 - property real storageUsed - property real storageTotal - property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 + property real gpuMemUsed + property string gpuName: "" + property real gpuPerc + property real gpuTemp + readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType + property real lastCpuIdle + property real lastCpuTotal + readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 + property real memTotal + property real memUsed + property int refCount + 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; + } - property real lastCpuIdle - property real lastCpuTotal + 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(); + } - property int refCount + 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; - const gib = 1024 ** 2; - const tib = 1024 ** 3; + function formatKib(kib: real): var { + const mib = 1024; + const gib = 1024 ** 2; + const tib = 1024 ** 3; - if (kib >= tib) - return { - value: kib / tib, - unit: "TiB" - }; - if (kib >= gib) - return { - value: kib / gib, - unit: "GiB" - }; - if (kib >= mib) - return { - value: kib / mib, - unit: "MiB" - }; - return { - value: kib, - unit: "KiB" - }; - } + if (kib >= tib) + return { + value: kib / tib, + unit: "TiB" + }; + if (kib >= gib) + return { + value: kib / gib, + unit: "GiB" + }; + if (kib >= mib) + return { + value: kib / mib, + unit: "MiB" + }; + return { + value: kib, + unit: "KiB" + }; + } - Timer { - running: root.refCount > 0 - interval: 3000 - repeat: true - triggeredOnStart: true - onTriggered: { - stat.reload(); - meminfo.reload(); - storage.running = true; - gpuUsage.running = true; - sensors.running = true; - } - } + Timer { + interval: Config.dashboard.resourceUpdateInterval + repeat: true + running: root.refCount > 0 + triggeredOnStart: true - FileView { - id: stat + onTriggered: { + stat.reload(); + meminfo.reload(); + storage.running = true; + gpuUsage.running = true; + sensors.running = true; + } + } - path: "/proc/stat" - onLoaded: { - const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (data) { - const stats = data.slice(1).map(n => parseInt(n, 10)); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3] + (stats[4] ?? 0); + FileView { + id: cpuinfoInit - const totalDiff = total - root.lastCpuTotal; - const idleDiff = idle - root.lastCpuIdle; - root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; + path: "/proc/cpuinfo" - root.lastCpuTotal = total; - root.lastCpuIdle = idle; - } - } - } + onLoaded: { + const nameMatch = text().match(/model name\s*:\s*(.+)/); + if (nameMatch) + root.cpuName = root.cleanCpuName(nameMatch[1]); + } + } - FileView { - id: meminfo + FileView { + id: stat - path: "/proc/meminfo" - onLoaded: { - const data = text(); - root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; - root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; - } - } + path: "/proc/stat" - Process { - id: storage + onLoaded: { + const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); + if (data) { + const stats = data.slice(1).map(n => parseInt(n, 10)); + const total = stats.reduce((a, b) => a + b, 0); + const idle = stats[3] + (stats[4] ?? 0); - command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] - stdout: StdioCollector { - onStreamFinished: { - const deviceMap = new Map(); + const totalDiff = total - root.lastCpuTotal; + const idleDiff = idle - root.lastCpuIdle; + root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; - for (const line of text.trim().split("\n")) { - if (line.trim() === "") - continue; + root.lastCpuTotal = total; + root.lastCpuIdle = idle; + } + } + } - 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; + FileView { + id: meminfo - // 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 - }); - } - } - } + path: "/proc/meminfo" - let totalUsed = 0; - let totalAvail = 0; + onLoaded: { + const data = text(); + root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; + root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; + } + } - for (const [device, stats] of deviceMap) { - totalUsed += stats.used; - totalAvail += stats.avail; - } + Process { + id: storage - root.storageUsed = totalUsed; - root.storageTotal = totalUsed + totalAvail; - } - } - } + command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"] - Process { - id: gpuTypeCheck + stdout: StdioCollector { + onStreamFinished: { + const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal } + const lines = text.trim().split("\n"); - running: !Config.services.gpuType - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } + 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="([^"]*)"/); - Process { - id: oneshotMem - command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] - running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0 - stdout: StdioCollector { - onStreamFinished: { - root.gpuMemTotal = Number(this.text.trim()) - oneshotMem.running = false - } - } - } + if (!nameMatch || !typeMatch) + continue; - Process { - id: gpuUsage + 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); - command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits"] : ["echo"] - stdout: StdioCollector { - onStreamFinished: { - if (root.gpuType === "GENERIC") { - const percs = text.trim().split("\n"); - const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); - root.gpuPerc = sum / percs.length / 100; - } else if (root.gpuType === "NVIDIA") { - const [usage, temp, mem] = text.trim().split(","); - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 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 totalSize = 0; + + 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.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]); + } + } + } + } + } + + Process { + id: gpuTypeCheck + + command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] + running: !Config.services.gpuType + + stdout: StdioCollector { + onStreamFinished: root.autoGpuType = text.trim() + } + } + + Process { + id: oneshotMem + + command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] + running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0 + + stdout: StdioCollector { + onStreamFinished: { + root.gpuMemTotal = Number(this.text.trim()); + oneshotMem.running = false; + } + } + } + + Process { + id: gpuUsage + + command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits"] : ["echo"] + + stdout: StdioCollector { + onStreamFinished: { + if (root.gpuType === "GENERIC") { + const percs = text.trim().split("\n"); + const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); + root.gpuPerc = sum / percs.length / 100; + } else if (root.gpuType === "NVIDIA") { + const [usage, temp, mem] = text.trim().split(","); + root.gpuPerc = parseInt(usage, 10) / 100; + root.gpuTemp = parseInt(temp, 10); root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; - } else { - root.gpuPerc = 0; - root.gpuTemp = 0; - } - } - } - } + } else { + root.gpuPerc = 0; + root.gpuTemp = 0; + } + } + } + } - Process { - id: sensors + Process { + id: sensors - command: ["sensors"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { - let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); - if (!cpuTemp) - // If AMD Tdie pattern failed, try fallback on Tctl - cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); + command: ["sensors"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) - if (cpuTemp) - root.cpuTemp = parseFloat(cpuTemp[1]); + stdout: StdioCollector { + onStreamFinished: { + let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); + if (!cpuTemp) + // If AMD Tdie pattern failed, try fallback on Tctl + cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); - if (root.gpuType !== "GENERIC") - return; + if (cpuTemp) + root.cpuTemp = parseFloat(cpuTemp[1]); - let eligible = false; - let sum = 0; - let count = 0; + if (root.gpuType !== "GENERIC") + return; - for (const line of text.trim().split("\n")) { - if (line === "Adapter: PCI adapter") - eligible = true; - else if (line === "") - eligible = false; - else if (eligible) { - let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - if (!match) - // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs) - match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); + let eligible = false; + let sum = 0; + let count = 0; - if (match) { - sum += parseFloat(match[2]); - count++; - } - } - } + for (const line of text.trim().split("\n")) { + if (line === "Adapter: PCI adapter") + eligible = true; + else if (line === "") + eligible = false; + else if (eligible) { + let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); + if (!match) + // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs) + match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); - root.gpuTemp = count > 0 ? sum / count : 0; - } - } - } + if (match) { + sum += parseFloat(match[2]); + count++; + } + } + } + + root.gpuTemp = count > 0 ? sum / count : 0; + } + } + } } diff --git a/Helpers/TextRender.qml b/Helpers/TextRender.qml index 715a3f1..bdc852c 100644 --- a/Helpers/TextRender.qml +++ b/Helpers/TextRender.qml @@ -1,6 +1,6 @@ import QtQuick Text { - renderType: Text.NativeRendering - textFormat: Text.PlainText + renderType: Text.NativeRendering + textFormat: Text.PlainText } diff --git a/Helpers/ThemeIcons.qml b/Helpers/ThemeIcons.qml index 714f114..62a3e8a 100644 --- a/Helpers/ThemeIcons.qml +++ b/Helpers/ThemeIcons.qml @@ -6,6 +6,30 @@ import Quickshell Singleton { id: root + property list entryList: [] + property var preppedIcons: [] + property var preppedIds: [] + property var preppedNames: [] + + // Dynamic fixups + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft-launcher" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] property real scoreThreshold: 0.2 // Manual overrides for tricky apps @@ -17,178 +41,23 @@ Singleton { "wps": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus", "footclient": "foot" - }) + }) - // Dynamic fixups - property var regexSubstitutions: [ - { - "regex": /^steam_app_(\d+)$/, - "replace": "steam_icon_$1" - }, - { - "regex": /Minecraft.*/, - "replace": "minecraft-launcher" - }, - { - "regex": /.*polkit.*/, - "replace": "system-lock-screen" - }, - { - "regex": /gcr.prompter/, - "replace": "system-lock-screen" - } - ] - - property list entryList: [] - property var preppedNames: [] - property var preppedIcons: [] - property var preppedIds: [] - - Component.onCompleted: refreshEntries() - - Connections { - target: DesktopEntries.applications - function onValuesChanged() { - refreshEntries(); - } - } - - function refreshEntries() { - if (typeof DesktopEntries === 'undefined') - return; - - const values = Array.from(DesktopEntries.applications.values); - if (values) { - entryList = values.sort((a, b) => a.name.localeCompare(b.name)); - updatePreppedData(); - } - } - - function updatePreppedData() { - if (typeof FuzzySort === 'undefined') - return; - - const list = Array.from(entryList); - preppedNames = list.map(a => ({ - name: FuzzySort.prepare(`${a.name} `), entry: a})); - preppedIcons = list.map(a => ({ - name: FuzzySort.prepare(`${a.icon} `), - entry: a - })); - preppedIds = list.map(a => ({ - name: FuzzySort.prepare(`${a.id} `), - entry: a - })); - } - - function iconForAppId(appId, fallbackName) { - const fallback = fallbackName || "application-x-executable"; - if (!appId) - return iconFromName(fallback, fallback); - - const entry = findAppEntry(appId); - if (entry) { - return iconFromName(entry.icon, fallback); - } - - return iconFromName(appId, fallback); - } - - // Robust lookup strategy - function findAppEntry(str) { - if (!str || str.length === 0) + function checkCleanMatch(str) { + if (!str || str.length <= 3) return null; - - let result = null; - - if (result = checkHeuristic(str)) - return result; - if (result = checkSubstitutions(str)) - return result; - if (result = checkRegex(str)) - return result; - if (result = checkSimpleTransforms(str)) - return result; - if (result = checkFuzzySearch(str)) - return result; - if (result = checkCleanMatch(str)) - return result; - - return null; - } - - function iconFromName(iconName, fallbackName) { - const fallback = fallbackName || "application-x-executable"; - try { - if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) { - const p = Quickshell.iconPath(iconName, fallback); - if (p && p !== "") - return p; - } - } catch (e) {} - - try { - return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""; - } catch (e2) { - return ""; - } - } - - function distroLogoPath() { - try { - return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""; - } catch (e) { - return ""; - } - } - - // --- Lookup Helpers --- - - function checkHeuristic(str) { - if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { - const entry = DesktopEntries.heuristicLookup(str); - if (entry) - return entry; - } - return null; - } - - function checkSubstitutions(str) { - let effectiveStr = substitutions[str]; - if (!effectiveStr) - effectiveStr = substitutions[str.toLowerCase()]; - - if (effectiveStr && effectiveStr !== str) { - return findAppEntry(effectiveStr); - } - return null; - } - - function checkRegex(str) { - for (let i = 0; i < regexSubstitutions.length; i++) { - const sub = regexSubstitutions[i]; - const replaced = str.replace(sub.regex, sub.replace); - if (replaced !== str) { - return findAppEntry(replaced); - } - } - return null; - } - - function checkSimpleTransforms(str) { if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) return null; - const lower = str.toLowerCase(); + // Aggressive fallback: strip all separators + const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, ''); + const list = Array.from(entryList); - const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()]; - - for (let i = 0; i < variants.length; i++) { - const variant = variants[i]; - if (variant) { - const entry = DesktopEntries.byId(variant); - if (entry) - return entry; + for (let i = 0; i < list.length; i++) { + const entry = list[i]; + const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, ''); + if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) { + return entry; } } return null; @@ -227,33 +96,108 @@ Singleton { return null; } - function checkCleanMatch(str) { - if (!str || str.length <= 3) - return null; + // --- Lookup Helpers --- + + function checkHeuristic(str) { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(str); + if (entry) + return entry; + } + return null; + } + + function checkRegex(str) { + for (let i = 0; i < regexSubstitutions.length; i++) { + const sub = regexSubstitutions[i]; + const replaced = str.replace(sub.regex, sub.replace); + if (replaced !== str) { + return findAppEntry(replaced); + } + } + return null; + } + + function checkSimpleTransforms(str) { if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) return null; - // Aggressive fallback: strip all separators - const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, ''); - const list = Array.from(entryList); + const lower = str.toLowerCase(); - for (let i = 0; i < list.length; i++) { - const entry = list[i]; - const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, ''); - if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) { - return entry; + const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()]; + + for (let i = 0; i < variants.length; i++) { + const variant = variants[i]; + if (variant) { + const entry = DesktopEntries.byId(variant); + if (entry) + return entry; } } return null; } + function checkSubstitutions(str) { + let effectiveStr = substitutions[str]; + if (!effectiveStr) + effectiveStr = substitutions[str.toLowerCase()]; + + if (effectiveStr && effectiveStr !== str) { + return findAppEntry(effectiveStr); + } + return null; + } + + function distroLogoPath() { + try { + return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""; + } catch (e) { + return ""; + } + } + + // Robust lookup strategy + function findAppEntry(str) { + if (!str || str.length === 0) + return null; + + let result = null; + + if (result = checkHeuristic(str)) + return result; + if (result = checkSubstitutions(str)) + return result; + if (result = checkRegex(str)) + return result; + if (result = checkSimpleTransforms(str)) + return result; + if (result = checkFuzzySearch(str)) + return result; + if (result = checkCleanMatch(str)) + return result; + + return null; + } + function fuzzyQuery(search, preppedData) { if (!search || !preppedData || preppedData.length === 0) return []; return FuzzySort.go(search, preppedData, { - all: true, - key: "name" - }).map(r => r.obj.entry); + all: true, + key: "name" + }).map(r => r.obj.entry); + } + + function getFromReverseDomain(str) { + if (!str) + return ""; + return str.split('.').slice(-1)[0]; + } + + // Deprecated shim + function guessIcon(str) { + const entry = findAppEntry(str); + return entry ? entry.icon : "image-missing"; } function iconExists(iconName) { @@ -266,10 +210,34 @@ Singleton { return path && path.length > 0 && !path.includes("image-missing"); } - function getFromReverseDomain(str) { - if (!str) + function iconForAppId(appId, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + if (!appId) + return iconFromName(fallback, fallback); + + const entry = findAppEntry(appId); + if (entry) { + return iconFromName(entry.icon, fallback); + } + + return iconFromName(appId, fallback); + } + + function iconFromName(iconName, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + try { + if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) { + const p = Quickshell.iconPath(iconName, fallback); + if (p && p !== "") + return p; + } + } catch (e) {} + + try { + return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""; + } catch (e2) { return ""; - return str.split('.').slice(-1)[0]; + } } function normalizeWithHyphens(str) { @@ -278,9 +246,43 @@ Singleton { return str.toLowerCase().replace(/\s+/g, "-"); } - // Deprecated shim - function guessIcon(str) { - const entry = findAppEntry(str); - return entry ? entry.icon : "image-missing"; + function refreshEntries() { + if (typeof DesktopEntries === 'undefined') + return; + + const values = Array.from(DesktopEntries.applications.values); + if (values) { + entryList = values.sort((a, b) => a.name.localeCompare(b.name)); + updatePreppedData(); + } + } + + function updatePreppedData() { + if (typeof FuzzySort === 'undefined') + return; + + const list = Array.from(entryList); + preppedNames = list.map(a => ({ + name: FuzzySort.prepare(`${a.name} `), + entry: a + })); + preppedIcons = list.map(a => ({ + name: FuzzySort.prepare(`${a.icon} `), + entry: a + })); + preppedIds = list.map(a => ({ + name: FuzzySort.prepare(`${a.id} `), + entry: a + })); + } + + Component.onCompleted: refreshEntries() + + Connections { + function onValuesChanged() { + refreshEntries(); + } + + target: DesktopEntries.applications } } diff --git a/Helpers/Time.qml b/Helpers/Time.qml index 11a67f7..f9ef387 100644 --- a/Helpers/Time.qml +++ b/Helpers/Time.qml @@ -3,24 +3,24 @@ pragma Singleton import Quickshell Singleton { - property alias enabled: clock.enabled - readonly property date date: clock.date - readonly property int hours: clock.hours - readonly property int minutes: clock.minutes - readonly property int seconds: clock.seconds + readonly property string amPmStr: timeComponents[2] ?? "" + readonly property date date: clock.date + property alias enabled: clock.enabled + readonly property string hourStr: timeComponents[0] ?? "" + readonly property int hours: clock.hours + readonly property string minuteStr: timeComponents[1] ?? "" + readonly property int minutes: clock.minutes + readonly property int seconds: clock.seconds + readonly property list timeComponents: timeStr.split(":") + readonly property string timeStr: format("hh:mm") - readonly property string timeStr: format("hh:mm") - readonly property list timeComponents: timeStr.split(":") - readonly property string hourStr: timeComponents[0] ?? "" - readonly property string minuteStr: timeComponents[1] ?? "" - readonly property string amPmStr: timeComponents[2] ?? "" + function format(fmt: string): string { + return Qt.formatDateTime(clock.date, fmt); + } - function format(fmt: string): string { - return Qt.formatDateTime(clock.date, fmt); - } + SystemClock { + id: clock - SystemClock { - id: clock - precision: SystemClock.Seconds - } + precision: SystemClock.Seconds + } } diff --git a/Helpers/UPower.qml b/Helpers/UPower.qml index ed9f1ad..eec419f 100644 --- a/Helpers/UPower.qml +++ b/Helpers/UPower.qml @@ -3,11 +3,10 @@ pragma Singleton import Quickshell import Quickshell.Services.UPower - Singleton { id: root readonly property list devices: UPower.devices.values - readonly property bool onBattery: UPower.onBattery readonly property UPowerDevice displayDevice: UPower.displayDevice + readonly property bool onBattery: UPower.onBattery } diff --git a/Helpers/Visibilities.qml b/Helpers/Visibilities.qml index 5ddde0c..fe22047 100644 --- a/Helpers/Visibilities.qml +++ b/Helpers/Visibilities.qml @@ -3,14 +3,14 @@ pragma Singleton import Quickshell Singleton { - property var screens: new Map() - property var bars: new Map() + property var bars: new Map() + property var screens: new Map() - function load(screen: ShellScreen, visibilities: var): void { - screens.set(Hypr.monitorFor(screen), visibilities); - } + function getForActive(): PersistentProperties { + return screens.get(Hypr.focusedMonitor); + } - function getForActive(): PersistentProperties { - return screens.get(Hypr.focusedMonitor); - } + function load(screen: ShellScreen, visibilities: var): void { + screens.set(Hypr.monitorFor(screen), visibilities); + } } diff --git a/Helpers/WallpaperPath.qml b/Helpers/WallpaperPath.qml index 33e4e0d..89e9a06 100644 --- a/Helpers/WallpaperPath.qml +++ b/Helpers/WallpaperPath.qml @@ -5,22 +5,25 @@ import Quickshell.Io import qs.Paths Singleton { - id: root + id: root - property alias currentWallpaperPath: adapter.currentWallpaperPath + property alias currentWallpaperPath: adapter.currentWallpaperPath property alias lockscreenBg: adapter.lockscreenBg - FileView { - id: fileView - path: `${Paths.state}/wallpaper_path.json` + FileView { + id: fileView - watchChanges: true - onFileChanged: reload() - onAdapterUpdated: writeAdapter() - JsonAdapter { - id: adapter - property string currentWallpaperPath: "" + path: `${Paths.state}/wallpaper_path.json` + watchChanges: true + + onAdapterUpdated: writeAdapter() + onFileChanged: reload() + + JsonAdapter { + id: adapter + + property string currentWallpaperPath: "" property string lockscreenBg: `${Paths.state}/lockscreen_bg.png` - } - } + } + } } diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 068eff4..498bb5f 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -9,54 +9,52 @@ import qs.Helpers import qs.Paths Searcher { - id: root + id: root - property bool showPreview: false - readonly property string current: showPreview ? previewPath : actualCurrent - property string previewPath - property string actualCurrent: WallpaperPath.currentWallpaperPath + property string actualCurrent: WallpaperPath.currentWallpaperPath + readonly property string current: showPreview ? previewPath : actualCurrent + property string previewPath + property bool showPreview: false - function setWallpaper(path: string): void { - actualCurrent = path; - WallpaperPath.currentWallpaperPath = path; - if ( Config.general.color.wallust ) - Wallust.generateColors(WallpaperPath.currentWallpaperPath); + function preview(path: string): void { + previewPath = path; + if (Config.general.color.schemeGeneration) + Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + showPreview = true; + } + + function setWallpaper(path: string): void { + actualCurrent = path; + WallpaperPath.currentWallpaperPath = path; Quickshell.execDetached(["sh", "-c", `zshell-cli wallpaper lockscreen --input-image=${root.actualCurrent} --output-path=${Paths.state}/lockscreen_bg.png --blur-amount=${Config.lock.blurAmount}`]); - } + } - function preview(path: string): void { - previewPath = path; - if ( Config.general.color.schemeGeneration ) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --thumbnail-path ${Paths.cache}/imagecache/thumbnail.jpg --output ${Paths.state}/scheme.json --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); - showPreview = true; - } + function stopPreview(): void { + showPreview = false; + if (Config.general.color.schemeGeneration) + Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + } - function stopPreview(): void { - showPreview = false; - if ( Config.general.color.schemeGeneration ) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --thumbnail-path ${Paths.cache}/imagecache/thumbnail.jpg --output ${Paths.state}/scheme.json --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); - } - - list: wallpapers.entries - key: "relativePath" - useFuzzy: true - extraOpts: useFuzzy ? ({}) : ({ - forward: false - }) + extraOpts: useFuzzy ? ({}) : ({ + forward: false + }) + key: "relativePath" + list: wallpapers.entries + useFuzzy: true IpcHandler { - target: "wallpaper" - function set(path: string): void { root.setWallpaper(path); } + + target: "wallpaper" } - FileSystemModel { - id: wallpapers + FileSystemModel { + id: wallpapers - recursive: true - path: Config.general.wallpaperPath - filter: FileSystemModel.Images - } + filter: FileSystemModel.Images + path: Config.general.wallpaperPath + recursive: true + } } diff --git a/Helpers/Wallust.qml b/Helpers/Wallust.qml new file mode 100644 index 0000000..9b7e83d --- /dev/null +++ b/Helpers/Wallust.qml @@ -0,0 +1,26 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick +import qs.Config + +Singleton { + id: root + + property var args + readonly property string mode: Config.general.color.mode + readonly property string threshold: mode === "dark" ? "--threshold=9" : "--dynamic-threshold" + + function generateColors(wallpaperPath) { + root.args = wallpaperPath; + wallustProc.running = true; + } + + Process { + id: wallustProc + + command: ["wallust", "run", root.args, `--palette=${root.mode}`, "--ignore-sequence=cursor", `${root.threshold}`] + running: false + } +} diff --git a/Helpers/Weather.qml b/Helpers/Weather.qml index df28df5..480c119 100644 --- a/Helpers/Weather.qml +++ b/Helpers/Weather.qml @@ -6,200 +6,200 @@ import ZShell import qs.Config Singleton { - id: root + id: root - property string city - property string loc - property var cc - property list forecast - property list hourlyForecast + readonly property var cachedCities: new Map() + property var cc + property string city + readonly property string description: cc?.weatherDesc ?? qsTr("No weather") + readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C` + property list forecast + property list hourlyForecast + readonly property int humidity: cc?.humidity ?? 0 + readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" + property string loc + readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--" + readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--" + readonly property string temp: `${cc?.tempC ?? 0}°C` + readonly property real windSpeed: cc?.windSpeed ?? 0 - readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" - readonly property string description: cc?.weatherDesc ?? qsTr("No weather") - readonly property string temp: `${cc?.tempC ?? 0}°C` - readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C` - readonly property int humidity: cc?.humidity ?? 0 - readonly property real windSpeed: cc?.windSpeed ?? 0 - readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--" - readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--" + function fetchCityFromCoords(coords: string): void { + if (cachedCities.has(coords)) { + city = cachedCities.get(coords); + return; + } - readonly property var cachedCities: new Map() + const [lat, lon] = coords.split(","); + const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; + Requests.get(url, text => { + const geo = JSON.parse(text).features?.[0]?.properties.geocoding; + if (geo) { + const geoCity = geo.type === "city" ? geo.name : geo.city; + city = geoCity; + cachedCities.set(coords, geoCity); + } else { + city = "Unknown City"; + } + }); + } - function reload(): void { - const configLocation = Config.services.weatherLocation; + function fetchCoordsFromCity(cityName: string): void { + const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`; - if (configLocation) { - if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { - loc = configLocation; - fetchCityFromCoords(configLocation); - } else { - fetchCoordsFromCity(configLocation); - } - } else if (!loc || timer.elapsed() > 900) { - Requests.get("https://ipinfo.io/json", text => { - const response = JSON.parse(text); - if (response.loc) { - loc = response.loc; - city = response.city ?? ""; - timer.restart(); - } - }); - } - } + Requests.get(url, text => { + const json = JSON.parse(text); + if (json.results && json.results.length > 0) { + const result = json.results[0]; + loc = result.latitude + "," + result.longitude; + city = result.name; + } else { + loc = ""; + reload(); + } + }); + } - function fetchCityFromCoords(coords: string): void { - if (cachedCities.has(coords)) { - city = cachedCities.get(coords); - return; - } + function fetchWeatherData(): void { + const url = getWeatherUrl(); + if (url === "") + return; - const [lat, lon] = coords.split(","); - const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; - Requests.get(url, text => { - const geo = JSON.parse(text).features?.[0]?.properties.geocoding; - if (geo) { - const geoCity = geo.type === "city" ? geo.name : geo.city; - city = geoCity; - cachedCities.set(coords, geoCity); - } else { - city = "Unknown City"; - } - }); - } + Requests.get(url, text => { + const json = JSON.parse(text); + if (!json.current || !json.daily) + return; - function fetchCoordsFromCity(cityName: string): void { - const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`; + cc = { + weatherCode: json.current.weather_code, + weatherDesc: getWeatherCondition(json.current.weather_code), + tempC: Math.round(json.current.temperature_2m), + tempF: Math.round(toFahrenheit(json.current.temperature_2m)), + feelsLikeC: Math.round(json.current.apparent_temperature), + feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)), + humidity: json.current.relative_humidity_2m, + windSpeed: json.current.wind_speed_10m, + isDay: json.current.is_day, + sunrise: json.daily.sunrise[0], + sunset: json.daily.sunset[0] + }; - Requests.get(url, text => { - const json = JSON.parse(text); - if (json.results && json.results.length > 0) { - const result = json.results[0]; - loc = result.latitude + "," + result.longitude; - city = result.name; - } else { - loc = ""; - reload(); - } - }); - } + const forecastList = []; + for (let i = 0; i < json.daily.time.length; i++) + forecastList.push({ + date: json.daily.time[i], + maxTempC: Math.round(json.daily.temperature_2m_max[i]), + maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), + minTempC: Math.round(json.daily.temperature_2m_min[i]), + minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])), + weatherCode: json.daily.weather_code[i], + icon: Icons.getWeatherIcon(json.daily.weather_code[i]) + }); + forecast = forecastList; - function fetchWeatherData(): void { - const url = getWeatherUrl(); - if (url === "") - return; + const hourlyList = []; + const now = new Date(); + for (let i = 0; i < json.hourly.time.length; i++) { + const time = new Date(json.hourly.time[i]); + if (time < now) + continue; - Requests.get(url, text => { - const json = JSON.parse(text); - if (!json.current || !json.daily) - return; + hourlyList.push({ + timestamp: json.hourly.time[i], + hour: time.getHours(), + tempC: Math.round(json.hourly.temperature_2m[i]), + tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])), + weatherCode: json.hourly.weather_code[i], + icon: Icons.getWeatherIcon(json.hourly.weather_code[i]) + }); + } + hourlyForecast = hourlyList; + }); + } - cc = { - weatherCode: json.current.weather_code, - weatherDesc: getWeatherCondition(json.current.weather_code), - tempC: Math.round(json.current.temperature_2m), - tempF: Math.round(toFahrenheit(json.current.temperature_2m)), - feelsLikeC: Math.round(json.current.apparent_temperature), - feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)), - humidity: json.current.relative_humidity_2m, - windSpeed: json.current.wind_speed_10m, - isDay: json.current.is_day, - sunrise: json.daily.sunrise[0], - sunset: json.daily.sunset[0] - }; + function getWeatherCondition(code: string): string { + const conditions = { + "0": "Clear", + "1": "Clear", + "2": "Partly cloudy", + "3": "Overcast", + "45": "Fog", + "48": "Fog", + "51": "Drizzle", + "53": "Drizzle", + "55": "Drizzle", + "56": "Freezing drizzle", + "57": "Freezing drizzle", + "61": "Light rain", + "63": "Rain", + "65": "Heavy rain", + "66": "Light rain", + "67": "Heavy rain", + "71": "Light snow", + "73": "Snow", + "75": "Heavy snow", + "77": "Snow", + "80": "Light rain", + "81": "Rain", + "82": "Heavy rain", + "85": "Light snow showers", + "86": "Heavy snow showers", + "95": "Thunderstorm", + "96": "Thunderstorm with hail", + "99": "Thunderstorm with hail" + }; + return conditions[code] || "Unknown"; + } - const forecastList = []; - for (let i = 0; i < json.daily.time.length; i++) - forecastList.push({ - date: json.daily.time[i], - maxTempC: Math.round(json.daily.temperature_2m_max[i]), - maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), - minTempC: Math.round(json.daily.temperature_2m_min[i]), - minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])), - weatherCode: json.daily.weather_code[i], - icon: Icons.getWeatherIcon(json.daily.weather_code[i]) - }); - forecast = forecastList; + function getWeatherUrl(): string { + if (!loc || loc.indexOf(",") === -1) + return ""; - const hourlyList = []; - const now = new Date(); - for (let i = 0; i < json.hourly.time.length; i++) { - const time = new Date(json.hourly.time[i]); - if (time < now) - continue; + const [lat, lon] = loc.split(","); + const baseUrl = "https://api.open-meteo.com/v1/forecast"; + const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; - hourlyList.push({ - timestamp: json.hourly.time[i], - hour: time.getHours(), - tempC: Math.round(json.hourly.temperature_2m[i]), - tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])), - weatherCode: json.hourly.weather_code[i], - icon: Icons.getWeatherIcon(json.hourly.weather_code[i]) - }); - } - hourlyForecast = hourlyList; - }); - } + return baseUrl + "?" + params.join("&"); + } - function toFahrenheit(celcius: real): real { - return celcius * 9 / 5 + 32; - } + function reload(): void { + const configLocation = Config.services.weatherLocation; - function getWeatherUrl(): string { - if (!loc || loc.indexOf(",") === -1) - return ""; + if (configLocation) { + if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { + loc = configLocation; + fetchCityFromCoords(configLocation); + } else { + fetchCoordsFromCity(configLocation); + } + } else if (!loc || timer.elapsed() > 900) { + Requests.get("https://ipinfo.io/json", text => { + const response = JSON.parse(text); + if (response.loc) { + loc = response.loc; + city = response.city ?? ""; + timer.restart(); + } + }); + } + } - const [lat, lon] = loc.split(","); - const baseUrl = "https://api.open-meteo.com/v1/forecast"; - const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; + function toFahrenheit(celcius: real): real { + return celcius * 9 / 5 + 32; + } - return baseUrl + "?" + params.join("&"); - } + onLocChanged: fetchWeatherData() - function getWeatherCondition(code: string): string { - const conditions = { - "0": "Clear", - "1": "Clear", - "2": "Partly cloudy", - "3": "Overcast", - "45": "Fog", - "48": "Fog", - "51": "Drizzle", - "53": "Drizzle", - "55": "Drizzle", - "56": "Freezing drizzle", - "57": "Freezing drizzle", - "61": "Light rain", - "63": "Rain", - "65": "Heavy rain", - "66": "Light rain", - "67": "Heavy rain", - "71": "Light snow", - "73": "Snow", - "75": "Heavy snow", - "77": "Snow", - "80": "Light rain", - "81": "Rain", - "82": "Heavy rain", - "85": "Light snow showers", - "86": "Heavy snow showers", - "95": "Thunderstorm", - "96": "Thunderstorm with hail", - "99": "Thunderstorm with hail" - }; - return conditions[code] || "Unknown"; - } + // Refresh current location hourly + Timer { + interval: 3600000 // 1 hour + repeat: true + running: true - onLocChanged: fetchWeatherData() + onTriggered: fetchWeatherData() + } - // Refresh current location hourly - Timer { - interval: 3600000 // 1 hour - running: true - repeat: true - onTriggered: fetchWeatherData() - } + ElapsedTimer { + id: timer - ElapsedTimer { - id: timer - } + } } diff --git a/Helpers/Year.qml b/Helpers/Year.qml index 03ad712..5dec12d 100644 --- a/Helpers/Year.qml +++ b/Helpers/Year.qml @@ -1,420 +1,56 @@ import Quickshell.Io JsonObject { - property list week_0: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_1: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_2: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_3: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_4: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_5: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_6: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_7: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_8: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_9: [ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_10:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_11:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_12:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_13:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_14:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_15:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_16:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_17:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_18:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_19:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_20:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_21:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_22:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_23:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_24:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_25:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_26:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_27:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_28:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_29:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_30:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_31:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_32:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_33:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_34:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_35:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_36:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_37:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_38:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_39:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_40:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_41:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_42:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_43:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_44:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_45:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_46:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_47:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_48:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_49:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_50:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] - property list week_51:[ - 0, - 0, - 0, - 0, - 0, - 0, - ] + property list week_0: [0, 0, 0, 0, 0, 0,] + property list week_1: [0, 0, 0, 0, 0, 0,] + property list week_10: [0, 0, 0, 0, 0, 0,] + property list week_11: [0, 0, 0, 0, 0, 0,] + property list week_12: [0, 0, 0, 0, 0, 0,] + property list week_13: [0, 0, 0, 0, 0, 0,] + property list week_14: [0, 0, 0, 0, 0, 0,] + property list week_15: [0, 0, 0, 0, 0, 0,] + property list week_16: [0, 0, 0, 0, 0, 0,] + property list week_17: [0, 0, 0, 0, 0, 0,] + property list week_18: [0, 0, 0, 0, 0, 0,] + property list week_19: [0, 0, 0, 0, 0, 0,] + property list week_2: [0, 0, 0, 0, 0, 0,] + property list week_20: [0, 0, 0, 0, 0, 0,] + property list week_21: [0, 0, 0, 0, 0, 0,] + property list week_22: [0, 0, 0, 0, 0, 0,] + property list week_23: [0, 0, 0, 0, 0, 0,] + property list week_24: [0, 0, 0, 0, 0, 0,] + property list week_25: [0, 0, 0, 0, 0, 0,] + property list week_26: [0, 0, 0, 0, 0, 0,] + property list week_27: [0, 0, 0, 0, 0, 0,] + property list week_28: [0, 0, 0, 0, 0, 0,] + property list week_29: [0, 0, 0, 0, 0, 0,] + property list week_3: [0, 0, 0, 0, 0, 0,] + property list week_30: [0, 0, 0, 0, 0, 0,] + property list week_31: [0, 0, 0, 0, 0, 0,] + property list week_32: [0, 0, 0, 0, 0, 0,] + property list week_33: [0, 0, 0, 0, 0, 0,] + property list week_34: [0, 0, 0, 0, 0, 0,] + property list week_35: [0, 0, 0, 0, 0, 0,] + property list week_36: [0, 0, 0, 0, 0, 0,] + property list week_37: [0, 0, 0, 0, 0, 0,] + property list week_38: [0, 0, 0, 0, 0, 0,] + property list week_39: [0, 0, 0, 0, 0, 0,] + property list week_4: [0, 0, 0, 0, 0, 0,] + property list week_40: [0, 0, 0, 0, 0, 0,] + property list week_41: [0, 0, 0, 0, 0, 0,] + property list week_42: [0, 0, 0, 0, 0, 0,] + property list week_43: [0, 0, 0, 0, 0, 0,] + property list week_44: [0, 0, 0, 0, 0, 0,] + property list week_45: [0, 0, 0, 0, 0, 0,] + property list week_46: [0, 0, 0, 0, 0, 0,] + property list week_47: [0, 0, 0, 0, 0, 0,] + property list week_48: [0, 0, 0, 0, 0, 0,] + property list week_49: [0, 0, 0, 0, 0, 0,] + property list week_5: [0, 0, 0, 0, 0, 0,] + property list week_50: [0, 0, 0, 0, 0, 0,] + property list week_51: [0, 0, 0, 0, 0, 0,] + property list week_6: [0, 0, 0, 0, 0, 0,] + property list week_7: [0, 0, 0, 0, 0, 0,] + property list week_8: [0, 0, 0, 0, 0, 0,] + property list week_9: [0, 0, 0, 0, 0, 0,] } diff --git a/Modules/Anim.qml b/Modules/Anim.qml deleted file mode 100644 index c32e713..0000000 --- a/Modules/Anim.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick -import qs.Modules -import qs.Config - -NumberAnimation { - duration: MaterialEasing.standardTime - easing.type: Easing.BezierSpline - easing.bezierCurve: MaterialEasing.standard -} diff --git a/Modules/AudioPopup.qml b/Modules/AudioPopup.qml index 190ccb5..db8b4a9 100644 --- a/Modules/AudioPopup.qml +++ b/Modules/AudioPopup.qml @@ -12,68 +12,65 @@ import qs.Daemons import qs.Helpers Item { - id: root + id: root - implicitWidth: layout.implicitWidth + 5 * 2 - implicitHeight: layout.implicitHeight + 5 * 2 - - readonly property int topMargin: 0 readonly property int rounding: 6 + readonly property int topMargin: 0 + required property var wrapper - required property var wrapper + implicitHeight: layout.implicitHeight + 5 * 2 + implicitWidth: layout.implicitWidth + 5 * 2 - ColumnLayout { - id: layout + ColumnLayout { + id: layout - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top implicitWidth: stack.currentItem ? stack.currentItem.childrenRect.height : 0 spacing: 12 RowLayout { id: tabBar - spacing: 6 - Layout.fillWidth: true + property int tabHeight: 36 + Layout.fillWidth: true + spacing: 6 + CustomClippingRect { - radius: 6 Layout.fillWidth: true Layout.preferredHeight: tabBar.tabHeight - color: stack.currentIndex === 0 ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer + radius: 6 StateLayer { - function onClicked(): void { stack.currentIndex = 0; } CustomText { - text: qsTr("Volumes") anchors.centerIn: parent color: stack.currentIndex === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary + text: qsTr("Volumes") } } } CustomClippingRect { - radius: 6 Layout.fillWidth: true Layout.preferredHeight: tabBar.tabHeight - color: stack.currentIndex === 1 ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer + radius: 6 StateLayer { - function onClicked(): void { stack.currentIndex = 1; } CustomText { - text: qsTr("Devices") anchors.centerIn: parent color: stack.currentIndex === 1 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary + text: qsTr("Devices") } } } @@ -81,175 +78,246 @@ Item { StackLayout { id: stack + Layout.fillWidth: true Layout.preferredHeight: currentIndex === 0 ? vol.childrenRect.height : dev.childrenRect.height currentIndex: 0 - VolumesTab { id: vol } - DevicesTab { id: dev } - Behavior on currentIndex { SequentialAnimation { ParallelAnimation { Anim { - target: stack - property: "opacity" - to: 0 duration: MaterialEasing.expressiveEffectsTime + property: "opacity" + target: stack + to: 0 } Anim { - target: stack - property: "scale" - to: 0.9 duration: MaterialEasing.expressiveEffectsTime + property: "scale" + target: stack + to: 0.9 } } - PropertyAction {} + PropertyAction { + } ParallelAnimation { Anim { - target: stack - property: "opacity" - to: 1 duration: MaterialEasing.expressiveEffectsTime + property: "opacity" + target: stack + to: 1 } Anim { - target: stack - property: "scale" - to: 1 duration: MaterialEasing.expressiveEffectsTime + property: "scale" + target: stack + to: 1 } } } } - } - } + VolumesTab { + id: vol + + } + + DevicesTab { + id: dev + + } + } + } + + component DevicesTab: ColumnLayout { + spacing: 12 + + ButtonGroup { + id: sinks + + } + + ButtonGroup { + id: sources + + } + + CustomText { + font.weight: 500 + text: qsTr("Output device") + } + + Repeater { + model: Audio.sinks + + CustomRadioButton { + required property PwNode modelData + + ButtonGroup.group: sinks + checked: Audio.sink?.id === modelData.id + text: modelData.description + + onClicked: Audio.setAudioSink(modelData) + } + } + + CustomText { + Layout.topMargin: 10 + font.weight: 500 + text: qsTr("Input device") + } + + Repeater { + model: Audio.sources + + CustomRadioButton { + required property PwNode modelData + + ButtonGroup.group: sources + checked: Audio.source?.id === modelData.id + text: modelData.description + + onClicked: Audio.setAudioSource(modelData) + } + } + } component VolumesTab: ColumnLayout { spacing: 12 CustomRect { - Layout.topMargin: root.topMargin - Layout.preferredHeight: 42 + Appearance.spacing.smaller * 2 Layout.fillWidth: true + Layout.preferredHeight: 42 + Appearance.spacing.smaller * 2 + Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer - radius: root.rounding RowLayout { id: outputVolume - anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left - anchors.right: parent.right anchors.margins: Appearance.spacing.smaller + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter spacing: 15 + CustomRect { - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 color: DynamicColors.palette.m3primary radius: 1000 + MaterialIcon { + anchors.alignWhenCentered: false anchors.centerIn: parent color: DynamicColors.palette.m3onPrimary - text: "speaker" font.pointSize: 22 + text: "speaker" } } ColumnLayout { Layout.fillWidth: true + RowLayout { Layout.fillWidth: true CustomText { - text: "Output Volume" - Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillWidth: true + text: "Output Volume" } CustomText { - text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`); - font.bold: true Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + font.bold: true + text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) } } CustomMouseArea { + Layout.bottomMargin: 5 Layout.fillWidth: true Layout.preferredHeight: 10 - Layout.bottomMargin: 5 CustomSlider { anchors.left: parent.left anchors.right: parent.right implicitHeight: 10 value: Audio.volume - onMoved: Audio.setVolume(value) - Behavior on value { Anim {} } + Behavior on value { + Anim { + } + } + + onMoved: Audio.setVolume(value) } } } } } - CustomRect { - Layout.topMargin: root.topMargin Layout.fillWidth: true Layout.preferredHeight: 42 + Appearance.spacing.smaller * 2 + Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer - radius: root.rounding RowLayout { id: inputVolume - anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left - anchors.right: parent.right anchors.margins: Appearance.spacing.smaller + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter spacing: 15 + Rectangle { - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 color: DynamicColors.palette.m3primary radius: 1000 + MaterialIcon { - anchors.centerIn: parent anchors.alignWhenCentered: false + anchors.centerIn: parent color: DynamicColors.palette.m3onPrimary - text: "mic" font.pointSize: 22 + text: "mic" } } ColumnLayout { Layout.fillWidth: true + RowLayout { - Layout.fillWidth: true Layout.fillHeight: true + Layout.fillWidth: true CustomText { - text: "Input Volume" - Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillWidth: true + text: "Input Volume" } CustomText { - text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`); - font.bold: true Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + font.bold: true + text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`) } } CustomMouseArea { - Layout.fillWidth: true Layout.bottomMargin: 5 + Layout.fillWidth: true implicitHeight: 10 CustomSlider { @@ -257,9 +325,13 @@ Item { anchors.right: parent.right implicitHeight: 10 value: Audio.sourceVolume - onMoved: Audio.setSourceVolume(value) - Behavior on value { Anim {} } + Behavior on value { + Anim { + } + } + + onMoved: Audio.setSourceVolume(value) } } } @@ -267,10 +339,9 @@ Item { } Rectangle { - Layout.topMargin: root.topMargin Layout.fillWidth: true Layout.preferredHeight: 1 - + Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3outline } @@ -280,39 +351,40 @@ Item { CustomRect { id: appBox - Layout.topMargin: root.topMargin + required property int index + required property var modelData + Layout.fillWidth: true Layout.preferredHeight: 42 + Appearance.spacing.smaller * 2 + Layout.topMargin: root.topMargin color: DynamicColors.tPalette.m3surfaceContainer - radius: root.rounding - - required property var modelData - required property int index - RowLayout { id: layoutVolume + anchors.fill: parent anchors.margins: Appearance.spacing.smaller spacing: 15 - CustomRect { - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 color: DynamicColors.palette.m3primary radius: 1000 + MaterialIcon { id: icon + anchors.centerIn: parent - text: "volume_up" - font.pointSize: 22 color: DynamicColors.palette.m3onPrimary + font.pointSize: 22 + text: "volume_up" StateLayer { radius: 1000 + onClicked: { appBox.modelData.audio.muted = !appBox.modelData.audio.muted; } @@ -326,42 +398,46 @@ Item { TextMetrics { id: metrics - text: Audio.getStreamName(appBox.modelData) + elide: Text.ElideRight elideWidth: root.width - 50 + text: Audio.getStreamName(appBox.modelData) } RowLayout { - Layout.fillWidth: true Layout.fillHeight: true + Layout.fillWidth: true + CustomText { - text: metrics.elidedText - elide: Text.ElideRight - Layout.fillWidth: true - Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + Layout.fillWidth: true + elide: Text.ElideRight + text: metrics.elidedText } CustomText { - text: qsTr("%1").arg(appBox.modelData.audio.muted ? qsTr("Muted") : `${Math.round(appBox.modelData.audio.volume * 100)}%`); - font.bold: true - Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.fillHeight: true + font.bold: true + text: qsTr("%1").arg(appBox.modelData.audio.muted ? qsTr("Muted") : `${Math.round(appBox.modelData.audio.volume * 100)}%`) } } CustomMouseArea { - Layout.fillWidth: true - Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + Layout.fillWidth: true implicitHeight: 10 + CustomSlider { anchors.left: parent.left anchors.right: parent.right implicitHeight: 10 value: appBox.modelData.audio.volume + onMoved: { - Audio.setStreamVolume(appBox.modelData, value) + Audio.setStreamVolume(appBox.modelData, value); } } } @@ -370,48 +446,4 @@ Item { } } } - - component DevicesTab: ColumnLayout { - spacing: 12 - - ButtonGroup { id: sinks } - ButtonGroup { id: sources } - - CustomText { - text: qsTr("Output device") - font.weight: 500 - } - - Repeater { - model: Audio.sinks - - CustomRadioButton { - required property PwNode modelData - - ButtonGroup.group: sinks - checked: Audio.sink?.id === modelData.id - onClicked: Audio.setAudioSink(modelData) - text: modelData.description - } - } - - CustomText { - Layout.topMargin: 10 - text: qsTr("Input device") - font.weight: 500 - } - - Repeater { - model: Audio.sources - - CustomRadioButton { - required property PwNode modelData - - ButtonGroup.group: sources - checked: Audio.source?.id === modelData.id - onClicked: Audio.setAudioSource(modelData) - text: modelData.description - } - } - } } diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index 114d56e..d9031cb 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -8,113 +8,95 @@ import qs.Config import qs.Components Item { - id: root - implicitWidth: expanded ? 300 : 150 - anchors.top: parent.top - anchors.bottom: parent.bottom + id: root - property bool expanded: false - property color textColor: DynamicColors.palette.m3onSurface - property color barColor: DynamicColors.palette.m3primary + property color barColor: DynamicColors.palette.m3primary + property color textColor: DynamicColors.palette.m3onSurface - Behavior on implicitWidth { - NumberAnimation { - duration: 300 - easing.type: Easing.OutCubic - } - } + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: 150 - Rectangle { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.right: parent.right - height: 22 - radius: height / 2 - color: DynamicColors.tPalette.m3surfaceContainer + Behavior on implicitWidth { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } - Behavior on color { - CAnim {} - } + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + radius: height / 2 + } - Rectangle { - anchors.centerIn: parent - width: parent.width - height: parent.height - radius: width / 2 - color: "transparent" - border.color: "#30ffffff" - border.width: 0 - } + RowLayout { + id: layout - RowLayout { - anchors { - fill: parent - leftMargin: 10 - rightMargin: 15 - } + anchors.fill: parent + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small * 2 + anchors.verticalCenter: parent.verticalCenter - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: Audio.muted ? "volume_off" : "volume_up" - color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - } + 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 { - Layout.fillWidth: true + CustomRect { + Layout.fillWidth: true + color: "#50ffffff" + implicitHeight: 4 + radius: 20 - implicitHeight: 4 - radius: 20 - color: "#50ffffff" + CustomRect { + id: sinkVolumeBar - Rectangle { - id: sinkVolumeBar - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } + color: Audio.muted ? DynamicColors.palette.m3error : root.barColor + implicitWidth: parent.width * (Audio.volume ?? 0) + radius: parent.radius - implicitWidth: parent.width * ( Audio.volume ?? 0 ) - radius: parent.radius - color: Audio.muted ? DynamicColors.palette.m3error : root.barColor - Behavior on color { - CAnim {} - } - } - } + anchors { + bottom: parent.bottom + left: parent.left + top: parent.top + } + } + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: Audio.sourceMuted ? "mic_off" : "mic" - color: ( Audio.sourceMuted ?? false ) ? DynamicColors.palette.m3error : root.textColor - } + 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 + CustomRect { + Layout.fillWidth: true + color: "#50ffffff" + implicitHeight: 4 + radius: 20 - implicitHeight: 4 - radius: 20 - color: "#50ffffff" + CustomRect { + id: sourceVolumeBar - Rectangle { - id: sourceVolumeBar - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } + color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.barColor + implicitWidth: parent.width * (Audio.sourceVolume ?? 0) + radius: parent.radius - implicitWidth: parent.width * ( Audio.sourceVolume ?? 0 ) - radius: parent.radius - color: ( Audio.sourceMuted ?? false ) ? DynamicColors.palette.m3error : root.barColor - - Behavior on color { - CAnim {} - } - } - } - } - } + anchors { + bottom: parent.bottom + left: parent.left + top: parent.top + } + } + } + } } diff --git a/Modules/Background.qml b/Modules/Background.qml index 560503d..483ce94 100644 --- a/Modules/Background.qml +++ b/Modules/Background.qml @@ -1,66 +1,68 @@ import QtQuick import QtQuick.Shapes +import qs.Components import qs.Config ShapePath { - id: root + id: root - required property Wrapper wrapper - required property bool invertBottomRounding - readonly property real rounding: 8 - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - property real ibr: invertBottomRounding ? -1 : 1 + readonly property bool flatten: wrapper.height < rounding * 2 + property real ibr: invertBottomRounding ? -1 : 1 + required property bool invertBottomRounding + readonly property real rounding: 8 + readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + required property Wrapper wrapper - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } + Behavior on fillColor { + CAnim { + } + } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY - root.roundingY * root.ibr - } + PathArc { + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: root.roundingY + } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY * root.ibr - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: root.invertBottomRounding ? PathArc.Clockwise : PathArc.Counterclockwise - } + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.roundingY - root.roundingY * root.ibr + } - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } + PathArc { + direction: root.invertBottomRounding ? PathArc.Clockwise : PathArc.Counterclockwise + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: root.roundingY * root.ibr + } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } + PathLine { + relativeX: root.wrapper.width - root.rounding * 2 + relativeY: 0 + } - 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 + } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } + PathLine { + relativeX: 0 + relativeY: -(root.wrapper.height - root.roundingY * 2) + } - Behavior on fillColor { - CAnim {} - } + PathArc { + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: -root.roundingY + } } diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 282a9d1..2c3a70a 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -13,24 +13,23 @@ import qs.Modules.Network RowLayout { id: root - anchors.fill: parent - readonly property int vPadding: 6 - required property Wrapper popouts - required property PersistentProperties visibilities required property PanelWindow bar + required property Wrapper popouts required property ShellScreen screen + readonly property int vPadding: 6 + required property PersistentProperties visibilities 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; @@ -38,49 +37,30 @@ RowLayout { const item = ch.item; const itemWidth = item.implicitWidth; - if (id === "audio" && Config.barConfig.popouts.audio) { popouts.currentName = "audio"; - popouts.currentCenter = Qt.binding( () => item.mapToItem(root, itemWidth / 2, 0 ).x ); + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.hasCurrent = true; - } else if ( id === "resources" && Config.barConfig.popouts.resources ) { - popouts.currentName = "resources"; - popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); - popouts.hasCurrent = true; - } else if ( id === "tray" && Config.barConfig.popouts.tray ) { - const index = Math.floor((( x - top ) / item.implicitWidth ) * item.items.count ); - const trayItem = item.items.itemAt( index ); - if ( trayItem ) { - // popouts.currentName = `traymenu${ index }`; - // popouts.currentCenter = Qt.binding( () => trayItem.mapToItem( root, trayItem.implicitWidth / 2, 0 ).x ); - // popouts.hasCurrent = true; - } else { - // popouts.hasCurrent = false; - } - } else if ( id === "clock" && Config.barConfig.popouts.clock ) { - // Calendar.displayYear = new Date().getFullYear(); - // Calendar.displayMonth = new Date().getMonth(); - // popouts.currentName = "calendar"; - // popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); - // popouts.hasCurrent = true; - } else if ( id === "network" && Config.barConfig.popouts.network ) { + } else if (id === "network" && Config.barConfig.popouts.network) { popouts.currentName = "network"; - popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); popouts.hasCurrent = true; - } else if ( id === "upower" && Config.barConfig.popouts.upower ) { + } else if (id === "upower" && Config.barConfig.popouts.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; } } + implicitHeight: 34 + CustomShortcut { name: "toggle-overview" onPressed: { Hyprland.refreshWorkspaces(); Hyprland.refreshMonitors(); - if ( root.popouts.hasCurrent && root.popouts.currentName === "overview" ) { + if (root.popouts.hasCurrent && root.popouts.currentName === "overview") { root.popouts.hasCurrent = false; } else { root.popouts.currentName = "overview"; @@ -92,6 +72,8 @@ RowLayout { Repeater { id: repeater + + // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) model: Config.barConfig.entries DelegateChooser { @@ -99,67 +81,88 @@ RowLayout { DelegateChoice { roleValue: "spacer" + delegate: WrappedLoader { Layout.fillWidth: true } } + DelegateChoice { roleValue: "workspaces" + delegate: WrappedLoader { sourceComponent: Workspaces { bar: root.bar } } } + DelegateChoice { roleValue: "audio" + delegate: WrappedLoader { - sourceComponent: AudioWidget {} + sourceComponent: AudioWidget { + } } } + DelegateChoice { roleValue: "tray" + delegate: WrappedLoader { sourceComponent: TrayWidget { bar: root.bar - popouts: root.popouts loader: root + popouts: root.popouts } } } + DelegateChoice { roleValue: "resources" + delegate: WrappedLoader { - sourceComponent: Resources {} + sourceComponent: Resources { + visibilities: root.visibilities + } } } + DelegateChoice { roleValue: "updates" + delegate: WrappedLoader { - sourceComponent: UpdatesWidget {} + sourceComponent: UpdatesWidget { + } } } + DelegateChoice { roleValue: "notifBell" + delegate: WrappedLoader { sourceComponent: NotifBell { - visibilities: root.visibilities popouts: root.popouts + visibilities: root.visibilities } } } + DelegateChoice { roleValue: "clock" + delegate: WrappedLoader { sourceComponent: Clock { + loader: root popouts: root.popouts visibilities: root.visibilities - loader: root } } } + DelegateChoice { roleValue: "activeWindow" + delegate: WrappedLoader { sourceComponent: WindowTitle { bar: root @@ -167,26 +170,33 @@ RowLayout { } } } + DelegateChoice { roleValue: "upower" + delegate: WrappedLoader { - sourceComponent: UPowerWidget {} + sourceComponent: UPowerWidget { + } } } + DelegateChoice { roleValue: "network" + delegate: WrappedLoader { - sourceComponent: NetworkWidget {} + sourceComponent: NetworkWidget { + } + } + } + + DelegateChoice { + roleValue: "media" + + delegate: WrappedLoader { + sourceComponent: MediaWidget { + } } } - // DelegateChoice { - // roleValue: "dash" - // delegate: WrappedLoader { - // sourceComponent: DashWidget { - // visibilities: root.visibilities - // } - // } - // } } } @@ -195,15 +205,12 @@ RowLayout { required property string id required property int index - Layout.alignment: Qt.AlignVCenter - Layout.fillHeight: true - function findFirstEnabled(): Item { const count = repeater.count; for (let i = 0; i < count; i++) { const item = repeater.itemAt(i); if (item?.enabled) - return item; + return item; } return null; } @@ -212,15 +219,16 @@ RowLayout { for (let i = repeater.count - 1; i >= 0; i--) { const item = repeater.itemAt(i); if (item?.enabled) - return item; + return item; } return null; } + Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 - - visible: enabled active: enabled + visible: enabled } } diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index 8bb4e20..875d218 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -8,42 +8,44 @@ import qs.Config import qs.Components Item { - id: root + id: root - required property Item bar + required property Item bar required property PersistentProperties visibilities - anchors.fill: parent + anchors.fill: parent - CustomRect { - anchors.fill: parent - color: Config.barConfig.autoHide && !root.visibilities.bar ? "transparent" : DynamicColors.palette.m3surface + CustomRect { + anchors.fill: parent + color: Config.barConfig.autoHide && !root.visibilities.bar ? "transparent" : DynamicColors.palette.m3surface + layer.enabled: true - layer.enabled: true + layer.effect: MultiEffect { + maskEnabled: true + maskInverted: true + maskSource: mask + maskSpreadAtMin: 1 + maskThresholdMin: 0.5 + } + } - layer.effect: MultiEffect { - maskSource: mask - maskEnabled: true - maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - } + Item { + id: mask - Item { - id: mask - anchors.fill: parent - layer.enabled: true - visible: false + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight + topLeftRadius: 8 + topRightRadius: 8 - Rectangle { - anchors.fill: parent - anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight - topLeftRadius: 8 - topRightRadius: 8 Behavior on anchors.topMargin { - Anim {} + Anim { + } } - } - } + } + } } diff --git a/Modules/CAnim.qml b/Modules/CAnim.qml deleted file mode 100644 index 09b33b1..0000000 --- a/Modules/CAnim.qml +++ /dev/null @@ -1,9 +0,0 @@ -import QtQuick -import qs.Modules -import qs.Config - -ColorAnimation { - duration: MaterialEasing.standardTime - easing.type: Easing.BezierSpline - easing.bezierCurve: MaterialEasing.standard -} diff --git a/Modules/Calendar/CalendarHeader.qml b/Modules/Calendar/CalendarHeader.qml index 97d6b2b..bc7b158 100644 --- a/Modules/Calendar/CalendarHeader.qml +++ b/Modules/Calendar/CalendarHeader.qml @@ -10,17 +10,17 @@ RowLayout { spacing: 12 Rectangle { - Layout.preferredWidth: 40 Layout.preferredHeight: 40 + Layout.preferredWidth: 40 color: "transparent" radius: 1000 MaterialIcon { anchors.centerIn: parent - text: "arrow_back_2" + color: DynamicColors.palette.m3onSurface fill: 1 font.pointSize: 24 - color: DynamicColors.palette.m3onSurface + text: "arrow_back_2" } StateLayer { @@ -36,30 +36,27 @@ RowLayout { } CustomText { - text: new Date(Calendar.displayYear, Calendar.displayMonth, 1).toLocaleDateString( - Qt.locale(), - "MMMM yyyy" - ) - font.weight: 600 - font.pointSize: 14 - color: DynamicColors.palette.m3onSurface Layout.fillWidth: true + color: DynamicColors.palette.m3onSurface + font.pointSize: 14 + font.weight: 600 horizontalAlignment: Text.AlignHCenter + text: new Date(Calendar.displayYear, Calendar.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") } Rectangle { - Layout.preferredWidth: 40 Layout.preferredHeight: 40 + Layout.preferredWidth: 40 color: "transparent" radius: 1000 MaterialIcon { anchors.centerIn: parent - text: "arrow_back_2" + color: DynamicColors.palette.m3onSurface fill: 1 font.pointSize: 24 rotation: 180 - color: DynamicColors.palette.m3onSurface + text: "arrow_back_2" } StateLayer { diff --git a/Modules/Calendar/CalendarPopup.qml b/Modules/Calendar/CalendarPopup.qml index a25db29..0dcf110 100644 --- a/Modules/Calendar/CalendarPopup.qml +++ b/Modules/Calendar/CalendarPopup.qml @@ -11,8 +11,8 @@ Item { required property Item wrapper - implicitWidth: layout.childrenRect.width + layout.anchors.margins * 2 implicitHeight: layout.childrenRect.height + layout.anchors.margins * 2 + implicitWidth: layout.childrenRect.width + layout.anchors.margins * 2 ColumnLayout { id: layout @@ -59,17 +59,17 @@ Item { DayOfWeekRow { id: dayOfWeekRow - locale: Qt.locale() + Layout.fillWidth: true Layout.preferredHeight: 30 + locale: Qt.locale() } MonthGrid { - locale: Qt.locale() - - wrapper: root.wrapper - Layout.preferredWidth: childrenRect.width Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: childrenRect.width + locale: Qt.locale() + wrapper: root.wrapper } } } diff --git a/Modules/Calendar/DayOfWeekRow.qml b/Modules/Calendar/DayOfWeekRow.qml index 39505e5..10fa64c 100644 --- a/Modules/Calendar/DayOfWeekRow.qml +++ b/Modules/Calendar/DayOfWeekRow.qml @@ -7,39 +7,36 @@ import qs.Config import qs.Helpers RowLayout { - id: root + id: root - required property var locale + required property var locale - spacing: 4 + spacing: 4 - Repeater { - model: 7 + Repeater { + model: 7 - Item { - required property int index + Item { + readonly property string dayName: { + // Get the day name for this column + const dayIndex = (index + Calendar.weekStartDay) % 7; + return root.locale.dayName(dayIndex, Locale.ShortFormat); + } + required property int index - Layout.fillWidth: true - Layout.preferredHeight: 30 + Layout.fillWidth: true + Layout.preferredHeight: 30 - readonly property string dayName: { - // Get the day name for this column - const dayIndex = (index + Calendar.weekStartDay) % 7; - return root.locale.dayName(dayIndex, Locale.ShortFormat); - } - - CustomText { - anchors.centerIn: parent - - text: parent.dayName - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: DynamicColors.palette.m3onSurfaceVariant - opacity: 0.8 - font.weight: 500 - font.pointSize: 11 - } - } - } + CustomText { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 11 + font.weight: 500 + horizontalAlignment: Text.AlignHCenter + opacity: 0.8 + text: parent.dayName + verticalAlignment: Text.AlignVCenter + } + } + } } - diff --git a/Modules/Calendar/MonthGrid.qml b/Modules/Calendar/MonthGrid.qml index d1e61c3..5ab370b 100644 --- a/Modules/Calendar/MonthGrid.qml +++ b/Modules/Calendar/MonthGrid.qml @@ -13,48 +13,49 @@ GridLayout { required property var locale required property Item wrapper + columnSpacing: 4 columns: 7 rowSpacing: 4 - columnSpacing: 4 - uniformCellWidths: true uniformCellHeights: true - - component Anim: NumberAnimation { - target: root - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } + uniformCellWidths: true Repeater { id: repeater + model: ScriptModel { values: Calendar.getWeeksForMonth(Calendar.displayMonth, Calendar.displayYear) Behavior on values { SequentialAnimation { id: switchAnim + ParallelAnimation { Anim { - property: "opacity" from: 1.0 + property: "opacity" to: 0.0 } + Anim { - property: "scale" from: 1.0 + property: "scale" to: 0.8 } } - PropertyAction {} + + PropertyAction { + } + ParallelAnimation { Anim { - property: "opacity" from: 0.0 + property: "opacity" to: 1.0 } + Anim { - property: "scale" from: 0.8 + property: "scale" to: 1.0 } } @@ -63,55 +64,55 @@ GridLayout { } Rectangle { - - required property var modelData required property int index + required property var modelData - Layout.preferredWidth: 40 Layout.preferredHeight: width - - - radius: 1000 + Layout.preferredWidth: 40 color: { if (modelData.isToday) { - console.log(width); return DynamicColors.palette.m3primaryContainer; } return "transparent"; } + radius: 1000 Behavior on color { - ColorAnimation { duration: 200 } + ColorAnimation { + duration: 200 + } } CustomText { anchors.centerIn: parent - - text: parent.modelData.day.toString() - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: parent.modelData.isCurrentMonth ? 1.0 : 0.4 color: { if (parent.modelData.isToday) { return DynamicColors.palette.m3onPrimaryContainer; } return DynamicColors.palette.m3onSurface; } + horizontalAlignment: Text.AlignHCenter + opacity: parent.modelData.isCurrentMonth ? 1.0 : 0.4 + text: parent.modelData.day.toString() + verticalAlignment: Text.AlignVCenter Behavior on color { - ColorAnimation { duration: 200 } + ColorAnimation { + duration: 200 + } } Behavior on opacity { - NumberAnimation { duration: 200 } - } - } - - StateLayer { - color: DynamicColors.palette.m3onSurface - onClicked: { - console.log(`Selected date: ${parent.modelData.day}/${parent.modelData.month + 1}/${parent.modelData.year}`); + NumberAnimation { + duration: 200 + } } } } } + + component Anim: NumberAnimation { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + target: root + } } diff --git a/Modules/Calendar/WeekNumberColumn.qml b/Modules/Calendar/WeekNumberColumn.qml index 8fdf5c1..44e30b6 100644 --- a/Modules/Calendar/WeekNumberColumn.qml +++ b/Modules/Calendar/WeekNumberColumn.qml @@ -8,38 +8,38 @@ import qs.Config import qs.Helpers ColumnLayout { - id: root + id: root - spacing: 4 + readonly property var weekNumbers: Calendar.getWeekNumbers(Calendar.displayMonth, Calendar.displayYear) - readonly property var weekNumbers: Calendar.getWeekNumbers(Calendar.displayMonth, Calendar.displayYear) + spacing: 4 - Repeater { - model: ScriptModel { + Repeater { + model: ScriptModel { values: root.weekNumbers } - Item { - id: weekItem - Layout.preferredHeight: 40 - Layout.preferredWidth: 20 - Layout.alignment: Qt.AlignHCenter + Item { + id: weekItem - required property int index - required property var modelData + required property int index + required property var modelData - CustomText { - id: weekText + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 40 + Layout.preferredWidth: 20 - anchors.centerIn: parent + CustomText { + id: weekText - text: weekItem.modelData - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: DynamicColors.palette.m3onSurfaceVariant - opacity: 0.5 - font.pointSize: 10 - } - } - } + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + horizontalAlignment: Text.AlignHCenter + opacity: 0.5 + text: weekItem.modelData + verticalAlignment: Text.AlignVCenter + } + } + } } diff --git a/Modules/Clock.qml b/Modules/Clock.qml index 27d5505..9598bc7 100644 --- a/Modules/Clock.qml +++ b/Modules/Clock.qml @@ -8,35 +8,38 @@ import qs.Components Item { id: root - required property PersistentProperties visibilities - required property Wrapper popouts - required property RowLayout loader - implicitWidth: timeText.contentWidth + 5 * 2 - anchors.top: parent.top - anchors.bottom: parent.bottom + required property RowLayout loader + required property Wrapper popouts + required property PersistentProperties visibilities + + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: timeText.contentWidth + 5 * 2 CustomRect { + anchors.bottomMargin: 3 anchors.fill: parent anchors.topMargin: 3 - anchors.bottomMargin: 3 - radius: 4 color: "transparent" + radius: 4 + CustomText { id: timeText anchors.centerIn: parent - - text: Time.dateStr color: DynamicColors.palette.m3onSurface + text: Time.dateStr Behavior on color { - CAnim {} + CAnim { + } } } StateLayer { acceptedButtons: Qt.LeftButton + onClicked: { root.visibilities.dashboard = !root.visibilities.dashboard; } diff --git a/Modules/Content.qml b/Modules/Content.qml index 3f0b30e..80e16d0 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -4,80 +4,74 @@ import Quickshell import Quickshell.Services.SystemTray import QtQuick import qs.Config +import qs.Components import qs.Modules.Calendar import qs.Modules.WSOverview -import qs.Modules.Polkit -import qs.Modules.Dashboard import qs.Modules.Network import qs.Modules.UPower Item { - id: root + id: root - required property Item wrapper - readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null - readonly property Item current: currentPopout?.item ?? null + readonly property Item current: currentPopout?.item ?? null + readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null + required property Item wrapper - implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 - implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 + implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2 - Item { - id: content + Item { + id: content - anchors.fill: parent + anchors.fill: parent - Popout { - name: "audio" - sourceComponent: AudioPopup { - wrapper: root.wrapper - } - } + Popout { + name: "audio" - Popout { - name: "resources" - sourceComponent: ResourcePopout { - wrapper: root.wrapper - } - } + sourceComponent: AudioPopup { + wrapper: root.wrapper + } + } - Repeater { - model: ScriptModel { - values: [ ...SystemTray.items.values ] - } + Repeater { + model: ScriptModel { + values: [...SystemTray.items.values] + } - Popout { - id: trayMenu + Popout { + id: trayMenu - required property SystemTrayItem modelData - required property int index + required property int index + required property SystemTrayItem modelData - name: `traymenu${index}` - sourceComponent: trayMenuComponent + name: `traymenu${index}` + sourceComponent: trayMenuComponent - Connections { - target: root.wrapper + Connections { + function onHasCurrentChanged(): void { + if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { + trayMenu.sourceComponent = null; + trayMenu.sourceComponent = trayMenuComponent; + } + } - function onHasCurrentChanged(): void { - if ( root.wrapper.hasCurrent && trayMenu.shouldBeActive ) { - trayMenu.sourceComponent = null; - trayMenu.sourceComponent = trayMenuComponent; - } - } - } + target: root.wrapper + } - Component { - id: trayMenuComponent + Component { + id: trayMenuComponent - TrayMenuPopout { - popouts: root.wrapper - trayItem: trayMenu.modelData.menu - } - } - } - } + TrayMenuPopout { + popouts: root.wrapper + trayItem: trayMenu.modelData.menu + } + } + } + } Popout { name: "calendar" + sourceComponent: CalendarPopup { wrapper: root.wrapper } @@ -87,8 +81,8 @@ Item { name: "overview" sourceComponent: OverviewPopout { - wrapper: root.wrapper screen: root.wrapper.screen + wrapper: root.wrapper } } @@ -107,63 +101,63 @@ Item { wrapper: root.wrapper } } - } + } - component Popout: Loader { - id: popout + component Popout: Loader { + id: popout - required property string name - readonly property bool shouldBeActive: root.wrapper.currentName === name + required property string name + readonly property bool shouldBeActive: root.wrapper.currentName === name - anchors.top: parent.top - anchors.topMargin: 5 - anchors.horizontalCenter: parent.horizontalCenter + active: false + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 5 + opacity: 0 + scale: 0.8 - opacity: 0 - scale: 0.8 - active: false + states: State { + name: "active" + when: popout.shouldBeActive - states: State { - name: "active" - when: popout.shouldBeActive + PropertyChanges { + popout.active: true + popout.opacity: 1 + popout.scale: 1 + } + } + transitions: [ + Transition { + from: "active" + to: "" - PropertyChanges { - popout.active: true - popout.opacity: 1 - popout.scale: 1 - } - } + SequentialAnimation { + Anim { + duration: MaterialEasing.expressiveEffectsTime + properties: "opacity,scale" + } - transitions: [ - Transition { - from: "active" - to: "" + PropertyAction { + property: "active" + target: popout + } + } + }, + Transition { + from: "" + to: "active" - SequentialAnimation { - Anim { - properties: "opacity,scale" - duration: MaterialEasing.expressiveEffectsTime - } - PropertyAction { - target: popout - property: "active" - } - } - }, - Transition { - from: "" - to: "active" + SequentialAnimation { + PropertyAction { + property: "active" + target: popout + } - SequentialAnimation { - PropertyAction { - target: popout - property: "active" - } - Anim { - properties: "opacity,scale" - } - } - } - ] - } + Anim { + properties: "opacity,scale" + } + } + } + ] + } } diff --git a/Modules/DashWidget.qml b/Modules/DashWidget.qml index 72b1c93..9131930 100644 --- a/Modules/DashWidget.qml +++ b/Modules/DashWidget.qml @@ -9,12 +9,12 @@ CustomRect { required property PersistentProperties visibilities - anchors.top: parent.top anchors.bottom: parent.bottom - anchors.topMargin: 6 anchors.bottomMargin: 6 - implicitWidth: 40 + anchors.top: parent.top + anchors.topMargin: 6 color: DynamicColors.tPalette.m3surfaceContainer + implicitWidth: 40 radius: 1000 StateLayer { @@ -25,7 +25,7 @@ CustomRect { MaterialIcon { anchors.centerIn: parent - text: "widgets" color: DynamicColors.palette.m3onSurface + text: "widgets" } } diff --git a/Modules/Dashboard/Background.qml b/Modules/Dashboard/Background.qml index 9dd284b..b97793b 100644 --- a/Modules/Dashboard/Background.qml +++ b/Modules/Dashboard/Background.qml @@ -1,67 +1,65 @@ import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes ShapePath { - id: root + id: root - required property Wrapper wrapper - readonly property real rounding: 8 - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + 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 - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } + Behavior on fillColor { + CAnim { + } + } - 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 + } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.roundingY * 2 + } - 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 + } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } + PathLine { + relativeX: root.wrapper.width - root.rounding * 2 + relativeY: 0 + } - 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 + } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } + PathLine { + relativeX: 0 + relativeY: -(root.wrapper.height) + } - Behavior on fillColor { - Modules.CAnim {} - } + PathArc { + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: -root.roundingY + } } diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index 4ff2028..ad1fd3d 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -5,84 +5,82 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts import qs.Config -import qs.Modules +import qs.Components Item { - id: root + id: root - required property PersistentProperties visibilities + readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 + readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 required property PersistentProperties state - readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 - readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 + required property PersistentProperties visibilities - implicitWidth: nonAnimWidth - implicitHeight: nonAnimHeight + implicitHeight: nonAnimHeight + implicitWidth: nonAnimWidth - ClippingRectangle { - id: viewWrapper + Behavior on implicitHeight { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + Behavior on implicitWidth { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + ClippingRectangle { + id: viewWrapper + + anchors.bottom: parent.bottom + anchors.left: parent.left anchors.margins: Appearance.padding.smaller + anchors.right: parent.right + anchors.top: parent.top + color: "transparent" + radius: 6 - radius: 6 - color: "transparent" + Item { + id: view - Item { - id: view + readonly property int currentIndex: root.state.currentTab + readonly property Item currentItem: row.children[currentIndex] - readonly property int currentIndex: root.state.currentTab - readonly property Item currentItem: row.children[currentIndex] + anchors.fill: parent + implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem.implicitWidth - anchors.fill: parent + RowLayout { + id: row - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + Pane { + index: 0 - RowLayout { - id: row - - Pane { - index: 0 - sourceComponent: Dash { - state: root.state + sourceComponent: Dash { + state: root.state visibilities: root.visibilities - } - } - } - } - } + } + } + } + } + } - Behavior on implicitWidth { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + component Pane: Loader { + id: pane - Behavior on implicitHeight { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + required property int index - component Pane: Loader { - id: pane + Layout.alignment: Qt.AlignTop - required property int index - - Layout.alignment: Qt.AlignTop - - Component.onCompleted: active = Qt.binding(() => { - // Always keep current tab loaded - if (pane.index === view.currentIndex) - return true; - const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); - const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); - return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); - }) - } + Component.onCompleted: active = Qt.binding(() => { + // Always keep current tab loaded + if (pane.index === view.currentIndex) + return true; + const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); + const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); + return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); + }) + } } diff --git a/Modules/Dashboard/Dash.qml b/Modules/Dashboard/Dash.qml index 27e1a80..44851e8 100644 --- a/Modules/Dashboard/Dash.qml +++ b/Modules/Dashboard/Dash.qml @@ -3,24 +3,21 @@ import QtQuick import QtQuick.Layouts import qs.Helpers import qs.Components -import qs.Paths import qs.Modules import qs.Config import qs.Modules.Dashboard.Dash GridLayout { - id: root + id: root - required property PersistentProperties visibilities - required property PersistentProperties state readonly property bool dashboardVisible: visibilities.dashboard - property int radius: 6 + required property PersistentProperties state + required property PersistentProperties visibilities - rowSpacing: Appearance.spacing.smaller - columnSpacing: Appearance.spacing.smaller - + columnSpacing: Appearance.spacing.smaller opacity: 0 + rowSpacing: Appearance.spacing.smaller scale: 0.9 onDashboardVisibleChanged: { @@ -33,115 +30,105 @@ GridLayout { ParallelAnimation { id: openAnim + Anim { - target: root property: "opacity" + target: root to: 1 } + Anim { - target: root property: "scale" + target: root to: 1 } } ParallelAnimation { id: closeAnim + Anim { - target: root property: "opacity" + target: root to: 0 } + Anim { - target: root property: "scale" + target: root to: 0.9 } } - Rect { - Layout.column: 2 - Layout.columnSpan: 3 - Layout.preferredWidth: user.implicitWidth - Layout.preferredHeight: user.implicitHeight + Rect { + Layout.column: 2 + Layout.columnSpan: 3 + Layout.preferredHeight: user.implicitHeight + Layout.preferredWidth: user.implicitWidth + radius: root.radius - radius: root.radius + User { + id: user - User { - id: user + state: root.state + } + } - state: root.state - } - } - - Rect { - Layout.row: 0 - Layout.columnSpan: 2 - Layout.preferredWidth: Config.dashboard.sizes.weatherWidth - Layout.fillHeight: true - - radius: root.radius - - Weather {} - } - - // Rect { - // Layout.row: 1 - // Layout.preferredWidth: dateTime.implicitWidth - // Layout.fillHeight: true - // - // radius: root.radius - // - // DateTime { - // id: dateTime - // } - // } - - Rect { - Layout.row: 1 - Layout.column: 0 - Layout.columnSpan: 3 - Layout.fillWidth: true - Layout.preferredHeight: calendar.implicitHeight - - radius: root.radius - - Calendar { - id: calendar - - state: root.state - } - } - - Rect { - Layout.row: 1 - Layout.column: 3 + Rect { Layout.columnSpan: 2 - Layout.preferredWidth: resources.implicitWidth - Layout.fillHeight: true + Layout.fillHeight: true + Layout.preferredWidth: Config.dashboard.sizes.weatherWidth + Layout.row: 0 + radius: root.radius - radius: root.radius + Weather { + } + } - Resources { - id: resources - } - } + Rect { + Layout.column: 0 + Layout.columnSpan: 3 + Layout.fillWidth: true + Layout.preferredHeight: calendar.implicitHeight + Layout.row: 1 + radius: root.radius - Rect { - Layout.row: 0 - Layout.column: 5 - Layout.rowSpan: 2 - Layout.preferredWidth: media.implicitWidth - Layout.fillHeight: true + Calendar { + id: calendar - radius: root.radius + state: root.state + } + } - Media { - id: media - } - } + Rect { + Layout.column: 3 + Layout.columnSpan: 2 + Layout.fillHeight: true + Layout.preferredWidth: resources.implicitWidth + Layout.row: 1 + radius: root.radius - component Rect: CustomRect { - color: DynamicColors.tPalette.m3surfaceContainer - } + Resources { + id: resources + + } + } + + Rect { + Layout.column: 0 + Layout.columnSpan: 5 + Layout.fillWidth: true + Layout.preferredHeight: media.implicitHeight + Layout.row: 2 + radius: root.radius + + Media { + id: media + + } + } + + component Rect: CustomRect { + color: DynamicColors.tPalette.m3surfaceContainer + } } diff --git a/Modules/Dashboard/Dash/Calendar.qml b/Modules/Dashboard/Dash/Calendar.qml index 277278f..dcb9c3c 100644 --- a/Modules/Dashboard/Dash/Calendar.qml +++ b/Modules/Dashboard/Dash/Calendar.qml @@ -9,244 +9,232 @@ import qs.Config import qs.Modules CustomMouseArea { - id: root + id: root - required property var state + readonly property int currMonth: state.currentDate.getMonth() + readonly property int currYear: state.currentDate.getFullYear() + required property var state - readonly property int currMonth: state.currentDate.getMonth() - readonly property int currYear: state.currentDate.getFullYear() + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y > 0) + root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + else if (event.angleDelta.y < 0) + root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 + acceptedButtons: Qt.MiddleButton + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 - acceptedButtons: Qt.MiddleButton - onClicked: root.state.currentDate = new Date() + onClicked: root.state.currentDate = new Date() - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } + ColumnLayout { + id: inner - ColumnLayout { - id: inner + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small - anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + RowLayout { + id: monthNavigationRow - RowLayout { - id: monthNavigationRow + Layout.fillWidth: true + spacing: Appearance.spacing.small - Layout.fillWidth: true - spacing: Appearance.spacing.small + Item { + implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight - Item { - implicitWidth: implicitHeight - implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 + StateLayer { + id: prevMonthStateLayer - StateLayer { - id: prevMonthStateLayer + function onClicked(): void { + root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + } - radius: Appearance.rounding.full + radius: Appearance.rounding.full + } - function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - } - } + MaterialIcon { + id: prevMonthText - MaterialIcon { - id: prevMonthText + anchors.centerIn: parent + color: DynamicColors.palette.m3tertiary + font.pointSize: Appearance.font.size.normal + font.weight: 700 + text: "chevron_left" + } + } - anchors.centerIn: parent - text: "chevron_left" - color: DynamicColors.palette.m3tertiary - font.pointSize: Appearance.font.size.normal - font.weight: 700 - } - } + Item { + Layout.fillWidth: true + implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 + implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 - Item { - Layout.fillWidth: true + StateLayer { + function onClicked(): void { + root.state.currentDate = new Date(); + } - implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 - implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 + anchors.fill: monthYearDisplay + anchors.leftMargin: -Appearance.padding.normal + anchors.margins: -Appearance.padding.small + anchors.rightMargin: -Appearance.padding.normal + disabled: { + const now = new Date(); + return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); + } + radius: Appearance.rounding.full + } - StateLayer { - anchors.fill: monthYearDisplay - anchors.margins: -Appearance.padding.small - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal + CustomText { + id: monthYearDisplay - radius: Appearance.rounding.full - disabled: { - const now = new Date(); - return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); - } + anchors.centerIn: parent + color: DynamicColors.palette.m3primary + font.capitalization: Font.Capitalize + font.pointSize: Appearance.font.size.normal + font.weight: 500 + text: grid.title + } + } - function onClicked(): void { - root.state.currentDate = new Date(); - } - } + Item { + implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight - CustomText { - id: monthYearDisplay + StateLayer { + id: nextMonthStateLayer - anchors.centerIn: parent - text: grid.title - color: DynamicColors.palette.m3primary - font.pointSize: Appearance.font.size.normal - font.weight: 500 - font.capitalization: Font.Capitalize - } - } + function onClicked(): void { + root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } - Item { - implicitWidth: implicitHeight - implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + } - StateLayer { - id: nextMonthStateLayer + MaterialIcon { + id: nextMonthText - radius: Appearance.rounding.full + anchors.centerIn: parent + color: DynamicColors.palette.m3tertiary + font.pointSize: Appearance.font.size.normal + font.weight: 700 + text: "chevron_right" + } + } + } - function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - } + DayOfWeekRow { + id: daysRow - MaterialIcon { - id: nextMonthText + Layout.fillWidth: true + locale: grid.locale - anchors.centerIn: parent - text: "chevron_right" - color: DynamicColors.palette.m3tertiary - font.pointSize: Appearance.font.size.normal - font.weight: 700 - } - } - } + delegate: CustomText { + required property var model - DayOfWeekRow { - id: daysRow + color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant + font.weight: 500 + horizontalAlignment: Text.AlignHCenter + text: model.shortName + } + } - Layout.fillWidth: true - locale: grid.locale + Item { + Layout.fillWidth: true + implicitHeight: grid.implicitHeight - delegate: CustomText { - required property var model + MonthGrid { + id: grid - horizontalAlignment: Text.AlignHCenter - text: model.shortName - font.weight: 500 - color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant - } - } + anchors.fill: parent + locale: Qt.locale("en_SE") + month: root.currMonth + spacing: 3 + year: root.currYear - Item { - Layout.fillWidth: true - implicitHeight: grid.implicitHeight + delegate: Item { + id: dayItem - MonthGrid { - id: grid + required property var model - month: root.currMonth - year: root.currYear + implicitHeight: text.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight - anchors.fill: parent + CustomText { + id: text - spacing: 3 - locale: Qt.locale("en_SE") + anchors.centerIn: parent + color: { + const dayOfWeek = dayItem.model.date.getUTCDay(); + if (dayOfWeek === 6) + return DynamicColors.palette.m3secondary; - delegate: Item { - id: dayItem + return DynamicColors.palette.m3onSurfaceVariant; + } + font.pointSize: Appearance.font.size.normal + font.weight: 500 + horizontalAlignment: Text.AlignHCenter + opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 + text: grid.locale.toString(dayItem.model.day) + } + } + } - required property var model + CustomRect { + id: todayIndicator - implicitWidth: implicitHeight - implicitHeight: text.implicitHeight + Appearance.padding.small * 2 + property Item today + readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null - CustomText { - id: text + clip: true + color: DynamicColors.palette.m3primary + implicitHeight: today?.implicitHeight ?? 0 + implicitWidth: today?.implicitWidth ?? 0 + opacity: todayItem ? 1 : 0 + radius: Appearance.rounding.full + scale: todayItem ? 1 : 0.7 + x: today ? today.x + (today.width - implicitWidth) / 2 : 0 + y: today?.y ?? 0 - anchors.centerIn: parent + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } - horizontalAlignment: Text.AlignHCenter - text: grid.locale.toString(dayItem.model.day) - color: { - const dayOfWeek = dayItem.model.date.getUTCDay(); - if (dayOfWeek === 6) - return DynamicColors.palette.m3secondary; + onTodayItemChanged: { + if (todayItem) + today = todayItem; + } - return DynamicColors.palette.m3onSurfaceVariant; - } - opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - font.pointSize: Appearance.font.size.normal - font.weight: 500 - } - } - } - - CustomRect { - id: todayIndicator - - readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null - property Item today - - onTodayItemChanged: { - if (todayItem) - today = todayItem; - } - - x: today ? today.x + (today.width - implicitWidth) / 2 : 0 - y: today?.y ?? 0 - - implicitWidth: today?.implicitWidth ?? 0 - implicitHeight: today?.implicitHeight ?? 0 - - clip: true - radius: Appearance.rounding.full - color: DynamicColors.palette.m3primary - - opacity: todayItem ? 1 : 0 - scale: todayItem ? 1 : 0.7 - - Coloriser { - x: -todayIndicator.x - y: -todayIndicator.y - - implicitWidth: grid.width - implicitHeight: grid.height - - source: grid - sourceColor: DynamicColors.palette.m3onSurface - colorizationColor: DynamicColors.palette.m3onPrimary - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } - } + Coloriser { + colorizationColor: DynamicColors.palette.m3onPrimary + implicitHeight: grid.height + implicitWidth: grid.width + source: grid + sourceColor: DynamicColors.palette.m3onSurface + x: -todayIndicator.x + y: -todayIndicator.y + } + } + } + } } diff --git a/Modules/Dashboard/Dash/DateTime.qml b/Modules/Dashboard/Dash/DateTime.qml index 5efa85b..3f95bd9 100644 --- a/Modules/Dashboard/Dash/DateTime.qml +++ b/Modules/Dashboard/Dash/DateTime.qml @@ -7,44 +7,44 @@ import qs.Config import qs.Helpers Item { - id: root + id: root - anchors.top: parent.top - anchors.bottom: parent.bottom - implicitWidth: 110 + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: 110 - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 0 + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 - CustomText { - Layout.bottomMargin: -(font.pointSize * 0.4) - Layout.alignment: Qt.AlignHCenter - text: Time.hourStr - color: DynamicColors.palette.m3secondary - font.pointSize: 18 - font.family: "Rubik" - font.weight: 600 - } + CustomText { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: -(font.pointSize * 0.4) + color: DynamicColors.palette.m3secondary + font.family: "Rubik" + font.pointSize: 18 + font.weight: 600 + text: Time.hourStr + } - CustomText { - Layout.alignment: Qt.AlignHCenter - text: "•••" - color: DynamicColors.palette.m3primary - font.pointSize: 18 * 0.9 - font.family: "Rubik" - } + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3primary + font.family: "Rubik" + font.pointSize: 18 * 0.9 + text: "•••" + } - CustomText { - Layout.topMargin: -(font.pointSize * 0.4) - Layout.alignment: Qt.AlignHCenter - text: Time.minuteStr - color: DynamicColors.palette.m3secondary - font.pointSize: 18 - font.family: "Rubik" - font.weight: 600 - } - } + CustomText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: -(font.pointSize * 0.4) + color: DynamicColors.palette.m3secondary + font.family: "Rubik" + font.pointSize: 18 + font.weight: 600 + text: Time.minuteStr + } + } } diff --git a/Modules/Dashboard/Dash/Media.qml b/Modules/Dashboard/Dash/Media.qml index 3928d71..4b47b81 100644 --- a/Modules/Dashboard/Dash/Media.qml +++ b/Modules/Dashboard/Dash/Media.qml @@ -1,238 +1,319 @@ -import ZShell.Services +pragma ComponentBehavior: Bound + +import Quickshell import QtQuick +import QtQuick.Layouts import QtQuick.Shapes +import ZShell.Services import qs.Daemons import qs.Components import qs.Config import qs.Helpers -import qs.Modules -import qs.Paths Item { - id: root + id: root - property real playerProgress: { - const active = Players.active; - return active?.length ? active.position / active.length : 0; - } + property real playerProgress: { + const active = Players.active; + return active?.length ? active.position / active.length : 0; + } + property int rowHeight: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small - anchors.top: parent.top - anchors.bottom: parent.bottom - implicitWidth: Config.dashboard.sizes.mediaWidth + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: cover.height + rowHeight * 2 - Behavior on playerProgress { - Anim { - duration: Appearance.anim.durations.large - } - } + Behavior on playerProgress { + Anim { + duration: Appearance.anim.durations.large + } + } - Timer { - running: Players.active?.isPlaying ?? false - interval: Config.dashboard.mediaUpdateInterval - triggeredOnStart: true - repeat: true - onTriggered: Players.active?.positionChanged() - } + Timer { + interval: Config.dashboard.mediaUpdateInterval + repeat: true + running: Players.active?.isPlaying ?? false + triggeredOnStart: true - ServiceRef { - service: Audio.beatTracker - } + onTriggered: Players.active?.positionChanged() + } - Shape { - preferredRendererType: Shape.CurveRenderer + ServiceRef { + service: Audio.cava + } - ShapePath { - fillColor: "transparent" - strokeColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + Shape { + id: visualizer - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep - } + 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 - Behavior on strokeColor { - CAnim {} - } - } + anchors.fill: layout + asynchronous: true + data: visualizerBars.instances + preferredRendererType: Shape.CurveRenderer + } - ShapePath { - fillColor: "transparent" - strokeColor: DynamicColors.palette.m3primary - strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + Variants { + id: visualizerBars - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress - } + model: Array.from({ + length: Config.services.visualizerBars + }, (_, i) => i) - Behavior on strokeColor { - CAnim {} - } - } - } + ShapePath { + id: visualizerBar - CustomClippingRect { - id: cover + 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]) - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + 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 - implicitHeight: width - color: DynamicColors.tPalette.m3surfaceContainerHigh - radius: Infinity + Behavior on strokeColor { + CAnim { + } + } - MaterialIcon { - anchors.centerIn: parent + PathLine { + relativeX: 0 + relativeY: -visualizerBar.magnitude + } + } + } - grade: 200 - text: "art_track" - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: (parent.width * 0.4) || 1 - } + Shape { + preferredRendererType: Shape.CurveRenderer - Image { - id: image + ShapePath { + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + fillColor: "transparent" + strokeColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + strokeWidth: Config.dashboard.sizes.mediaProgressThickness - anchors.fill: parent + Behavior on strokeColor { + CAnim { + } + } - source: Players.active?.trackArtUrl ?? "" - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height - } - } + PathAngleArc { + centerX: cover.x + cover.width / 2 + centerY: cover.y + cover.height / 2 + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 + sweepAngle: Config.dashboard.sizes.mediaProgressSweep + } + } - CustomText { - id: title + ShapePath { + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + fillColor: "transparent" + strokeColor: DynamicColors.palette.m3primary + strokeWidth: Config.dashboard.sizes.mediaProgressThickness - anchors.top: cover.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.normal + Behavior on strokeColor { + CAnim { + } + } - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") - color: DynamicColors.palette.m3primary - font.pointSize: Appearance.font.size.normal + PathAngleArc { + centerX: cover.x + cover.width / 2 + centerY: cover.y + cover.height / 2 + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 + sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress + } + } + } - width: parent.implicitWidth - Appearance.padding.large * 2 - elide: Text.ElideRight - } + RowLayout { + id: layout - CustomText { - id: album + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: root.implicitHeight - anchors.top: title.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + CustomClippingRect { + id: cover - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") - color: DynamicColors.palette.m3outline - font.pointSize: Appearance.font.size.small + Layout.alignment: Qt.AlignLeft + Layout.bottomMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + Layout.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + Layout.preferredHeight: Config.dashboard.sizes.mediaCoverArtSize + Layout.preferredWidth: Config.dashboard.sizes.mediaCoverArtSize + Layout.topMargin: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Infinity - width: parent.implicitWidth - Appearance.padding.large * 2 - elide: Text.ElideRight - } + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: (parent.width * 0.4) || 1 + grade: 200 + text: "art_track" + } - CustomText { - id: artist + Image { + id: image - anchors.top: album.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + source: Players.active?.trackArtUrl ?? "" + sourceSize.height: Math.floor(height) + sourceSize.width: Math.floor(width) + } + } - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") - color: DynamicColors.palette.m3secondary + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height - width: parent.implicitWidth - Appearance.padding.large * 2 - elide: Text.ElideRight - } + MarqueeText { + id: title - Row { - id: controls + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: DynamicColors.palette.m3primary + font.pointSize: Appearance.font.size.normal + horizontalAlignment: Text.AlignHCenter + pauseMs: 4000 + text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") + width: parent.width - Appearance.padding.large * 4 + } - anchors.top: artist.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.smaller + MarqueeText { + id: album - spacing: Appearance.spacing.small + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: title.bottom + anchors.topMargin: Appearance.spacing.small + color: DynamicColors.palette.m3outline + font.pointSize: Appearance.font.size.small + horizontalAlignment: Text.AlignHCenter + pauseMs: 4000 + text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") + width: parent.width - Appearance.padding.large * 4 + } - Control { - icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false + MarqueeText { + id: artist - function onClicked(): void { - Players.active?.previous(); - } - } + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: album.bottom + anchors.topMargin: Appearance.spacing.small + color: DynamicColors.palette.m3secondary + horizontalAlignment: Text.AlignHCenter + pauseMs: 4000 + text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") + width: parent.width - Appearance.padding.large * 4 + } - Control { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false + RowLayout { + id: controls - function onClicked(): void { - Players.active?.togglePlaying(); - } - } + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: artist.bottom + anchors.topMargin: Appearance.spacing.smaller + spacing: Appearance.spacing.small - Control { - icon: "skip_next" - canUse: Players.active?.canGoNext ?? false + Control { + function onClicked(): void { + Players.active?.previous(); + } - function onClicked(): void { - Players.active?.next(); - } - } - } + canUse: Players.active?.canGoPrevious ?? false + icon: "skip_previous" + } - component Control: CustomRect { - id: control + Control { + function onClicked(): void { + Players.active?.togglePlaying(); + } - required property string icon - required property bool canUse - function onClicked(): void { - } + canUse: Players.active?.canTogglePlaying ?? false + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + } - implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small - implicitHeight: implicitWidth + Control { + function onClicked(): void { + Players.active?.next(); + } - StateLayer { - disabled: !control.canUse - radius: Appearance.rounding.full + canUse: Players.active?.canGoNext ?? false + icon: "skip_next" + } + } + } + } - function onClicked(): void { - control.onClicked(); - } - } + component Control: CustomRect { + id: control - MaterialIcon { - id: icon + required property bool canUse + required property string icon + property int level: 1 + property string set_color: "Secondary" - anchors.centerIn: parent - anchors.verticalCenterOffset: font.pointSize * 0.05 + function onClicked(): void { + } - animate: true - text: control.icon - color: control.canUse ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - font.pointSize: Appearance.font.size.large - } - } + 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 + } + + MaterialIcon { + id: icon + + anchors.centerIn: parent + anchors.verticalCenterOffset: font.pointSize * 0.05 + animate: true + 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/Resources.qml b/Modules/Dashboard/Dash/Resources.qml index acaf12a..cc34a52 100644 --- a/Modules/Dashboard/Dash/Resources.qml +++ b/Modules/Dashboard/Dash/Resources.qml @@ -2,98 +2,92 @@ import QtQuick import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules Row { - id: root + id: root - anchors.top: parent.top anchors.bottom: parent.bottom + anchors.top: parent.top + padding: Appearance.padding.large + spacing: Appearance.spacing.large - padding: Appearance.padding.large - spacing: Appearance.spacing.large + Ref { + service: SystemUsage + } - Ref { - service: SystemUsage - } + Resource { + color: DynamicColors.palette.m3primary + icon: "memory" + value: SystemUsage.cpuPerc + } - Resource { - icon: "memory" - value: SystemUsage.cpuPerc - color: DynamicColors.palette.m3primary - } + Resource { + color: DynamicColors.palette.m3secondary + icon: "memory_alt" + value: SystemUsage.memPerc + } - Resource { - icon: "memory_alt" - value: SystemUsage.memPerc - color: DynamicColors.palette.m3secondary - } + Resource { + color: DynamicColors.palette.m3tertiary + icon: "gamepad" + value: SystemUsage.gpuPerc + } - Resource { - icon: "gamepad" - value: SystemUsage.gpuPerc - color: DynamicColors.palette.m3tertiary - } + Resource { + color: DynamicColors.palette.m3primary + icon: "host" + value: SystemUsage.gpuMemUsed + } - Resource { - icon: "host" - value: SystemUsage.gpuMemUsed - color: DynamicColors.palette.m3primary - } + Resource { + color: DynamicColors.palette.m3secondary + icon: "hard_disk" + value: SystemUsage.storagePerc + } - Resource { - icon: "hard_disk" - value: SystemUsage.storagePerc - color: DynamicColors.palette.m3secondary - } + component Resource: Item { + id: res - component Resource: Item { - id: res + required property color color + required property string icon + required property real value - required property string icon - required property real value - required property color color + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.large + anchors.top: parent.top + implicitWidth: icon.implicitWidth - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.large - implicitWidth: icon.implicitWidth + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } - CustomRect { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: icon.top - anchors.bottomMargin: Appearance.spacing.small + CustomRect { + anchors.bottom: icon.top + anchors.bottomMargin: Appearance.spacing.small + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitWidth: Config.dashboard.sizes.resourceProgessThickness + radius: Appearance.rounding.full - implicitWidth: Config.dashboard.sizes.resourceProgessThickness + CustomRect { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + color: res.color + implicitHeight: res.value * parent.height + radius: Appearance.rounding.full + } + } - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + MaterialIcon { + id: icon - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - implicitHeight: res.value * parent.height - - color: res.color - radius: Appearance.rounding.full - } - } - - MaterialIcon { - id: icon - - anchors.bottom: parent.bottom - - text: res.icon - color: res.color - } - - Behavior on value { - Modules.Anim { - duration: Appearance.anim.durations.large - } - } - } + anchors.bottom: parent.bottom + color: res.color + text: res.icon + } + } } diff --git a/Modules/Dashboard/Dash/User.qml b/Modules/Dashboard/Dash/User.qml index 4295da0..45e8ad8 100644 --- a/Modules/Dashboard/Dash/User.qml +++ b/Modules/Dashboard/Dash/User.qml @@ -7,122 +7,116 @@ import Quickshell import QtQuick Row { - id: root + id: root - required property PersistentProperties state + required property PersistentProperties state - padding: 20 - spacing: 12 + padding: 20 + spacing: 12 - CustomClippingRect { - implicitWidth: info.implicitHeight - implicitHeight: info.implicitHeight + CustomClippingRect { + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: info.implicitHeight + implicitWidth: info.implicitHeight + radius: 8 - radius: 8 - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + MaterialIcon { + anchors.centerIn: parent + fill: 1 + font.pointSize: Math.floor(info.implicitHeight / 2) || 1 + grade: 200 + text: "person" + } - MaterialIcon { - anchors.centerIn: parent + CachingImage { + id: pfp - text: "person" - fill: 1 - grade: 200 - font.pointSize: Math.floor(info.implicitHeight / 2) || 1 - } + anchors.fill: parent + path: `${Paths.home}/.face` + } + } - CachingImage { - id: pfp + Column { + id: info - anchors.fill: parent - path: `${Paths.home}/.face` - } - } + anchors.verticalCenter: parent.verticalCenter + spacing: 12 - Column { - id: info + Item { + id: line - anchors.verticalCenter: parent.verticalCenter - spacing: 12 + implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) + implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - Item { - id: line + ColoredIcon { + id: icon - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) + anchors.left: parent.left + anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + color: DynamicColors.palette.m3primary + implicitSize: Math.floor(13 * 1.34) + source: SystemInfo.osLogo + } - ColoredIcon { - id: icon + CustomText { + id: text - anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.left: icon.right + anchors.leftMargin: icon.anchors.leftMargin + anchors.verticalCenter: icon.verticalCenter + elide: Text.ElideRight + font.pointSize: 13 + text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}` + width: Config.dashboard.sizes.infoWidth + } + } - source: SystemInfo.osLogo - implicitSize: Math.floor(13 * 1.34) - color: DynamicColors.palette.m3primary - } + InfoLine { + colour: DynamicColors.palette.m3secondary + icon: "select_window_2" + text: SystemInfo.wm + } - CustomText { - id: text + InfoLine { + id: uptime - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}` - font.pointSize: 13 + colour: DynamicColors.palette.m3tertiary + icon: "timer" + text: qsTr("%1").arg(SystemInfo.uptime) + } + } - width: Config.dashboard.sizes.infoWidth - elide: Text.ElideRight - } - } + component InfoLine: Item { + id: line - InfoLine { - icon: "select_window_2" - text: SystemInfo.wm - colour: DynamicColors.palette.m3secondary - } + required property color colour + required property string icon + required property string text - InfoLine { - id: uptime + implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) + implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - icon: "timer" - text: qsTr("up %1").arg(SystemInfo.uptime) - colour: DynamicColors.palette.m3tertiary - } - } + MaterialIcon { + id: icon - component InfoLine: Item { - id: line + anchors.left: parent.left + anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + color: line.colour + fill: 1 + font.pointSize: 13 + text: line.icon + } - required property string icon - required property string text - required property color colour + CustomText { + id: text - implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin - implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) - - MaterialIcon { - id: icon - - anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 - - fill: 1 - text: line.icon - color: line.colour - font.pointSize: 13 - } - - CustomText { - id: text - - anchors.verticalCenter: icon.verticalCenter - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${line.text}` - font.pointSize: 13 - - width: Config.dashboard.sizes.infoWidth - elide: Text.ElideRight - } - } + anchors.left: icon.right + anchors.leftMargin: icon.anchors.leftMargin + anchors.verticalCenter: icon.verticalCenter + elide: Text.ElideNone + font.pointSize: 13 + text: `: ${line.text}` + width: Config.dashboard.sizes.infoWidth + } + } } diff --git a/Modules/Dashboard/Dash/Weather.qml b/Modules/Dashboard/Dash/Weather.qml index ef8cb76..4592aeb 100644 --- a/Modules/Dashboard/Dash/Weather.qml +++ b/Modules/Dashboard/Dash/Weather.qml @@ -4,53 +4,47 @@ import qs.Components import qs.Config Item { - id: root + id: root - anchors.centerIn: parent + anchors.centerIn: parent + implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin - implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + Component.onCompleted: Weather.reload() - Component.onCompleted: Weather.reload() + MaterialIcon { + id: icon - MaterialIcon { - id: icon + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + animate: true + color: DynamicColors.palette.m3secondary + font.pointSize: 54 + text: Weather.icon + } - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left + Column { + id: info - animate: true - text: Weather.icon - color: DynamicColors.palette.m3secondary - font.pointSize: 54 - } - - Column { - id: info - - anchors.verticalCenter: parent.verticalCenter - anchors.left: icon.right + anchors.left: icon.right anchors.leftMargin: Appearance.spacing.large + anchors.verticalCenter: parent.verticalCenter + spacing: 8 - spacing: 8 + CustomText { + anchors.horizontalCenter: parent.horizontalCenter + animate: true + color: DynamicColors.palette.m3primary + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + text: Weather.temp + } - CustomText { - anchors.horizontalCenter: parent.horizontalCenter - - animate: true - text: Weather.temp - color: DynamicColors.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } - - CustomText { - anchors.horizontalCenter: parent.horizontalCenter - - animate: true - text: Weather.description - - elide: Text.ElideRight - width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - 24 * 2) - } - } + CustomText { + anchors.horizontalCenter: parent.horizontalCenter + animate: true + elide: Text.ElideRight + text: Weather.description + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - 24 * 2) + } + } } diff --git a/Modules/Dashboard/Tabs.qml b/Modules/Dashboard/Tabs.qml index 83246f4..0e05337 100644 --- a/Modules/Dashboard/Tabs.qml +++ b/Modules/Dashboard/Tabs.qml @@ -10,237 +10,233 @@ import QtQuick import QtQuick.Controls Item { - id: root + id: root - required property real nonAnimWidth - required property PersistentProperties state - readonly property alias count: bar.count + readonly property alias count: bar.count + required property real nonAnimWidth + required property PersistentProperties state - implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight + implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight - TabBar { - id: bar + TabBar { + id: bar - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + background: null + currentIndex: root.state.currentTab - currentIndex: root.state.currentTab - background: null + onCurrentIndexChanged: root.state.currentTab = currentIndex - onCurrentIndexChanged: root.state.currentTab = currentIndex + Tab { + iconName: "dashboard" + text: qsTr("Dashboard") + } - Tab { - iconName: "dashboard" - text: qsTr("Dashboard") - } + Tab { + iconName: "queue_music" + text: qsTr("Media") + } - Tab { - iconName: "queue_music" - text: qsTr("Media") - } + Tab { + iconName: "speed" + text: qsTr("Performance") + } - Tab { - iconName: "speed" - text: qsTr("Performance") - } + Tab { + iconName: "cloud" + text: qsTr("Weather") + } - Tab { - iconName: "cloud" - text: qsTr("Weather") - } + // Tab { + // iconName: "workspaces" + // text: qsTr("Workspaces") + // } + } - // Tab { - // iconName: "workspaces" - // text: qsTr("Workspaces") - // } - } + Item { + id: indicator - Item { - id: indicator + anchors.top: bar.bottom + clip: true + implicitHeight: 40 + implicitWidth: bar.currentItem.implicitWidth + x: { + const tab = bar.currentItem; + const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; + return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; + } - anchors.top: bar.bottom + Behavior on implicitWidth { + Anim { + } + } + Behavior on x { + Anim { + } + } - implicitWidth: bar.currentItem.implicitWidth - implicitHeight: 40 + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + color: DynamicColors.palette.m3primary + implicitHeight: parent.implicitHeight * 2 + radius: 1000 + } + } - x: { - const tab = bar.currentItem; - const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; - return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; - } + CustomRect { + id: separator - clip: true + anchors.left: parent.left + anchors.right: parent.right + anchors.top: indicator.bottom + color: DynamicColors.palette.m3outlineVariant + implicitHeight: 1 + } - CustomRect { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: parent.implicitHeight * 2 + component Tab: TabButton { + id: tab - color: DynamicColors.palette.m3primary - radius: 1000 - } + readonly property bool current: TabBar.tabBar.currentItem === this + required property string iconName - Behavior on x { - Anim {} - } + background: null - Behavior on implicitWidth { - Anim {} - } - } + contentItem: CustomMouseArea { + id: mouse - CustomRect { - id: separator + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); + else if (event.angleDelta.y > 0) + root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + } - anchors.top: indicator.bottom - anchors.left: parent.left - anchors.right: parent.right + cursorShape: Qt.PointingHandCursor + implicitHeight: icon.height + label.height + implicitWidth: Math.max(icon.width, label.width) - implicitHeight: 1 - color: DynamicColors.palette.m3outlineVariant - } + onPressed: event => { + root.state.currentTab = tab.TabBar.index; - component Tab: TabButton { - id: tab + const stateY = stateWrapper.y; + rippleAnim.x = event.x; + rippleAnim.y = event.y - stateY; - required property string iconName - readonly property bool current: TabBar.tabBar.currentItem === this + const dist = (ox, oy) => ox * ox + oy * oy; + rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y))); - background: null + rippleAnim.restart(); + } - contentItem: CustomMouseArea { - id: mouse + SequentialAnimation { + id: rippleAnim - implicitWidth: Math.max(icon.width, label.width) - implicitHeight: icon.height + label.height + property real radius + property real x + property real y - cursorShape: Qt.PointingHandCursor + PropertyAction { + property: "x" + target: ripple + value: rippleAnim.x + } - onPressed: event => { - root.state.currentTab = tab.TabBar.index; + PropertyAction { + property: "y" + target: ripple + value: rippleAnim.y + } - const stateY = stateWrapper.y; - rippleAnim.x = event.x; - rippleAnim.y = event.y - stateY; + PropertyAction { + property: "opacity" + target: ripple + value: 0.08 + } - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y + stateY), dist(event.x, stateWrapper.height - event.y), dist(width - event.x, event.y + stateY), dist(width - event.x, stateWrapper.height - event.y))); - - rippleAnim.restart(); - } - - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); - else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); - } - - SequentialAnimation { - id: rippleAnim - - property real x - property real y - property real radius - - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 + Anim { duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - Anim { - target: ripple - property: "opacity" - to: 0 - easing.type: Easing.BezierSpline + easing.bezierCurve: MaterialEasing.expressiveEffects + from: 0 + properties: "implicitWidth,implicitHeight" + target: ripple + to: rippleAnim.radius * 2 + } + + Anim { duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + easing.bezierCurve: MaterialEasing.expressiveEffects + easing.type: Easing.BezierSpline + property: "opacity" + target: ripple + to: 0 + } + } - ClippingRectangle { - id: stateWrapper + ClippingRectangle { + id: stateWrapper - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: parent.height + 8 * 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: "transparent" + implicitHeight: parent.height + 8 * 2 + radius: 8 - color: "transparent" - radius: 8 + CustomRect { + id: stateLayer - CustomRect { - id: stateLayer + anchors.fill: parent + color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 - anchors.fill: parent + Behavior on opacity { + Anim { + } + } + } - color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 + CustomRect { + id: ripple - Behavior on opacity { - Anim {} - } - } + color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface + opacity: 0 + radius: 1000 - CustomRect { - id: ripple + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } - radius: 1000 - color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - opacity: 0 + MaterialIcon { + id: icon - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 - } - } - } + anchors.bottom: label.top + anchors.horizontalCenter: parent.horizontalCenter + color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + fill: tab.current ? 1 : 0 + font.pointSize: 18 + text: tab.iconName - MaterialIcon { - id: icon + Behavior on fill { + Anim { + } + } + } - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: label.top + CustomText { + id: label - text: tab.iconName - color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - fill: tab.current ? 1 : 0 - font.pointSize: 18 - - Behavior on fill { - Anim {} - } - } - - CustomText { - id: label - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - text: tab.text - color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - } - } - } + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + color: tab.current ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + text: tab.text + } + } + } } diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 123972f..930b8a6 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -1,93 +1,88 @@ pragma ComponentBehavior: Bound -import ZShell import Quickshell import QtQuick import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules Item { - id: root + id: root - required property PersistentProperties visibilities - readonly property PersistentProperties dashState: PersistentProperties { - property int currentTab - property date currentDate: new Date() + readonly property PersistentProperties dashState: PersistentProperties { + property date currentDate: new Date() + property int currentTab - reloadableId: "dashboardState" - } + reloadableId: "dashboardState" + } + readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 + required property PersistentProperties visibilities - readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 + implicitHeight: 0 + implicitWidth: content.implicitWidth + visible: height > 0 - visible: height > 0 - implicitHeight: 0 - implicitWidth: content.implicitWidth + states: State { + name: "visible" + when: root.visibilities.dashboard && Config.dashboard.enabled - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } + PropertyChanges { + root.implicitHeight: content.implicitHeight + } + } + transitions: [ + Transition { + from: "" + to: "visible" - states: State { - name: "visible" - when: root.visibilities.dashboard && Config.dashboard.enabled + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: root + } + }, + Transition { + from: "visible" + to: "" - PropertyChanges { - root.implicitHeight: content.implicitHeight - } - } + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: root + } + } + ] - transitions: [ - Transition { - from: "" - to: "visible" + onStateChanged: { + if (state === "visible" && timer.running) { + timer.triggered(); + timer.stop(); + } + } - Modules.Anim { - target: root - property: "implicitHeight" - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - }, - Transition { - from: "visible" - to: "" + Timer { + id: timer - Modules.Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - ] + interval: Appearance.anim.durations.extraLarge + running: true - Timer { - id: timer + onTriggered: { + content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); + content.visible = true; + } + } - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); - content.visible = true; - } - } + Loader { + id: content - Loader { - id: content + active: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + visible: false - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - - visible: false - active: true - - sourceComponent: Content { - visibilities: root.visibilities - state: root.dashState - } - } + sourceComponent: Content { + state: root.dashState + visibilities: root.visibilities + } + } } diff --git a/Modules/Dock/Background.qml b/Modules/Dock/Background.qml new file mode 100644 index 0000000..37f8c08 --- /dev/null +++ b/Modules/Dock/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: 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 { + 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 + } + + PathLine { + relativeX: root.wrapper.width - root.rounding * 2 + relativeY: 0 + } + + 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 - root.roundingY * 2 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: root.roundingY + } +} diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml new file mode 100644 index 0000000..6089700 --- /dev/null +++ b/Modules/Dock/Content.qml @@ -0,0 +1,28 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Components +import qs.Helpers +import qs.Config + +Item { + id: root + + readonly property int padding: Appearance.padding.small + required property var panels + readonly property int rounding: Appearance.rounding.large + required property PersistentProperties visibilities + + implicitHeight: Config.dock.height + root.padding * 2 + implicitWidth: dockRow.implicitWidth + root.padding * 2 + + RowLayout { + id: dockRow + + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: Appearance.spacing.small + } +} diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml new file mode 100644 index 0000000..5d7e36d --- /dev/null +++ b/Modules/Dock/Wrapper.qml @@ -0,0 +1,100 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Components +import qs.Config + +Item { + id: root + + property int contentHeight + required property var panels + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.dock + required property PersistentProperties visibilities + + implicitHeight: 0 + implicitWidth: content.implicitWidth + visible: height > 0 + + onShouldBeActiveChanged: { + if (shouldBeActive) { + timer.stop(); + hideAnim.stop(); + showAnim.start(); + } else { + showAnim.stop(); + hideAnim.start(); + } + } + + SequentialAnimation { + id: showAnim + + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + property: "implicitHeight" + target: root + to: root.contentHeight + } + + ScriptAction { + script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + } + } + + SequentialAnimation { + id: hideAnim + + ScriptAction { + script: root.implicitHeight = root.implicitHeight + } + + Anim { + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + property: "implicitHeight" + target: root + to: 0 + } + } + + Timer { + id: timer + + interval: Appearance.anim.durations.small + + onRunningChanged: { + if (running && !root.shouldBeActive) { + content.visible = false; + content.active = true; + } else { + content.active = Qt.binding(() => root.shouldBeActive || root.visible); + content.visible = true; + if (showAnim.running) { + showAnim.stop(); + showAnim.start(); + } + } + } + } + + Loader { + id: content + + active: false + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + visible: false + + sourceComponent: Content { + panels: root.panels + visibilities: root.visibilities + + Component.onCompleted: root.contentHeight = implicitHeight + } + + Component.onCompleted: timer.start() + } +} diff --git a/Modules/GetIcons.qml b/Modules/GetIcons.qml deleted file mode 100644 index dad9408..0000000 --- a/Modules/GetIcons.qml +++ /dev/null @@ -1,18 +0,0 @@ -pragma Singleton - -import Quickshell -import Quickshell.Services.Notifications - -Singleton { - id: root - - function getTrayIcon(id: string, icon: string): string { - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`); - } else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") { - icon = icon.replace("qspixmap", "icon/discord-tray"); - } - return icon; - } -} diff --git a/Modules/Launcher/AppList.qml b/Modules/Launcher/AppList.qml index 1c3fdee..a9dc2a5 100644 --- a/Modules/Launcher/AppList.qml +++ b/Modules/Launcher/AppList.qml @@ -5,216 +5,226 @@ import QtQuick import qs.Modules.Launcher.Services import qs.Modules.Launcher.Items import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules CustomListView { - id: root + id: root - required property CustomTextField search - required property PersistentProperties visibilities + required property CustomTextField search + required property PersistentProperties visibilities - model: ScriptModel { - id: model + highlightFollowsCurrentItem: false + highlightRangeMode: ListView.ApplyRange + implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing + orientation: Qt.Vertical + preferredHighlightBegin: 0 + preferredHighlightEnd: height + spacing: Appearance.spacing.small + state: { + const text = search.text; + const prefix = Config.launcher.actionPrefix; + if (text.startsWith(prefix)) { + for (const action of ["calc", "scheme", "variant"]) + if (text.startsWith(`${prefix}${action} `)) + return action; - onValuesChanged: root.currentIndex = 0 - } + return "actions"; + } + return "apps"; + } verticalLayoutDirection: ListView.BottomToTop - spacing: Appearance.spacing.small - orientation: Qt.Vertical - implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing - preferredHighlightBegin: 0 - preferredHighlightEnd: height - highlightRangeMode: ListView.ApplyRange + CustomScrollBar.vertical: CustomScrollBar { + flickable: root + } + add: Transition { + enabled: !root.state - highlightFollowsCurrentItem: false - highlight: CustomRect { - radius: 8 - color: DynamicColors.palette.m3onSurface - opacity: 0.08 + Anim { + from: 0 + properties: "opacity,scale" + to: 1 + } + } + addDisplaced: Transition { + Anim { + duration: Appearance.anim.durations.small + property: "y" + } - y: root.currentItem?.y ?? 0 - implicitWidth: root.width - implicitHeight: root.currentItem?.implicitHeight ?? 0 + Anim { + properties: "opacity,scale" + to: 1 + } + } + displaced: Transition { + Anim { + property: "y" + } - Behavior on y { - Modules.Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } - } + Anim { + properties: "opacity,scale" + to: 1 + } + } + highlight: CustomRect { + color: DynamicColors.palette.m3onSurface + implicitHeight: root.currentItem?.implicitHeight ?? 0 + implicitWidth: root.width + opacity: 0.08 + radius: 8 + y: root.currentItem?.y ?? 0 - state: { - const text = search.text; - const prefix = Config.launcher.actionPrefix; - if (text.startsWith(prefix)) { - for (const action of ["calc", "scheme", "variant"]) - if (text.startsWith(`${prefix}${action} `)) - return action; + Behavior on y { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + } + } + } + model: ScriptModel { + id: model - return "actions"; - } + onValuesChanged: root.currentIndex = 0 + } + move: Transition { + Anim { + property: "y" + } - return "apps"; - } + Anim { + properties: "opacity,scale" + to: 1 + } + } + remove: Transition { + enabled: !root.state - states: [ - State { - name: "apps" + Anim { + from: 1 + properties: "opacity,scale" + to: 0 + } + } + states: [ + State { + name: "apps" - PropertyChanges { - model.values: Apps.search(search.text) - root.delegate: appItem - } - }, - State { - name: "actions" + PropertyChanges { + model.values: Apps.search(search.text) + root.delegate: appItem + } + }, + State { + name: "actions" - PropertyChanges { - model.values: Actions.query(search.text) - root.delegate: actionItem - } - }, - State { - name: "calc" + PropertyChanges { + model.values: Actions.query(search.text) + root.delegate: actionItem + } + }, + State { + name: "calc" - PropertyChanges { - model.values: [0] - root.delegate: calcItem - } - }, - ] + PropertyChanges { + model.values: [0] + root.delegate: calcItem + } + }, + State { + name: "variant" - transitions: Transition { - SequentialAnimation { - ParallelAnimation { - Modules.Anim { - target: root - property: "opacity" - from: 1 - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - Modules.Anim { - target: root - property: "scale" - from: 1 - to: 0.9 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } - PropertyAction { - targets: [model, root] - properties: "values,delegate" - } - ParallelAnimation { - Modules.Anim { - target: root - property: "opacity" - from: 0 - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - Modules.Anim { - target: root - property: "scale" - from: 0.9 - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } - PropertyAction { - targets: [root.add, root.remove] - property: "enabled" - value: true - } - } - } + PropertyChanges { + model.values: SchemeVariants.query(search.text) + root.delegate: variantItem + } + } + ] + transitions: Transition { + SequentialAnimation { + ParallelAnimation { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + from: 1 + property: "opacity" + target: root + to: 0 + } - CustomScrollBar.vertical: CustomScrollBar { - flickable: root - } + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + from: 1 + property: "scale" + target: root + to: 0.9 + } + } - add: Transition { - enabled: !root.state + PropertyAction { + properties: "values,delegate" + targets: [model, root] + } - Modules.Anim { - properties: "opacity,scale" - from: 0 - to: 1 - } - } + ParallelAnimation { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + from: 0 + property: "opacity" + target: root + to: 1 + } - remove: Transition { - enabled: !root.state + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + from: 0.9 + property: "scale" + target: root + to: 1 + } + } - Modules.Anim { - properties: "opacity,scale" - from: 1 - to: 0 - } - } + PropertyAction { + property: "enabled" + targets: [root.add, root.remove] + value: true + } + } + } - move: Transition { - Modules.Anim { - property: "y" - } - Modules.Anim { - properties: "opacity,scale" - to: 1 - } - } + Component { + id: appItem - addDisplaced: Transition { - Modules.Anim { - property: "y" - duration: Appearance.anim.durations.small - } - Modules.Anim { - properties: "opacity,scale" - to: 1 - } - } + AppItem { + visibilities: root.visibilities + } + } - displaced: Transition { - Modules.Anim { - property: "y" - } - Modules.Anim { - properties: "opacity,scale" - to: 1 - } - } + Component { + id: actionItem - Component { - id: appItem + ActionItem { + list: root + } + } - AppItem { - visibilities: root.visibilities - } - } + Component { + id: calcItem - Component { - id: actionItem + CalcItem { + list: root + } + } - ActionItem { - list: root - } - } + Component { + id: variantItem - Component { - id: calcItem - - CalcItem { - list: root - } - } + VariantItem { + list: root + } + } } diff --git a/Modules/Launcher/Background.qml b/Modules/Launcher/Background.qml index ac13d4f..584bd97 100644 --- a/Modules/Launcher/Background.qml +++ b/Modules/Launcher/Background.qml @@ -1,61 +1,66 @@ import QtQuick import QtQuick.Shapes import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules ShapePath { - id: root + id: root - required property Wrapper wrapper - readonly property real rounding: Config.barConfig.rounding + 5 - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + readonly property bool flatten: wrapper.height < rounding * 2 + readonly property real rounding: Config.barConfig.rounding + 5 + readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + required property Wrapper wrapper - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } + Behavior on fillColor { + CAnim { + } + } - Behavior on fillColor { - Modules.CAnim {} - } + 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 + } + + PathLine { + relativeX: root.wrapper.width - root.rounding * 2 + relativeY: 0 + } + + 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 - root.roundingY * 2 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: root.rounding + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.rounding + relativeY: root.roundingY + } } diff --git a/Modules/Launcher/Content.qml b/Modules/Launcher/Content.qml index 252f15a..2365fca 100644 --- a/Modules/Launcher/Content.qml +++ b/Modules/Launcher/Content.qml @@ -6,186 +6,166 @@ import qs.Modules.Launcher.Services import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules Item { - id: root + id: root - required property PersistentProperties visibilities - required property var panels - required property real maxHeight + required property real maxHeight + readonly property int padding: Appearance.padding.small + required property var panels + readonly property int rounding: Appearance.rounding.large + required property PersistentProperties visibilities - readonly property int padding: Appearance.padding.small - readonly property int rounding: Appearance.rounding.large + implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + implicitWidth: listWrapper.width + padding * 2 - implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + Item { + id: listWrapper - Item { - id: listWrapper + anchors.bottom: searchWrapper.top + anchors.bottomMargin: root.padding + anchors.horizontalCenter: parent.horizontalCenter + implicitHeight: list.height + root.padding + implicitWidth: list.width - implicitWidth: list.width - implicitHeight: list.height + root.padding + ContentList { + id: list - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: searchWrapper.top - anchors.bottomMargin: root.padding + content: root + maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3 + padding: root.padding + panels: root.panels + rounding: root.rounding + search: search + visibilities: root.visibilities + } + } - ContentList { - id: list + CustomRect { + id: searchWrapper - content: root - visibilities: root.visibilities - panels: root.panels - maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3 - search: search - padding: root.padding - rounding: root.rounding - } - } + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: root.padding + anchors.right: parent.right + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) + radius: 8 - CustomRect { - id: searchWrapper + MaterialIcon { + id: searchIcon - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - radius: 8 + anchors.left: parent.left + anchors.leftMargin: root.padding + 10 + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onSurfaceVariant + text: "search" + } - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: root.padding + CustomTextField { + id: search - implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) + anchors.left: searchIcon.right + anchors.leftMargin: Appearance.spacing.small + anchors.right: clearIcon.left + anchors.rightMargin: Appearance.spacing.small + bottomPadding: Appearance.padding.larger + placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + topPadding: Appearance.padding.larger - MaterialIcon { - id: searchIcon + Component.onCompleted: forceActiveFocus() + Keys.onDownPressed: list.currentList?.decrementCurrentIndex() + Keys.onEscapePressed: root.visibilities.launcher = false + Keys.onPressed: event => { + if (!Config.launcher.vimKeybinds) + return; - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: root.padding + 10 + if (event.modifiers & Qt.ControlModifier) { + if (event.key === Qt.Key_J) { + list.currentList?.incrementCurrentIndex(); + event.accepted = true; + } else if (event.key === Qt.Key_K) { + list.currentList?.decrementCurrentIndex(); + event.accepted = true; + } + } else if (event.key === Qt.Key_Tab) { + list.currentList?.incrementCurrentIndex(); + event.accepted = true; + } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { + list.currentList?.decrementCurrentIndex(); + event.accepted = true; + } + } + Keys.onUpPressed: list.currentList?.incrementCurrentIndex() + onAccepted: { + const currentItem = list.currentList?.currentItem; + if (currentItem) { + if (list.showWallpapers) { + if (DynamicColors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent) + Wallpapers.previewColourLock = true; + Wallpapers.setWallpaper(currentItem.modelData.path); + root.visibilities.launcher = false; + } else if (text.startsWith(Config.launcher.actionPrefix)) { + if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) + currentItem.onClicked(); + else + currentItem.modelData.onClicked(list.currentList); + } else { + Apps.launch(currentItem.modelData); + root.visibilities.launcher = false; + } + } + } - text: "search" - color: DynamicColors.palette.m3onSurfaceVariant - } + Connections { + function onLauncherChanged(): void { + if (!root.visibilities.launcher) + search.text = ""; + } - CustomTextField { - id: search + target: root.visibilities + } + } - anchors.left: searchIcon.right - anchors.right: clearIcon.left - anchors.leftMargin: Appearance.spacing.small - anchors.rightMargin: Appearance.spacing.small + MaterialIcon { + id: clearIcon - topPadding: Appearance.padding.larger - bottomPadding: Appearance.padding.larger + anchors.right: parent.right + anchors.rightMargin: root.padding + 10 + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onSurfaceVariant + opacity: { + if (!search.text) + return 0; + if (mouse.pressed) + return 0.7; + if (mouse.containsMouse) + return 0.8; + return 1; + } + text: "close" + width: search.text ? implicitWidth : implicitWidth / 2 - placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + Behavior on width { + Anim { + duration: Appearance.anim.durations.small + } + } - onAccepted: { - const currentItem = list.currentList?.currentItem; - if (currentItem) { - if (list.showWallpapers) { - if (DynamicColors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent) - Wallpapers.previewColourLock = true; - Wallpapers.setWallpaper(currentItem.modelData.path); - root.visibilities.launcher = false; - } else if (text.startsWith(Config.launcher.actionPrefix)) { - if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) - currentItem.onClicked(); - else - currentItem.modelData.onClicked(list.currentList); - } else { - Apps.launch(currentItem.modelData); - root.visibilities.launcher = false; - } - } - } + MouseArea { + id: mouse - Keys.onUpPressed: list.currentList?.incrementCurrentIndex() - Keys.onDownPressed: list.currentList?.decrementCurrentIndex() + anchors.fill: parent + cursorShape: search.text ? Qt.PointingHandCursor : undefined + hoverEnabled: true - Keys.onEscapePressed: root.visibilities.launcher = false - - Keys.onPressed: event => { - if (!Config.launcher.vimKeybinds) - return; - - if (event.modifiers & Qt.ControlModifier) { - if (event.key === Qt.Key_J) { - list.currentList?.incrementCurrentIndex(); - event.accepted = true; - } else if (event.key === Qt.Key_K) { - list.currentList?.decrementCurrentIndex(); - event.accepted = true; - } - } else if (event.key === Qt.Key_Tab) { - list.currentList?.incrementCurrentIndex(); - event.accepted = true; - } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { - list.currentList?.decrementCurrentIndex(); - event.accepted = true; - } - } - - Component.onCompleted: forceActiveFocus() - - Connections { - target: root.visibilities - - function onLauncherChanged(): void { - if (!root.visibilities.launcher) - search.text = ""; - } - - function onSessionChanged(): void { - if (!root.visibilities.session) - search.forceActiveFocus(); - } - } - } - - MaterialIcon { - id: clearIcon - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: root.padding + 10 - - width: search.text ? implicitWidth : implicitWidth / 2 - opacity: { - if (!search.text) - return 0; - if (mouse.pressed) - return 0.7; - if (mouse.containsMouse) - return 0.8; - return 1; - } - - text: "close" - color: DynamicColors.palette.m3onSurfaceVariant - - MouseArea { - id: mouse - - anchors.fill: parent - hoverEnabled: true - cursorShape: search.text ? Qt.PointingHandCursor : undefined - - onClicked: search.text = "" - } - - Behavior on width { - Modules.Anim { - duration: Appearance.anim.durations.small - } - } - - Behavior on opacity { - Modules.Anim { - duration: Appearance.anim.durations.small - } - } - } - } + onClicked: search.text = "" + } + } + } } diff --git a/Modules/Launcher/ContentList.qml b/Modules/Launcher/ContentList.qml index fadd172..40fec4c 100644 --- a/Modules/Launcher/ContentList.qml +++ b/Modules/Launcher/ContentList.qml @@ -9,162 +9,156 @@ import qs.Helpers import qs.Config Item { - id: root + id: root - required property var content - required property PersistentProperties visibilities - required property var panels - required property real maxHeight - required property CustomTextField search - required property int padding - required property int rounding + required property var content + readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item + required property real maxHeight + required property int padding + required property var panels + required property int rounding + required property CustomTextField search + readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) + required property PersistentProperties visibilities - readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) - readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + clip: true + state: showWallpapers ? "wallpapers" : "apps" - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom + Behavior on implicitHeight { + enabled: root.visibilities.launcher - clip: true - state: showWallpapers ? "wallpapers" : "apps" + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + } + } + Behavior on implicitWidth { + enabled: root.visibilities.launcher - states: [ - State { - name: "apps" + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + } + } + Behavior on state { + SequentialAnimation { + Anim { + duration: Appearance.anim.durations.small + from: 1 + property: "opacity" + target: root + to: 0 + } - PropertyChanges { - root.implicitWidth: Config.launcher.sizes.itemWidth - root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) - appList.active: true - } + PropertyAction { + } - AnchorChanges { - anchors.left: root.parent.left - anchors.right: root.parent.right - } - }, - State { - name: "wallpapers" + Anim { + duration: Appearance.anim.durations.small + from: 0 + property: "opacity" + target: root + to: 1 + } + } + } + states: [ + State { + name: "apps" - PropertyChanges { - root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) - root.implicitHeight: Config.launcher.sizes.wallpaperHeight - wallpaperList.active: true - } - } - ] + PropertyChanges { + appList.active: true + root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) + root.implicitWidth: Config.launcher.sizes.itemWidth + } - Behavior on state { - SequentialAnimation { - Anim { - target: root - property: "opacity" - from: 1 - to: 0 - duration: Appearance.anim.durations.small - } - PropertyAction {} - Anim { - target: root - property: "opacity" - from: 0 - to: 1 - duration: Appearance.anim.durations.small - } - } - } + AnchorChanges { + anchors.left: root.parent.left + anchors.right: root.parent.right + } + }, + State { + name: "wallpapers" - Loader { - id: appList + PropertyChanges { + root.implicitHeight: Config.launcher.sizes.wallpaperHeight + root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) + wallpaperList.active: true + } + } + ] - active: false + Loader { + id: appList - anchors.fill: parent + active: false + anchors.fill: parent - sourceComponent: AppList { - search: root.search - visibilities: root.visibilities - } - } + sourceComponent: AppList { + search: root.search + visibilities: root.visibilities + } + } - Loader { - id: wallpaperList + Loader { + id: wallpaperList - active: false + active: false + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter + sourceComponent: WallpaperList { + content: root.content + panels: root.panels + search: root.search + visibilities: root.visibilities + } + } - sourceComponent: WallpaperList { - search: root.search - visibilities: root.visibilities - panels: root.panels - content: root.content - } - } + Row { + id: empty - Row { - id: empty + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + opacity: root.currentList?.count === 0 ? 1 : 0 + padding: Appearance.padding.large + scale: root.currentList?.count === 0 ? 1 : 0.5 + spacing: Appearance.spacing.normal - opacity: root.currentList?.count === 0 ? 1 : 0 - scale: root.currentList?.count === 0 ? 1 : 0.5 + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } - spacing: Appearance.spacing.normal - padding: Appearance.padding.large + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.extraLarge + text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" + } - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter + Column { + anchors.verticalCenter: parent.verticalCenter - MaterialIcon { - text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge + CustomText { + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.larger + font.weight: 500 + text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") + } - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.verticalCenter: parent.verticalCenter - - CustomText { - text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.larger - font.weight: 500 - } - - CustomText { - text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - } - - Behavior on implicitWidth { - enabled: root.visibilities.launcher - - Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } - - Behavior on implicitHeight { - enabled: root.visibilities.launcher - - Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } + CustomText { + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") + } + } + } } diff --git a/Modules/Launcher/Items/ActionItem.qml b/Modules/Launcher/Items/ActionItem.qml index ea869ce..1815f51 100644 --- a/Modules/Launcher/Items/ActionItem.qml +++ b/Modules/Launcher/Items/ActionItem.qml @@ -5,66 +5,61 @@ import qs.Helpers import qs.Config Item { - id: root + id: root - required property var modelData - required property var list + required property var list + required property var modelData - implicitHeight: Config.launcher.sizes.itemHeight + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: Config.launcher.sizes.itemHeight - anchors.left: parent?.left - anchors.right: parent?.right + StateLayer { + function onClicked(): void { + root.modelData?.onClicked(root.list); + } - StateLayer { - radius: Appearance.rounding.normal + radius: Appearance.rounding.normal + } - function onClicked(): void { - root.modelData?.onClicked(root.list); - } - } + Item { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.larger + anchors.margins: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.larger - Item { - anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + MaterialIcon { + id: icon - MaterialIcon { - id: icon + anchors.verticalCenter: parent.verticalCenter + font.pointSize: Appearance.font.size.extraLarge + text: root.modelData?.icon ?? "" + } - text: root.modelData?.icon ?? "" - font.pointSize: Appearance.font.size.extraLarge + Item { + anchors.left: icon.right + anchors.leftMargin: Appearance.spacing.normal + anchors.verticalCenter: icon.verticalCenter + implicitHeight: name.implicitHeight + desc.implicitHeight + implicitWidth: parent.width - icon.width - anchors.verticalCenter: parent.verticalCenter - } + CustomText { + id: name - Item { - anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal - anchors.verticalCenter: icon.verticalCenter + font.pointSize: Appearance.font.size.normal + text: root.modelData?.name ?? "" + } - implicitWidth: parent.width - icon.width - implicitHeight: name.implicitHeight + desc.implicitHeight + CustomText { + id: desc - CustomText { - id: name - - text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal - } - - CustomText { - id: desc - - text: root.modelData?.desc ?? "" - font.pointSize: Appearance.font.size.small - color: DynamicColors.palette.m3outline - - elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 - - anchors.top: name.bottom - } - } - } + anchors.top: name.bottom + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: root.modelData?.desc ?? "" + width: root.width - icon.width - Appearance.rounding.normal * 2 + } + } + } } diff --git a/Modules/Launcher/Items/AppItem.qml b/Modules/Launcher/Items/AppItem.qml index ccee343..25cf90d 100644 --- a/Modules/Launcher/Items/AppItem.qml +++ b/Modules/Launcher/Items/AppItem.qml @@ -8,67 +8,62 @@ import qs.Config import qs.Modules Item { - id: root + id: root - required property DesktopEntry modelData - required property PersistentProperties visibilities + required property DesktopEntry modelData + required property PersistentProperties visibilities - implicitHeight: Config.launcher.sizes.itemHeight + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: Config.launcher.sizes.itemHeight - anchors.left: parent?.left - anchors.right: parent?.right + StateLayer { + function onClicked(): void { + Apps.launch(root.modelData); + root.visibilities.launcher = false; + } - StateLayer { - radius: 8 + radius: 8 + } - function onClicked(): void { - Apps.launch(root.modelData); - root.visibilities.launcher = false; - } - } + Item { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.larger + anchors.margins: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.larger - Item { - anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + IconImage { + id: icon - IconImage { - id: icon + anchors.verticalCenter: parent.verticalCenter + implicitSize: parent.height + source: Quickshell.iconPath(root.modelData?.icon, "image-missing") + } - source: Quickshell.iconPath(root.modelData?.icon, "image-missing") - implicitSize: parent.height + Item { + anchors.left: icon.right + anchors.leftMargin: Appearance.spacing.normal + anchors.verticalCenter: icon.verticalCenter + implicitHeight: name.implicitHeight + comment.implicitHeight + implicitWidth: parent.width - icon.width - anchors.verticalCenter: parent.verticalCenter - } + CustomText { + id: name - Item { - anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal - anchors.verticalCenter: icon.verticalCenter + font.pointSize: Appearance.font.size.normal + text: root.modelData?.name ?? "" + } - implicitWidth: parent.width - icon.width - implicitHeight: name.implicitHeight + comment.implicitHeight + CustomText { + id: comment - CustomText { - id: name - - text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal - } - - CustomText { - id: comment - - text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" - font.pointSize: Appearance.font.size.small - color: DynamicColors.palette.m3outline - - elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 - - anchors.top: name.bottom - } - } - } + anchors.top: name.bottom + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" + width: root.width - icon.width - Appearance.rounding.normal * 2 + } + } + } } diff --git a/Modules/Launcher/Items/CalcItem.qml b/Modules/Launcher/Items/CalcItem.qml index 56e937b..39f7470 100644 --- a/Modules/Launcher/Items/CalcItem.qml +++ b/Modules/Launcher/Items/CalcItem.qml @@ -8,117 +8,109 @@ import qs.Helpers import qs.Config Item { - id: root + id: root - required property var list - readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) + required property var list + readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) - function onClicked(): void { - Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); - root.list.visibilities.launcher = false; - } + function onClicked(): void { + Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); + root.list.visibilities.launcher = false; + } - implicitHeight: Config.launcher.sizes.itemHeight + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: Config.launcher.sizes.itemHeight - anchors.left: parent?.left - anchors.right: parent?.right + StateLayer { + function onClicked(): void { + root.onClicked(); + } - StateLayer { - radius: Appearance.rounding.normal + radius: Appearance.rounding.normal + } - function onClicked(): void { - root.onClicked(); - } - } + RowLayout { + anchors.left: parent.left + anchors.margins: Appearance.padding.larger + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.larger + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + font.pointSize: Appearance.font.size.extraLarge + text: "function" + } - spacing: Appearance.spacing.normal + CustomText { + id: result - MaterialIcon { - text: "function" - font.pointSize: Appearance.font.size.extraLarge - Layout.alignment: Qt.AlignVCenter - } + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: { + if (text.includes("error: ") || text.includes("warning: ")) + return DynamicColors.palette.m3error; + if (!root.math) + return DynamicColors.palette.m3onSurfaceVariant; + return DynamicColors.palette.m3onSurface; + } + elide: Text.ElideLeft + text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") + } - CustomText { - id: result + CustomRect { + Layout.alignment: Qt.AlignVCenter + clip: true + color: DynamicColors.palette.m3tertiary + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal - color: { - if (text.includes("error: ") || text.includes("warning: ")) - return DynamicColors.palette.m3error; - if (!root.math) - return DynamicColors.palette.m3onSurfaceVariant; - return DynamicColors.palette.m3onSurface; - } + Behavior on implicitWidth { + Anim { + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + } + } - text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") - elide: Text.ElideLeft + StateLayer { + id: stateLayer - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } + function onClicked(): void { + Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); + root.list.visibilities.launcher = false; + } - CustomRect { - color: DynamicColors.palette.m3tertiary - radius: Appearance.rounding.normal - clip: true + color: DynamicColors.palette.m3onTertiary + } - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 + CustomText { + id: label - Layout.alignment: Qt.AlignVCenter + anchors.right: icon.left + anchors.rightMargin: Appearance.spacing.small + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onTertiary + font.pointSize: Appearance.font.size.normal + opacity: stateLayer.containsMouse ? 1 : 0 + text: qsTr("Open in calculator") - StateLayer { - id: stateLayer + Behavior on opacity { + Anim { + } + } + } - color: DynamicColors.palette.m3onTertiary + MaterialIcon { + id: icon - function onClicked(): void { - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); - root.list.visibilities.launcher = false; - } - } - - CustomText { - id: label - - anchors.verticalCenter: parent.verticalCenter - anchors.right: icon.left - anchors.rightMargin: Appearance.spacing.small - - text: qsTr("Open in calculator") - color: DynamicColors.palette.m3onTertiary - font.pointSize: Appearance.font.size.normal - - opacity: stateLayer.containsMouse ? 1 : 0 - - Behavior on opacity { - Anim {} - } - } - - MaterialIcon { - id: icon - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal - - text: "open_in_new" - color: DynamicColors.palette.m3onTertiary - font.pointSize: Appearance.font.size.large - } - - Behavior on implicitWidth { - Anim { - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } - } - } + anchors.right: parent.right + anchors.rightMargin: Appearance.padding.normal + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onTertiary + font.pointSize: Appearance.font.size.large + text: "open_in_new" + } + } + } } diff --git a/Modules/Launcher/Items/VariantItem.qml b/Modules/Launcher/Items/VariantItem.qml new file mode 100644 index 0000000..2b0b096 --- /dev/null +++ b/Modules/Launcher/Items/VariantItem.qml @@ -0,0 +1,74 @@ +import QtQuick +import qs.Components +import qs.Modules.Launcher.Services +import qs.Config + +Item { + id: root + + required property var list + required property SchemeVariants.Variant modelData + + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: Config.launcher.sizes.itemHeight + + StateLayer { + function onClicked(): void { + root.modelData?.onClicked(root.list); + } + + radius: Appearance.rounding.normal + } + + Item { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.larger + anchors.margins: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.larger + + MaterialIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + font.pointSize: Appearance.font.size.extraLarge + text: root.modelData?.icon ?? "" + } + + Column { + anchors.left: icon.right + anchors.leftMargin: Appearance.spacing.larger + anchors.verticalCenter: icon.verticalCenter + spacing: 0 + width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) + + CustomText { + font.pointSize: Appearance.font.size.normal + text: root.modelData?.name ?? "" + } + + CustomText { + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: root.modelData?.description ?? "" + } + } + + Loader { + id: current + + active: root.modelData?.variant === Config.colors.schemeType + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.large + text: "check" + } + } + } +} diff --git a/Modules/Launcher/Items/WallpaperItem.qml b/Modules/Launcher/Items/WallpaperItem.qml index d7533f9..65d9418 100644 --- a/Modules/Launcher/Items/WallpaperItem.qml +++ b/Modules/Launcher/Items/WallpaperItem.qml @@ -7,92 +7,89 @@ import qs.Config import qs.Modules Item { - id: root + id: root - required property FileSystemEntry modelData - required property PersistentProperties visibilities + required property FileSystemEntry modelData + required property PersistentProperties visibilities - scale: 0.5 - opacity: 0 - z: PathView.z ?? 0 + implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal + implicitWidth: image.width + Appearance.padding.larger * 2 + opacity: 0 + scale: 0.5 + z: PathView.z ?? 0 - Component.onCompleted: { - scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); - opacity = Qt.binding(() => PathView.onPath ? 1 : 0); - } + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } - implicitWidth: image.width + Appearance.padding.larger * 2 - implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal + Component.onCompleted: { + scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); + opacity = Qt.binding(() => PathView.onPath ? 1 : 0); + } - StateLayer { - radius: Appearance.rounding.normal + StateLayer { + function onClicked(): void { + Wallpapers.setWallpaper(root.modelData.path); + root.visibilities.launcher = false; + } - function onClicked(): void { - console.log(root.modelData.path); - Wallpapers.setWallpaper(root.modelData.path); - root.visibilities.launcher = false; - } - } + radius: Appearance.rounding.normal + } - Elevation { - anchors.fill: image - radius: image.radius - opacity: root.PathView.isCurrentItem ? 1 : 0 - level: 4 + Elevation { + anchors.fill: image + level: 4 + opacity: root.PathView.isCurrentItem ? 1 : 0 + radius: image.radius - Behavior on opacity { - Anim {} - } - } + Behavior on opacity { + Anim { + } + } + } - CustomClippingRect { - id: image + CustomClippingRect { + id: image - anchors.horizontalCenter: parent.horizontalCenter - y: Appearance.padding.large - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: implicitWidth / 16 * 9 + implicitWidth: Config.launcher.sizes.wallpaperWidth + radius: Appearance.rounding.small + y: Appearance.padding.large - implicitWidth: Config.launcher.sizes.wallpaperWidth - implicitHeight: implicitWidth / 16 * 9 + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.tPalette.m3outline + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 600 + text: "image" + } - MaterialIcon { - anchors.centerIn: parent - text: "image" - color: DynamicColors.tPalette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 2 - font.weight: 600 - } + CachingImage { + anchors.fill: parent + cache: true + path: root.modelData.path + smooth: !root.PathView.view.moving + } + } - CachingImage { - path: root.modelData.path - smooth: !root.PathView.view.moving - cache: true + CustomText { + id: label - anchors.fill: parent - } - } - - CustomText { - id: label - - anchors.top: image.bottom - anchors.topMargin: Appearance.spacing.small / 2 - anchors.horizontalCenter: parent.horizontalCenter - - width: image.width - Appearance.padding.normal * 2 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - renderType: Text.QtRendering - text: root.modelData.relativePath - font.pointSize: Appearance.font.size.normal - } - - Behavior on scale { - Anim {} - } - - Behavior on opacity { - Anim {} - } + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: image.bottom + anchors.topMargin: Appearance.spacing.small / 2 + elide: Text.ElideRight + font.pointSize: Appearance.font.size.normal + horizontalAlignment: Text.AlignHCenter + renderType: Text.QtRendering + text: root.modelData.relativePath + width: image.width - Appearance.padding.normal * 2 + } } diff --git a/Modules/Launcher/Services/Actions.qml b/Modules/Launcher/Services/Actions.qml index f428656..ae4f6fa 100644 --- a/Modules/Launcher/Services/Actions.qml +++ b/Modules/Launcher/Services/Actions.qml @@ -7,45 +7,46 @@ import Quickshell import QtQuick Searcher { - id: root + id: root - function transformSearch(search: string): string { - return search.slice(Config.launcher.actionPrefix.length); - } + function transformSearch(search: string): string { + return search.slice(Config.launcher.actionPrefix.length); + } - list: variants.instances - useFuzzy: Config.launcher.useFuzzy.actions + list: variants.instances + useFuzzy: Config.launcher.useFuzzy.actions - Variants { - id: variants + Variants { + id: variants - model: Config.launcher.actions.filter(a => (a.enabled ?? true)) + model: Config.launcher.actions.filter(a => (a.enabled ?? true)) - Action {} - } + Action { + } + } - component Action: QtObject { - required property var modelData - readonly property string name: modelData.name ?? qsTr("Unnamed") - readonly property string desc: modelData.description ?? qsTr("No description") - readonly property string icon: modelData.icon ?? "help_outline" - readonly property list command: modelData.command ?? [] - readonly property bool enabled: modelData.enabled ?? true - readonly property bool dangerous: modelData.dangerous ?? false + component Action: QtObject { + readonly property list command: modelData.command ?? [] + readonly property bool dangerous: modelData.dangerous ?? false + readonly property string desc: modelData.description ?? qsTr("No description") + readonly property bool enabled: modelData.enabled ?? true + readonly property string icon: modelData.icon ?? "help_outline" + required property var modelData + readonly property string name: modelData.name ?? qsTr("Unnamed") - function onClicked(list: AppList): void { - if (command.length === 0) - return; + function onClicked(list: AppList): void { + if (command.length === 0) + return; - if (command[0] === "autocomplete" && command.length > 1) { - list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; - } else if (command[0] === "setMode" && command.length > 1) { - list.visibilities.launcher = false; - Colours.setMode(command[1]); - } else { - list.visibilities.launcher = false; - Quickshell.execDetached(command); - } - } - } + if (command[0] === "autocomplete" && command.length > 1) { + list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; + } else if (command[0] === "setMode" && command.length > 1) { + list.visibilities.launcher = false; + DynamicColors.setMode(command[1]); + } else { + list.visibilities.launcher = false; + Quickshell.execDetached(command); + } + } + } } diff --git a/Modules/Launcher/Services/Apps.qml b/Modules/Launcher/Services/Apps.qml index 27d54a1..57a30b0 100644 --- a/Modules/Launcher/Services/Apps.qml +++ b/Modules/Launcher/Services/Apps.qml @@ -7,72 +7,72 @@ import qs.Helpers import qs.Paths Searcher { - id: root + id: root - function launch(entry: DesktopEntry): void { - appDb.incrementFrequency(entry.id); + function launch(entry: DesktopEntry): void { + appDb.incrementFrequency(entry.id); - if (entry.runInTerminal) - Quickshell.execDetached({ - command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], - workingDirectory: entry.workingDirectory - }); - else - Quickshell.execDetached({ - command: ["app2unit", "--", ...entry.command], - workingDirectory: entry.workingDirectory - }); - } + if (entry.runInTerminal) + Quickshell.execDetached({ + command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], + workingDirectory: entry.workingDirectory + }); + else + Quickshell.execDetached({ + command: ["app2unit", "--", ...entry.command], + workingDirectory: entry.workingDirectory + }); + } - function search(search: string): list { - const prefix = Config.launcher.specialPrefix; + function search(search: string): list { + const prefix = Config.launcher.specialPrefix; - if (search.startsWith(`${prefix}i `)) { - keys = ["id", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}c `)) { - keys = ["categories", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}d `)) { - keys = ["comment", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}e `)) { - keys = ["execString", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}w `)) { - keys = ["startupClass", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}g `)) { - keys = ["genericName", "name"]; - weights = [0.9, 0.1]; - } else if (search.startsWith(`${prefix}k `)) { - keys = ["keywords", "name"]; - weights = [0.9, 0.1]; - } else { - keys = ["name"]; - weights = [1]; + if (search.startsWith(`${prefix}i `)) { + keys = ["id", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}c `)) { + keys = ["categories", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}d `)) { + keys = ["comment", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}e `)) { + keys = ["execString", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}w `)) { + keys = ["startupClass", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}g `)) { + keys = ["genericName", "name"]; + weights = [0.9, 0.1]; + } else if (search.startsWith(`${prefix}k `)) { + keys = ["keywords", "name"]; + weights = [0.9, 0.1]; + } else { + keys = ["name"]; + weights = [1]; - if (!search.startsWith(`${prefix}t `)) - return query(search).map(e => e.entry); - } + if (!search.startsWith(`${prefix}t `)) + return query(search).map(e => e.entry); + } - const results = query(search.slice(prefix.length + 2)).map(e => e.entry); - if (search.startsWith(`${prefix}t `)) - return results.filter(a => a.runInTerminal); - return results; - } + const results = query(search.slice(prefix.length + 2)).map(e => e.entry); + if (search.startsWith(`${prefix}t `)) + return results.filter(a => a.runInTerminal); + return results; + } - function selector(item: var): string { - return keys.map(k => item[k]).join(" "); - } + function selector(item: var): string { + return keys.map(k => item[k]).join(" "); + } - list: appDb.apps - useFuzzy: Config.launcher.useFuzzy.apps + list: appDb.apps + useFuzzy: Config.launcher.useFuzzy.apps - AppDb { - id: appDb + AppDb { + id: appDb - path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values - } + entries: DesktopEntries.applications.values + path: `${Paths.state}/apps.sqlite` + } } diff --git a/Modules/Launcher/Services/SchemeVariants.qml b/Modules/Launcher/Services/SchemeVariants.qml new file mode 100644 index 0000000..c943010 --- /dev/null +++ b/Modules/Launcher/Services/SchemeVariants.qml @@ -0,0 +1,88 @@ +pragma Singleton + +import Quickshell +import QtQuick +import qs.Modules.Launcher +import qs.Config +import qs.Helpers + +Searcher { + id: root + + function transformSearch(search: string): string { + return search.slice(`${Config.launcher.actionPrefix}variant `.length); + } + + useFuzzy: Config.launcher.useFuzzy.variants + + list: [ + Variant { + description: qsTr("Maximum chroma at each tone.") + icon: "sentiment_very_dissatisfied" + name: qsTr("Vibrant") + variant: "vibrant" + }, + Variant { + description: qsTr("Pastel palette with a low chroma.") + icon: "android" + name: qsTr("Tonal Spot") + variant: "tonalspot" + }, + Variant { + description: qsTr("Hue-shifted, artistic or playful colors.") + icon: "compare_arrows" + name: qsTr("Expressive") + variant: "expressive" + }, + Variant { + description: qsTr("Preserve source color exactly.") + icon: "compare" + name: qsTr("Fidelity") + variant: "fidelity" + }, + Variant { + description: qsTr("Almost identical to fidelity.") + icon: "sentiment_calm" + name: qsTr("Content") + variant: "content" + }, + Variant { + description: qsTr("The seed colour's hue does not appear in the theme.") + icon: "nutrition" + name: qsTr("Fruit Salad") + variant: "fruit-salad" + }, + Variant { + description: qsTr("Like Fruit Salad but different hues.") + icon: "looks" + name: qsTr("Rainbow") + variant: "rainbow" + }, + Variant { + description: qsTr("Close to grayscale, a hint of chroma.") + icon: "contrast" + name: qsTr("Neutral") + variant: "neutral" + }, + Variant { + description: qsTr("All colours are grayscale, no chroma.") + icon: "filter_b_and_w" + name: qsTr("Monochrome") + variant: "monochrome" + } + ] + + component Variant: QtObject { + required property string description + required property string icon + required property string name + required property string variant + + function onClicked(list: AppList): void { + list.visibilities.launcher = false; + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--scheme", variant]); + Config.colors.schemeType = variant; + Config.save(); + } + } +} diff --git a/Modules/Launcher/WallpaperList.qml b/Modules/Launcher/WallpaperList.qml index a898190..94d6ab1 100644 --- a/Modules/Launcher/WallpaperList.qml +++ b/Modules/Launcher/WallpaperList.qml @@ -8,90 +8,86 @@ import qs.Config import qs.Modules.Launcher.Items PathView { - id: root + id: root - required property CustomTextField search - required property var visibilities - required property var panels - required property var content + required property var content + 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) + return 0; - readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 + // Screen width - 4x outer rounding - 2x max side thickness (cause centered) + const barMargins = panels.bar.implicitWidth; + let outerMargins = 0; + if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins) + outerMargins = panels.utilities.implicitWidth; + const maxWidth = screen.width - Config.barConfig.rounding * 4 - (barMargins + outerMargins) * 2; - readonly property int numItems: { - const screen = QsWindow.window?.screen; - if (!screen) - return 0; + if (maxWidth <= 0) + return 0; - // 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; + const maxItemsOnScreen = Math.floor(maxWidth / itemWidth); + const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length); - if (maxWidth <= 0) - return 0; + if (visible === 2) + return 1; + if (visible > 1 && visible % 2 === 0) + return visible - 1; + return visible; + } + required property var panels + required property CustomTextField search + required property var visibilities - const maxItemsOnScreen = Math.floor(maxWidth / itemWidth); - const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length); + cacheItemCount: 4 + highlightRangeMode: PathView.StrictlyEnforceRange + implicitWidth: Math.min(numItems, count) * itemWidth + pathItemCount: numItems + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + snapMode: PathView.SnapToItem - if (visible === 2) - return 1; - if (visible > 1 && visible % 2 === 0) - return visible - 1; - return visible; - } + delegate: WallpaperItem { + visibilities: root.visibilities + } + model: ScriptModel { + id: scriptModel - model: ScriptModel { - id: scriptModel + readonly property string search: root.search.text.split(" ").slice(1).join(" ") - readonly property string search: root.search.text.split(" ").slice(1).join(" ") + values: Wallpapers.query(search) - values: Wallpapers.query(search) - onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent) - } + onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent) + } + path: Path { + startY: root.height / 2 - Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent) - Component.onDestruction: Wallpapers.stopPreview() + PathAttribute { + name: "z" + value: 0 + } - onCurrentItemChanged: { - if (currentItem) - Wallpapers.preview(currentItem.modelData.path); - } + PathLine { + relativeY: 0 + x: root.width / 2 + } - implicitWidth: Math.min(numItems, count) * itemWidth - pathItemCount: numItems - cacheItemCount: 4 + PathAttribute { + name: "z" + value: 1 + } - snapMode: PathView.SnapToItem - preferredHighlightBegin: 0.5 - preferredHighlightEnd: 0.5 - highlightRangeMode: PathView.StrictlyEnforceRange + PathLine { + relativeY: 0 + x: root.width + } + } - delegate: WallpaperItem { - visibilities: root.visibilities - } - - path: Path { - startY: root.height / 2 - - PathAttribute { - name: "z" - value: 0 - } - PathLine { - x: root.width / 2 - relativeY: 0 - } - PathAttribute { - name: "z" - value: 1 - } - PathLine { - x: root.width - relativeY: 0 - } - } + Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent) + Component.onDestruction: Wallpapers.stopPreview() + onCurrentItemChanged: { + if (currentItem) + Wallpapers.preview(currentItem.modelData.path); + } } diff --git a/Modules/Launcher/Wrapper.qml b/Modules/Launcher/Wrapper.qml index cba412a..4c5b6e5 100644 --- a/Modules/Launcher/Wrapper.qml +++ b/Modules/Launcher/Wrapper.qml @@ -4,128 +4,129 @@ import Quickshell import QtQuick import qs.Components import qs.Config -import qs.Modules as Modules Item { - id: root + id: root - required property ShellScreen screen - required property PersistentProperties visibilities - required property var panels + property int contentHeight + readonly property real maxHeight: { + let max = screen.height - Appearance.spacing.large * 2; + if (visibilities.resources) + max -= panels.resources.nonAnimHeight; + if (visibilities.dashboard && panels.dashboard.x < root.x + root.implicitWidth) + max -= panels.dashboard.nonAnimHeight; + return max; + } + required property var panels + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.launcher + required property PersistentProperties visibilities - readonly property bool shouldBeActive: visibilities.launcher - property int contentHeight + implicitHeight: 0 + implicitWidth: content.implicitWidth + visible: height > 0 - readonly property real maxHeight: { - let max = screen.height - Appearance.spacing.large; - if (visibilities.dashboard) - max -= panels.dashboard.nonAnimHeight; - return max; - } + onMaxHeightChanged: timer.start() + onShouldBeActiveChanged: { + if (shouldBeActive) { + timer.stop(); + hideAnim.stop(); + showAnim.start(); + } else { + showAnim.stop(); + hideAnim.start(); + } + } - onMaxHeightChanged: timer.start() + SequentialAnimation { + id: showAnim - visible: height > 0 - implicitHeight: 0 - implicitWidth: content.implicitWidth + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + property: "implicitHeight" + target: root + to: root.contentHeight + } - onShouldBeActiveChanged: { - if (shouldBeActive) { - timer.stop(); - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } - } + ScriptAction { + script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + } + } - SequentialAnimation { - id: showAnim + SequentialAnimation { + id: hideAnim - Modules.Anim { - target: root - property: "implicitHeight" - to: root.contentHeight - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - } - } + ScriptAction { + script: root.implicitHeight = root.implicitHeight + } - SequentialAnimation { - id: hideAnim + Anim { + easing.bezierCurve: Appearance.anim.curves.expressiveEffects + property: "implicitHeight" + target: root + to: 0 + } + } - ScriptAction { - script: root.implicitHeight = root.implicitHeight - } - Modules.Anim { - target: root - property: "implicitHeight" - to: 0 - easing.bezierCurve: Appearance.anim.curves.expressiveEffects - } - } + Connections { + function onEnabledChanged(): void { + timer.start(); + } - Connections { - target: Config.launcher + function onMaxShownChanged(): void { + timer.start(); + } - function onEnabledChanged(): void { - timer.start(); - } + target: Config.launcher + } - function onMaxShownChanged(): void { - timer.start(); - } - } + Connections { + function onValuesChanged(): void { + if (DesktopEntries.applications.values.length < Config.launcher.maxAppsShown) + timer.start(); + } - Connections { - target: DesktopEntries.applications + target: DesktopEntries.applications + } - function onValuesChanged(): void { - if (DesktopEntries.applications.values.length < Config.launcher.maxAppsShown) - timer.start(); - } - } + Timer { + id: timer - Timer { - id: timer + interval: Appearance.anim.durations.small - interval: Appearance.anim.durations.small - onRunningChanged: { - if (running && !root.shouldBeActive) { - content.visible = false; - content.active = true; - } else { - root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; - if (showAnim.running) { - showAnim.stop(); - showAnim.start(); - } - } - } - } + onRunningChanged: { + if (running && !root.shouldBeActive) { + content.visible = false; + content.active = true; + } else { + root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); + content.active = Qt.binding(() => root.shouldBeActive || root.visible); + content.visible = true; + if (showAnim.running) { + showAnim.stop(); + showAnim.start(); + } + } + } + } - Loader { - id: content + Loader { + id: content - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter + active: false + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + visible: false - visible: false - active: false - Component.onCompleted: timer.start() + sourceComponent: Content { + maxHeight: root.maxHeight + panels: root.panels + visibilities: root.visibilities - sourceComponent: Content { - visibilities: root.visibilities - panels: root.panels - maxHeight: root.maxHeight + Component.onCompleted: root.contentHeight = implicitHeight + } - Component.onCompleted: root.contentHeight = implicitHeight - } - } + Component.onCompleted: timer.start() + } } diff --git a/Modules/Lock/Center.qml b/Modules/Lock/Center.qml index 7293d84..a336503 100644 --- a/Modules/Lock/Center.qml +++ b/Modules/Lock/Center.qml @@ -9,391 +9,382 @@ import qs.Config import qs.Modules ColumnLayout { - id: root - - required property var lock - readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) - readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale - - Layout.preferredWidth: centerWidth - Layout.fillWidth: false - Layout.fillHeight: true - - spacing: Appearance.spacing.large * 2 - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small - - CustomText { - Layout.alignment: Qt.AlignVCenter - text: Time.hourStr - color: DynamicColors.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - - CustomText { - Layout.alignment: Qt.AlignVCenter - text: ":" - color: DynamicColors.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - - CustomText { - Layout.alignment: Qt.AlignVCenter - text: Time.minuteStr - color: DynamicColors.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock - font.bold: true - } - } - - CustomText { - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Appearance.padding.large * 2 - - text: Time.format("dddd, d MMMM yyyy") - color: DynamicColors.palette.m3tertiary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) - font.family: Appearance.font.family.mono - font.bold: true - } - - CustomClippingRect { - Layout.topMargin: Appearance.spacing.large * 2 - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth / 2 - implicitHeight: root.centerWidth / 2 - - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.full - - MaterialIcon { - anchors.centerIn: parent - - text: "person" - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Math.floor(root.centerWidth / 4) - } - - CachingImage { - id: pfp - - anchors.fill: parent - path: `${Paths.home}/.face` - } - } - - CustomRect { - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Appearance.padding.small * 2 - - color: DynamicColors.tPalette.m3surfaceContainer - radius: Appearance.rounding.full - - focus: true - onActiveFocusChanged: { - if (!activeFocus) - forceActiveFocus(); - } - - Keys.onPressed: event => { - if (root.lock.unlocking) - return; - - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) - inputField.placeholder.animate = false; - - root.lock.pam.handleKey(event); - } - - StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - - function onClicked(): void { - parent.forceActiveFocus(); - } - } - - RowLayout { - id: input - - anchors.fill: parent - anchors.margins: Appearance.padding.small - spacing: Appearance.spacing.normal - - Item { - implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 - - MaterialIcon { - id: fprintIcon - - anchors.centerIn: parent - animate: true - text: { - if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) - return "fingerprint_off"; - if (root.lock.pam.fprint.active) - return "fingerprint"; - return "lock"; - } - color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface - opacity: root.lock.pam.passwd.active ? 0 : 1 - - Behavior on opacity { - Anim {} - } - } - - CircularIndicator { - anchors.fill: parent - running: root.lock.pam.passwd.active - } - } - - InputField { - id: inputField - - pam: root.lock.pam - } - - CustomRect { - implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 - - color: root.lock.pam.buffer ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full - - StateLayer { - color: root.lock.pam.buffer ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - - function onClicked(): void { - root.lock.pam.passwd.start(); - } - } - - MaterialIcon { - id: enterIcon - - anchors.centerIn: parent - text: "arrow_forward" - color: root.lock.pam.buffer ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - font.weight: 500 - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.topMargin: -Appearance.spacing.large - - implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) - - Behavior on implicitHeight { - Anim {} - } - - CustomText { - id: stateMessage - - readonly property string msg: { - if (Hypr.kbLayout !== Hypr.defaultKbLayout) { - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.capsLock) - return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - if (Hypr.numLock) - return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); - return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); - } - - if (Hypr.capsLock && Hypr.numLock) - return qsTr("Caps lock and Num lock are ON."); - if (Hypr.capsLock) - return qsTr("Caps lock is ON."); - if (Hypr.numLock) - return qsTr("Num lock is ON."); - - return ""; - } - - property bool shouldBeVisible - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - } else { - text = msg; - } - shouldBeVisible = true; - } else { - shouldBeVisible = false; - } - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: shouldBeVisible && !message.msg ? 1 : 0.7 - opacity: shouldBeVisible && !message.msg ? 1 : 0 - color: DynamicColors.palette.m3onSurfaceVariant - animateProp: "opacity" - - font.family: Appearance.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - lineHeight: 1.2 - - Behavior on scale { - Anim {} - } - - Behavior on opacity { - Anim {} - } - } - - CustomText { - id: message - - readonly property Pam pam: root.lock.pam - readonly property string msg: { - if (pam.fprintState === "error") - return qsTr("FP ERROR: %1").arg(pam.fprint.message); - if (pam.state === "error") - return qsTr("PW ERROR: %1").arg(pam.passwd.message); - - if (pam.lockMessage) - return pam.lockMessage; - - if (pam.state === "max" && pam.fprintState === "max") - return qsTr("Maximum password and fingerprint attempts reached."); - if (pam.state === "max") { - if (pam.fprint.available) - return qsTr("Maximum password attempts reached. Please use fingerprint."); - return qsTr("Maximum password attempts reached."); - } - if (pam.fprintState === "max") - return qsTr("Maximum fingerprint attempts reached. Please use password."); - - if (pam.state === "fail") { - if (pam.fprint.available) - return qsTr("Incorrect password. Please try again or use fingerprint."); - return qsTr("Incorrect password. Please try again."); - } - if (pam.fprintState === "fail") - return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); - - return ""; - } - - anchors.left: parent.left - anchors.right: parent.right - - scale: 0.7 - opacity: 0 - color: DynamicColors.palette.m3error - - font.pointSize: Appearance.font.size.small - font.family: Appearance.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - - onMsgChanged: { - if (msg) { - if (opacity > 0) { - animate = true; - text = msg; - animate = false; - - exitAnim.stop(); - if (scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } else { - text = msg; - exitAnim.stop(); - appearAnim.restart(); - } - } else { - appearAnim.stop(); - flashAnim.stop(); - exitAnim.start(); - } - } - - Connections { - target: root.lock.pam - - function onFlashMsg(): void { - exitAnim.stop(); - if (message.scale < 1) - appearAnim.restart(); - else - flashAnim.restart(); - } - } - - Anim { - id: appearAnim - - target: message - properties: "scale,opacity" - to: 1 - onFinished: flashAnim.restart() - } - - SequentialAnimation { - id: flashAnim - - loops: 2 - - FlashAnim { - to: 0.3 - } - FlashAnim { - to: 1 - } - } - - ParallelAnimation { - id: exitAnim - - Anim { - target: message - property: "scale" - to: 0.7 - duration: Appearance.anim.durations.large - } - Anim { - target: message - property: "opacity" - to: 0 - duration: Appearance.anim.durations.large - } - } - } - } - - component FlashAnim: NumberAnimation { - target: message - property: "opacity" - duration: Appearance.anim.durations.small - easing.type: Easing.Linear - } + id: root + + readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) + readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale + required property var lock + + Layout.fillHeight: true + Layout.fillWidth: false + Layout.preferredWidth: centerWidth + spacing: Appearance.spacing.large * 2 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small + + CustomText { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3secondary + font.bold: true + font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) + text: Time.hourStr + } + + CustomText { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3primary + font.bold: true + font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) + text: ":" + } + + CustomText { + Layout.alignment: Qt.AlignVCenter + color: DynamicColors.palette.m3secondary + font.bold: true + font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) + text: Time.minuteStr + } + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: -Appearance.padding.large * 2 + color: DynamicColors.palette.m3tertiary + font.bold: true + font.family: Appearance.font.family.mono + font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) + text: Time.format("dddd, d MMMM yyyy") + } + + CustomClippingRect { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.large * 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.centerWidth / 2 + implicitWidth: root.centerWidth / 2 + radius: Appearance.rounding.full + + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Math.floor(root.centerWidth / 4) + text: "person" + } + + CachingImage { + id: pfp + + anchors.fill: parent + path: `${Paths.home}/.face` + } + } + + CustomRect { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.tPalette.m3surfaceContainer + focus: true + implicitHeight: input.implicitHeight + Appearance.padding.small * 2 + implicitWidth: root.centerWidth * 0.8 + radius: Appearance.rounding.full + + Keys.onPressed: event => { + if (root.lock.unlocking) + return; + + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) + inputField.placeholder.animate = false; + + root.lock.pam.handleKey(event); + } + onActiveFocusChanged: { + if (!activeFocus) + forceActiveFocus(); + } + + StateLayer { + function onClicked(): void { + parent.forceActiveFocus(); + } + + cursorShape: Qt.IBeamCursor + hoverEnabled: false + } + + RowLayout { + id: input + + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + + Item { + implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight + + MaterialIcon { + id: fprintIcon + + anchors.centerIn: parent + animate: true + color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurface + opacity: root.lock.pam.passwd.active ? 0 : 1 + text: { + if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) + return "fingerprint_off"; + if (root.lock.pam.fprint.active) + return "fingerprint"; + return "lock"; + } + + Behavior on opacity { + Anim { + } + } + } + + CircularIndicator { + anchors.fill: parent + running: root.lock.pam.passwd.active + } + } + + InputField { + id: inputField + + pam: root.lock.pam + } + + CustomRect { + color: root.lock.pam.buffer ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + StateLayer { + function onClicked(): void { + root.lock.pam.passwd.start(); + } + + color: root.lock.pam.buffer ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + } + + MaterialIcon { + id: enterIcon + + anchors.centerIn: parent + color: root.lock.pam.buffer ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + font.weight: 500 + text: "arrow_forward" + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.topMargin: -Appearance.spacing.large + implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) + + Behavior on implicitHeight { + Anim { + } + } + + CustomText { + id: stateMessage + + readonly property string msg: { + if (Hypr.kbLayout !== Hypr.defaultKbLayout) { + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.capsLock) + return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + if (Hypr.numLock) + return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); + return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); + } + + if (Hypr.capsLock && Hypr.numLock) + return qsTr("Caps lock and Num lock are ON."); + if (Hypr.capsLock) + return qsTr("Caps lock is ON."); + if (Hypr.numLock) + return qsTr("Num lock is ON."); + + return ""; + } + property bool shouldBeVisible + + anchors.left: parent.left + anchors.right: parent.right + animateProp: "opacity" + color: DynamicColors.palette.m3onSurfaceVariant + font.family: Appearance.font.family.mono + horizontalAlignment: Qt.AlignHCenter + lineHeight: 1.2 + opacity: shouldBeVisible && !message.msg ? 1 : 0 + scale: shouldBeVisible && !message.msg ? 1 : 0.7 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + + onMsgChanged: { + if (msg) { + if (opacity > 0) { + animate = true; + text = msg; + animate = false; + } else { + text = msg; + } + shouldBeVisible = true; + } else { + shouldBeVisible = false; + } + } + } + + CustomText { + id: message + + readonly property string msg: { + if (pam.fprintState === "error") + return qsTr("FP ERROR: %1").arg(pam.fprint.message); + if (pam.state === "error") + return qsTr("PW ERROR: %1").arg(pam.passwd.message); + + if (pam.lockMessage) + return pam.lockMessage; + + if (pam.state === "max" && pam.fprintState === "max") + return qsTr("Maximum password and fingerprint attempts reached."); + if (pam.state === "max") { + if (pam.fprint.available) + return qsTr("Maximum password attempts reached. Please use fingerprint."); + return qsTr("Maximum password attempts reached."); + } + if (pam.fprintState === "max") + return qsTr("Maximum fingerprint attempts reached. Please use password."); + + if (pam.state === "fail") { + if (pam.fprint.available) + return qsTr("Incorrect password. Please try again or use fingerprint."); + return qsTr("Incorrect password. Please try again."); + } + if (pam.fprintState === "fail") + return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); + + return ""; + } + readonly property Pam pam: root.lock.pam + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.palette.m3error + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.small + horizontalAlignment: Qt.AlignHCenter + opacity: 0 + scale: 0.7 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + onMsgChanged: { + if (msg) { + if (opacity > 0) { + animate = true; + text = msg; + animate = false; + + exitAnim.stop(); + if (scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } else { + text = msg; + exitAnim.stop(); + appearAnim.restart(); + } + } else { + appearAnim.stop(); + flashAnim.stop(); + exitAnim.start(); + } + } + + Connections { + function onFlashMsg(): void { + exitAnim.stop(); + if (message.scale < 1) + appearAnim.restart(); + else + flashAnim.restart(); + } + + target: root.lock.pam + } + + Anim { + id: appearAnim + + properties: "scale,opacity" + target: message + to: 1 + + onFinished: flashAnim.restart() + } + + SequentialAnimation { + id: flashAnim + + loops: 2 + + FlashAnim { + to: 0.3 + } + + FlashAnim { + to: 1 + } + } + + ParallelAnimation { + id: exitAnim + + Anim { + duration: Appearance.anim.durations.large + property: "scale" + target: message + to: 0.7 + } + + Anim { + duration: Appearance.anim.durations.large + property: "opacity" + target: message + to: 0 + } + } + } + } + + component FlashAnim: NumberAnimation { + duration: Appearance.anim.durations.small + easing.type: Easing.Linear + property: "opacity" + target: message + } } diff --git a/Modules/Lock/Content.qml b/Modules/Lock/Content.qml index 3174d8c..bf86285 100644 --- a/Modules/Lock/Content.qml +++ b/Modules/Lock/Content.qml @@ -5,78 +5,76 @@ import qs.Helpers import qs.Config RowLayout { - id: root + id: root - required property var lock + required property var lock - spacing: Appearance.spacing.large * 2 + spacing: Appearance.spacing.large * 2 - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - CustomRect { - Layout.fillWidth: true - implicitHeight: weather.implicitHeight + CustomRect { + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: weather.implicitHeight + radius: Appearance.rounding.small + topLeftRadius: Appearance.rounding.large - topLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small - color: DynamicColors.tPalette.m3surfaceContainer + WeatherInfo { + id: weather - WeatherInfo { - id: weather - - rootHeight: root.height - } - } - - CustomRect { - Layout.fillWidth: true - implicitHeight: resources.implicitHeight - - radius: Appearance.rounding.small - color: DynamicColors.tPalette.m3surfaceContainer - - Resources { - id: resources + rootHeight: root.height } - } + } - CustomClippingRect { - Layout.fillWidth: true - Layout.fillHeight: true + CustomRect { + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: resources.implicitHeight + radius: Appearance.rounding.small - bottomLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small - color: DynamicColors.tPalette.m3surfaceContainer + Resources { + id: resources - Media { - id: media + } + } - lock: root.lock - } - } - } + CustomClippingRect { + Layout.fillHeight: true + Layout.fillWidth: true + bottomLeftRadius: Appearance.rounding.large + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.small - Center { - lock: root.lock - } + Media { + id: media - ColumnLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.normal - CustomRect { - Layout.fillWidth: true - Layout.fillHeight: true + lock: root.lock + } + } + } - topRightRadius: Appearance.rounding.large - bottomRightRadius: Appearance.rounding.large - radius: Appearance.rounding.small - color: DynamicColors.tPalette.m3surfaceContainer + Center { + lock: root.lock + } - NotifDock { - lock: root.lock - } - } - } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + CustomRect { + Layout.fillHeight: true + Layout.fillWidth: true + bottomRightRadius: Appearance.rounding.large + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.small + topRightRadius: Appearance.rounding.large + + NotifDock { + lock: root.lock + } + } + } } diff --git a/Modules/Lock/Fetch.qml b/Modules/Lock/Fetch.qml index bdc93ef..32ff10c 100644 --- a/Modules/Lock/Fetch.qml +++ b/Modules/Lock/Fetch.qml @@ -9,156 +9,153 @@ import qs.Helpers import qs.Config ColumnLayout { - id: root + id: root - anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + anchors.topMargin: Appearance.padding.large + spacing: Appearance.spacing.small - spacing: Appearance.spacing.small + RowLayout { + Layout.fillHeight: false + Layout.fillWidth: true + spacing: Appearance.spacing.normal - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: Appearance.spacing.normal + CustomRect { + color: DynamicColors.palette.m3primary + implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.small - CustomRect { - implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 + MonoText { + id: prompt - color: DynamicColors.palette.m3primary - radius: Appearance.rounding.small + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + text: ">" + } + } - MonoText { - id: prompt + MonoText { + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + text: "caelestiafetch.sh" + } - anchors.centerIn: parent - text: ">" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal - color: DynamicColors.palette.m3onPrimary - } - } + WrappedLoader { + Layout.fillHeight: true + active: !iconLoader.active - MonoText { - Layout.fillWidth: true - text: "caelestiafetch.sh" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal - elide: Text.ElideRight - } + sourceComponent: OsLogo { + } + } + } - WrappedLoader { - Layout.fillHeight: true - active: !iconLoader.active + RowLayout { + Layout.fillHeight: false + Layout.fillWidth: true + spacing: height * 0.15 - sourceComponent: OsLogo {} - } - } + WrappedLoader { + id: iconLoader - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - spacing: height * 0.15 + Layout.fillHeight: true + active: root.width > 320 - WrappedLoader { - id: iconLoader + sourceComponent: OsLogo { + } + } - Layout.fillHeight: true - active: root.width > 320 + ColumnLayout { + Layout.bottomMargin: Appearance.padding.normal + Layout.fillWidth: true + Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 + Layout.topMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal - sourceComponent: OsLogo {} - } + WrappedLoader { + Layout.fillWidth: true + active: !batLoader.active && root.height > 200 - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: Appearance.padding.normal - Layout.bottomMargin: Appearance.padding.normal - Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 - spacing: Appearance.spacing.normal + sourceComponent: FetchText { + text: `OS : ${SystemInfo.osPrettyName || SysInfo.osName}` + } + } - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active && root.height > 200 + WrappedLoader { + Layout.fillWidth: true + active: root.height > (batLoader.active ? 200 : 110) - sourceComponent: FetchText { - text: `OS : ${SystemInfo.osPrettyName || SysInfo.osName}` - } - } + sourceComponent: FetchText { + text: `WM : ${SystemInfo.wm}` + } + } - WrappedLoader { - Layout.fillWidth: true - active: root.height > (batLoader.active ? 200 : 110) + WrappedLoader { + Layout.fillWidth: true + active: !batLoader.active || root.height > 110 - sourceComponent: FetchText { - text: `WM : ${SystemInfo.wm}` - } - } + sourceComponent: FetchText { + text: `USER: ${SystemInfo.user}` + } + } - WrappedLoader { - Layout.fillWidth: true - active: !batLoader.active || root.height > 110 + FetchText { + text: `UP : ${SystemInfo.uptime}` + } - sourceComponent: FetchText { - text: `USER: ${SystemInfo.user}` - } - } + WrappedLoader { + id: batLoader - FetchText { - text: `UP : ${SystemInfo.uptime}` - } + Layout.fillWidth: true + active: UPower.displayDevice.isLaptopBattery - WrappedLoader { - id: batLoader + sourceComponent: FetchText { + text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%` + } + } + } + } - Layout.fillWidth: true - active: UPower.displayDevice.isLaptopBattery + WrappedLoader { + Layout.alignment: Qt.AlignHCenter + active: root.height > 180 - sourceComponent: FetchText { - text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%` - } - } - } - } + sourceComponent: RowLayout { + spacing: Appearance.spacing.large - WrappedLoader { - Layout.alignment: Qt.AlignHCenter - active: root.height > 180 + Repeater { + model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) - sourceComponent: RowLayout { - spacing: Appearance.spacing.large + CustomRect { + required property int index - Repeater { - model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) + color: DynamicColors.palette[`term${index}`] + implicitHeight: Appearance.font.size.larger * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.small + } + } + } + } - CustomRect { - required property int index - - implicitWidth: implicitHeight - implicitHeight: Appearance.font.size.larger * 2 - color: DynamicColors.palette[`term${index}`] - radius: Appearance.rounding.small - } - } - } - } - - component WrappedLoader: Loader { - visible: active - } - - component OsLogo: ColoredIcon { - source: SystemInfo.osLogo - implicitSize: height - color: DynamicColors.palette.m3primary - layer.enabled: Config.lock.recolorLogo || SystemInfo.isDefaultLogo - } - - component FetchText: MonoText { - Layout.fillWidth: true - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal - elide: Text.ElideRight - } - - component MonoText: CustomText { - font.family: Appearance.font.family.mono - } + component FetchText: MonoText { + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + } + component MonoText: CustomText { + font.family: Appearance.font.family.mono + } + component OsLogo: ColoredIcon { + color: DynamicColors.palette.m3primary + implicitSize: height + layer.enabled: Config.lock.recolorLogo || SystemInfo.isDefaultLogo + source: SystemInfo.osLogo + } + component WrappedLoader: Loader { + visible: active + } } diff --git a/Modules/Lock/IdleInhibitor.qml b/Modules/Lock/IdleInhibitor.qml index e5312de..e69de29 100644 --- a/Modules/Lock/IdleInhibitor.qml +++ b/Modules/Lock/IdleInhibitor.qml @@ -1,39 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import Quickshell.Wayland -import qs.Config -import qs.Helpers - -Scope { - id: root - - required property Lock lock - readonly property bool enabled: !Players.list.some( p => p.isPlaying ) - - function handleIdleAction( action: var ): void { - if ( !action ) - return; - - if ( action === "lock" ) - lock.lock.locked = true; - else if ( action === "unlock" ) - lock.lock.locked = false; - else if ( typeof action === "string" ) - Hypr.dispatch( action ); - else - Quickshell.execDetached( action ); - } - - Variants { - model: Config.general.idle.timeouts - - IdleMonitor { - required property var modelData - - enabled: root.enabled && modelData.timeout > 0 ? true : false - timeout: modelData.timeout - onIsIdleChanged: root.handleIdleAction( isIdle ? modelData.idleAction : modelData.activeAction ) - } - } -} diff --git a/Modules/Lock/IdleMonitors.qml b/Modules/Lock/IdleMonitors.qml new file mode 100644 index 0000000..e5312de --- /dev/null +++ b/Modules/Lock/IdleMonitors.qml @@ -0,0 +1,39 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Wayland +import qs.Config +import qs.Helpers + +Scope { + id: root + + required property Lock lock + readonly property bool enabled: !Players.list.some( p => p.isPlaying ) + + function handleIdleAction( action: var ): void { + if ( !action ) + return; + + if ( action === "lock" ) + lock.lock.locked = true; + else if ( action === "unlock" ) + lock.lock.locked = false; + else if ( typeof action === "string" ) + Hypr.dispatch( action ); + else + Quickshell.execDetached( action ); + } + + Variants { + model: Config.general.idle.timeouts + + IdleMonitor { + required property var modelData + + enabled: root.enabled && modelData.timeout > 0 ? true : false + timeout: modelData.timeout + onIsIdleChanged: root.handleIdleAction( isIdle ? modelData.idleAction : modelData.activeAction ) + } + } +} diff --git a/Modules/Lock/InputField.qml b/Modules/Lock/InputField.qml index 1bfc40d..dedf7bd 100644 --- a/Modules/Lock/InputField.qml +++ b/Modules/Lock/InputField.qml @@ -9,142 +9,138 @@ import qs.Helpers import qs.Config Item { - id: root + id: root - required property Pam pam - readonly property alias placeholder: placeholder - property string buffer + property string buffer + required property Pam pam + readonly property alias placeholder: placeholder - Layout.fillWidth: true - Layout.fillHeight: true + Layout.fillHeight: true + Layout.fillWidth: true + clip: true - clip: true + Connections { + function onBufferChanged(): void { + if (root.pam.buffer.length > root.buffer.length) { + charList.bindImWidth(); + } else if (root.pam.buffer.length === 0) { + charList.implicitWidth = charList.implicitWidth; + placeholder.animate = true; + } - Connections { - target: root.pam + root.buffer = root.pam.buffer; + } - function onBufferChanged(): void { - if (root.pam.buffer.length > root.buffer.length) { - charList.bindImWidth(); - } else if (root.pam.buffer.length === 0) { - charList.implicitWidth = charList.implicitWidth; - placeholder.animate = true; - } + target: root.pam + } - root.buffer = root.pam.buffer; - } - } + CustomText { + id: placeholder - CustomText { - id: placeholder + anchors.centerIn: parent + animate: true + color: root.pam.passwd.active ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.normal + opacity: root.buffer ? 0 : 1 + text: { + if (root.pam.passwd.active) + return qsTr("Loading..."); + if (root.pam.state === "max") + return qsTr("You have reached the maximum number of tries"); + return qsTr("Enter your password"); + } - anchors.centerIn: parent + Behavior on opacity { + Anim { + } + } + } - text: { - if (root.pam.passwd.active) - return qsTr("Loading..."); - if (root.pam.state === "max") - return qsTr("You have reached the maximum number of tries"); - return qsTr("Enter your password"); - } + ListView { + id: charList - animate: true - color: root.pam.passwd.active ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + readonly property int fullWidth: count * (implicitHeight + spacing) - spacing - opacity: root.buffer ? 0 : 1 + function bindImWidth(): void { + imWidthBehavior.enabled = false; + implicitWidth = Qt.binding(() => fullWidth); + imWidthBehavior.enabled = true; + } - Behavior on opacity { - Anim {} - } - } + anchors.centerIn: parent + anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 + implicitHeight: Appearance.font.size.normal + implicitWidth: fullWidth + interactive: false + orientation: Qt.Horizontal + spacing: Appearance.spacing.small / 2 - ListView { - id: charList + delegate: CustomRect { + id: ch - readonly property int fullWidth: count * (implicitHeight + spacing) - spacing + color: DynamicColors.palette.m3onSurface + implicitHeight: charList.implicitHeight + implicitWidth: implicitHeight + opacity: 0 + radius: Appearance.rounding.small / 2 + scale: 0 - function bindImWidth(): void { - imWidthBehavior.enabled = false; - implicitWidth = Qt.binding(() => fullWidth); - imWidthBehavior.enabled = true; - } + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } - anchors.centerIn: parent - anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 + Component.onCompleted: { + opacity = 1; + scale = 1; + } + ListView.onRemove: removeAnim.start() - implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + SequentialAnimation { + id: removeAnim - orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 - interactive: false + PropertyAction { + property: "ListView.delayRemove" + target: ch + value: true + } - model: ScriptModel { - values: root.buffer.split("") - } + ParallelAnimation { + Anim { + property: "opacity" + target: ch + to: 0 + } - delegate: CustomRect { - id: ch + Anim { + property: "scale" + target: ch + to: 0.5 + } + } - implicitWidth: implicitHeight - implicitHeight: charList.implicitHeight + PropertyAction { + property: "ListView.delayRemove" + target: ch + value: false + } + } + } + Behavior on implicitWidth { + id: imWidthBehavior - color: DynamicColors.palette.m3onSurface - radius: Appearance.rounding.small / 2 - - opacity: 0 - scale: 0 - Component.onCompleted: { - opacity = 1; - scale = 1; - } - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: true - } - ParallelAnimation { - Anim { - target: ch - property: "opacity" - to: 0 - } - Anim { - target: ch - property: "scale" - to: 0.5 - } - } - PropertyAction { - target: ch - property: "ListView.delayRemove" - value: false - } - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - } - - Behavior on implicitWidth { - id: imWidthBehavior - - Anim {} - } - } + Anim { + } + } + model: ScriptModel { + values: root.buffer.split("") + } + } } diff --git a/Modules/Lock/Lock.qml b/Modules/Lock/Lock.qml index 54b41a4..083d876 100644 --- a/Modules/Lock/Lock.qml +++ b/Modules/Lock/Lock.qml @@ -18,11 +18,12 @@ Scope { WlSessionLock { id: lock - signal unlock signal requestLock + signal unlock LockSurface { id: lockSurface + lock: lock pam: pam } @@ -35,16 +36,17 @@ Scope { } IpcHandler { - target: "lock" - function lock() { return lock.locked = true; } + + target: "lock" } CustomShortcut { - name: "lock" description: "Lock the current session" + name: "lock" + onPressed: { lock.locked = true; } diff --git a/Modules/Lock/LockSurface.qml b/Modules/Lock/LockSurface.qml index c32e3f2..73ee33b 100644 --- a/Modules/Lock/LockSurface.qml +++ b/Modules/Lock/LockSurface.qml @@ -1,197 +1,218 @@ pragma ComponentBehavior: Bound -import Quickshell import Quickshell.Wayland import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import QtQuick.Effects import qs.Config import qs.Helpers -import qs.Effects import qs.Components -import qs.Modules as Modules WlSessionLockSurface { - id: root + id: root - required property WlSessionLock lock - required property Pam pam + required property WlSessionLock lock + required property Pam pam + readonly property alias unlocking: unlockAnim.running - readonly property alias unlocking: unlockAnim.running + color: "transparent" - color: "transparent" + Connections { + function onUnlock(): void { + unlockAnim.start(); + } - Connections { - target: root.lock + target: root.lock + } - function onUnlock(): void { - unlockAnim.start(); - } - } + SequentialAnimation { + id: unlockAnim - SequentialAnimation { - id: unlockAnim + ParallelAnimation { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + properties: "implicitWidth,implicitHeight" + target: lockContent + to: lockContent.size + } - ParallelAnimation { - Modules.Anim { - target: lockContent - properties: "implicitWidth,implicitHeight" - to: lockContent.size - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Modules.Anim { - target: lockBg - property: "radius" - to: lockContent.radius - } - Modules.Anim { - target: content - property: "scale" - to: 0 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Modules.Anim { - target: content - property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - } - Modules.Anim { - target: lockIcon - property: "opacity" - to: 1 - duration: Appearance.anim.durations.large - } - SequentialAnimation { - PauseAnimation { - duration: Appearance.anim.durations.small - } - Modules.Anim { - target: lockContent - property: "opacity" - to: 0 - } - } - } - PropertyAction { - target: root.lock - property: "locked" - value: false - } - } + Anim { + property: "radius" + target: lockBg + to: lockContent.radius + } - ParallelAnimation { - id: initAnim + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "scale" + target: content + to: 0 + } - running: true + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: content + to: 0 + } - SequentialAnimation { - ParallelAnimation { - Modules.Anim { - target: lockContent - property: "scale" - to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - ParallelAnimation { - Modules.Anim { - target: lockIcon - property: "opacity" - to: 0 - } - Modules.Anim { - target: content - property: "opacity" - to: 1 - } - Modules.Anim { - target: content - property: "scale" - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Modules.Anim { - target: lockBg - property: "radius" - to: Appearance.rounding.large * 1.5 - } - Modules.Anim { - target: lockContent - property: "implicitWidth" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - Modules.Anim { - target: lockContent - property: "implicitHeight" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } + Anim { + duration: Appearance.anim.durations.large + property: "opacity" + target: lockIcon + to: 1 + } + + SequentialAnimation { + PauseAnimation { + duration: Appearance.anim.durations.small + } + + Anim { + property: "opacity" + target: lockContent + to: 0 + } + } + } + + PropertyAction { + property: "locked" + target: root.lock + value: false + } + } + + ParallelAnimation { + id: initAnim + + running: true + + SequentialAnimation { + ParallelAnimation { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + property: "scale" + target: lockContent + to: 1 + } + } + + ParallelAnimation { + Anim { + property: "opacity" + target: lockIcon + to: 0 + } + + Anim { + property: "opacity" + target: content + to: 1 + } + + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "scale" + target: content + to: 1 + } + + Anim { + property: "radius" + target: lockBg + to: Appearance.rounding.large * 1.5 + } + + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "implicitWidth" + target: lockContent + to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio + } + + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "implicitHeight" + target: lockContent + to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult + } + } + } + } Image { id: background + anchors.fill: parent source: WallpaperPath.lockscreenBg } - Item { - id: lockContent + Item { + id: lockContent - readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 - readonly property int radius: size / 4 * Appearance.rounding.scale + readonly property int radius: size / 4 * Appearance.rounding.scale + readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 - anchors.centerIn: parent - implicitWidth: size - implicitHeight: size + anchors.centerIn: parent + implicitHeight: size + implicitWidth: size + scale: 0 - scale: 0 + // MultiEffect { + // anchors.fill: lockBg + // autoPaddingEnabled: false + // blur: 1 + // blurEnabled: true + // blurMax: 64 + // maskEnabled: true + // maskSource: lockBg + // + // source: ShaderEffectSource { + // sourceItem: background + // sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height) + // } + // } - CustomRect { - id: lockBg + CustomRect { + id: lockBg - anchors.fill: parent - color: DynamicColors.palette.m3surface + anchors.fill: parent + color: DynamicColors.palette.m3surface + layer.enabled: true + opacity: DynamicColors.transparency.enabled ? DynamicColors.transparency.base : 1 radius: lockContent.radius - opacity: DynamicColors.transparency.enabled ? DynamicColors.transparency.base : 1 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 0.7) - } - } + layer.effect: MultiEffect { + blurMax: 15 + shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 0.7) + shadowEnabled: true + } + } - MaterialIcon { - id: lockIcon + MaterialIcon { + id: lockIcon - anchors.centerIn: parent - text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 4 - font.bold: true - } + anchors.centerIn: parent + font.bold: true + font.pointSize: Appearance.font.size.extraLarge * 4 + text: "lock" + } - Content { - id: content + Content { + id: content - anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 - height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 - - lock: root - opacity: 0 - scale: 0 - } - } + anchors.centerIn: parent + height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 + lock: root + opacity: 0 + scale: 0 + width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 + } + } } diff --git a/Modules/Lock/Media.qml b/Modules/Lock/Media.qml index 4509a6d..a241e4b 100644 --- a/Modules/Lock/Media.qml +++ b/Modules/Lock/Media.qml @@ -8,198 +8,195 @@ import qs.Helpers import qs.Config Item { - id: root + id: root - required property var lock + required property var lock anchors.fill: parent - Image { - anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + Image { + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + layer.enabled: true + opacity: status === Image.Ready ? 1 : 0 + source: Players.active?.trackArtUrl ?? "" + sourceSize.height: height + sourceSize.width: width - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + layer.effect: OpacityMask { + maskSource: mask + } + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.extraLarge + } + } + } - layer.enabled: true - layer.effect: OpacityMask { - maskSource: mask - } + Rectangle { + id: mask - opacity: status === Image.Ready ? 1 : 0 + anchors.fill: parent + layer.enabled: true + visible: false - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.extraLarge - } - } - } + gradient: Gradient { + orientation: Gradient.Horizontal - Rectangle { - id: mask + GradientStop { + color: Qt.rgba(0, 0, 0, 0.5) + position: 0 + } - anchors.fill: parent - layer.enabled: true - visible: false + GradientStop { + color: Qt.rgba(0, 0, 0, 0.2) + position: 0.4 + } - gradient: Gradient { - orientation: Gradient.Horizontal + GradientStop { + color: Qt.rgba(0, 0, 0, 0) + position: 0.8 + } + } + } - GradientStop { - position: 0 - color: Qt.rgba(0, 0, 0, 0.5) - } - GradientStop { - position: 0.4 - color: Qt.rgba(0, 0, 0, 0.2) - } - GradientStop { - position: 0.8 - color: Qt.rgba(0, 0, 0, 0) - } - } - } + ColumnLayout { + id: layout - ColumnLayout { - id: layout + anchors.fill: parent + anchors.margins: Appearance.padding.large - anchors.fill: parent - anchors.margins: Appearance.padding.large + CustomText { + Layout.bottomMargin: Appearance.spacing.larger + Layout.topMargin: Appearance.padding.large + color: DynamicColors.palette.m3onSurfaceVariant + font.family: Appearance.font.family.mono + font.weight: 500 + text: qsTr("Now playing") + } - CustomText { - Layout.topMargin: Appearance.padding.large - Layout.bottomMargin: Appearance.spacing.larger - text: qsTr("Now playing") - color: DynamicColors.palette.m3onSurfaceVariant - font.family: Appearance.font.family.mono - font.weight: 500 - } + CustomText { + Layout.fillWidth: true + animate: true + color: DynamicColors.palette.m3primary + elide: Text.ElideRight + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.large + font.weight: 600 + horizontalAlignment: Text.AlignHCenter + text: Players.active?.trackArtist ?? qsTr("No media") + } - CustomText { - Layout.fillWidth: true - animate: true - text: Players.active?.trackArtist ?? qsTr("No media") - color: DynamicColors.palette.m3primary - horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono - font.weight: 600 - elide: Text.ElideRight - } + CustomText { + Layout.fillWidth: true + animate: true + elide: Text.ElideRight + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.larger + horizontalAlignment: Text.AlignHCenter + text: Players.active?.trackTitle ?? qsTr("No media") + } - CustomText { - Layout.fillWidth: true - animate: true - text: Players.active?.trackTitle ?? qsTr("No media") - horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger - font.family: Appearance.font.family.mono - elide: Text.ElideRight - } + RowLayout { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: Appearance.padding.large + Layout.topMargin: Appearance.spacing.large * 1.2 + spacing: Appearance.spacing.large - RowLayout { - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.large * 1.2 - Layout.bottomMargin: Appearance.padding.large + PlayerControl { + function onClicked(): void { + if (Players.active?.canGoPrevious) + Players.active.previous(); + } - spacing: Appearance.spacing.large + icon: "skip_previous" + } - PlayerControl { - icon: "skip_previous" + PlayerControl { + function onClicked(): void { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } - function onClicked(): void { - if (Players.active?.canGoPrevious) - Players.active.previous(); - } - } + active: Players.active?.isPlaying ?? false + animate: true + icon: active ? "pause" : "play_arrow" + level: active ? 2 : 1 + set_color: "Primary" + } - PlayerControl { - animate: true - icon: active ? "pause" : "play_arrow" - colour: "Primary" - level: active ? 2 : 1 - active: Players.active?.isPlaying ?? false + PlayerControl { + function onClicked(): void { + if (Players.active?.canGoNext) + Players.active.next(); + } - function onClicked(): void { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } - } + icon: "skip_next" + } + } + } - PlayerControl { - icon: "skip_next" + component PlayerControl: CustomRect { + id: control - function onClicked(): void { - if (Players.active?.canGoNext) - Players.active.next(); - } - } - } - } + property bool active + property alias animate: controlIcon.animate + property alias icon: controlIcon.text + property int level: 1 + property string set_color: "Secondary" - component PlayerControl: CustomRect { - id: control + function onClicked(): void { + } - property alias animate: controlIcon.animate - property alias icon: controlIcon.text - property bool active - property string colour: "Secondary" - property int level: 1 + Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) + 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 - function onClicked(): void { - } + 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 + } + } - Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) - implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 - implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 + Elevation { + anchors.fill: parent + level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level + radius: parent.radius + z: -1 + } - color: active ? DynamicColors.palette[`m3${colour.toLowerCase()}`] : DynamicColors.palette[`m3${colour.toLowerCase()}Container`] - radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal + StateLayer { + id: controlState - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level - } + function onClicked(): void { + control.onClicked(); + } - StateLayer { - id: controlState + color: control.active ? DynamicColors.palette[`m3on${control.colour}`] : DynamicColors.palette[`m3on${control.colour}Container`] + } - color: control.active ? DynamicColors.palette[`m3on${control.colour}`] : DynamicColors.palette[`m3on${control.colour}Container`] + MaterialIcon { + id: controlIcon - function onClicked(): void { - control.onClicked(); - } - } + anchors.centerIn: parent + color: control.active ? DynamicColors.palette[`m3on${control.colour}`] : DynamicColors.palette[`m3on${control.colour}Container`] + fill: control.active ? 1 : 0 + font.pointSize: Appearance.font.size.large - MaterialIcon { - id: controlIcon - - anchors.centerIn: parent - color: control.active ? DynamicColors.palette[`m3on${control.colour}`] : DynamicColors.palette[`m3on${control.colour}Container`] - font.pointSize: Appearance.font.size.large - fill: control.active ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - - 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 - } - } - } + Behavior on fill { + Anim { + } + } + } + } } diff --git a/Modules/Lock/NotifDock.qml b/Modules/Lock/NotifDock.qml index 6fca53c..15f4c90 100644 --- a/Modules/Lock/NotifDock.qml +++ b/Modules/Lock/NotifDock.qml @@ -11,135 +11,131 @@ import qs.Config import qs.Daemons ColumnLayout { - id: root + id: root - required property var lock + required property var lock - anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.smaller - spacing: Appearance.spacing.smaller + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.family: Appearance.font.family.mono + font.weight: 500 + text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications") + } - CustomText { - Layout.fillWidth: true - text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications") - color: DynamicColors.palette.m3outline - font.family: Appearance.font.family.mono - font.weight: 500 - elide: Text.ElideRight - } + ClippingRectangle { + id: clipRect - ClippingRectangle { - id: clipRect + Layout.fillHeight: true + Layout.fillWidth: true + color: "transparent" + radius: Appearance.rounding.small - Layout.fillWidth: true - Layout.fillHeight: true + Loader { + active: opacity > 0 + anchors.centerIn: parent + opacity: NotifServer.list.length > 0 ? 0 : 1 - radius: Appearance.rounding.small - color: "transparent" + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.extraLarge + } + } + sourceComponent: ColumnLayout { + spacing: Appearance.spacing.large - Loader { - anchors.centerIn: parent - active: opacity > 0 - opacity: NotifServer.list.length > 0 ? 0 : 1 + Image { + asynchronous: true + fillMode: Image.PreserveAspectFit + layer.enabled: true + source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) + sourceSize.width: clipRect.width * 0.8 - sourceComponent: ColumnLayout { - spacing: Appearance.spacing.large + layer.effect: Coloriser { + brightness: 1 + colorizationColor: DynamicColors.palette.m3outlineVariant + } + } - Image { - asynchronous: true - source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) - fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outlineVariant + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.large + font.weight: 500 + text: qsTr("No Notifications") + } + } + } - layer.enabled: true - layer.effect: Coloriser { - colorizationColor: DynamicColors.palette.m3outlineVariant - brightness: 1 - } - } + CustomListView { + anchors.fill: parent + clip: true + spacing: Appearance.spacing.small - CustomText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") - color: DynamicColors.palette.m3outlineVariant - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono - font.weight: 500 - } - } + add: Transition { + Anim { + from: 0 + property: "opacity" + to: 1 + } - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.extraLarge - } - } - } + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + from: 0 + property: "scale" + to: 1 + } + } + delegate: NotifGroup { + } + displaced: Transition { + Anim { + properties: "opacity,scale" + to: 1 + } - CustomListView { - anchors.fill: parent + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "y" + } + } + model: ScriptModel { + values: { + const list = NotifServer.notClosed.map(n => [n.appName, null]); + return [...new Map(list).keys()]; + } + } + move: Transition { + Anim { + properties: "opacity,scale" + to: 1 + } - spacing: Appearance.spacing.small - clip: true + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "y" + } + } + remove: Transition { + Anim { + property: "opacity" + to: 0 + } - model: ScriptModel { - values: { - const list = NotifServer.notClosed.map(n => [n.appName, null]); - return [...new Map(list).keys()]; - } - } - - delegate: NotifGroup {} - - add: Transition { - Anim { - property: "opacity" - from: 0 - to: 1 - } - Anim { - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - remove: Transition { - Anim { - property: "opacity" - to: 0 - } - Anim { - property: "scale" - to: 0.6 - } - } - - move: Transition { - Anim { - properties: "opacity,scale" - to: 1 - } - Anim { - property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - displaced: Transition { - Anim { - properties: "opacity,scale" - to: 1 - } - Anim { - property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } + Anim { + property: "scale" + to: 0.6 + } + } + } + } } diff --git a/Modules/Lock/NotifGroup.qml b/Modules/Lock/NotifGroup.qml index 9c74445..a0d0d21 100644 --- a/Modules/Lock/NotifGroup.qml +++ b/Modules/Lock/NotifGroup.qml @@ -12,305 +12,304 @@ import qs.Config import qs.Daemons CustomRect { - id: root + id: root - required property string modelData + readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" + property bool expanded + readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" + required property string modelData + readonly property list notifs: NotifServer.list.filter(notif => notif.appName === modelData) + readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" - readonly property list notifs: NotifServer.list.filter(notif => notif.appName === modelData) - readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" - readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" + anchors.left: parent?.left + anchors.right: parent?.right + clip: true + color: root.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal - property bool expanded + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } - anchors.left: parent?.left - anchors.right: parent?.right - implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + RowLayout { + id: content - clip: true - radius: Appearance.rounding.normal - color: root.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + anchors.left: parent.left + anchors.margins: Appearance.padding.normal + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal - RowLayout { - id: content + Item { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + implicitHeight: Config.notifs.sizes.image + implicitWidth: Config.notifs.sizes.image - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Appearance.padding.normal + Component { + id: imageComp - spacing: Appearance.spacing.normal + Image { + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop + height: Config.notifs.sizes.image + source: Qt.resolvedUrl(root.image) + width: Config.notifs.sizes.image + } + } - Item { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + Component { + id: appIconComp - Component { - id: imageComp + ColoredIcon { + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer + implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + layer.enabled: root.appIcon.endsWith("symbolic") + source: Quickshell.iconPath(root.appIcon) + } + } - Image { - source: Qt.resolvedUrl(root.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image - } - } + Component { + id: materialIconComp - Component { - id: appIconComp + MaterialIcon { + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) + } + } - ColoredIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) - source: Quickshell.iconPath(root.appIcon) - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") - } - } + ClippingRectangle { + anchors.fill: parent + color: root.urgency === "critical" ? DynamicColors.palette.m3error : root.urgency === "low" ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3) : DynamicColors.palette.m3secondaryContainer + radius: Appearance.rounding.full - Component { - id: materialIconComp + Loader { + anchors.centerIn: parent + sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp + } + } - MaterialIcon { - text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large - } - } + Loader { + active: root.appIcon && root.image + anchors.bottom: parent.bottom + anchors.right: parent.right - ClippingRectangle { - anchors.fill: parent - color: root.urgency === "critical" ? DynamicColors.palette.m3error : root.urgency === "low" ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3) : DynamicColors.palette.m3secondaryContainer - radius: Appearance.rounding.full + sourceComponent: CustomRect { + color: root.urgency === "critical" ? DynamicColors.palette.m3error : root.urgency === "low" ? DynamicColors.palette.m3surfaceContainerHighest : DynamicColors.palette.m3secondaryContainer + implicitHeight: Config.notifs.sizes.badge + implicitWidth: Config.notifs.sizes.badge + radius: Appearance.rounding.full - Loader { - anchors.centerIn: parent - sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp - } - } + ColoredIcon { + anchors.centerIn: parent + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer + implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + layer.enabled: root.appIcon.endsWith("symbolic") + source: Quickshell.iconPath(root.appIcon) + } + } + } + } - Loader { - anchors.right: parent.right - anchors.bottom: parent.bottom - active: root.appIcon && root.image + ColumnLayout { + Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.fillWidth: true + Layout.topMargin: -Appearance.padding.small + spacing: Math.round(Appearance.spacing.small / 2) - sourceComponent: CustomRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + RowLayout { + Layout.bottomMargin: -parent.spacing + Layout.fillWidth: true + spacing: Appearance.spacing.smaller - color: root.urgency === "critical" ? DynamicColors.palette.m3error : root.urgency === "low" ? DynamicColors.palette.m3surfaceContainerHighest : DynamicColors.palette.m3secondaryContainer - radius: Appearance.rounding.full + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + text: root.modelData + } - ColoredIcon { - anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) - source: Quickshell.iconPath(root.appIcon) - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : root.urgency === "low" ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") - } - } - } - } + CustomText { + animate: true + color: DynamicColors.palette.m3outline + font.pointSize: Appearance.font.size.small + text: root.notifs[0]?.timeStr ?? "" + } - ColumnLayout { - Layout.topMargin: -Appearance.padding.small - Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) - Layout.fillWidth: true - spacing: Math.round(Appearance.spacing.small / 2) + CustomRect { + Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 + color: root.urgency === "critical" ? DynamicColors.palette.m3error : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + implicitHeight: groupCount.implicitHeight + Appearance.padding.small + implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 + opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 + radius: Appearance.rounding.full - RowLayout { - Layout.bottomMargin: -parent.spacing - Layout.fillWidth: true - spacing: Appearance.spacing.smaller + Behavior on Layout.preferredWidth { + Anim { + } + } + Behavior on opacity { + Anim { + } + } - CustomText { - Layout.fillWidth: true - text: root.modelData - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small - elide: Text.ElideRight - } + StateLayer { + function onClicked(): void { + root.expanded = !root.expanded; + } - CustomText { - animate: true - text: root.notifs[0]?.timeStr ?? "" - color: DynamicColors.palette.m3outline - font.pointSize: Appearance.font.size.small - } + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + } - CustomRect { - implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Appearance.padding.small + RowLayout { + id: expandBtn - color: root.urgency === "critical" ? DynamicColors.palette.m3error : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) - radius: Appearance.rounding.full + anchors.centerIn: parent + spacing: Appearance.spacing.small / 2 - opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 - Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 + CustomText { + id: groupCount - StateLayer { - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + Layout.leftMargin: Appearance.padding.small / 2 + animate: true + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + font.pointSize: Appearance.font.size.small + text: root.notifs.length + } - function onClicked(): void { - root.expanded = !root.expanded; - } - } + MaterialIcon { + Layout.rightMargin: -Appearance.padding.small / 2 + animate: true + color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + text: root.expanded ? "expand_less" : "expand_more" + } + } + } + } - RowLayout { - id: expandBtn + Repeater { + model: ScriptModel { + values: root.notifs.slice(0, Config.notifs.groupPreviewNum) + } - anchors.centerIn: parent - spacing: Appearance.spacing.small / 2 + NotifLine { + id: notif - CustomText { - id: groupCount + ParallelAnimation { + running: true - Layout.leftMargin: Appearance.padding.small / 2 - animate: true - text: root.notifs.length - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface - font.pointSize: Appearance.font.size.small - } + Anim { + from: 0 + property: "opacity" + target: notif + to: 1 + } - MaterialIcon { - Layout.rightMargin: -Appearance.padding.small / 2 - animate: true - text: root.expanded ? "expand_less" : "expand_more" - color: root.urgency === "critical" ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface - } - } + Anim { + from: 0.7 + property: "scale" + target: notif + to: 1 + } - Behavior on opacity { - Anim {} - } + Anim { + from: 0 + property: "preferredHeight" + target: notif.Layout + to: notif.implicitHeight + } + } - Behavior on Layout.preferredWidth { - Anim {} - } - } - } + ParallelAnimation { + running: notif.modelData.closed - Repeater { - model: ScriptModel { - values: root.notifs.slice(0, Config.notifs.groupPreviewNum) - } + onFinished: notif.modelData.unlock(notif) - NotifLine { - id: notif + Anim { + property: "opacity" + target: notif + to: 0 + } - ParallelAnimation { - running: true + Anim { + property: "scale" + target: notif + to: 0.7 + } - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 - } - Anim { - target: notif.Layout - property: "preferredHeight" - from: 0 - to: notif.implicitHeight - } - } + Anim { + property: "preferredHeight" + target: notif.Layout + to: 0 + } + } + } + } - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) + Loader { + Layout.fillWidth: true + Layout.preferredHeight: root.expanded ? implicitHeight : 0 + active: opacity > 0 + opacity: root.expanded ? 1 : 0 - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "scale" - to: 0.7 - } - Anim { - target: notif.Layout - property: "preferredHeight" - to: 0 - } - } - } - } + Behavior on opacity { + Anim { + } + } + sourceComponent: ColumnLayout { + Repeater { + model: ScriptModel { + values: root.notifs.slice(Config.notifs.groupPreviewNum) + } - Loader { - Layout.fillWidth: true + NotifLine { + } + } + } + } + } + } - opacity: root.expanded ? 1 : 0 - Layout.preferredHeight: root.expanded ? implicitHeight : 0 - active: opacity > 0 + component NotifLine: CustomText { + id: notifLine - sourceComponent: ColumnLayout { - Repeater { - model: ScriptModel { - values: root.notifs.slice(Config.notifs.groupPreviewNum) - } + required property NotifServer.Notif modelData - NotifLine {} - } - } + Layout.fillWidth: true + color: root.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + text: { + const summary = modelData.summary.replace(/\n/g, " "); + const body = modelData.body.replace(/\n/g, " "); + const color = root.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline; - Behavior on opacity { - Anim {} - } - } - } - } + if (metrics.text === metrics.elidedText) + return `${summary} ${body}`; - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + const t = metrics.elidedText.length - 3; + if (t < summary.length) + return `${summary.slice(0, t)}...`; - component NotifLine: CustomText { - id: notifLine + return `${summary} ${body.slice(0, t - summary.length)}...`; + } + textFormat: Text.MarkdownText - required property NotifServer.Notif modelData + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: { - const summary = modelData.summary.replace(/\n/g, " "); - const body = modelData.body.replace(/\n/g, " "); - const color = root.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline; + TextMetrics { + id: metrics - if (metrics.text === metrics.elidedText) - return `${summary} ${body}`; - - const t = metrics.elidedText.length - 3; - if (t < summary.length) - return `${summary.slice(0, t)}...`; - - return `${summary} ${body.slice(0, t - summary.length)}...`; - } - color: root.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - TextMetrics { - id: metrics - - text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ") - font.pointSize: notifLine.font.pointSize - font.family: notifLine.font.family - elideWidth: notifLine.width - elide: Text.ElideRight - } - } + elide: Text.ElideRight + elideWidth: notifLine.width + font.family: notifLine.font.family + font.pointSize: notifLine.font.pointSize + text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ") + } + } } diff --git a/Modules/Lock/Pam.qml b/Modules/Lock/Pam.qml index 9009b83..ea31407 100644 --- a/Modules/Lock/Pam.qml +++ b/Modules/Lock/Pam.qml @@ -6,188 +6,189 @@ import QtQuick import qs.Config Scope { - id: root + id: root - required property WlSessionLock lock + property string buffer + readonly property alias fprint: fprint + property string fprintState + required property WlSessionLock lock + property string lockMessage + readonly property alias passwd: passwd + property string state - readonly property alias passwd: passwd - readonly property alias fprint: fprint - property string lockMessage - property string state - property string fprintState - property string buffer + signal flashMsg - signal flashMsg + function handleKey(event: KeyEvent): void { + if (passwd.active || state === "max") + return; - function handleKey(event: KeyEvent): void { - if (passwd.active || state === "max") - return; + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { + passwd.start(); + } else if (event.key === Qt.Key_Backspace) { + if (event.modifiers & Qt.ControlModifier) { + buffer = ""; + } else { + buffer = buffer.slice(0, -1); + } + } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { + // No illegal characters (you are insane if you use unicode in your password) + buffer += event.text; + } + } - if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { - passwd.start(); - } else if (event.key === Qt.Key_Backspace) { - if (event.modifiers & Qt.ControlModifier) { - buffer = ""; - } else { - buffer = buffer.slice(0, -1); - } - } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { - // No illegal characters (you are insane if you use unicode in your password) - buffer += event.text; - } - } + PamContext { + id: passwd - PamContext { - id: passwd + config: "passwd" + configDirectory: Quickshell.shellDir + "/assets/pam.d" - config: "passwd" - configDirectory: Quickshell.shellDir + "/assets/pam.d" + onCompleted: res => { + if (res === PamResult.Success) + return root.lock.unlock(); - onMessageChanged: { - if (message.startsWith("The account is locked")) - root.lockMessage = message; - else if (root.lockMessage && message.endsWith(" left to unlock)")) - root.lockMessage += "\n" + message; - } + if (res === PamResult.Error) + root.state = "error"; + else if (res === PamResult.MaxTries) + root.state = "max"; + else if (res === PamResult.Failed) + root.state = "fail"; - onResponseRequiredChanged: { - if (!responseRequired) - return; + root.flashMsg(); + stateReset.restart(); + } + onMessageChanged: { + if (message.startsWith("The account is locked")) + root.lockMessage = message; + else if (root.lockMessage && message.endsWith(" left to unlock)")) + root.lockMessage += "\n" + message; + } + onResponseRequiredChanged: { + if (!responseRequired) + return; - respond(root.buffer); - root.buffer = ""; - } + respond(root.buffer); + root.buffer = ""; + } + } - onCompleted: res => { - if (res === PamResult.Success) - return root.lock.unlock(); + PamContext { + id: fprint - if (res === PamResult.Error) - root.state = "error"; - else if (res === PamResult.MaxTries) - root.state = "max"; - else if (res === PamResult.Failed) - root.state = "fail"; + property bool available + property int errorTries + property int tries - root.flashMsg(); - stateReset.restart(); - } - } + function checkAvail(): void { + if (!available || !Config.lock.enableFprint || !root.lock.secure) { + abort(); + return; + } - PamContext { - id: fprint + tries = 0; + errorTries = 0; + start(); + } - property bool available - property int tries - property int errorTries + config: "fprint" + configDirectory: Quickshell.shellDir + "/assets/pam.d" - function checkAvail(): void { - if (!available || !Config.lock.enableFprint || !root.lock.secure) { - abort(); - return; - } + onCompleted: res => { + if (!available) + return; - tries = 0; - errorTries = 0; - start(); - } + if (res === PamResult.Success) + return root.lock.unlock(); - config: "fprint" - configDirectory: Quickshell.shellDir + "/assets/pam.d" + if (res === PamResult.Error) { + root.fprintState = "error"; + errorTries++; + if (errorTries < 5) { + abort(); + errorRetry.restart(); + } + } else if (res === PamResult.MaxTries) { + // Isn't actually the real max tries as pam only reports completed + // when max tries is reached. + tries++; + if (tries < Config.lock.maxFprintTries) { + // Restart if not actually real max tries + root.fprintState = "fail"; + start(); + } else { + root.fprintState = "max"; + abort(); + } + } - onCompleted: res => { - if (!available) - return; + root.flashMsg(); + fprintStateReset.start(); + } + } - if (res === PamResult.Success) - return root.lock.unlock(); + Process { + id: availProc - if (res === PamResult.Error) { - root.fprintState = "error"; - errorTries++; - if (errorTries < 5) { - abort(); - errorRetry.restart(); - } - } else if (res === PamResult.MaxTries) { - // Isn't actually the real max tries as pam only reports completed - // when max tries is reached. - tries++; - if (tries < Config.lock.maxFprintTries) { - // Restart if not actually real max tries - root.fprintState = "fail"; - start(); - } else { - root.fprintState = "max"; - abort(); - } - } + command: ["sh", "-c", "fprintd-list $USER"] - root.flashMsg(); - fprintStateReset.start(); - } - } + onExited: code => { + fprint.available = code === 0; + fprint.checkAvail(); + } + } - Process { - id: availProc + Timer { + id: errorRetry - command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { - fprint.available = code === 0; - fprint.checkAvail(); - } - } + interval: 800 - Timer { - id: errorRetry + onTriggered: fprint.start() + } - interval: 800 - onTriggered: fprint.start() - } + Timer { + id: stateReset - Timer { - id: stateReset + interval: 4000 - interval: 4000 - onTriggered: { - if (root.state !== "max") - root.state = ""; - } - } + onTriggered: { + if (root.state !== "max") + root.state = ""; + } + } - Timer { - id: fprintStateReset + Timer { + id: fprintStateReset - interval: 4000 - onTriggered: { - root.fprintState = ""; - fprint.errorTries = 0; - } - } + interval: 4000 - Connections { - target: root.lock + onTriggered: { + root.fprintState = ""; + fprint.errorTries = 0; + } + } - function onSecureChanged(): void { - if (root.lock.secure) { - availProc.running = true; - root.buffer = ""; - root.state = ""; - root.fprintState = ""; - root.lockMessage = ""; - } - } + Connections { + function onSecureChanged(): void { + if (root.lock.secure) { + availProc.running = true; + root.buffer = ""; + root.state = ""; + root.fprintState = ""; + root.lockMessage = ""; + } + } - function onUnlock(): void { - fprint.abort(); - } - } + function onUnlock(): void { + fprint.abort(); + } - Connections { - target: Config.lock + target: root.lock + } - function onEnableFprintChanged(): void { - fprint.checkAvail(); - } - } + Connections { + function onEnableFprintChanged(): void { + fprint.checkAvail(); + } + + target: Config.lock + } } diff --git a/Modules/Lock/Resources.qml b/Modules/Lock/Resources.qml index 3eaa2d7..060c717 100644 --- a/Modules/Lock/Resources.qml +++ b/Modules/Lock/Resources.qml @@ -6,75 +6,73 @@ import qs.Helpers import qs.Config GridLayout { - id: root + id: root - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Appearance.padding.large + anchors.left: parent.left + anchors.margins: Appearance.padding.large + anchors.right: parent.right + columnSpacing: Appearance.spacing.large + columns: 2 + rowSpacing: Appearance.spacing.large + rows: 1 - rowSpacing: Appearance.spacing.large - columnSpacing: Appearance.spacing.large - rows: 1 - columns: 2 + Ref { + service: SystemUsage + } - Ref { - service: SystemUsage - } + Resource { + Layout.bottomMargin: Appearance.padding.large + Layout.topMargin: Appearance.padding.large + colour: DynamicColors.palette.m3primary + icon: "memory" + value: SystemUsage.cpuPerc + } - Resource { - Layout.bottomMargin: Appearance.padding.large - Layout.topMargin: Appearance.padding.large - icon: "memory" - value: SystemUsage.cpuPerc - colour: DynamicColors.palette.m3primary - } + Resource { + Layout.bottomMargin: Appearance.padding.large + Layout.topMargin: Appearance.padding.large + colour: DynamicColors.palette.m3secondary + icon: "memory_alt" + value: SystemUsage.memPerc + } - Resource { - Layout.bottomMargin: Appearance.padding.large - Layout.topMargin: Appearance.padding.large - icon: "memory_alt" - value: SystemUsage.memPerc - colour: DynamicColors.palette.m3secondary - } + component Resource: CustomRect { + id: res - component Resource: CustomRect { - id: res + required property color colour + required property string icon + required property real value - required property string icon - required property real value - required property color colour + Layout.fillWidth: true + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: width + radius: Appearance.rounding.large - Layout.fillWidth: true - implicitHeight: width + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.large + CircularProgress { + id: circ - CircularProgress { - id: circ + anchors.fill: parent + bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3) + fgColour: res.colour + padding: Appearance.padding.large * 3 + strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal + value: res.value + } - anchors.fill: parent - value: res.value - padding: Appearance.padding.large * 3 - fgColour: res.colour - bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3) - strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal - } + MaterialIcon { + id: icon - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: res.icon - color: res.colour - font.pointSize: (circ.arcRadius * 0.7) || 1 - font.weight: 600 - } - - Behavior on value { - Anim { - duration: Appearance.anim.durations.large - } - } - } + anchors.centerIn: parent + color: res.colour + font.pointSize: (circ.arcRadius * 0.7) || 1 + font.weight: 600 + text: res.icon + } + } } diff --git a/Modules/Lock/UserImage.qml b/Modules/Lock/UserImage.qml index 987fefe..60fcfb0 100644 --- a/Modules/Lock/UserImage.qml +++ b/Modules/Lock/UserImage.qml @@ -7,18 +7,18 @@ Item { id: root ClippingRectangle { - radius: 1000 anchors.fill: parent + radius: 1000 + Image { id: userImage anchors.fill: parent - - sourceSize.width: parent.width - sourceSize.height: parent.height asynchronous: true fillMode: Image.PreserveAspectCrop source: `${Paths.home}/.face` + sourceSize.height: parent.height + sourceSize.width: parent.width } } } diff --git a/Modules/Lock/WeatherInfo.qml b/Modules/Lock/WeatherInfo.qml index b7c8e77..56bcdb5 100644 --- a/Modules/Lock/WeatherInfo.qml +++ b/Modules/Lock/WeatherInfo.qml @@ -7,169 +7,163 @@ import qs.Helpers import qs.Config ColumnLayout { - id: root + id: root - required property int rootHeight + required property int rootHeight - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: Appearance.padding.large * 2 + anchors.left: parent.left + anchors.margins: Appearance.padding.large * 2 + anchors.right: parent.right + spacing: Appearance.spacing.small - spacing: Appearance.spacing.small + Loader { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: -Appearance.padding.large + Layout.topMargin: Appearance.padding.large * 2 + active: root.rootHeight > 610 + visible: active - Loader { - Layout.topMargin: Appearance.padding.large * 2 - Layout.bottomMargin: -Appearance.padding.large - Layout.alignment: Qt.AlignHCenter + sourceComponent: CustomText { + color: DynamicColors.palette.m3primary + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + text: qsTr("Weather") + } + } - active: root.rootHeight > 610 - visible: active + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.large - sourceComponent: CustomText { - text: qsTr("Weather") - color: DynamicColors.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - } - } + MaterialIcon { + animate: true + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge * 2.5 + text: Weather.icon + } - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.large + ColumnLayout { + spacing: Appearance.spacing.small - MaterialIcon { - animate: true - text: Weather.icon - color: DynamicColors.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge * 2.5 - } + CustomText { + Layout.fillWidth: true + animate: true + color: DynamicColors.palette.m3secondary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + font.weight: 500 + text: Weather.description + } - ColumnLayout { - spacing: Appearance.spacing.small + CustomText { + Layout.fillWidth: true + animate: true + color: DynamicColors.palette.m3onSurfaceVariant + elide: Text.ElideRight + font.pointSize: Appearance.font.size.normal + text: qsTr("Humidity: %1%").arg(Weather.humidity) + } + } - CustomText { - Layout.fillWidth: true + Loader { + Layout.rightMargin: Appearance.padding.smaller + active: root.width > 400 + visible: active - animate: true - text: Weather.description - color: DynamicColors.palette.m3secondary - font.pointSize: Appearance.font.size.large - font.weight: 500 - elide: Text.ElideRight - } + sourceComponent: ColumnLayout { + spacing: Appearance.spacing.small - CustomText { - Layout.fillWidth: true + CustomText { + Layout.fillWidth: true + animate: true + color: DynamicColors.palette.m3primary + elide: Text.ElideLeft + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + horizontalAlignment: Text.AlignRight + text: Weather.temp + } - animate: true - text: qsTr("Humidity: %1%").arg(Weather.humidity) - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal - elide: Text.ElideRight - } - } + CustomText { + Layout.fillWidth: true + animate: true + color: DynamicColors.palette.m3outline + elide: Text.ElideLeft + font.pointSize: Appearance.font.size.smaller + horizontalAlignment: Text.AlignRight + text: qsTr("Feels like: %1").arg(Weather.feelsLike) + } + } + } + } - Loader { - Layout.rightMargin: Appearance.padding.smaller - active: root.width > 400 - visible: active + Loader { + id: forecastLoader - sourceComponent: ColumnLayout { - spacing: Appearance.spacing.small + Layout.bottomMargin: Appearance.padding.large * 2 + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.smaller + active: root.rootHeight > 820 + visible: active - CustomText { - Layout.fillWidth: true + sourceComponent: RowLayout { + spacing: Appearance.spacing.large - animate: true - text: Weather.temp - color: DynamicColors.palette.m3primary - horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.extraLarge - font.weight: 500 - elide: Text.ElideLeft - } + Repeater { + model: { + const forecast = Weather.hourlyForecast; + const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5; + if (!forecast) + return Array.from({ + length: count + }, () => null); - CustomText { - Layout.fillWidth: true + return forecast.slice(0, count); + } - animate: true - text: qsTr("Feels like: %1").arg(Weather.feelsLike) - color: DynamicColors.palette.m3outline - horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.smaller - elide: Text.ElideLeft - } - } - } - } + ColumnLayout { + id: forecastHour - Loader { - id: forecastLoader + required property var modelData - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: Appearance.padding.large * 2 - Layout.fillWidth: true + Layout.fillWidth: true + spacing: Appearance.spacing.small - active: root.rootHeight > 820 - visible: active + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3outline + font.pointSize: Appearance.font.size.larger + horizontalAlignment: Text.AlignHCenter + text: { + const hour = forecastHour.modelData?.hour ?? 0; + return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`; + } + } - sourceComponent: RowLayout { - spacing: Appearance.spacing.large + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + font.pointSize: Appearance.font.size.extraLarge * 1.5 + font.weight: 500 + text: forecastHour.modelData?.icon ?? "cloud_alert" + } - Repeater { - model: { - const forecast = Weather.hourlyForecast; - const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5; - if (!forecast) - return Array.from({ - length: count - }, () => null); + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.larger + text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` + } + } + } + } + } - return forecast.slice(0, count); - } + Timer { + interval: 900000 // 15 minutes + repeat: true + running: true + triggeredOnStart: true - ColumnLayout { - id: forecastHour - - required property var modelData - - Layout.fillWidth: true - spacing: Appearance.spacing.small - - CustomText { - Layout.fillWidth: true - text: { - const hour = forecastHour.modelData?.hour ?? 0; - return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`; - } - color: DynamicColors.palette.m3outline - horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger - } - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: forecastHour.modelData?.icon ?? "cloud_alert" - font.pointSize: Appearance.font.size.extraLarge * 1.5 - font.weight: 500 - } - - CustomText { - Layout.alignment: Qt.AlignHCenter - text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` - color: DynamicColors.palette.m3secondary - font.pointSize: Appearance.font.size.larger - } - } - } - } - } - - Timer { - running: true - triggeredOnStart: true - repeat: true - interval: 900000 // 15 minutes - onTriggered: Weather.reload() - } + onTriggered: Weather.reload() + } } diff --git a/Modules/MediaWidget.qml b/Modules/MediaWidget.qml new file mode 100644 index 0000000..cbb67d2 --- /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: root.parent.height - ((Appearance.padding.small - 1) * 2) + 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/Network/NetworkPopout.qml b/Modules/Network/NetworkPopout.qml index e9bcd6a..519bc6c 100644 --- a/Modules/Network/NetworkPopout.qml +++ b/Modules/Network/NetworkPopout.qml @@ -14,8 +14,6 @@ Item { required property var wrapper - Component.onCompleted: console.log(Networking.backend.toString()) - ColumnLayout { id: layout @@ -26,12 +24,12 @@ Item { CustomRadioButton { id: network - visible: modelData.name !== "lo" required property NetworkDevice modelData checked: Helpers.Network.activeDevice?.name === modelData.name text: modelData.description + visible: modelData.name !== "lo" } } } diff --git a/Modules/Network/NetworkWidget.qml b/Modules/Network/NetworkWidget.qml index ac1e378..53463ee 100644 --- a/Modules/Network/NetworkWidget.qml +++ b/Modules/Network/NetworkWidget.qml @@ -7,19 +7,19 @@ import qs.Modules Item { id: root - anchors.top: parent.top anchors.bottom: parent.bottom - + anchors.top: parent.top implicitWidth: layout.implicitWidth RowLayout { id: layout - anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.top: parent.top MaterialIcon { - text: "android_wifi_4_bar" Layout.alignment: Qt.AlignVCenter + text: "android_wifi_4_bar" } } } diff --git a/Modules/NotifBell.qml b/Modules/NotifBell.qml index 3f43d1e..ebe9746 100644 --- a/Modules/NotifBell.qml +++ b/Modules/NotifBell.qml @@ -6,40 +6,42 @@ import qs.Helpers import qs.Components Item { - id: root + id: root - required property PersistentProperties visibilities required property Wrapper popouts + required property PersistentProperties visibilities - implicitWidth: 25 - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: 30 CustomRect { + anchors.bottomMargin: 3 anchors.fill: parent anchors.topMargin: 3 - anchors.bottomMargin: 3 color: "transparent" radius: 4 + MaterialIcon { id: notificationCenterIcon - anchors.centerIn: parent - property color iconColor: DynamicColors.palette.m3onSurface - text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" - font.family: "Material Symbols Rounded" - font.pixelSize: 20 + anchors.centerIn: parent color: iconColor + font.family: "Material Symbols Rounded" + font.pointSize: 16 + text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" Behavior on color { - CAnim {} + CAnim { + } } } StateLayer { cursorShape: Qt.PointingHandCursor + onClicked: { root.visibilities.sidebar = !root.visibilities.sidebar; } diff --git a/Modules/Notifications/Background.qml b/Modules/Notifications/Background.qml index 07e45f1..9e68e20 100644 --- a/Modules/Notifications/Background.qml +++ b/Modules/Notifications/Background.qml @@ -1,54 +1,59 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes ShapePath { - id: root + id: root - required property Wrapper wrapper - required property var sidebar - readonly property real rounding: 8 - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + readonly property bool flatten: wrapper.height < rounding * 2 + readonly property real rounding: 8 + readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + required property var sidebar + required property Wrapper wrapper - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathLine { - relativeX: -(root.wrapper.width + root.rounding) - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.sidebar.notifsRoundingX - relativeY: root.roundingY - radiusX: root.sidebar.notifsRoundingX - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.rounding - radiusX: root.rounding - radiusY: root.rounding - } + Behavior on fillColor { + CAnim { + } + } - Behavior on fillColor { - Modules.CAnim {} - } + PathLine { + relativeX: -(root.wrapper.width + root.rounding) + relativeY: 0 + } + + 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 - root.roundingY * 2 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: root.sidebar.notifsRoundingX + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.sidebar.notifsRoundingX + relativeY: root.roundingY + } + + PathLine { + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width + relativeY: 0 + } + + PathArc { + radiusX: root.rounding + radiusY: root.rounding + relativeX: root.rounding + relativeY: root.rounding + } } diff --git a/Modules/Notifications/Content.qml b/Modules/Notifications/Content.qml index 90447c9..b153743 100644 --- a/Modules/Notifications/Content.qml +++ b/Modules/Notifications/Content.qml @@ -6,198 +6,196 @@ import Quickshell.Widgets import QtQuick Item { - id: root + id: root - required property PersistentProperties visibilities - required property Item panels - readonly property int padding: 6 + readonly property int padding: 6 + required property Item panels + required property PersistentProperties visibilities - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: { + const count = list.count; + if (count === 0) + return 0; - implicitWidth: Config.notifs.sizes.width + padding * 2 - implicitHeight: { - const count = list.count; - if (count === 0) - return 0; + let height = (count - 1) * 8; + for (let i = 0; i < count; i++) + height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - let height = (count - 1) * 8; - for (let i = 0; i < count; i++) - height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; + if (visibilities && panels) { + if (visibilities.osd) { + const h = panels.osd.y - 8 * 2 - padding * 2; + if (height > h) + height = h; + } - if (visibilities && panels) { - if (visibilities.osd) { - const h = panels.osd.y - 8 * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities.session) { + const h = panels.session.y - 8 * 2 - padding * 2; + if (height > h) + height = h; + } + } - if (visibilities.session) { - const h = panels.session.y - 8 * 2 - padding * 2; - if (height > h) - height = h; - } - } + return Math.min((QsWindow.window?.screen?.height ?? 0) - 1 * 2, height + padding * 2); + } + implicitWidth: Config.notifs.sizes.width + padding * 2 - return Math.min((QsWindow.window?.screen?.height ?? 0) - 1 * 2, height + padding * 2); - } + Behavior on implicitHeight { + Anim { + } + } - ClippingWrapperRectangle { - anchors.fill: parent - anchors.margins: root.padding + ClippingWrapperRectangle { + anchors.fill: parent + anchors.margins: root.padding + color: "transparent" + radius: 4 - color: "transparent" - radius: 4 + CustomListView { + id: list - CustomListView { - id: list + anchors.fill: parent + cacheBuffer: QsWindow.window?.screen.height ?? 0 + orientation: Qt.Vertical + spacing: 0 - model: ScriptModel { - values: NotifServer.popups.filter(n => !n.closed) - } + delegate: Item { + id: wrapper - anchors.fill: parent + property int idx + required property int index + required property NotifServer.Notif modelData + readonly property alias nonAnimHeight: notif.nonAnimHeight - orientation: Qt.Vertical - spacing: 0 - cacheBuffer: QsWindow.window?.screen.height ?? 0 + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : 8) + implicitWidth: notif.implicitWidth - delegate: Item { - id: wrapper + ListView.onRemove: removeAnim.start() + onIndexChanged: { + if (index !== -1) + idx = index; + } - required property NotifServer.Notif modelData - required property int index - readonly property alias nonAnimHeight: notif.nonAnimHeight - property int idx + SequentialAnimation { + id: removeAnim - onIndexChanged: { - if (index !== -1) - idx = index; - } + PropertyAction { + property: "ListView.delayRemove" + target: wrapper + value: true + } - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : 8) + PropertyAction { + property: "enabled" + target: wrapper + value: false + } - ListView.onRemove: removeAnim.start() + PropertyAction { + property: "implicitHeight" + target: wrapper + value: 0 + } - SequentialAnimation { - id: removeAnim + PropertyAction { + property: "z" + target: wrapper + value: 1 + } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: true - } - PropertyAction { - target: wrapper - property: "enabled" - value: false - } - PropertyAction { - target: wrapper - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: wrapper - property: "z" - value: 1 - } - Anim { - target: notif - property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: false - } - } + property: "x" + target: notif + to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + } - ClippingRectangle { - anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : 8 + PropertyAction { + property: "ListView.delayRemove" + target: wrapper + value: false + } + } - color: "transparent" - radius: 4 - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + ClippingRectangle { + anchors.top: parent.top + anchors.topMargin: wrapper.idx === 0 ? 0 : 8 + color: "transparent" + implicitHeight: notif.implicitHeight + implicitWidth: notif.implicitWidth + radius: 4 - Notification { - id: notif + Notification { + id: notif - modelData: wrapper.modelData - } - } - } + modelData: wrapper.modelData + } + } + } + displaced: Transition { + Anim { + property: "y" + } + } + model: ScriptModel { + values: NotifServer.popups.filter(n => !n.closed) + } + move: Transition { + Anim { + property: "y" + } + } - move: Transition { - Anim { - property: "y" - } - } + ExtraIndicator { + anchors.top: parent.top + extra: { + const count = list.count; + if (count === 0) + return 0; - displaced: Transition { - Anim { - property: "y" - } - } + const scrollY = list.contentY; - ExtraIndicator { - anchors.top: parent.top - extra: { - const count = list.count; - if (count === 0) - return 0; + let height = 0; + for (let i = 0; i < count; i++) { + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; - const scrollY = list.contentY; + if (height - 8 >= scrollY) + return i; + } - let height = 0; - for (let i = 0; i < count; i++) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; + return count; + } + } - if (height - 8 >= scrollY) - return i; - } + ExtraIndicator { + anchors.bottom: parent.bottom + extra: { + const count = list.count; + if (count === 0) + return 0; - return count; - } - } + const scrollY = list.contentHeight - (list.contentY + list.height); - ExtraIndicator { - anchors.bottom: parent.bottom - extra: { - const count = list.count; - if (count === 0) - return 0; + let height = 0; + for (let i = count - 1; i >= 0; i--) { + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; - const scrollY = list.contentHeight - (list.contentY + list.height); + if (height - 8 >= scrollY) + return count - i - 1; + } - let height = 0; - for (let i = count - 1; i >= 0; i--) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + 8; + return 0; + } + } + } + } - if (height - 8 >= scrollY) - return count - i - 1; - } - - return 0; - } - } - } - } - - Behavior on implicitHeight { - Anim {} - } - - component Anim: NumberAnimation { - easing.type: Easing.BezierSpline + component Anim: NumberAnimation { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } + easing.type: Easing.BezierSpline + } } diff --git a/Modules/Notifications/Notification.qml b/Modules/Notifications/Notification.qml index fd94053..aaa3579 100644 --- a/Modules/Notifications/Notification.qml +++ b/Modules/Notifications/Notification.qml @@ -12,473 +12,456 @@ import QtQuick import QtQuick.Layouts CustomRect { - id: root + id: root - required property NotifServer.Notif modelData - readonly property bool hasImage: modelData.image.length > 0 - readonly property bool hasAppIcon: modelData.appIcon.length > 0 - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 - property bool expanded: Config.notifs.openExpanded + property bool expanded: Config.notifs.openExpanded + readonly property bool hasAppIcon: modelData.appIcon.length > 0 + readonly property bool hasImage: modelData.image.length > 0 + required property NotifServer.Notif modelData + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer - radius: 6 - implicitWidth: Config.notifs.sizes.width - implicitHeight: inner.implicitHeight + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer + implicitHeight: inner.implicitHeight + implicitWidth: Config.notifs.sizes.width + radius: 6 + x: Config.notifs.sizes.width - x: Config.notifs.sizes.width - Component.onCompleted: { - x = 0; - modelData.lock(this); - } - Component.onDestruction: modelData.unlock(this) - - Behavior on x { - Anim { + Behavior on x { + Anim { easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } - MouseArea { - property int startY + Component.onCompleted: { + x = 0; + modelData.lock(this); + } + Component.onDestruction: modelData.unlock(this) - anchors.fill: parent - hoverEnabled: true - cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.MiddleButton - preventStealing: true + MouseArea { + property int startY - onEntered: root.modelData.timer.stop() - onExited: { - if (!pressed) - root.modelData.timer.start(); - } + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + anchors.fill: parent + cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + drag.axis: Drag.XAxis + drag.target: parent + hoverEnabled: true + preventStealing: true - drag.target: parent - drag.axis: Drag.XAxis + onClicked: event => { + if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) + return; - onPressed: event => { - root.modelData.timer.stop(); - startY = event.y; - if (event.button === Qt.MiddleButton) - root.modelData.close(); - } - onReleased: event => { - if (!containsMouse) - root.modelData.timer.start(); + const actions = root.modelData.actions; + if (actions?.length === 1) + actions[0].invoke(); + } + onEntered: root.modelData.timer.stop() + onExited: { + if (!pressed) + root.modelData.timer.start(); + } + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.expanded = diffY > 0; + } + } + onPressed: event => { + root.modelData.timer.stop(); + startY = event.y; + if (event.button === Qt.MiddleButton) + root.modelData.close(); + } + onReleased: event => { + if (!containsMouse) + root.modelData.timer.start(); - if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) - root.x = 0; - else - root.modelData.popup = false; - } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.expanded = diffY > 0; - } - } - onClicked: event => { - if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) - return; + if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) + root.x = 0; + else + root.modelData.popup = false; + } - const actions = root.modelData.actions; - if (actions?.length === 1) - actions[0].invoke(); - } + Item { + id: inner - Item { - id: inner + anchors.left: parent.left + anchors.margins: 8 + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: root.nonAnimHeight - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - - implicitHeight: root.nonAnimHeight - - Behavior on implicitHeight { - Anim { + Behavior on implicitHeight { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } - Loader { - id: image + Loader { + id: image - active: root.hasImage - - anchors.left: parent.left - anchors.top: parent.top - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image - visible: root.hasImage || root.hasAppIcon + active: root.hasImage + anchors.left: parent.left + anchors.top: parent.top asynchronous: true + height: Config.notifs.sizes.image + visible: root.hasImage || root.hasAppIcon + width: Config.notifs.sizes.image - sourceComponent: ClippingRectangle { - radius: 1000 - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + sourceComponent: ClippingRectangle { + implicitHeight: Config.notifs.sizes.image + implicitWidth: Config.notifs.sizes.image + radius: 1000 - Image { - anchors.fill: parent - source: Qt.resolvedUrl(root.modelData.image) - fillMode: Image.PreserveAspectCrop + Image { + anchors.fill: parent + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop mipmap: true - cache: false - asynchronous: true - } - } - } + source: Qt.resolvedUrl(root.modelData.image) + } + } + } - Loader { - id: appIcon + Loader { + id: appIcon - active: root.hasAppIcon || !root.hasImage - - anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter - anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter - anchors.right: root.hasImage ? image.right : undefined - anchors.bottom: root.hasImage ? image.bottom : undefined + active: root.hasAppIcon || !root.hasImage + anchors.bottom: root.hasImage ? image.bottom : undefined + anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter + anchors.right: root.hasImage ? image.right : undefined + anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter asynchronous: true - sourceComponent: CustomRect { - radius: 1000 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + sourceComponent: CustomRect { + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + radius: 1000 - Loader { - id: icon + Loader { + id: icon - active: root.hasAppIcon + active: root.hasAppIcon + anchors.centerIn: parent + asynchronous: true + height: Math.round(parent.width * 0.6) + width: Math.round(parent.width * 0.6) - anchors.centerIn: parent + sourceComponent: CustomIcon { + anchors.fill: parent + layer.enabled: root.modelData.appIcon.endsWith("symbolic") + source: Quickshell.iconPath(root.modelData.appIcon) + } + } + + Loader { + active: !root.hasAppIcon + anchors.centerIn: parent + anchors.horizontalCenterOffset: -18 * 0.02 + anchors.verticalCenterOffset: 18 * 0.02 asynchronous: true - width: Math.round(parent.width * 0.6) - height: Math.round(parent.width * 0.6) + sourceComponent: MaterialIcon { + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer + font.pointSize: 18 + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + } + } + } + } - sourceComponent: CustomIcon { - anchors.fill: parent - source: Quickshell.iconPath(root.modelData.appIcon) - layer.enabled: root.modelData.appIcon.endsWith("symbolic") - } - } + CustomText { + id: appName - Loader { - active: !root.hasAppIcon - anchors.centerIn: parent - anchors.horizontalCenterOffset: -18 * 0.02 - anchors.verticalCenterOffset: 18 * 0.02 - asynchronous: true + anchors.left: image.right + anchors.leftMargin: 10 + anchors.top: parent.top + animate: true + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + maximumLineCount: 1 + opacity: root.expanded ? 1 : 0 + text: appNameMetrics.elidedText - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + Behavior on opacity { + Anim { + } + } + } - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - font.pointSize: 18 - } - } - } - } + TextMetrics { + id: appNameMetrics - CustomText { - id: appName + elide: Text.ElideRight + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 + font.family: appName.font.family + font.pointSize: appName.font.pointSize + text: root.modelData.appName + } - anchors.top: parent.top - anchors.left: image.right - anchors.leftMargin: 10 + CustomText { + id: summary - animate: true - text: appNameMetrics.elidedText - maximumLineCount: 1 - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 + anchors.left: image.right + anchors.leftMargin: 10 + anchors.top: parent.top + animate: true + height: implicitHeight + maximumLineCount: 1 + text: summaryMetrics.elidedText - opacity: root.expanded ? 1 : 0 + Behavior on height { + Anim { + } + } + states: State { + name: "expanded" + when: root.expanded - Behavior on opacity { - Anim {} - } - } + PropertyChanges { + summary.maximumLineCount: undefined + } - TextMetrics { - id: appNameMetrics + AnchorChanges { + anchors.top: appName.bottom + target: summary + } + } + transitions: Transition { + PropertyAction { + property: "maximumLineCount" + target: summary + } - text: root.modelData.appName - font.family: appName.font.family - font.pointSize: appName.font.pointSize - elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 - } - - CustomText { - id: summary - - anchors.top: parent.top - anchors.left: image.right - anchors.leftMargin: 10 - - animate: true - text: summaryMetrics.elidedText - maximumLineCount: 1 - height: implicitHeight - - states: State { - name: "expanded" - when: root.expanded - - PropertyChanges { - summary.maximumLineCount: undefined - } - - AnchorChanges { - target: summary - anchors.top: appName.bottom - } - } - - transitions: Transition { - PropertyAction { - target: summary - property: "maximumLineCount" - } - AnchorAnimation { - easing.type: Easing.BezierSpline + AnchorAnimation { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - Behavior on height { - Anim {} - } - } - - TextMetrics { - id: summaryMetrics - - text: root.modelData.summary - font.family: summary.font.family - font.pointSize: summary.font.pointSize - elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 - } - - CustomText { - id: timeSep - - anchors.top: parent.top - anchors.left: summary.right - anchors.leftMargin: 7 - - text: "•" - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - - states: State { - name: "expanded" - when: root.expanded - - AnchorChanges { - target: timeSep - anchors.left: appName.right - } - } - - transitions: Transition { - AnchorAnimation { easing.type: Easing.BezierSpline + } + } + } + + TextMetrics { + id: summaryMetrics + + elide: Text.ElideRight + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 + font.family: summary.font.family + font.pointSize: summary.font.pointSize + text: root.modelData.summary + } + + CustomText { + id: timeSep + + anchors.left: summary.right + anchors.leftMargin: 7 + anchors.top: parent.top + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + text: "•" + + states: State { + name: "expanded" + when: root.expanded + + AnchorChanges { + anchors.left: appName.right + target: timeSep + } + } + transitions: Transition { + AnchorAnimation { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } + easing.type: Easing.BezierSpline + } + } + } - CustomText { - id: time + CustomText { + id: time - anchors.top: parent.top - anchors.left: timeSep.right - anchors.leftMargin: 7 + anchors.left: timeSep.right + anchors.leftMargin: 7 + anchors.top: parent.top + animate: true + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + horizontalAlignment: Text.AlignLeft + text: root.modelData.timeStr + } - animate: true - horizontalAlignment: Text.AlignLeft - text: root.modelData.timeStr - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - } + Item { + id: expandBtn - Item { - id: expandBtn + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: expandIcon.height + implicitWidth: expandIcon.height - anchors.right: parent.right - anchors.top: parent.top + StateLayer { + function onClicked() { + root.expanded = !root.expanded; + } - implicitWidth: expandIcon.height - implicitHeight: expandIcon.height + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + radius: 1000 + } - StateLayer { - radius: 1000 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + MaterialIcon { + id: expandIcon - function onClicked() { - root.expanded = !root.expanded; - } - } + anchors.centerIn: parent + animate: true + font.pointSize: 13 + text: root.expanded ? "expand_less" : "expand_more" + } + } - MaterialIcon { - id: expandIcon + CustomText { + id: bodyPreview - anchors.centerIn: parent + anchors.left: summary.left + anchors.right: expandBtn.left + anchors.rightMargin: 7 + anchors.top: summary.bottom + animate: true + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + opacity: root.expanded ? 0 : 1 + text: bodyPreviewMetrics.elidedText + textFormat: Text.MarkdownText - animate: true - text: root.expanded ? "expand_less" : "expand_more" - font.pointSize: 13 - } - } + Behavior on opacity { + Anim { + } + } + } - CustomText { - id: bodyPreview + TextMetrics { + id: bodyPreviewMetrics - anchors.left: summary.left - anchors.right: expandBtn.left - anchors.top: summary.bottom - anchors.rightMargin: 7 + elide: Text.ElideRight + elideWidth: bodyPreview.width + font.family: bodyPreview.font.family + font.pointSize: bodyPreview.font.pointSize + text: root.modelData.body + } - animate: true - textFormat: Text.MarkdownText - text: bodyPreviewMetrics.elidedText - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 + CustomText { + id: body - opacity: root.expanded ? 0 : 1 + anchors.left: summary.left + anchors.right: expandBtn.left + anchors.rightMargin: 7 + anchors.top: summary.bottom + animate: true + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + height: text ? implicitHeight : 0 + opacity: root.expanded ? 1 : 0 + text: root.modelData.body + textFormat: Text.MarkdownText + wrapMode: Text.WrapAtWordBoundaryOrAnywhere - Behavior on opacity { - Anim {} - } - } + Behavior on opacity { + Anim { + } + } - TextMetrics { - id: bodyPreviewMetrics + onLinkActivated: link => { + if (!root.expanded) + return; - text: root.modelData.body - font.family: bodyPreview.font.family - font.pointSize: bodyPreview.font.pointSize - elide: Text.ElideRight - elideWidth: bodyPreview.width - } + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.modelData.popup = false; + } + } - CustomText { - id: body + RowLayout { + id: actions - anchors.left: summary.left - anchors.right: expandBtn.left - anchors.top: summary.bottom - anchors.rightMargin: 7 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: body.bottom + anchors.topMargin: 7 + opacity: root.expanded ? 1 : 0 + spacing: 10 - animate: true - textFormat: Text.MarkdownText - text: root.modelData.body - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - height: text ? implicitHeight : 0 + Behavior on opacity { + Anim { + } + } - onLinkActivated: link => { - if (!root.expanded) - return; + Action { + modelData: QtObject { + readonly property string text: qsTr("Close") - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.modelData.popup = false; - } + function invoke(): void { + root.modelData.close(); + } + } + } - opacity: root.expanded ? 1 : 0 + Repeater { + model: root.modelData.actions - Behavior on opacity { - Anim {} - } - } + delegate: Component { + Action { + } + } + } + } + } + } - RowLayout { - id: actions + component Action: CustomRect { + id: action - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: body.bottom - anchors.topMargin: 7 + required property var modelData - spacing: 10 + Layout.preferredHeight: actionText.height + 4 * 2 + Layout.preferredWidth: actionText.width + 8 * 2 + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + implicitHeight: actionText.height + 4 * 2 + implicitWidth: actionText.width + 8 * 2 + radius: 1000 - opacity: root.expanded ? 1 : 0 + StateLayer { + function onClicked(): void { + action.modelData.invoke(); + } - Behavior on opacity { - Anim {} - } + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface + radius: 1000 + } - Action { - modelData: QtObject { - readonly property string text: qsTr("Close") - function invoke(): void { - root.modelData.close(); - } - } - } + CustomText { + id: actionText - Repeater { - model: root.modelData.actions + anchors.centerIn: parent + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant + font.pointSize: 10 + text: actionTextMetrics.elidedText + } - delegate: Component { - Action {} - } - } - } - } - } + TextMetrics { + id: actionTextMetrics - component Action: CustomRect { - id: action - - required property var modelData - - radius: 1000 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - - Layout.preferredWidth: actionText.width + 8 * 2 - Layout.preferredHeight: actionText.height + 4 * 2 - implicitWidth: actionText.width + 8 * 2 - implicitHeight: actionText.height + 4 * 2 - - StateLayer { - radius: 1000 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface - - function onClicked(): void { - action.modelData.invoke(); - } - } - - CustomText { - id: actionText - - anchors.centerIn: parent - text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - } - - TextMetrics { - id: actionTextMetrics - - text: action.modelData.text - font.family: actionText.font.family - font.pointSize: actionText.font.pointSize - elide: Text.ElideRight - elideWidth: { - const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2; - } - } - } + elide: Text.ElideRight + elideWidth: { + const numActions = root.modelData.actions.length + 1; + return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2; + } + font.family: actionText.font.family + font.pointSize: actionText.font.pointSize + text: action.modelData.text + } + } } diff --git a/Modules/Notifications/Sidebar/Background.qml b/Modules/Notifications/Sidebar/Background.qml index 3ca0a60..c41693b 100644 --- a/Modules/Notifications/Sidebar/Background.qml +++ b/Modules/Notifications/Sidebar/Background.qml @@ -1,54 +1,54 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes ShapePath { - id: root - - required property Wrapper wrapper - required property var panels - - readonly property real rounding: 8 - - readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width - readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding - - readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width - readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding + id: root readonly property bool flatten: wrapper.width < rounding * 2 + readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding + readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width + required property var panels + readonly property real rounding: 8 + readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding + readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width + required property Wrapper wrapper - strokeWidth: -1 fillColor: flatten ? "transparent" : DynamicColors.palette.m3surface + strokeWidth: -1 - PathLine { - relativeX: -root.wrapper.width - root.notifsRoundingX - relativeY: 0 - } - PathArc { - relativeX: root.notifsRoundingX - relativeY: root.rounding - radiusX: root.notifsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: -root.utilsRoundingX - relativeY: root.rounding - radiusX: root.utilsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: root.wrapper.width + root.utilsRoundingX - relativeY: 0 - } + Behavior on fillColor { + CAnim { + } + } - Behavior on fillColor { - Modules.CAnim {} - } + PathLine { + relativeX: -root.wrapper.width - root.notifsRoundingX + relativeY: 0 + } + + PathArc { + radiusX: root.notifsRoundingX + radiusY: root.rounding + relativeX: root.notifsRoundingX + relativeY: root.rounding + } + + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 2 + } + + PathArc { + radiusX: root.utilsRoundingX + radiusY: root.rounding + relativeX: -root.utilsRoundingX + relativeY: root.rounding + } + + PathLine { + relativeX: root.wrapper.width + root.utilsRoundingX + relativeY: 0 + } } diff --git a/Modules/Notifications/Sidebar/Content.qml b/Modules/Notifications/Sidebar/Content.qml index 7465efc..8ca9b36 100644 --- a/Modules/Notifications/Sidebar/Content.qml +++ b/Modules/Notifications/Sidebar/Content.qml @@ -4,36 +4,34 @@ import QtQuick import QtQuick.Layouts Item { - id: root + id: root - required property Props props - required property var visibilities + required property Props props + required property var visibilities - ColumnLayout { - id: layout + ColumnLayout { + id: layout - anchors.fill: parent - spacing: 8 + anchors.fill: parent + spacing: 8 - CustomRect { - Layout.fillWidth: true - Layout.fillHeight: true + CustomRect { + Layout.fillHeight: true + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainerLow + radius: 8 - radius: 8 - color: DynamicColors.tPalette.m3surfaceContainerLow + NotifDock { + props: root.props + visibilities: root.visibilities + } + } - NotifDock { - props: root.props - visibilities: root.visibilities - } - } - - CustomRect { - Layout.topMargin: 8 - layout.spacing - Layout.fillWidth: true - implicitHeight: 1 - - color: DynamicColors.tPalette.m3outlineVariant - } - } + CustomRect { + Layout.fillWidth: true + Layout.topMargin: 8 - layout.spacing + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + } + } } diff --git a/Modules/Notifications/Sidebar/Notif.qml b/Modules/Notifications/Sidebar/Notif.qml index 5ef7a42..1ee3e11 100644 --- a/Modules/Notifications/Sidebar/Notif.qml +++ b/Modules/Notifications/Sidebar/Notif.qml @@ -9,158 +9,153 @@ import QtQuick import QtQuick.Layouts CustomRect { - id: root + id: root - required property NotifServer.Notif modelData - required property Props props - required property bool expanded - required property var visibilities + readonly property CustomText body: expandedContent.item?.body ?? null + required property bool expanded + required property NotifServer.Notif modelData + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + 10 * 2 : summaryHeightMetrics.height + required property Props props + required property var visibilities - readonly property CustomText body: expandedContent.item?.body ?? null - readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + 10 * 2 : summaryHeightMetrics.height + color: { + const c = root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); + return expanded ? c : Qt.alpha(c, 0); + } + implicitHeight: nonAnimHeight + radius: 6 - implicitHeight: nonAnimHeight - - radius: 6 - color: { - const c = root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - return expanded ? c : Qt.alpha(c, 0); - } - - states: State { - name: "expanded" - when: root.expanded - - PropertyChanges { - summary.anchors.margins: 10 - dummySummary.anchors.margins: 10 - compactBody.anchors.margins: 10 - timeStr.anchors.margins: 10 - expandedContent.anchors.margins: 10 - summary.width: root.width - 10 * 2 - timeStr.implicitWidth - 7 - summary.maximumLineCount: Number.MAX_SAFE_INTEGER - } - } - - transitions: Transition { - Anim { - properties: "margins,width,maximumLineCount" - } - } - - TextMetrics { - id: summaryHeightMetrics - - font: summary.font - text: " " // Use this height to prevent weird characters from changing the line height - } - - CustomText { - id: summary - - anchors.top: parent.top - anchors.left: parent.left - - width: parent.width - text: root.modelData.summary - color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface - elide: Text.ElideRight - wrapMode: Text.WordWrap - maximumLineCount: 1 - } - - CustomText { - id: dummySummary - - anchors.top: parent.top - anchors.left: parent.left - - visible: false - text: root.modelData.summary - } - - WrappedLoader { - id: compactBody - - shouldBeActive: !root.expanded - anchors.top: parent.top - anchors.left: dummySummary.right - anchors.right: parent.right - anchors.leftMargin: 7 - - sourceComponent: CustomText { - textFormat: Text.StyledText - text: root.modelData.body.replace(/\n/g, " ") - color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline - elide: Text.ElideRight - } - } - - WrappedLoader { - id: timeStr - - shouldBeActive: root.expanded - anchors.top: parent.top - anchors.right: parent.right - - sourceComponent: CustomText { - animate: true - text: root.modelData.timeStr - color: DynamicColors.palette.m3outline - font.pointSize: 11 - } - } - - WrappedLoader { - id: expandedContent - - shouldBeActive: root.expanded - anchors.top: summary.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 7 / 2 - - sourceComponent: ColumnLayout { - readonly property alias body: body - - spacing: 10 - - CustomText { - id: body - - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given") - color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface - wrapMode: Text.WordWrap - - onLinkActivated: link => { - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.visibilities.sidebar = false; - } - } - - NotifActionList { - notif: root.modelData - } - } - } - - Behavior on implicitHeight { - Anim { + Behavior on implicitHeight { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } + states: State { + name: "expanded" + when: root.expanded - component WrappedLoader: Loader { - required property bool shouldBeActive + PropertyChanges { + compactBody.anchors.margins: 10 + dummySummary.anchors.margins: 10 + expandedContent.anchors.margins: 10 + summary.anchors.margins: 10 + summary.maximumLineCount: Number.MAX_SAFE_INTEGER + summary.width: root.width - 10 * 2 - timeStr.implicitWidth - 7 + timeStr.anchors.margins: 10 + } + } + transitions: Transition { + Anim { + properties: "margins,width,maximumLineCount" + } + } - opacity: shouldBeActive ? 1 : 0 - active: opacity > 0 + TextMetrics { + id: summaryHeightMetrics - Behavior on opacity { - Anim {} - } - } + font: summary.font + text: " " // Use this height to prevent weird characters from changing the line height + } + + CustomText { + id: summary + + anchors.left: parent.left + anchors.top: parent.top + color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface + elide: Text.ElideRight + maximumLineCount: 1 + text: root.modelData.summary + width: parent.width + wrapMode: Text.WordWrap + } + + CustomText { + id: dummySummary + + anchors.left: parent.left + anchors.top: parent.top + text: root.modelData.summary + visible: false + } + + WrappedLoader { + id: compactBody + + anchors.left: dummySummary.right + anchors.leftMargin: 7 + anchors.right: parent.right + anchors.top: parent.top + shouldBeActive: !root.expanded + + sourceComponent: CustomText { + color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline + elide: Text.ElideRight + text: root.modelData.body.replace(/\n/g, " ") + textFormat: Text.StyledText + } + } + + WrappedLoader { + id: timeStr + + anchors.right: parent.right + anchors.top: parent.top + shouldBeActive: root.expanded + + sourceComponent: CustomText { + animate: true + color: DynamicColors.palette.m3outline + font.pointSize: 11 + text: root.modelData.timeStr + } + } + + WrappedLoader { + id: expandedContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: summary.bottom + anchors.topMargin: 7 / 2 + shouldBeActive: root.expanded + + sourceComponent: ColumnLayout { + readonly property alias body: body + + spacing: 10 + + CustomText { + id: body + + Layout.fillWidth: true + color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface + text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given") + textFormat: Text.MarkdownText + wrapMode: Text.WordWrap + + onLinkActivated: link => { + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.visibilities.sidebar = false; + } + } + + NotifActionList { + notif: root.modelData + } + } + } + + component WrappedLoader: Loader { + required property bool shouldBeActive + + active: opacity > 0 + opacity: shouldBeActive ? 1 : 0 + + Behavior on opacity { + Anim { + } + } + } } diff --git a/Modules/Notifications/Sidebar/NotifActionList.qml b/Modules/Notifications/Sidebar/NotifActionList.qml index fcfcff6..3456d80 100644 --- a/Modules/Notifications/Sidebar/NotifActionList.qml +++ b/Modules/Notifications/Sidebar/NotifActionList.qml @@ -10,125 +10,124 @@ import QtQuick import QtQuick.Layouts Item { - id: root + id: root - required property NotifServer.Notif notif + required property NotifServer.Notif notif - Layout.fillWidth: true - implicitHeight: flickable.contentHeight + Layout.fillWidth: true + implicitHeight: flickable.contentHeight - CustomFlickable { - id: flickable + CustomFlickable { + id: flickable - anchors.fill: parent - contentWidth: Math.max(width, actionList.implicitWidth) - contentHeight: actionList.implicitHeight + anchors.fill: parent + contentHeight: actionList.implicitHeight + contentWidth: Math.max(width, actionList.implicitWidth) - RowLayout { - id: actionList + RowLayout { + id: actionList - anchors.fill: parent - spacing: 7 + anchors.fill: parent + spacing: 7 - Repeater { - model: [ - { - isClose: true - }, - ...root.notif.actions, - { - isCopy: true - } - ] + Repeater { + model: [ + { + isClose: true + }, + ...root.notif.actions, + { + isCopy: true + } + ] - CustomRect { - id: action + CustomRect { + id: action - required property var modelData + required property var modelData - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: actionInner.implicitWidth + 5 * 2 - implicitHeight: actionInner.implicitHeight + 5 * 2 + Layout.fillHeight: true + Layout.fillWidth: true + Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? 18 : 0) + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 4) + implicitHeight: actionInner.implicitHeight + 5 * 2 + implicitWidth: actionInner.implicitWidth + 5 * 2 + radius: actionStateLayer.pressed ? 6 / 2 : 6 - Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? 18 : 0) - radius: actionStateLayer.pressed ? 6 / 2 : 6 - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 4) - - Timer { - id: copyTimer - - interval: 1000 - onTriggered: actionInner.item.text = "content_copy" - } - - StateLayer { - id: actionStateLayer - - function onClicked(): void { - if (action.modelData.isClose) { - root.notif.close(); - } else if (action.modelData.isCopy) { - Quickshell.clipboardText = root.notif.body; - actionInner.item.text = "inventory"; - copyTimer.start(); - } else if (action.modelData.invoke) { - action.modelData.invoke(); - } else if (!root.notif.resident) { - root.notif.close(); - } - } - } - - Loader { - id: actionInner - - anchors.centerIn: parent - sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp - } - - Component { - id: iconBtn - - MaterialIcon { - animate: action.modelData.isCopy ?? false - text: action.modelData.isCopy ? "content_copy" : "close" - color: DynamicColors.palette.m3onSurfaceVariant - } - } - - Component { - id: iconComp - - IconImage { - source: Quickshell.iconPath(action.modelData.identifier) - } - } - - Component { - id: textComp - - CustomText { - text: action.modelData.text - color: DynamicColors.palette.m3onSurfaceVariant - } - } - - Behavior on Layout.preferredWidth { - Anim { + Behavior on Layout.preferredWidth { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - Behavior on radius { - Anim { + } + } + Behavior on radius { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } - } - } + } + } + + Timer { + id: copyTimer + + interval: 1000 + + onTriggered: actionInner.item.text = "content_copy" + } + + StateLayer { + id: actionStateLayer + + function onClicked(): void { + if (action.modelData.isClose) { + root.notif.close(); + } else if (action.modelData.isCopy) { + Quickshell.clipboardText = root.notif.body; + actionInner.item.text = "inventory"; + copyTimer.start(); + } else if (action.modelData.invoke) { + action.modelData.invoke(); + } else if (!root.notif.resident) { + root.notif.close(); + } + } + } + + Loader { + id: actionInner + + anchors.centerIn: parent + sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp + } + + Component { + id: iconBtn + + MaterialIcon { + animate: action.modelData.isCopy ?? false + color: DynamicColors.palette.m3onSurfaceVariant + text: action.modelData.isCopy ? "content_copy" : "close" + } + } + + Component { + id: iconComp + + IconImage { + source: Quickshell.iconPath(action.modelData.identifier) + } + } + + Component { + id: textComp + + CustomText { + color: DynamicColors.palette.m3onSurfaceVariant + text: action.modelData.text + } + } + } + } + } + } } diff --git a/Modules/Notifications/Sidebar/NotifDock.qml b/Modules/Notifications/Sidebar/NotifDock.qml index a97d4f5..4f1156e 100644 --- a/Modules/Notifications/Sidebar/NotifDock.qml +++ b/Modules/Notifications/Sidebar/NotifDock.qml @@ -10,183 +10,177 @@ import QtQuick import QtQuick.Layouts Item { - id: root + id: root - required property Props props - required property var visibilities - readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) + readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) + required property Props props + required property var visibilities - anchors.fill: parent - anchors.margins: 8 + anchors.fill: parent + anchors.margins: 8 - Component.onCompleted: NotifServer.list.forEach(n => n.popup = false) + Component.onCompleted: NotifServer.list.forEach(n => n.popup = false) - Item { - id: title + Item { + id: title - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 4 + anchors.left: parent.left + anchors.margins: 4 + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) - implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) + CustomText { + id: count - CustomText { - id: count + anchors.left: parent.left + anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3outline + font.family: "CaskaydiaCove NF" + font.pointSize: 13 + font.weight: 500 + opacity: root.notifCount > 0 ? 1 : 0 + text: root.notifCount - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin - opacity: root.notifCount > 0 ? 1 : 0 + Behavior on anchors.leftMargin { + Anim { + } + } + Behavior on opacity { + Anim { + } + } + } - text: root.notifCount - color: DynamicColors.palette.m3outline - font.pointSize: 13 - font.family: "CaskaydiaCove NF" - font.weight: 500 + CustomText { + id: titleText - Behavior on anchors.leftMargin { - Anim {} - } + anchors.left: count.right + anchors.leftMargin: 7 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3outline + elide: Text.ElideRight + font.family: "CaskaydiaCove NF" + font.pointSize: 13 + font.weight: 500 + text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") + } + } - Behavior on opacity { - Anim {} - } - } + ClippingRectangle { + id: clipRect - CustomText { - id: titleText + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.top: title.bottom + anchors.topMargin: 10 + color: "transparent" + radius: 6 - anchors.verticalCenter: parent.verticalCenter - anchors.left: count.right - anchors.right: parent.right - anchors.leftMargin: 7 + Loader { + active: opacity > 0 + anchors.centerIn: parent + opacity: root.notifCount > 0 ? 0 : 1 - text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") - color: DynamicColors.palette.m3outline - font.pointSize: 13 - font.family: "CaskaydiaCove NF" - font.weight: 500 - elide: Text.ElideRight - } - } - - ClippingRectangle { - id: clipRect - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: title.bottom - anchors.bottom: parent.bottom - anchors.topMargin: 10 - - radius: 6 - color: "transparent" - - Loader { - anchors.centerIn: parent - active: opacity > 0 - opacity: root.notifCount > 0 ? 0 : 1 - - sourceComponent: ColumnLayout { - spacing: 20 - - CustomText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") - color: DynamicColors.palette.m3outlineVariant - font.pointSize: 18 - font.family: "CaskaydiaCove NF" - font.weight: 500 - } - } - - Behavior on opacity { - Anim { + Behavior on opacity { + Anim { duration: MaterialEasing.expressiveEffectsTime - } - } - } + } + } + sourceComponent: ColumnLayout { + spacing: 20 - CustomFlickable { - id: view + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3outlineVariant + font.family: "CaskaydiaCove NF" + font.pointSize: 18 + font.weight: 500 + text: qsTr("No Notifications") + } + } + } - anchors.fill: parent + CustomFlickable { + id: view - flickableDirection: Flickable.VerticalFlick - contentWidth: width - contentHeight: notifList.implicitHeight + anchors.fill: parent + contentHeight: notifList.implicitHeight + contentWidth: width + flickableDirection: Flickable.VerticalFlick - CustomScrollBar.vertical: CustomScrollBar { - flickable: view - } + CustomScrollBar.vertical: CustomScrollBar { + flickable: view + } - NotifDockList { - id: notifList + NotifDockList { + id: notifList - props: root.props - visibilities: root.visibilities - container: view - } - } - } + container: view + props: root.props + visibilities: root.visibilities + } + } + } - Timer { - id: clearTimer + Timer { + id: clearTimer - repeat: true - interval: 50 - onTriggered: { - let next = null; - for (let i = 0; i < notifList.repeater.count; i++) { - next = notifList.repeater.itemAt(i); - if (!next?.closed) - break; - } - if (next) - next.closeAll(); - else - stop(); - } - } + interval: 50 + repeat: true - Loader { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 8 + onTriggered: { + let next = null; + for (let i = 0; i < notifList.repeater.count; i++) { + next = notifList.repeater.itemAt(i); + if (!next?.closed) + break; + } + if (next) + next.closeAll(); + else + stop(); + } + } - scale: root.notifCount > 0 ? 1 : 0.5 - opacity: root.notifCount > 0 ? 1 : 0 - active: opacity > 0 + Loader { + active: opacity > 0 + anchors.bottom: parent.bottom + anchors.margins: 8 + anchors.right: parent.right + opacity: root.notifCount > 0 ? 1 : 0 + scale: root.notifCount > 0 ? 1 : 0.5 - sourceComponent: IconButton { - id: clearBtn - - icon: "clear_all" - radius: 8 - padding: 8 - font.pointSize: Math.round(18 * 1.2) - onClicked: clearTimer.start() - - Elevation { - anchors.fill: parent - radius: parent.radius - z: -1 - level: clearBtn.stateLayer.containsMouse ? 4 : 3 - } - } - - Behavior on scale { - Anim { + Behavior on opacity { + Anim { + duration: MaterialEasing.expressiveEffectsTime + } + } + Behavior on scale { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } + sourceComponent: IconButton { + id: clearBtn - Behavior on opacity { - Anim { - duration: MaterialEasing.expressiveEffectsTime - } - } - } + font.pointSize: Math.round(18 * 1.2) + icon: "clear_all" + padding: 8 + radius: 8 + + onClicked: clearTimer.start() + + Elevation { + anchors.fill: parent + level: clearBtn.stateLayer.containsMouse ? 4 : 3 + radius: parent.radius + z: -1 + } + } + } } diff --git a/Modules/Notifications/Sidebar/NotifDockList.qml b/Modules/Notifications/Sidebar/NotifDockList.qml index 4c4d884..316b5eb 100644 --- a/Modules/Notifications/Sidebar/NotifDockList.qml +++ b/Modules/Notifications/Sidebar/NotifDockList.qml @@ -8,162 +8,158 @@ import Quickshell import QtQuick Item { - id: root + id: root - required property Props props - required property Flickable container - required property var visibilities + required property Flickable container + property bool flag + required property Props props + readonly property alias repeater: repeater + readonly property int spacing: 8 + required property var visibilities - readonly property alias repeater: repeater - readonly property int spacing: 8 - property bool flag + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: { + const item = repeater.itemAt(repeater.count - 1); + return item ? item.y + item.implicitHeight : 0; + } - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: { - const item = repeater.itemAt(repeater.count - 1); - return item ? item.y + item.implicitHeight : 0; - } + Repeater { + id: repeater - Repeater { - id: repeater + model: ScriptModel { + values: { + const map = new Map(); + for (const n of NotifServer.notClosed) + map.set(n.appName, null); + for (const n of NotifServer.list) + map.set(n.appName, null); + return [...map.keys()]; + } - model: ScriptModel { - values: { - const map = new Map(); - for (const n of NotifServer.notClosed) - map.set(n.appName, null); - for (const n of NotifServer.list) - map.set(n.appName, null); - return [...map.keys()]; - } - onValuesChanged: root.flagChanged() - } + onValuesChanged: root.flagChanged() + } - MouseArea { - id: notif + MouseArea { + id: notif - required property int index - required property string modelData + readonly property bool closed: notifInner.notifCount === 0 + required property int index + required property string modelData + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY - readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY - - function closeAll(): void { - for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) { - n.close(); + function closeAll(): void { + for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) { + n.close(); } - } + } - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.closed) - y += item.nonAnimHeight + root.spacing; - } - return y; - } + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + drag.axis: Drag.XAxis + drag.target: this + enabled: !closed + hoverEnabled: true + implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + preventStealing: true + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.closed) + y += item.nonAnimHeight + root.spacing; + } + return y; + } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } - } - - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight - - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: true - enabled: !closed - - drag.target: this - drag.axis: Drag.XAxis - - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - notifInner.toggleExpand(!notifInner.expanded); - else if (event.button === Qt.MiddleButton) - closeAll(); - } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - notifInner.toggleExpand(diffY > 0); - } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - closeAll(); - } - - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } + } + Behavior on x { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - ParallelAnimation { - running: notif.closed - - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "scale" - to: 0.6 - } - } - - NotifGroup { - id: notifInner - - modelData: notif.modelData - props: root.props - container: root.container - visibilities: root.visibilities - } - - Behavior on x { - Anim { + } + } + Behavior on y { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } - Behavior on y { - Anim { + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + notifInner.toggleExpand(diffY > 0); + } + } + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + notifInner.toggleExpand(!notifInner.expanded); + else if (event.button === Qt.MiddleButton) + closeAll(); + } + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + closeAll(); + } + + ParallelAnimation { + running: true + + Anim { + from: 0 + property: "opacity" + target: notif + to: 1 + } + + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } + from: 0 + property: "scale" + target: notif + to: 1 + } + } + + ParallelAnimation { + running: notif.closed + + Anim { + property: "opacity" + target: notif + to: 0 + } + + Anim { + property: "scale" + target: notif + to: 0.6 + } + } + + NotifGroup { + id: notifInner + + container: root.container + modelData: notif.modelData + props: root.props + visibilities: root.visibilities + } + } + } } diff --git a/Modules/Notifications/Sidebar/NotifGroup.qml b/Modules/Notifications/Sidebar/NotifGroup.qml index cff822e..0f624b1 100644 --- a/Modules/Notifications/Sidebar/NotifGroup.qml +++ b/Modules/Notifications/Sidebar/NotifGroup.qml @@ -11,229 +11,224 @@ import QtQuick import QtQuick.Layouts CustomRect { - id: root + id: root - required property string modelData - required property Props props - required property Flickable container - required property var visibilities + readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" + required property Flickable container + readonly property bool expanded: props.expandedNotifs.includes(modelData) + readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" + required property string modelData + readonly property int nonAnimHeight: { + const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0); + const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; + return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2); + } + readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) + readonly property list notifs: NotifServer.list.filter(n => n.appName === modelData) + required property Props props + readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low + required property var visibilities - readonly property list notifs: NotifServer.list.filter(n => n.appName === modelData) - readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) - readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" - readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low + function toggleExpand(expand: bool): void { + if (expand) { + if (!expanded) + props.expandedNotifs.push(modelData); + } else if (expanded) { + props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); + } + } - readonly property int nonAnimHeight: { - const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0); - const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; - return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2); - } - readonly property bool expanded: props.expandedNotifs.includes(modelData) + anchors.left: parent?.left + anchors.right: parent?.right + clip: true + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + implicitHeight: content.implicitHeight + 10 * 2 + radius: 8 - function toggleExpand(expand: bool): void { - if (expand) { - if (!expanded) - props.expandedNotifs.push(modelData); - } else if (expanded) { - props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); - } - } + Component.onDestruction: { + if (notifCount === 0 && expanded) + props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); + } - Component.onDestruction: { - if (notifCount === 0 && expanded) - props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); - } + RowLayout { + id: content - anchors.left: parent?.left - anchors.right: parent?.right - implicitHeight: content.implicitHeight + 10 * 2 + anchors.left: parent.left + anchors.margins: 10 + anchors.right: parent.right + anchors.top: parent.top + spacing: 10 - clip: true - radius: 8 - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + Item { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + implicitHeight: Config.notifs.sizes.image + implicitWidth: Config.notifs.sizes.image - RowLayout { - id: content + Component { + id: imageComp - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 10 + Image { + asynchronous: true + cache: false + fillMode: Image.PreserveAspectCrop + height: Config.notifs.sizes.image + source: Qt.resolvedUrl(root.image) + width: Config.notifs.sizes.image + } + } - spacing: 10 + Component { + id: appIconComp - Item { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + CustomIcon { + implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + layer.enabled: root.appIcon.endsWith("symbolic") + source: Quickshell.iconPath(root.appIcon) + } + } - Component { - id: imageComp + Component { + id: materialIconComp - Image { - source: Qt.resolvedUrl(root.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image - } - } + MaterialIcon { + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer + font.pointSize: 18 + text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) + } + } - Component { - id: appIconComp + CustomClippingRect { + anchors.fill: parent + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3) : DynamicColors.palette.m3secondaryContainer + radius: 1000 - CustomIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) - source: Quickshell.iconPath(root.appIcon) - layer.enabled: root.appIcon.endsWith("symbolic") - } - } + Loader { + anchors.centerIn: parent + sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp + } + } - Component { - id: materialIconComp + Loader { + active: root.appIcon && root.image + anchors.bottom: parent.bottom + anchors.right: parent.right - MaterialIcon { - text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - font.pointSize: 18 - } - } + sourceComponent: CustomRect { + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3secondaryContainer + implicitHeight: Config.notifs.sizes.badge + implicitWidth: Config.notifs.sizes.badge + radius: 1000 - CustomClippingRect { - anchors.fill: parent - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3) : DynamicColors.palette.m3secondaryContainer - radius: 1000 + CustomIcon { + anchors.centerIn: parent + implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + layer.enabled: root.appIcon.endsWith("symbolic") + source: Quickshell.iconPath(root.appIcon) + } + } + } + } - Loader { - anchors.centerIn: parent - sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp - } - } + ColumnLayout { + id: column - Loader { - anchors.right: parent.right - anchors.bottom: parent.bottom - active: root.appIcon && root.image + Layout.bottomMargin: -10 / 2 + Layout.fillWidth: true + Layout.topMargin: -10 + spacing: 0 - sourceComponent: CustomRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + RowLayout { + id: header - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3secondaryContainer - radius: 1000 + Layout.bottomMargin: root.expanded ? Math.round(7 / 2) : 0 + Layout.fillWidth: true + spacing: 5 - CustomIcon { - anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) - source: Quickshell.iconPath(root.appIcon) - layer.enabled: root.appIcon.endsWith("symbolic") - } - } - } - } + Behavior on Layout.bottomMargin { + Anim { + } + } - ColumnLayout { - id: column + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + elide: Text.ElideRight + font.pointSize: 11 + text: root.modelData + } - Layout.topMargin: -10 - Layout.bottomMargin: -10 / 2 - Layout.fillWidth: true - spacing: 0 + CustomText { + animate: true + color: DynamicColors.palette.m3outline + font.pointSize: 11 + text: root.notifs.find(n => !n.closed)?.timeStr ?? "" + } - RowLayout { - id: header + CustomRect { + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3) + implicitHeight: groupCount.implicitHeight + 10 + implicitWidth: expandBtn.implicitWidth + 7 * 2 + radius: 1000 - Layout.bottomMargin: root.expanded ? Math.round(7 / 2) : 0 - Layout.fillWidth: true - spacing: 5 + StateLayer { + function onClicked(): void { + root.toggleExpand(!root.expanded); + } - CustomText { - Layout.fillWidth: true - text: root.modelData - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 11 - elide: Text.ElideRight - } + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + } - CustomText { - animate: true - text: root.notifs.find(n => !n.closed)?.timeStr ?? "" - color: DynamicColors.palette.m3outline - font.pointSize: 11 - } + RowLayout { + id: expandBtn - CustomRect { - implicitWidth: expandBtn.implicitWidth + 7 * 2 - implicitHeight: groupCount.implicitHeight + 10 + anchors.centerIn: parent + spacing: 7 / 2 - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 3) - radius: 1000 + CustomText { + id: groupCount - StateLayer { - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + Layout.leftMargin: 10 / 2 + animate: true + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + font.pointSize: 11 + text: root.notifCount + } - function onClicked(): void { - root.toggleExpand(!root.expanded); - } - } + MaterialIcon { + Layout.rightMargin: -10 / 2 + Layout.topMargin: root.expanded ? -Math.floor(7 / 2) : 0 + color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface + rotation: root.expanded ? 180 : 0 + text: "expand_more" - RowLayout { - id: expandBtn - - anchors.centerIn: parent - spacing: 7 / 2 - - CustomText { - id: groupCount - - Layout.leftMargin: 10 / 2 - animate: true - text: root.notifCount - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface - font.pointSize: 11 - } - - MaterialIcon { - Layout.rightMargin: -10 / 2 - text: "expand_more" - color: root.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onSurface - rotation: root.expanded ? 180 : 0 - Layout.topMargin: root.expanded ? -Math.floor(7 / 2) : 0 - - Behavior on rotation { - Anim { + Behavior on Layout.topMargin { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - Behavior on Layout.topMargin { - Anim { + } + } + Behavior on rotation { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } - } + } + } + } + } + } + } - Behavior on Layout.bottomMargin { - Anim {} - } - } + NotifGroupList { + id: notifList - NotifGroupList { - id: notifList + container: root.container + expanded: root.expanded + notifs: root.notifs + props: root.props + visibilities: root.visibilities - props: root.props - notifs: root.notifs - expanded: root.expanded - container: root.container - visibilities: root.visibilities - onRequestToggleExpand: expand => root.toggleExpand(expand) - } - } - } + onRequestToggleExpand: expand => root.toggleExpand(expand) + } + } + } } diff --git a/Modules/Notifications/Sidebar/NotifGroupList.qml b/Modules/Notifications/Sidebar/NotifGroupList.qml index 382f028..0ba2a1f 100644 --- a/Modules/Notifications/Sidebar/NotifGroupList.qml +++ b/Modules/Notifications/Sidebar/NotifGroupList.qml @@ -9,206 +9,201 @@ import QtQuick import QtQuick.Layouts Item { - id: root + id: root - required property Props props - required property list notifs - required property bool expanded - required property Flickable container - required property var visibilities + required property Flickable container + required property bool expanded + property bool flag + readonly property real nonAnimHeight: { + let h = -root.spacing; + for (let i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) + h += item.nonAnimHeight + root.spacing; + } + return h; + } + required property list notifs + required property Props props + property bool showAllNotifs + readonly property int spacing: Math.round(7 / 2) + required property var visibilities - readonly property real nonAnimHeight: { - let h = -root.spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) - h += item.nonAnimHeight + root.spacing; - } - return h; - } + signal requestToggleExpand(expand: bool) - readonly property int spacing: Math.round(7 / 2) - property bool showAllNotifs - property bool flag + Layout.fillWidth: true + implicitHeight: nonAnimHeight - signal requestToggleExpand(expand: bool) - - onExpandedChanged: { - if (expanded) { - clearTimer.stop(); - showAllNotifs = true; - } else { - clearTimer.start(); - } - } - - Layout.fillWidth: true - implicitHeight: nonAnimHeight - - Timer { - id: clearTimer - - interval: MaterialEasing.standardTime - onTriggered: root.showAllNotifs = false - } - - Repeater { - id: repeater - - model: ScriptModel { - values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) - onValuesChanged: root.flagChanged() - } - - MouseArea { - id: notif - - required property int index - required property NotifServer.Notif modelData - - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; - - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; - - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; - } - return y; - } - - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } - } - - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 - - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight - - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed - - drag.target: this - drag.axis: Drag.XAxis - - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); - } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 - } - } - - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) - - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width - } - } - - Notif { - id: notifInner - - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities - } - - Behavior on opacity { - Anim {} - } - - Behavior on scale { - Anim {} - } - - Behavior on x { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - Behavior on y { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } - - Behavior on implicitHeight { - Anim { + Behavior on implicitHeight { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } + + onExpandedChanged: { + if (expanded) { + clearTimer.stop(); + showAllNotifs = true; + } else { + clearTimer.start(); + } + } + + Timer { + id: clearTimer + + interval: MaterialEasing.standardTime + + onTriggered: root.showAllNotifs = false + } + + Repeater { + id: repeater + + model: ScriptModel { + values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) + + onValuesChanged: root.flagChanged() + } + + MouseArea { + id: notif + + required property int index + required property NotifServer.Notif modelData + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + readonly property bool previewHidden: { + if (root.expanded) + return false; + + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i].closed) + extraHidden++; + + return index >= Config.notifs.groupPreviewNum + extraHidden; + } + property int startY + + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + drag.axis: Drag.XAxis + drag.target: this + enabled: !modelData.closed + hoverEnabled: true + implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + opacity: previewHidden ? 0 : 1 + preventStealing: !root.expanded + scale: previewHidden ? 0.7 : 1 + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) + y += item.nonAnimHeight + root.spacing; + } + return y; + } + + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } + } + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } + Behavior on x { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + Behavior on y { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); + } + } + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData.close(); + } + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData.close(); + } + + ParallelAnimation { + Component.onCompleted: running = !notif.previewHidden + + Anim { + from: 0 + property: "opacity" + target: notif + to: 1 + } + + Anim { + from: 0.7 + property: "scale" + target: notif + to: 1 + } + } + + ParallelAnimation { + running: notif.modelData.closed + + onFinished: notif.modelData.unlock(notif) + + Anim { + property: "opacity" + target: notif + to: 0 + } + + Anim { + property: "x" + target: notif + to: notif.x >= 0 ? notif.width : -notif.width + } + } + + Notif { + id: notifInner + + anchors.fill: parent + expanded: root.expanded + modelData: notif.modelData + props: root.props + visibilities: root.visibilities + } + } + } } diff --git a/Modules/Notifications/Sidebar/Props.qml b/Modules/Notifications/Sidebar/Props.qml index 4613942..21a2541 100644 --- a/Modules/Notifications/Sidebar/Props.qml +++ b/Modules/Notifications/Sidebar/Props.qml @@ -1,7 +1,7 @@ import Quickshell PersistentProperties { - property list expandedNotifs: [] + property list expandedNotifs: [] - reloadableId: "sidebar" + reloadableId: "sidebar" } diff --git a/Modules/Notifications/Sidebar/Utils/Background.qml b/Modules/Notifications/Sidebar/Utils/Background.qml index a081c24..748512d 100644 --- a/Modules/Notifications/Sidebar/Utils/Background.qml +++ b/Modules/Notifications/Sidebar/Utils/Background.qml @@ -1,55 +1,60 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes ShapePath { - id: root + id: root - required property Wrapper wrapper - required property var sidebar - readonly property real rounding: 8 - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + readonly property bool flatten: wrapper.height < rounding * 2 + readonly property real rounding: 8 + readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + required property var sidebar + required property Wrapper wrapper - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathLine { - relativeX: -(root.wrapper.width + root.rounding) - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - PathArc { - relativeX: root.sidebar.utilsRoundingX - relativeY: -root.roundingY - radiusX: root.sidebar.utilsRoundingX - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: -root.rounding - radiusX: root.rounding - radiusY: root.rounding - direction: PathArc.Counterclockwise - } + Behavior on fillColor { + CAnim { + } + } - Behavior on fillColor { - Modules.CAnim {} - } + PathLine { + relativeX: -(root.wrapper.width + root.rounding) + 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.sidebar.utilsRoundingX + radiusY: Math.min(root.rounding, root.wrapper.height) + relativeX: root.sidebar.utilsRoundingX + relativeY: -root.roundingY + } + + PathLine { + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width + relativeY: 0 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: root.rounding + radiusY: root.rounding + relativeX: root.rounding + relativeY: -root.rounding + } } diff --git a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml index 1244c91..a98fc48 100644 --- a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml +++ b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml @@ -1,41 +1,86 @@ +import Quickshell.Bluetooth +import Quickshell.Networking as QSNetwork +import QtQuick +import QtQuick.Layouts import qs.Components import qs.Config import qs.Modules import qs.Daemons -import QtQuick -import QtQuick.Layouts CustomRect { - id: root + id: root - required property var visibilities - required property Item popouts + required property Item popouts + required property var visibilities - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + 18 * 2 + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: layout.implicitHeight + 18 * 2 + radius: 8 - radius: 8 - color: DynamicColors.tPalette.m3surfaceContainer + ColumnLayout { + id: layout - ColumnLayout { - id: layout + anchors.fill: parent + anchors.margins: 18 + spacing: 10 - anchors.fill: parent - anchors.margins: 18 - spacing: 10 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 7 - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 7 + Toggle { + checked: Network.wifiEnabled + icon: Network.wifiEnabled ? "wifi" : "wifi_off" + visible: QSNetwork.Networking.devices.values.length > 0 - Toggle { + onClicked: Network.toggleWifi() + } + + Toggle { id: toggle - icon: "notifications_off" - checked: NotifServer.dnd - onClicked: NotifServer.dnd = !NotifServer.dnd - } - } - } + + checked: !NotifServer.dnd + icon: NotifServer.dnd ? "notifications_off" : "notifications" + + onClicked: NotifServer.dnd = !NotifServer.dnd + } + + Toggle { + checked: !Audio.sourceMuted + icon: Audio.sourceMuted ? "mic_off" : "mic" + + onClicked: { + const audio = Audio.source?.audio; + if (audio) + audio.muted = !audio.muted; + } + } + + Toggle { + checked: !Audio.muted + icon: Audio.muted ? "volume_off" : "volume_up" + + onClicked: { + const audio = Audio.sink?.audio; + if (audio) + audio.muted = !audio.muted; + } + } + + Toggle { + checked: Bluetooth.defaultAdapter?.enabled ?? false + icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled" + visible: Bluetooth.defaultAdapter ?? false + + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = !adapter.enabled; + } + } + } + } CustomShortcut { name: "toggle-dnd" @@ -45,20 +90,20 @@ CustomRect { } } - component Toggle: IconButton { - Layout.fillWidth: true - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0) - radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8 - inactiveColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) - toggle: true - radiusAnim.duration: MaterialEasing.expressiveEffectsTime - radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects + component Toggle: IconButton { + Layout.fillWidth: true + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0) + inactiveColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8 + radiusAnim.duration: MaterialEasing.expressiveEffectsTime + radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects + toggle: true - Behavior on Layout.preferredWidth { - Anim { + Behavior on Layout.preferredWidth { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } + } + } + } } diff --git a/Modules/Notifications/Sidebar/Utils/Content.qml b/Modules/Notifications/Sidebar/Utils/Content.qml index 6508f47..d9404ea 100644 --- a/Modules/Notifications/Sidebar/Utils/Content.qml +++ b/Modules/Notifications/Sidebar/Utils/Content.qml @@ -4,26 +4,27 @@ import QtQuick import QtQuick.Layouts Item { - id: root + id: root - required property var props - required property var visibilities - required property Item popouts + required property Item popouts + required property var props + required property var visibilities - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight + implicitHeight: layout.implicitHeight + implicitWidth: layout.implicitWidth - ColumnLayout { - id: layout + ColumnLayout { + id: layout - anchors.fill: parent - spacing: 8 + anchors.fill: parent + spacing: 8 - IdleInhibit {} + IdleInhibit { + } - Toggles { - visibilities: root.visibilities - popouts: root.popouts - } - } + Toggles { + popouts: root.popouts + visibilities: root.visibilities + } + } } diff --git a/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml b/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml index 1bd2a18..4c148f5 100644 --- a/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml +++ b/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml @@ -1,125 +1,119 @@ import qs.Components import qs.Config -import qs.Modules as Modules import qs.Helpers import QtQuick import QtQuick.Layouts CustomRect { - id: root + id: root - Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + 18 * 2 + Layout.fillWidth: true + clip: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + 18 * 2 + radius: 8 - radius: 8 - color: DynamicColors.tPalette.m3surfaceContainer - clip: true - - RowLayout { - id: layout - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 18 - spacing: 10 - - CustomRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + 7 * 2 - - radius: 1000 - color: IdleInhibitor.enabled ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer - - MaterialIcon { - id: icon - - anchors.centerIn: parent - text: "coffee" - color: IdleInhibitor.enabled ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer - font.pointSize: 18 - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - CustomText { - Layout.fillWidth: true - text: qsTr("Keep Awake") - font.pointSize: 13 - elide: Text.ElideRight - } - - CustomText { - Layout.fillWidth: true - text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 11 - elide: Text.ElideRight - } - } - - CustomSwitch { - checked: IdleInhibitor.enabled - onToggled: IdleInhibitor.enabled = checked - } - } - - Loader { - id: activeChip - - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.topMargin: 20 - anchors.bottomMargin: IdleInhibitor.enabled ? 18 : -implicitHeight - anchors.leftMargin: 18 - - opacity: IdleInhibitor.enabled ? 1 : 0 - scale: IdleInhibitor.enabled ? 1 : 0.5 - - Component.onCompleted: active = Qt.binding(() => opacity > 0) - - sourceComponent: CustomRect { - implicitWidth: activeText.implicitWidth + 10 * 2 - implicitHeight: activeText.implicitHeight + 10 * 2 - - radius: 1000 - color: DynamicColors.palette.m3primary - - CustomText { - id: activeText - - anchors.centerIn: parent - text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) - color: DynamicColors.palette.m3onPrimary - font.pointSize: Math.round(11 * 0.9) - } - } - - Behavior on anchors.bottomMargin { - Modules.Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - Behavior on opacity { - Modules.Anim { - duration: MaterialEasing.expressiveEffectsTime - } - } - - Behavior on scale { - Modules.Anim {} - } - } - - Behavior on implicitHeight { - Modules.Anim { + Behavior on implicitHeight { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + } + } + + RowLayout { + id: layout + + anchors.left: parent.left + anchors.margins: 18 + anchors.right: parent.right + anchors.top: parent.top + spacing: 10 + + CustomRect { + color: IdleInhibitor.enabled ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer + implicitHeight: icon.implicitHeight + 7 * 2 + implicitWidth: implicitHeight + radius: 1000 + + MaterialIcon { + id: icon + + anchors.centerIn: parent + color: IdleInhibitor.enabled ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer + font.pointSize: 18 + text: "coffee" + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + CustomText { + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: 13 + text: qsTr("Keep Awake") + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + elide: Text.ElideRight + font.pointSize: 11 + text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") + } + } + + CustomSwitch { + checked: IdleInhibitor.enabled + + onToggled: IdleInhibitor.enabled = checked + } + } + + Loader { + id: activeChip + + anchors.bottom: parent.bottom + anchors.bottomMargin: IdleInhibitor.enabled ? 18 : -implicitHeight + anchors.left: parent.left + anchors.leftMargin: 18 + anchors.topMargin: 20 + opacity: IdleInhibitor.enabled ? 1 : 0 + scale: IdleInhibitor.enabled ? 1 : 0.5 + + Behavior on anchors.bottomMargin { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + Behavior on opacity { + Anim { + duration: MaterialEasing.expressiveEffectsTime + } + } + Behavior on scale { + Anim { + } + } + sourceComponent: CustomRect { + color: DynamicColors.palette.m3primary + implicitHeight: activeText.implicitHeight + 10 * 2 + implicitWidth: activeText.implicitWidth + 10 * 2 + radius: 1000 + + CustomText { + id: activeText + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + font.pointSize: Math.round(11 * 0.9) + text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) + } + } + + Component.onCompleted: active = Qt.binding(() => opacity > 0) + } } diff --git a/Modules/Notifications/Sidebar/Utils/Wrapper.qml b/Modules/Notifications/Sidebar/Utils/Wrapper.qml index 2889a99..b37fd1c 100644 --- a/Modules/Notifications/Sidebar/Utils/Wrapper.qml +++ b/Modules/Notifications/Sidebar/Utils/Wrapper.qml @@ -2,96 +2,93 @@ pragma ComponentBehavior: Bound import qs.Components import qs.Config -import qs.Modules as Modules import Quickshell import QtQuick Item { - id: root + id: root - required property var visibilities - required property Item sidebar - required property Item popouts + required property Item popouts + readonly property PersistentProperties props: PersistentProperties { + property string recordingConfirmDelete + property bool recordingListExpanded: false + property string recordingMode - readonly property PersistentProperties props: PersistentProperties { - property bool recordingListExpanded: false - property string recordingConfirmDelete - property string recordingMode + reloadableId: "utilities" + } + readonly property bool shouldBeActive: visibilities.sidebar + required property Item sidebar + required property var visibilities - reloadableId: "utilities" - } - readonly property bool shouldBeActive: visibilities.sidebar + implicitHeight: 0 + implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + visible: height > 0 - visible: height > 0 - implicitHeight: 0 - implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + states: State { + name: "visible" + when: root.shouldBeActive - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } + PropertyChanges { + root.implicitHeight: content.implicitHeight + 8 * 2 + } + } + transitions: [ + Transition { + from: "" + to: "visible" - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.implicitHeight: content.implicitHeight + 8 * 2 - } - } - - transitions: [ - Transition { - from: "" - to: "visible" - - Modules.Anim { - target: root - property: "implicitHeight" + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - }, - Transition { - from: "visible" - to: "" + property: "implicitHeight" + target: root + } + }, + Transition { + from: "visible" + to: "" - Modules.Anim { - target: root - property: "implicitHeight" + Anim { easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - ] + property: "implicitHeight" + target: root + } + } + ] - Timer { - id: timer + onStateChanged: { + if (state === "visible" && timer.running) { + timer.triggered(); + timer.stop(); + } + } - running: true - interval: 1000 - onTriggered: { - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; - } - } + Timer { + id: timer - Loader { - id: content + interval: 1000 + running: true - anchors.top: parent.top - anchors.left: parent.left - anchors.margins: 8 + onTriggered: { + content.active = Qt.binding(() => root.shouldBeActive || root.visible); + content.visible = true; + } + } - visible: false - active: true + Loader { + id: content - sourceComponent: Content { - implicitWidth: root.implicitWidth - 8 * 2 - props: root.props - visibilities: root.visibilities - popouts: root.popouts - } - } + active: true + anchors.left: parent.left + anchors.margins: 8 + anchors.top: parent.top + visible: false + + sourceComponent: Content { + implicitWidth: root.implicitWidth - 8 * 2 + popouts: root.popouts + props: root.props + visibilities: root.visibilities + } + } } diff --git a/Modules/Notifications/Sidebar/Wrapper.qml b/Modules/Notifications/Sidebar/Wrapper.qml index 4fb280b..4e10cd4 100644 --- a/Modules/Notifications/Sidebar/Wrapper.qml +++ b/Modules/Notifications/Sidebar/Wrapper.qml @@ -2,68 +2,67 @@ pragma ComponentBehavior: Bound import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick Item { - id: root + id: root - required property var visibilities - required property var panels - readonly property Props props: Props {} + required property var panels + readonly property Props props: Props { + } + required property var visibilities - visible: width > 0 - implicitWidth: 0 + implicitWidth: 0 + visible: width > 0 - states: State { - name: "visible" - when: root.visibilities.sidebar + states: State { + name: "visible" + when: root.visibilities.sidebar - PropertyChanges { - root.implicitWidth: Config.sidebar.sizes.width - } - } + PropertyChanges { + root.implicitWidth: Config.sidebar.sizes.width + } + } + transitions: [ + Transition { + from: "" + to: "visible" - transitions: [ - Transition { - from: "" - to: "visible" - - Modules.Anim { - target: root - property: "implicitWidth" + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects - } - }, - Transition { - from: "visible" - to: "" + property: "implicitWidth" + target: root + } + }, + Transition { + from: "visible" + to: "" - Modules.Anim { - target: root - property: "implicitWidth" + Anim { easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - ] + property: "implicitWidth" + target: root + } + } + ] - Loader { - id: content + Loader { + id: content - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: 8 - anchors.bottomMargin: 0 + active: true + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.margins: 8 + anchors.top: parent.top - active: true - Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + sourceComponent: Content { + implicitWidth: Config.sidebar.sizes.width - 8 * 2 + props: root.props + visibilities: root.visibilities + } - sourceComponent: Content { - implicitWidth: Config.sidebar.sizes.width - 8 * 2 - props: root.props - visibilities: root.visibilities - } - } + Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + } } diff --git a/Modules/Notifications/Wrapper.qml b/Modules/Notifications/Wrapper.qml index 46e1763..fda5789 100644 --- a/Modules/Notifications/Wrapper.qml +++ b/Modules/Notifications/Wrapper.qml @@ -1,40 +1,38 @@ import QtQuick import qs.Components import qs.Config -import qs.Modules as Modules Item { - id: root + id: root - required property var visibilities - required property Item panels + required property Item panels + required property var visibilities - visible: height > 0 - implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) - implicitHeight: content.implicitHeight + implicitHeight: content.implicitHeight + implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) + visible: height > 0 - states: State { - name: "hidden" - when: root.visibilities.sidebar + states: State { + name: "hidden" + when: root.visibilities.sidebar - PropertyChanges { - root.implicitHeight: 0 - } - } + PropertyChanges { + root.implicitHeight: 0 + } + } + transitions: Transition { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: root + } + } - transitions: Transition { - Modules.Anim { - target: root - property: "implicitHeight" - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + Content { + id: content - Content { - id: content - - visibilities: root.visibilities - panels: root.panels - } + panels: root.panels + visibilities: root.visibilities + } } diff --git a/Modules/Osd/Background.qml b/Modules/Osd/Background.qml index 90f7711..441dad3 100644 --- a/Modules/Osd/Background.qml +++ b/Modules/Osd/Background.qml @@ -1,61 +1,66 @@ import QtQuick import QtQuick.Shapes import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules ShapePath { - id: root + id: root - required property Wrapper wrapper - readonly property real rounding: 10 - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding + readonly property bool flatten: wrapper.width < rounding * 2 + readonly property real rounding: 10 + readonly property real roundingX: flatten ? wrapper.width / 2 : rounding + required property Wrapper wrapper - strokeWidth: -1 - fillColor: DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX * 3) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX * 2 - relativeY: root.rounding * 2 - radiusX: Math.min(root.rounding * 2, root.wrapper.width) - radiusY: root.rounding * 2 - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 4 - } - PathArc { - relativeX: root.roundingX * 2 - relativeY: root.rounding * 2 - radiusX: Math.min(root.rounding * 2, root.wrapper.width) - radiusY: root.rounding * 2 - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 3 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } + Behavior on fillColor { + CAnim { + } + } - Behavior on fillColor { - Modules.CAnim {} - } + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: -(root.wrapper.width - root.roundingX * 3) + relativeY: 0 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding * 2, root.wrapper.width) + radiusY: root.rounding * 2 + relativeX: -root.roundingX * 2 + relativeY: root.rounding * 2 + } + + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 4 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding * 2, root.wrapper.width) + radiusY: root.rounding * 2 + relativeX: root.roundingX * 2 + relativeY: root.rounding * 2 + } + + PathLine { + relativeX: root.wrapper.width - root.roundingX * 3 + relativeY: 0 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } } diff --git a/Modules/Osd/Content.qml b/Modules/Osd/Content.qml index bddc67f..04d5b1b 100644 --- a/Modules/Osd/Content.qml +++ b/Modules/Osd/Content.qml @@ -6,132 +6,130 @@ import qs.Components import qs.Helpers import qs.Config import qs.Daemons -import qs.Modules as Modules Item { - id: root + id: root - required property Brightness.Monitor monitor - required property var visibilities + required property real brightness + required property Brightness.Monitor monitor + required property bool muted + required property bool sourceMuted + required property real sourceVolume + required property var visibilities + required property real volume - required property real volume - required property bool muted - required property real sourceVolume - required property bool sourceMuted - required property real brightness + implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 + implicitWidth: layout.implicitWidth + Appearance.padding.small * 2 - implicitWidth: layout.implicitWidth + Appearance.padding.small * 2 - implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 + ColumnLayout { + id: layout - ColumnLayout { - id: layout + anchors.centerIn: parent + spacing: Appearance.spacing.normal - anchors.centerIn: parent - spacing: Appearance.spacing.normal + // Speaker volume + CustomMouseArea { + function onWheel(event: WheelEvent) { + if (event.angleDelta.y > 0) + Audio.incrementVolume(); + else if (event.angleDelta.y < 0) + Audio.decrementVolume(); + } - // Speaker volume - CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Config.osd.sizes.sliderWidth - function onWheel(event: WheelEvent) { - if (event.angleDelta.y > 0) - Audio.incrementVolume(); - else if (event.angleDelta.y < 0) - Audio.decrementVolume(); - } - - FilledSlider { - anchors.fill: parent - - icon: Icons.getVolumeIcon(value, root.muted) - value: root.volume - to: Config.services.maxVolume + FilledSlider { + anchors.fill: parent color: Audio.muted ? DynamicColors.palette.m3error : DynamicColors.palette.m3secondary - onMoved: Audio.setVolume(value) - } - } + icon: Icons.getVolumeIcon(value, root.muted) + to: Config.services.maxVolume + value: root.volume - // Microphone volume - WrappedLoader { - shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) + onMoved: Audio.setVolume(value) + } + } - sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + // Microphone volume + WrappedLoader { + shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) - function onWheel(event: WheelEvent) { - if (event.angleDelta.y > 0) - Audio.incrementSourceVolume(); - else if (event.angleDelta.y < 0) - Audio.decrementSourceVolume(); - } + sourceComponent: CustomMouseArea { + function onWheel(event: WheelEvent) { + if (event.angleDelta.y > 0) + Audio.incrementSourceVolume(); + else if (event.angleDelta.y < 0) + Audio.decrementSourceVolume(); + } - FilledSlider { - anchors.fill: parent + implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Config.osd.sizes.sliderWidth - icon: Icons.getMicVolumeIcon(value, root.sourceMuted) - value: root.sourceVolume - to: Config.services.maxVolume + FilledSlider { + anchors.fill: parent color: Audio.sourceMuted ? DynamicColors.palette.m3error : DynamicColors.palette.m3secondary - onMoved: Audio.setSourceVolume(value) - } - } - } + icon: Icons.getMicVolumeIcon(value, root.sourceMuted) + to: Config.services.maxVolume + value: root.sourceVolume - // Brightness - WrappedLoader { - shouldBeActive: Config.osd.enableBrightness + onMoved: Audio.setSourceVolume(value) + } + } + } - sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + // Brightness + WrappedLoader { + shouldBeActive: Config.osd.enableBrightness - function onWheel(event: WheelEvent) { - const monitor = root.monitor; - if (!monitor) - return; - if (event.angleDelta.y > 0) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); - else if (event.angleDelta.y < 0) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); - } + sourceComponent: CustomMouseArea { + function onWheel(event: WheelEvent) { + const monitor = root.monitor; + if (!monitor) + return; + if (event.angleDelta.y > 0) + monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + else if (event.angleDelta.y < 0) + monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + } - FilledSlider { - anchors.fill: parent + implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Config.osd.sizes.sliderWidth - icon: `brightness_${(Math.round(value * 6) + 1)}` - value: root.brightness - onMoved: { - if ( Config.osd.allMonBrightness ) { - root.monitor?.setBrightness(value) + FilledSlider { + anchors.fill: parent + icon: `brightness_${(Math.round(value * 6) + 1)}` + value: root.brightness + + onMoved: { + if (Config.osd.allMonBrightness) { + root.monitor?.setBrightness(value); } else { for (const mon of Brightness.monitors) { - mon.setBrightness(value) + mon.setBrightness(value); } } } - } - } - } - } + } + } + } + } - component WrappedLoader: Loader { - required property bool shouldBeActive + component WrappedLoader: Loader { + required property bool shouldBeActive - Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 - opacity: shouldBeActive ? 1 : 0 - active: opacity > 0 - visible: active + Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 + active: opacity > 0 + opacity: shouldBeActive ? 1 : 0 + visible: active - Behavior on Layout.preferredHeight { - Modules.Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - Behavior on opacity { - Modules.Anim {} - } - } + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + Behavior on opacity { + Anim { + } + } + } } diff --git a/Modules/Osd/Wrapper.qml b/Modules/Osd/Wrapper.qml index 6943809..55f0311 100644 --- a/Modules/Osd/Wrapper.qml +++ b/Modules/Osd/Wrapper.qml @@ -5,132 +5,130 @@ import QtQuick import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules import qs.Daemons Item { - id: root + id: root - required property ShellScreen screen - required property var visibilities - property bool hovered - readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) - readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) + property real brightness + property bool hovered + readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) + property bool muted + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) + property bool sourceMuted + property real sourceVolume + required property var visibilities + property real volume - property real volume - property bool muted - property real sourceVolume - property bool sourceMuted - property real brightness + function show(): void { + visibilities.osd = true; + timer.restart(); + } - function show(): void { - visibilities.osd = true; - timer.restart(); - } + implicitHeight: content.implicitHeight + implicitWidth: 0 + visible: width > 0 - Component.onCompleted: { - volume = Audio.volume; - muted = Audio.muted; - sourceVolume = Audio.sourceVolume; - sourceMuted = Audio.sourceMuted; - brightness = root.monitor?.brightness ?? 0; - } + states: State { + name: "visible" + when: root.shouldBeActive - visible: width > 0 - implicitWidth: 0 - implicitHeight: content.implicitHeight + PropertyChanges { + root.implicitWidth: content.implicitWidth + } + } + transitions: [ + Transition { + from: "" + to: "visible" - states: State { - name: "visible" - when: root.shouldBeActive + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitWidth" + target: root + } + }, + Transition { + from: "visible" + to: "" - PropertyChanges { - root.implicitWidth: content.implicitWidth - } - } + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitWidth" + target: root + } + } + ] - transitions: [ - Transition { - from: "" - to: "visible" + Component.onCompleted: { + volume = Audio.volume; + muted = Audio.muted; + sourceVolume = Audio.sourceVolume; + sourceMuted = Audio.sourceMuted; + brightness = root.monitor?.brightness ?? 0; + } - Modules.Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: MaterialEasing.expressiveEffects - } - }, - Transition { - from: "visible" - to: "" + Connections { + function onMutedChanged(): void { + root.show(); + root.muted = Audio.muted; + } - Modules.Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - ] + function onSourceMutedChanged(): void { + root.show(); + root.sourceMuted = Audio.sourceMuted; + } - Connections { - target: Audio + function onSourceVolumeChanged(): void { + root.show(); + root.sourceVolume = Audio.sourceVolume; + } - function onMutedChanged(): void { - root.show(); - root.muted = Audio.muted; - } + function onVolumeChanged(): void { + root.show(); + root.volume = Audio.volume; + } - function onVolumeChanged(): void { - root.show(); - root.volume = Audio.volume; - } + target: Audio + } - function onSourceMutedChanged(): void { - root.show(); - root.sourceMuted = Audio.sourceMuted; - } + Connections { + function onBrightnessChanged(): void { + root.show(); + root.brightness = root.monitor?.brightness ?? 0; + } - function onSourceVolumeChanged(): void { - root.show(); - root.sourceVolume = Audio.sourceVolume; - } - } + target: root.monitor + } - Connections { - target: root.monitor + Timer { + id: timer - function onBrightnessChanged(): void { - root.show(); - root.brightness = root.monitor?.brightness ?? 0; - } - } + interval: Config.osd.hideDelay - Timer { - id: timer + onTriggered: { + if (!root.hovered) + root.visibilities.osd = false; + } + } - interval: Config.osd.hideDelay - onTriggered: { - if (!root.hovered) - root.visibilities.osd = false; - } - } + Loader { + id: content - Loader { - id: content + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left + sourceComponent: Content { + brightness: root.brightness + monitor: root.monitor + muted: root.muted + sourceMuted: root.sourceMuted + sourceVolume: root.sourceVolume + visibilities: root.visibilities + volume: root.volume + } - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) - - sourceComponent: Content { - monitor: root.monitor - visibilities: root.visibilities - volume: root.volume - muted: root.muted - sourceVolume: root.sourceVolume - sourceMuted: root.sourceMuted - brightness: root.brightness - } - } + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + } } diff --git a/Modules/Polkit/Polkit.qml b/Modules/Polkit/Polkit.qml index 3f34ef5..4304ae4 100644 --- a/Modules/Polkit/Polkit.qml +++ b/Modules/Polkit/Polkit.qml @@ -19,53 +19,57 @@ Scope { PanelWindow { id: panelWindow - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - WlrLayershell.namespace: "ZShell-Auth" - WlrLayershell.layer: WlrLayer.Overlay - visible: false - color: "transparent" property bool detailsOpen: false + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "ZShell-Auth" + color: "transparent" + visible: false + Connections { target: root onShouldShowChanged: { - if ( root.shouldShow ) { - panelWindow.visible = true - openAnim.start() + if (root.shouldShow) { + panelWindow.visible = true; + openAnim.start(); } else { - closeAnim.start() + closeAnim.start(); } } } Anim { id: openAnim - target: inputPanel - property: "opacity" - to: 1 + duration: MaterialEasing.expressiveEffectsTime + property: "opacity" + target: inputPanel + to: 1 } Anim { id: closeAnim - target: inputPanel - property: "opacity" - to: 0 + duration: MaterialEasing.expressiveEffectsTime - onStarted: { - panelWindow.detailsOpen = false - } + property: "opacity" + target: inputPanel + to: 0 + onFinished: { - panelWindow.visible = false + panelWindow.visible = false; + } + onStarted: { + panelWindow.detailsOpen = false; } } anchors { + bottom: true left: true right: true top: true - bottom: true } // mask: Region { item: inputPanel } @@ -73,106 +77,105 @@ Scope { Rectangle { id: inputPanel - color: DynamicColors.tPalette.m3surface - opacity: 0 - anchors.centerIn: parent + color: DynamicColors.tPalette.m3surface + implicitHeight: layout.childrenRect.height + 28 + implicitWidth: layout.childrenRect.width + 32 + opacity: 0 radius: 24 - implicitWidth: layout.childrenRect.width + 32 - implicitHeight: layout.childrenRect.height + 28 ColumnLayout { id: layout + anchors.centerIn: parent RowLayout { id: contentRow + spacing: 24 Item { - Layout.preferredWidth: icon.implicitSize - Layout.preferredHeight: icon.implicitSize - Layout.leftMargin: 16 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.leftMargin: 16 + Layout.preferredHeight: icon.implicitSize + Layout.preferredWidth: icon.implicitSize + IconImage { id: icon anchors.fill: parent - visible: `${source}`.includes("://") - - source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? "" implicitSize: 64 mipmap: true + source: Quickshell.iconPath(polkitAgent.flow?.iconName, true) ?? "" + visible: `${source}`.includes("://") } MaterialIcon { - visible: !icon.visible - - text: "security" anchors.fill: parent font.pointSize: 64 horizontalAlignment: Text.AlignHCenter + text: "security" verticalAlignment: Text.AlignVCenter + visible: !icon.visible } } ColumnLayout { id: contentColumn - Layout.fillWidth: true + Layout.fillHeight: true + Layout.fillWidth: true CustomText { - Layout.preferredWidth: Math.min(600, contentWidth) Layout.alignment: Qt.AlignLeft - + Layout.preferredWidth: Math.min(600, contentWidth) + font.bold: true + font.pointSize: 16 text: polkitAgent.flow?.message wrapMode: Text.WordWrap - font.pointSize: 16 - font.bold: true } CustomText { - Layout.preferredWidth: Math.min(600, contentWidth) Layout.alignment: Qt.AlignLeft - - text: polkitAgent.flow?.supplementaryMessage || "No Additional Information" + Layout.preferredWidth: Math.min(600, contentWidth) color: DynamicColors.tPalette.m3onSurfaceVariant - wrapMode: Text.WordWrap - font.pointSize: 12 font.bold: true + font.pointSize: 12 + text: polkitAgent.flow?.supplementaryMessage || "No Additional Information" + wrapMode: Text.WordWrap } TextField { id: passInput + Layout.preferredHeight: 40 + Layout.preferredWidth: contentColumn.implicitWidth + color: DynamicColors.palette.m3onSurfaceVariant echoMode: polkitAgent.flow?.responseVisible ? TextInput.Normal : TextInput.Password placeholderText: polkitAgent.flow?.failed ? " Incorrect Password" : " Input Password" - selectByMouse: true - onAccepted: okButton.clicked() - - color: DynamicColors.palette.m3onSurfaceVariant placeholderTextColor: polkitAgent.flow?.failed ? DynamicColors.palette.m3onError : DynamicColors.tPalette.m3onSurfaceVariant - - Layout.preferredWidth: contentColumn.implicitWidth - Layout.preferredHeight: 40 + selectByMouse: true background: CustomRect { - radius: 8 + color: (polkitAgent.flow?.failed && passInput.text === "") ? DynamicColors.palette.m3error : DynamicColors.tPalette.m3surfaceVariant implicitHeight: 40 - color: ( polkitAgent.flow?.failed && passInput.text === "" ) ? DynamicColors.palette.m3error : DynamicColors.tPalette.m3surfaceVariant + radius: 8 } + + onAccepted: okButton.clicked() } CustomCheckbox { id: showPassCheckbox - text: "Show Password" - checked: polkitAgent.flow?.responseVisible - onCheckedChanged: { - passInput.echoMode = checked ? TextInput.Normal : TextInput.Password - passInput.forceActiveFocus() - } Layout.alignment: Qt.AlignLeft + checked: polkitAgent.flow?.responseVisible + text: "Show Password" + + onCheckedChanged: { + passInput.echoMode = checked ? TextInput.Normal : TextInput.Password; + passInput.forceActiveFocus(); + } } } } @@ -180,51 +183,49 @@ Scope { CustomRect { id: detailsPanel - visible: true - color: DynamicColors.tPalette.m3surfaceContainerLow - radius: 16 - clip: true - implicitHeight: 0 + property bool open: panelWindow.detailsOpen + Layout.fillWidth: true Layout.preferredHeight: implicitHeight - - property bool open: panelWindow.detailsOpen + clip: true + color: DynamicColors.tPalette.m3surfaceContainerLow + implicitHeight: 0 + radius: 16 + visible: true Behavior on open { ParallelAnimation { Anim { - target: detailsPanel - + duration: MaterialEasing.expressiveEffectsTime property: "implicitHeight" + target: detailsPanel to: !detailsPanel.open ? textDetailsColumn.childrenRect.height + 16 : 0 - duration: MaterialEasing.expressiveEffectsTime } Anim { - target: textDetailsColumn - + duration: MaterialEasing.expressiveEffectsTime property: "opacity" + target: textDetailsColumn to: !detailsPanel.open ? 1 : 0 - duration: MaterialEasing.expressiveEffectsTime } Anim { - target: textDetailsColumn - - property: "scale" - to: !detailsPanel.open ? 1 : 0.9 duration: MaterialEasing.expressiveEffectsTime + property: "scale" + target: textDetailsColumn + to: !detailsPanel.open ? 1 : 0.9 } } } ColumnLayout { id: textDetailsColumn - spacing: 8 + anchors.fill: parent anchors.margins: 8 opacity: 0 scale: 0.9 + spacing: 8 CustomText { text: `actionId: ${polkitAgent.flow?.actionId}` @@ -239,64 +240,67 @@ Scope { } RowLayout { - spacing: 8 Layout.preferredWidth: contentRow.implicitWidth + spacing: 8 CustomButton { id: detailsButton - text: "Details" - textColor: DynamicColors.palette.m3onSurface + + Layout.alignment: Qt.AlignLeft + Layout.preferredHeight: 40 + Layout.preferredWidth: 92 bgColor: DynamicColors.palette.m3surfaceContainer enabled: true radius: 1000 - - Layout.preferredWidth: 92 - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignLeft + text: "Details" + textColor: DynamicColors.palette.m3onSurface onClicked: { - panelWindow.detailsOpen = !panelWindow.detailsOpen - console.log(panelWindow.detailsOpen) + panelWindow.detailsOpen = !panelWindow.detailsOpen; } } Item { id: spacer + Layout.fillWidth: true } CustomButton { id: okButton - text: "OK" - textColor: DynamicColors.palette.m3onPrimary + + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 76 bgColor: DynamicColors.palette.m3primary enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired radius: 1000 - Layout.preferredWidth: 76 - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignRight + text: "OK" + textColor: DynamicColors.palette.m3onPrimary + onClicked: { - polkitAgent.flow.submit(passInput.text) - passInput.text = "" - passInput.forceActiveFocus() + polkitAgent.flow.submit(passInput.text); + passInput.text = ""; + passInput.forceActiveFocus(); } } CustomButton { id: cancelButton - text: "Cancel" - textColor: DynamicColors.palette.m3onSurface + + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 76 bgColor: DynamicColors.palette.m3surfaceContainer enabled: passInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired radius: 1000 - Layout.preferredWidth: 76 - Layout.preferredHeight: 40 - Layout.alignment: Qt.AlignRight + text: "Cancel" + textColor: DynamicColors.palette.m3onSurface + onClicked: { - root.shouldShow = false - console.log(icon.source, icon.visible) - polkitAgent.flow.cancelAuthenticationRequest() - passInput.text = "" + root.shouldShow = false; + polkitAgent.flow.cancelAuthenticationRequest(); + passInput.text = ""; } } } @@ -304,48 +308,49 @@ Scope { } Connections { - target: polkitAgent.flow - function onIsResponseRequiredChanged() { - passInput.text = "" - if ( polkitAgent.flow?.isResponseRequired ) - root.shouldShow = true - passInput.forceActiveFocus() + passInput.text = ""; + if (polkitAgent.flow?.isResponseRequired) + root.shouldShow = true; + passInput.forceActiveFocus(); } function onIsSuccessfulChanged() { - if ( polkitAgent.flow?.isSuccessful ) - root.shouldShow = false - passInput.text = "" + if (polkitAgent.flow?.isSuccessful) + root.shouldShow = false; + passInput.text = ""; } + + target: polkitAgent.flow } } PolkitAgent { id: polkitAgent + } Variants { model: Quickshell.screens PanelWindow { - required property var modelData color: root.shouldShow ? "#80000000" : "transparent" - screen: modelData exclusionMode: ExclusionMode.Ignore + screen: modelData visible: panelWindow.visible Behavior on color { - CAnim {} + CAnim { + } } anchors { + bottom: true left: true right: true top: true - bottom: true } } } diff --git a/Modules/Resource-old.qml b/Modules/Resource-old.qml new file mode 100644 index 0000000..3be26f9 --- /dev/null +++ b/Modules/Resource-old.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import qs.Components +import qs.Config + +Item { + id: root + + property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor + required property color mainColor + required property double percentage + property bool shown: true + property color usageColor: warning ? DynamicColors.palette.m3error : mainColor + property bool warning: percentage * 100 >= warningThreshold + property int warningThreshold: 100 + + clip: true + implicitHeight: 22 + implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth + visible: width > 0 && height > 0 + + Behavior on percentage { + NumberAnimation { + duration: 300 + easing.type: Easing.InOutQuad + } + } + + RowLayout { + id: resourceRowLayout + + spacing: 2 + x: shown ? 0 : -resourceRowLayout.width + + anchors { + verticalCenter: parent.verticalCenter + } + + Item { + Layout.alignment: Qt.AlignVCenter + implicitHeight: root.implicitHeight + implicitWidth: 14 + + Rectangle { + id: backgroundCircle + + anchors.centerIn: parent + border.color: "#404040" + border.width: 1 + color: "#40000000" + height: 14 + radius: height / 2 + width: 14 + } + + Shape { + anchors.fill: backgroundCircle + preferredRendererType: Shape.CurveRenderer + smooth: true + + ShapePath { + fillColor: root.usageColor + startX: backgroundCircle.width / 2 + startY: backgroundCircle.height / 2 + strokeWidth: 0 + + Behavior on fillColor { + CAnim { + } + } + + PathLine { + x: backgroundCircle.width / 2 + y: 0 + (1 / 2) + } + + PathAngleArc { + centerX: backgroundCircle.width / 2 + centerY: backgroundCircle.height / 2 + radiusX: backgroundCircle.width / 2 - (1 / 2) + radiusY: backgroundCircle.height / 2 - (1 / 2) + startAngle: -90 + sweepAngle: 360 * root.percentage + } + + PathLine { + x: backgroundCircle.width / 2 + y: backgroundCircle.height / 2 + } + } + + ShapePath { + capStyle: ShapePath.FlatCap + fillColor: "transparent" + strokeColor: root.borderColor + strokeWidth: 1 + + Behavior on strokeColor { + CAnim { + } + } + + PathAngleArc { + centerX: backgroundCircle.width / 2 + centerY: backgroundCircle.height / 2 + radiusX: backgroundCircle.width / 2 - (1 / 2) + radiusY: backgroundCircle.height / 2 - (1 / 2) + startAngle: -90 + sweepAngle: 360 * root.percentage + } + } + } + } + } +} diff --git a/Modules/Resource.qml b/Modules/Resource.qml index e4f0182..e26085c 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -1,111 +1,93 @@ -import qs.Modules import QtQuick import QtQuick.Layouts import QtQuick.Shapes -import Quickshell +import qs.Components import qs.Config Item { - id: root - required property double percentage - property int warningThreshold: 100 - property bool shown: true - clip: true - visible: width > 0 && height > 0 - implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth - implicitHeight: 22 - property bool warning: percentage * 100 >= warningThreshold + id: root + + property color accentColor: warning ? DynamicColors.palette.m3error : mainColor + property real animatedPercentage: 0 + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property string icon required property color mainColor - property color usageColor: warning ? DynamicColors.palette.m3error : mainColor - property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor + required property double percentage + property bool shown: true + property color usageColor: warning ? DynamicColors.palette.m3error : mainColor + property bool warning: percentage * 100 >= warningThreshold + property int warningThreshold: 80 - Behavior on percentage { - NumberAnimation { - duration: 300 - easing.type: Easing.InOutQuad - } - } + clip: true + height: implicitHeight + implicitHeight: 34 + implicitWidth: 34 + percentage: 0 + visible: width > 0 && height > 0 + width: implicitWidth - RowLayout { - id: resourceRowLayout - spacing: 2 - x: shown ? 0 : -resourceRowLayout.width - anchors { - verticalCenter: parent.verticalCenter - } + Behavior on animatedPercentage { + Anim { + duration: Appearance.anim.durations.large + } + } - Item { - Layout.alignment: Qt.AlignVCenter - implicitWidth: 14 - implicitHeight: root.implicitHeight + Component.onCompleted: animatedPercentage = percentage + onPercentageChanged: animatedPercentage = percentage - Rectangle { - id: backgroundCircle - anchors.centerIn: parent - width: 14 - height: 14 - radius: height / 2 - color: "#40000000" - border.color: "#404040" - border.width: 1 - } + Canvas { + id: gaugeCanvas - Shape { - anchors.fill: backgroundCircle + anchors.centerIn: parent + height: width + width: Math.min(parent.width, parent.height) - smooth: true - preferredRendererType: Shape.CurveRenderer + Component.onCompleted: requestPaint() + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = (height / 2) + 1; + const radius = (Math.min(width, height) - 12) / 2; + const lineWidth = 3; + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); + ctx.stroke(); + if (root.animatedPercentage > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = root.accentColor; + ctx.stroke(); + } + } - ShapePath { - strokeWidth: 0 - fillColor: root.usageColor - startX: backgroundCircle.width / 2 - startY: backgroundCircle.height / 2 + Connections { + function onAnimatedPercentageChanged() { + gaugeCanvas.requestPaint(); + } - Behavior on fillColor { - CAnim {} - } + target: root + } - PathLine { - x: backgroundCircle.width / 2 - y: 0 + ( 1 / 2 ) - } + Connections { + function onPaletteChanged() { + gaugeCanvas.requestPaint(); + } - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - ( 1 / 2 ) - radiusY: backgroundCircle.height / 2 - ( 1 / 2 ) - startAngle: -90 - sweepAngle: 360 * root.percentage - } + target: DynamicColors + } + } - PathLine { - x: backgroundCircle.width / 2 - y: backgroundCircle.height / 2 - } - } - - ShapePath { - strokeWidth: 1 - strokeColor: root.borderColor - fillColor: "transparent" - capStyle: ShapePath.FlatCap - - Behavior on strokeColor { - CAnim {} - } - - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - ( 1 / 2 ) - radiusY: backgroundCircle.height / 2 - ( 1 / 2 ) - startAngle: -90 - sweepAngle: 360 * root.percentage - } - } - } - } - } + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurface + font.pointSize: 12 + text: root.icon + } } diff --git a/Modules/ResourceDetail.qml b/Modules/ResourceDetail.qml index 9ceb6f0..1ec97f5 100644 --- a/Modules/ResourceDetail.qml +++ b/Modules/ResourceDetail.qml @@ -5,71 +5,73 @@ import qs.Config import qs.Components Item { - id: root - required property string resourceName - required property double percentage - required property int warningThreshold - required property string details - required property string iconString - property color barColor: DynamicColors.palette.m3primary - property color warningBarColor: DynamicColors.palette.m3error - property color textColor: DynamicColors.palette.m3onSurface + id: root - Layout.preferredWidth: 158 - Layout.preferredHeight: columnLayout.implicitHeight + property color barColor: DynamicColors.palette.m3primary + required property string details + required property string iconString + required property double percentage + required property string resourceName + property color textColor: DynamicColors.palette.m3onSurface + property color warningBarColor: DynamicColors.palette.m3error + required property int warningThreshold - ColumnLayout { - id: columnLayout - anchors.fill: parent - spacing: 4 + Layout.preferredHeight: columnLayout.implicitHeight + Layout.preferredWidth: 158 - Row { - spacing: 6 - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true + ColumnLayout { + id: columnLayout - MaterialIcon { - font.family: "Material Symbols Rounded" - font.pointSize: 28 - text: root.iconString - color: root.textColor - } + anchors.fill: parent + spacing: 4 - CustomText { - anchors.verticalCenter: parent.verticalCenter - text: root.resourceName - font.pointSize: 12 - color: root.textColor - } - } + Row { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + spacing: 6 - Rectangle { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.preferredHeight: 6 - radius: height / 2 - color: "#40000000" + MaterialIcon { + color: root.textColor + font.family: "Material Symbols Rounded" + font.pointSize: 28 + text: root.iconString + } - Rectangle { - width: parent.width * Math.min(root.percentage, 1) - height: parent.height - radius: height / 2 - color: root.percentage * 100 >= root.warningThreshold ? root.warningBarColor : root.barColor + CustomText { + anchors.verticalCenter: parent.verticalCenter + color: root.textColor + font.pointSize: 12 + text: root.resourceName + } + } - Behavior on width { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } + Rectangle { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.preferredHeight: 6 + color: "#40000000" + radius: height / 2 - CustomText { - Layout.alignment: Qt.AlignLeft - text: root.details - font.pointSize: 10 - color: root.textColor - } - } + Rectangle { + color: root.percentage * 100 >= root.warningThreshold ? root.warningBarColor : root.barColor + height: parent.height + radius: height / 2 + width: parent.width * Math.min(root.percentage, 1) + + Behavior on width { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + } + } + + CustomText { + Layout.alignment: Qt.AlignLeft + color: root.textColor + font.pointSize: 10 + text: root.details + } + } } diff --git a/Modules/ResourcePopout-old.qml b/Modules/ResourcePopout-old.qml new file mode 100644 index 0000000..4dcc35c --- /dev/null +++ b/Modules/ResourcePopout-old.qml @@ -0,0 +1,59 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config + +Item { + id: popoutWindow + + required property var wrapper + + implicitHeight: contentColumn.implicitHeight + 10 + implicitWidth: contentColumn.implicitWidth + 10 * 2 + + // ShadowRect { + // anchors.fill: contentRect + // radius: 8 + // } + + ColumnLayout { + id: contentColumn + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 10 + + ResourceDetail { + details: qsTr("%1 of %2 MB used").arg(Math.round(ResourceUsage.memoryUsed * 0.001)).arg(Math.round(ResourceUsage.memoryTotal * 0.001)) + iconString: "\uf7a3" + percentage: ResourceUsage.memoryUsedPercentage + resourceName: qsTr("Memory Usage") + warningThreshold: 95 + } + + ResourceDetail { + details: qsTr("%1% used").arg(Math.round(ResourceUsage.cpuUsage * 100)) + iconString: "\ue322" + percentage: ResourceUsage.cpuUsage + resourceName: qsTr("CPU Usage") + warningThreshold: 95 + } + + ResourceDetail { + details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuUsage * 100)) + iconString: "\ue30f" + percentage: ResourceUsage.gpuUsage + resourceName: qsTr("GPU Usage") + warningThreshold: 95 + } + + ResourceDetail { + details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuMemUsage * 100)) + iconString: "\ue30d" + percentage: ResourceUsage.gpuMemUsage + resourceName: qsTr("VRAM Usage") + warningThreshold: 95 + } + } +} diff --git a/Modules/ResourcePopout.qml b/Modules/ResourcePopout.qml deleted file mode 100644 index c38d378..0000000 --- a/Modules/ResourcePopout.qml +++ /dev/null @@ -1,61 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Config - -Item { - id: popoutWindow - implicitWidth: contentColumn.implicitWidth + 10 * 2 - implicitHeight: contentColumn.implicitHeight + 10 - required property var wrapper - - // ShadowRect { - // anchors.fill: contentRect - // radius: 8 - // } - - ColumnLayout { - id: contentColumn - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - spacing: 10 - - ResourceDetail { - resourceName: qsTr( "Memory Usage" ) - iconString: "\uf7a3" - percentage: ResourceUsage.memoryUsedPercentage - warningThreshold: 95 - details: qsTr( "%1 of %2 MB used" ) - .arg( Math.round( ResourceUsage.memoryUsed * 0.001 )) - .arg( Math.round( ResourceUsage.memoryTotal * 0.001 )) - } - - ResourceDetail { - resourceName: qsTr( "CPU Usage" ) - iconString: "\ue322" - percentage: ResourceUsage.cpuUsage - warningThreshold: 95 - details: qsTr( "%1% used" ) - .arg( Math.round( ResourceUsage.cpuUsage * 100 )) - } - - ResourceDetail { - resourceName: qsTr( "GPU Usage" ) - iconString: "\ue30f" - percentage: ResourceUsage.gpuUsage - warningThreshold: 95 - details: qsTr( "%1% used" ) - .arg( Math.round( ResourceUsage.gpuUsage * 100 )) - } - - ResourceDetail { - resourceName: qsTr( "VRAM Usage" ) - iconString: "\ue30d" - percentage: ResourceUsage.gpuMemUsage - warningThreshold: 95 - details: qsTr( "%1% used" ) - .arg( Math.round( ResourceUsage.gpuMemUsage * 100 )) - } - } -} diff --git a/Modules/ResourceUsage.qml b/Modules/ResourceUsage.qml index 415b8b0..f1b01ce 100644 --- a/Modules/ResourceUsage.qml +++ b/Modules/ResourceUsage.qml @@ -7,98 +7,117 @@ import Quickshell.Io import qs.Config Singleton { - id: root - property double memoryTotal: 1 + id: root + + property string autoGpuType: "NONE" + property double cpuUsage: 0 + property double gpuMemUsage: 0 + readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType + property double gpuUsage: 0 property double memoryFree: 1 + property double memoryTotal: 1 property double memoryUsed: memoryTotal - memoryFree - property double memoryUsedPercentage: memoryUsed / memoryTotal - property double swapTotal: 1 + property double memoryUsedPercentage: memoryUsed / memoryTotal + property var previousCpuStats property double swapFree: 1 + property double swapTotal: 1 property double swapUsed: swapTotal - swapFree - property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 - property double cpuUsage: 0 - property var previousCpuStats - property double gpuUsage: 0 - property double gpuMemUsage: 0 - property double totalMem: 0 - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType - property string autoGpuType: "NONE" + property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 + property double totalMem: 0 Timer { interval: 1 - running: true - repeat: true + repeat: true + running: true + onTriggered: { - // Reload files - fileMeminfo.reload() - fileStat.reload() + // Reload files + fileMeminfo.reload(); + fileStat.reload(); - // Parse memory and swap usage - const textMeminfo = fileMeminfo.text() - memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1) - memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0) - swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1) - swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0) + // Parse memory and swap usage + const textMeminfo = fileMeminfo.text(); + memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1); + memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0); + swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1); + swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0); - // Parse CPU usage - const textStat = fileStat.text() - const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) - if (cpuLine) { - const stats = cpuLine.slice(1).map(Number) - const total = stats.reduce((a, b) => a + b, 0) - const idle = stats[3] + // Parse CPU usage + const textStat = fileStat.text(); + const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); + if (cpuLine) { + const stats = cpuLine.slice(1).map(Number); + const total = stats.reduce((a, b) => a + b, 0); + const idle = stats[3]; - if (previousCpuStats) { - const totalDiff = total - previousCpuStats.total - const idleDiff = idle - previousCpuStats.idle - cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0 - } + if (previousCpuStats) { + const totalDiff = total - previousCpuStats.total; + const idleDiff = idle - previousCpuStats.idle; + cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; + } - previousCpuStats = { total, idle } - } - if ( root.gpuType === "NVIDIA" ) { - processGpu.running = true - } + previousCpuStats = { + total, + idle + }; + } + if (root.gpuType === "NVIDIA") { + processGpu.running = true; + } - interval = 3000 - } + interval = 3000; + } } - FileView { id: fileMeminfo; path: "/proc/meminfo" } - FileView { id: fileStat; path: "/proc/stat" } + FileView { + id: fileMeminfo - Process { - id: gpuTypeCheck + path: "/proc/meminfo" + } - running: !Config.services.gpuType - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } + FileView { + id: fileStat - Process { - id: oneshotMem - command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] - running: root.gpuType === "NVIDIA" && totalMem === 0 - stdout: StdioCollector { - onStreamFinished: { - totalMem = Number(this.text.trim()) - oneshotMem.running = false - } - } - } + path: "/proc/stat" + } - Process { - id: processGpu - command: ["nvidia-smi", "--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"] - running: false - stdout: StdioCollector { - onStreamFinished: { - const parts = this.text.trim().split(", ") - gpuUsage = Number(parts[0]) / 100 - gpuMemUsage = Number(parts[1]) / totalMem - } - } - } + Process { + id: gpuTypeCheck + + command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] + running: !Config.services.gpuType + + stdout: StdioCollector { + onStreamFinished: root.autoGpuType = text.trim() + } + } + + Process { + id: oneshotMem + + command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] + running: root.gpuType === "NVIDIA" && totalMem === 0 + + stdout: StdioCollector { + onStreamFinished: { + totalMem = Number(this.text.trim()); + oneshotMem.running = false; + } + } + } + + Process { + id: processGpu + + command: ["nvidia-smi", "--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const parts = this.text.trim().split(", "); + gpuUsage = Number(parts[0]) / 100; + gpuMemUsage = Number(parts[1]) / totalMem; + } + } + } } diff --git a/Modules/Resources.qml b/Modules/Resources.qml index 053246b..ab36c71 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -3,88 +3,74 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import QtQuick.Layouts -import Quickshell.Wayland +import qs.Helpers import qs.Modules import qs.Config -import qs.Effects import qs.Components Item { - id: root - implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin - implicitHeight: 34 - clip: true + id: root - Rectangle { - id: backgroundRect - anchors { - left: parent.left - right: parent.right - verticalCenter: parent.verticalCenter - } - implicitHeight: 22 - color: DynamicColors.tPalette.m3surfaceContainer - radius: height / 2 - Behavior on color { - CAnim {} - } - RowLayout { - id: rowLayout + required property PersistentProperties visibilities - spacing: 6 - anchors.fill: parent - anchors.leftMargin: 5 - anchors.rightMargin: 5 + anchors.bottom: parent.bottom + anchors.top: parent.top + clip: true + implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: "memory_alt" - color: DynamicColors.palette.m3onSurface - } + CustomRect { + id: backgroundRect - Resource { - percentage: ResourceUsage.memoryUsedPercentage - warningThreshold: 95 - mainColor: DynamicColors.palette.m3primary - } + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + radius: height / 2 - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: "memory" - color: DynamicColors.palette.m3onSurface - } + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + } - Resource { - percentage: ResourceUsage.cpuUsage - warningThreshold: 80 - mainColor: DynamicColors.palette.m3secondary - } + StateLayer { + onClicked: root.visibilities.resources = !root.visibilities.resources + } + } - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: "gamepad" - color: DynamicColors.palette.m3onSurface - } + RowLayout { + id: rowLayout - Resource { - percentage: ResourceUsage.gpuUsage - mainColor: DynamicColors.palette.m3tertiary - } + anchors.centerIn: parent + spacing: 0 - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - font.pixelSize: 18 - text: "developer_board" - color: DynamicColors.palette.m3onSurface - } + Ref { + service: SystemUsage + } - Resource { - percentage: ResourceUsage.gpuMemUsage - mainColor: DynamicColors.palette.m3primary - } - } - } + Resource { + Layout.alignment: Qt.AlignVCenter + icon: "memory" + mainColor: DynamicColors.palette.m3primary + percentage: SystemUsage.cpuPerc + warningThreshold: 95 + } + + Resource { + icon: "memory_alt" + mainColor: DynamicColors.palette.m3secondary + percentage: SystemUsage.memPerc + warningThreshold: 80 + } + + Resource { + icon: "gamepad" + mainColor: DynamicColors.palette.m3tertiary + percentage: SystemUsage.gpuPerc + } + + Resource { + icon: "developer_board" + mainColor: DynamicColors.palette.m3primary + percentage: SystemUsage.gpuMemUsed + } + } } 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/Shortcuts.qml b/Modules/Shortcuts.qml index c4ff3dc..7da6200 100644 --- a/Modules/Shortcuts.qml +++ b/Modules/Shortcuts.qml @@ -7,29 +7,29 @@ import qs.Helpers Scope { id: root - property bool launcherInterrupted readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + property bool launcherInterrupted + CustomShortcut { + description: "Toggle launcher" + name: "toggle-launcher" - CustomShortcut { - name: "toggle-launcher" - description: "Toggle launcher" - onPressed: root.launcherInterrupted = false - onReleased: { - if (!root.launcherInterrupted && !root.hasFullscreen) { - const visibilities = Visibilities.getForActive(); - visibilities.launcher = !visibilities.launcher; - } - root.launcherInterrupted = false; - } - } + onPressed: root.launcherInterrupted = false + onReleased: { + if (!root.launcherInterrupted && !root.hasFullscreen) { + const visibilities = Visibilities.getForActive(); + visibilities.launcher = !visibilities.launcher; + } + root.launcherInterrupted = false; + } + } CustomShortcut { name: "toggle-nc" onPressed: { - const visibilities = Visibilities.getForActive() - visibilities.sidebar = !visibilities.sidebar + const visibilities = Visibilities.getForActive(); + visibilities.sidebar = !visibilities.sidebar; } } @@ -37,8 +37,17 @@ Scope { name: "show-osd" onPressed: { - const visibilities = Visibilities.getForActive() - visibilities.osd = !visibilities.osd + const visibilities = Visibilities.getForActive(); + visibilities.osd = !visibilities.osd; + } + } + + CustomShortcut { + name: "toggle-settings" + + onPressed: { + const visibilities = Visibilities.getForActive(); + visibilities.settings = !visibilities.settings; } } } diff --git a/Modules/Time.qml b/Modules/Time.qml index 230f413..542f13f 100644 --- a/Modules/Time.qml +++ b/Modules/Time.qml @@ -5,25 +5,26 @@ import QtQuick Singleton { id: root - property alias enabled: clock.enabled - readonly property date date: clock.date - readonly property int hours: clock.hours - readonly property int minutes: clock.minutes - readonly property int seconds: clock.seconds - readonly property string timeStr: format("hh:mm:ss") + readonly property date date: clock.date readonly property string dateStr: format("ddd d MMM - hh:mm:ss") - readonly property list timeComponents: timeStr.split(":") - readonly property string hourStr: timeComponents[0] ?? "" - readonly property string minuteStr: timeComponents[1] ?? "" - readonly property string secondStr: timeComponents[2] ?? "" + property alias enabled: clock.enabled + readonly property string hourStr: timeComponents[0] ?? "" + readonly property int hours: clock.hours + readonly property string minuteStr: timeComponents[1] ?? "" + readonly property int minutes: clock.minutes + readonly property string secondStr: timeComponents[2] ?? "" + readonly property int seconds: clock.seconds + readonly property list timeComponents: timeStr.split(":") + readonly property string timeStr: format("hh:mm:ss") - function format(fmt: string): string { - return Qt.formatDateTime(clock.date, fmt); - } + function format(fmt: string): string { + return Qt.formatDateTime(clock.date, fmt); + } SystemClock { id: clock + precision: SystemClock.Seconds } } diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index e2eb390..c613677 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -1,37 +1,35 @@ import QtQuick.Layouts -import QtQuick.Effects import QtQuick import Quickshell import Quickshell.Services.SystemTray -import Quickshell.Io -import Quickshell.Widgets import qs.Modules import qs.Components import qs.Config Item { - id: root + id: root - required property SystemTrayItem item - required property PanelWindow bar + required property PanelWindow bar + property bool hasLoaded: false required property int ind - required property Wrapper popouts + required property SystemTrayItem item required property RowLayout loader - property bool hasLoaded: false + required property Wrapper popouts StateLayer { + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent anchors.margins: 3 radius: 6 - acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { - if ( mouse.button === Qt.LeftButton ) { + if (mouse.button === Qt.LeftButton) { root.item.activate(); - } else if ( mouse.button === Qt.RightButton ) { - root.popouts.currentName = `traymenu${ root.ind }`; - root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x ); + } else if (mouse.button === Qt.RightButton) { + root.popouts.currentName = `traymenu${root.ind}`; + root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.hasCurrent = true; - if ( visibilities.sidebar || visibilities.dashboard ) { + if (visibilities.sidebar || visibilities.dashboard) { visibilities.sidebar = false; visibilities.dashboard = false; } @@ -43,27 +41,27 @@ Item { id: icon anchors.centerIn: parent - source: root.item.icon - implicitSize: 22 color: DynamicColors.palette.m3onSurface + implicitSize: 22 layer.enabled: DynamicColors.light + source: root.item.icon } - // Image { - // id: icon - // - // property bool batteryHDPI: root.bar.screen.x < 0 && root.item.icon.includes("battery") - // property bool nmHDPI: root.bar.screen.x < 0 && root.item.icon.includes("nm-") - // - // anchors.centerIn: parent - // width: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // height: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // source: root.item.icon - // mipmap: true - // smooth: ( batteryHDPI || nmHDPI ) ? false : true - // asynchronous: true - // sourceSize.width: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // sourceSize.height: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // fillMode: Image.PreserveAspectFit - // } + // Image { + // id: icon + // + // property bool batteryHDPI: root.bar.screen.x < 0 && root.item.icon.includes("battery") + // property bool nmHDPI: root.bar.screen.x < 0 && root.item.icon.includes("nm-") + // + // anchors.centerIn: parent + // width: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) + // height: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) + // source: root.item.icon + // mipmap: true + // smooth: ( batteryHDPI || nmHDPI ) ? false : true + // asynchronous: true + // sourceSize.width: ( batteryHDPI || nmHDPI ) ? 16 : 22 + // sourceSize.height: ( batteryHDPI || nmHDPI ) ? 16 : 22 + // fillMode: Image.PreserveAspectFit + // } } diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml index 60d29e6..f10ed9c 100644 --- a/Modules/TrayMenu.qml +++ b/Modules/TrayMenu.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import Quickshell -import Quickshell.DBusMenu import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects @@ -9,350 +8,385 @@ import Quickshell.Hyprland import QtQml import qs.Effects import qs.Config -import qs.Modules PanelWindow { - id: root + id: root - signal menuActionTriggered() - signal finishedLoading() - required property QsMenuHandle trayMenu - required property point trayItemRect - required property PanelWindow bar - property var menuStack: [] - property real scaleValue: 0 - property alias focusGrab: grab.active - property int entryHeight: 30 - property int biggestWidth: 0 - property int menuItemCount: menuOpener.children.values.length + 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 - property color backgroundColor: DynamicColors.tPalette.m3surface - property color highlightColor: DynamicColors.tPalette.m3primaryContainer - property color textColor: DynamicColors.palette.m3onSurface - property color disabledHighlightColor: DynamicColors.layer(DynamicColors.palette.m3primaryContainer, 0) - property color disabledTextColor: DynamicColors.layer(DynamicColors.palette.m3onSurface, 0) + signal finishedLoading + signal menuActionTriggered - QsMenuOpener { - id: menuOpener - menu: root.trayMenu - } + function goBack() { + if (root.menuStack.length > 0) { + menuChangeAnimation.start(); + root.biggestWidth = 0; + root.trayMenu = root.menuStack.pop(); + listLayout.positionViewAtBeginning(); + backEntry.visible = false; + } + } - // onTrayMenuChanged: { - // listLayout.forceLayout(); - // } + function updateMask() { + root.mask.changed(); + } - visible: false - color: "transparent" - anchors { - top: true - left: true - right: true - bottom: true - } + color: "transparent" - mask: Region { id: mask; item: menuRect } + // onTrayMenuChanged: { + // listLayout.forceLayout(); + // } - function goBack() { - if ( root.menuStack.length > 0 ) { - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = root.menuStack.pop(); - listLayout.positionViewAtBeginning(); - backEntry.visible = false; - } - } + visible: false - function updateMask() { - root.mask.changed(); - } + mask: Region { + id: mask - onVisibleChanged: { - if ( !visible ) - root.menuStack.pop(); - backEntry.visible = false; + item: menuRect + } - openAnim.start(); - } + onMenuActionTriggered: { + if (root.menuStack.length > 0) { + backEntry.visible = true; + } + } + onVisibleChanged: { + if (!visible) + root.menuStack.pop(); + backEntry.visible = false; - HyprlandFocusGrab { - id: grab - windows: [ root ] - active: false - onCleared: { - closeAnim.start(); - } - } + openAnim.start(); + } - SequentialAnimation { - id: menuChangeAnimation - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "x" - target: translateAnim - to: -listLayout.width / 2 - } + QsMenuOpener { + id: menuOpener - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 1 - property: "opacity" - target: columnLayout - to: 0 - } - } + menu: root.trayMenu + } - PropertyAction { - property: "menu" - target: columnLayout - } + anchors { + bottom: true + left: true + right: true + top: true + } - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 0 - property: "opacity" - target: columnLayout - to: 1 - } + HyprlandFocusGrab { + id: grab - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: listLayout.width / 2 - property: "x" - target: translateAnim - to: 0 - } - } - } + active: false + windows: [root] - onMenuActionTriggered: { - if ( root.menuStack.length > 0 ) { - backEntry.visible = true; - } - } + onCleared: { + closeAnim.start(); + } + } - ParallelAnimation { - id: closeAnim - Anim { - target: menuRect - property: "implicitHeight" - to: 0 - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - Anim { - targets: [ menuRect, shadowRect ] - property: "opacity" - from: 1 - to: 0 - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - onFinished: { - root.visible = false; - } - } + SequentialAnimation { + id: menuChangeAnimation - ParallelAnimation { - id: openAnim - Anim { - target: menuRect - property: "implicitHeight" - from: 0 - to: listLayout.contentHeight + ( root.menuStack.length > 0 ? root.entryHeight + 10 : 10 ) - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - Anim { - targets: [ menuRect, shadowRect ] - property: "opacity" - from: 0 - to: 1 - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + ParallelAnimation { + NumberAnimation { + duration: MaterialEasing.standardTime / 2 + easing.bezierCurve: MaterialEasing.expressiveEffects + from: 0 + property: "x" + target: translateAnim + to: -listLayout.width / 2 + } - ShadowRect { - id: shadowRect - anchors.fill: menuRect - radius: menuRect.radius - } + NumberAnimation { + duration: MaterialEasing.standardTime / 2 + easing.bezierCurve: MaterialEasing.standard + from: 1 + property: "opacity" + target: columnLayout + to: 0 + } + } - Rectangle { - id: menuRect - x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 ) - y: Math.round( root.trayItemRect.y - 5 ) - implicitWidth: listLayout.contentWidth + 10 - implicitHeight: listLayout.contentHeight + ( root.menuStack.length > 0 ? root.entryHeight + 10 : 10 ) - color: root.backgroundColor - radius: 8 - clip: true + PropertyAction { + property: "menu" + target: columnLayout + } - Behavior on implicitWidth { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + ParallelAnimation { + NumberAnimation { + duration: MaterialEasing.standardTime / 2 + easing.bezierCurve: MaterialEasing.standard + from: 0 + property: "opacity" + target: columnLayout + to: 1 + } - Behavior on implicitHeight { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + NumberAnimation { + duration: MaterialEasing.standardTime / 2 + easing.bezierCurve: MaterialEasing.expressiveEffects + from: listLayout.width / 2 + property: "x" + target: translateAnim + to: 0 + } + } + } - 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 - spacing: 0 - contentWidth: root.biggestWidth - contentHeight: contentItem.childrenRect.height - model: menuOpener.children + ParallelAnimation { + id: closeAnim - delegate: Rectangle { - id: menuItem - required property int index - required property QsMenuEntry modelData - property var child: QsMenuOpener { - menu: menuItem.modelData - } - property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled - property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled - width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20 - anchors.left: parent.left - anchors.right: parent.right - height: menuItem.modelData.isSeparator ? 1 : root.entryHeight - color: menuItem.modelData.isSeparator ? "#20FFFFFF" : containsMouseAndEnabled ? root.highlightColor : containsMouseAndNotEnabled ? root.disabledHighlightColor : "transparent" - radius: 4 - visible: true + onFinished: { + root.visible = false; + } - Behavior on color { - CAnim { - duration: 150 - } - } + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: menuRect + to: 0 + } - 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; - } - } + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + from: 1 + property: "opacity" + targets: [menuRect, shadowRect] + to: 0 + } + } - TextMetrics { - id: widthMetrics - text: menuItem.modelData.text - } + ParallelAnimation { + id: openAnim - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - preventStealing: true - propagateComposedEvents: true - acceptedButtons: Qt.LeftButton - 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(); - } - } - } + 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) + } - RowLayout { - anchors.fill: parent - Text { - id: menuText - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.leftMargin: 10 - text: menuItem.modelData.text - color: menuItem.modelData.enabled ? root.textColor : root.disabledTextColor - } - Image { - id: iconImage - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.rightMargin: 10 - Layout.maximumWidth: 20 - Layout.maximumHeight: 20 - source: menuItem.modelData.icon - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - layer.enabled: true - layer.effect: ColorOverlay { - color: menuItem.modelData.enabled ? "white" : "gray" - } - } - Text { - id: textArrow - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.rightMargin: 10 - Layout.bottomMargin: 5 - Layout.maximumWidth: 20 - Layout.maximumHeight: 20 - text: "" - color: menuItem.modelData.enabled ? "white" : "gray" - visible: menuItem.modelData.hasChildren ?? false - } - } - } - } - Rectangle { - id: backEntry - visible: false - Layout.fillWidth: true - Layout.preferredHeight: root.entryHeight - color: mouseAreaBack.containsMouse ? "#15FFFFFF" : "transparent" - radius: 4 + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + from: 0 + property: "opacity" + targets: [menuRect, shadowRect] + to: 1 + } + } - MouseArea { - id: mouseAreaBack - anchors.fill: parent - hoverEnabled: true - onClicked: { - root.goBack(); - } - } + ShadowRect { + id: shadowRect - Text { - anchors.fill: parent - anchors.leftMargin: 10 - verticalAlignment: Text.AlignVCenter - text: "Back " - color: "white" - } - } - } - } + 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/TrayMenuPopout.qml b/Modules/TrayMenuPopout.qml index cb0cd24..7115d9b 100644 --- a/Modules/TrayMenuPopout.qml +++ b/Modules/TrayMenuPopout.qml @@ -9,239 +9,238 @@ import QtQuick.Controls import QtQuick.Effects StackView { - id: root + id: root - required property Item popouts - required property QsMenuHandle trayItem + property int biggestWidth: 0 + required property Item popouts + property int rootWidth: 0 + required property QsMenuHandle trayItem - property int rootWidth: 0 - property int biggestWidth: 0 + implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem.implicitWidth - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + initialItem: SubMenu { + handle: root.trayItem + } + popEnter: NoAnim { + } + popExit: NoAnim { + } + pushEnter: NoAnim { + } + pushExit: NoAnim { + } - initialItem: SubMenu { - handle: root.trayItem - } + Component { + id: subMenuComp - pushEnter: NoAnim {} - pushExit: NoAnim {} - popEnter: NoAnim {} - popExit: NoAnim {} + SubMenu { + } + } - component NoAnim: Transition { - NumberAnimation { - duration: 0 - } - } + component NoAnim: Transition { + NumberAnimation { + duration: 0 + } + } + component SubMenu: Column { + id: menu - component SubMenu: Column { - id: menu + required property QsMenuHandle handle + property bool isSubMenu + property bool shown - required property QsMenuHandle handle - property bool isSubMenu - property bool shown + opacity: shown ? 1 : 0 + padding: 0 + scale: shown ? 1 : 0.8 + spacing: 4 - padding: 0 - spacing: 4 + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } + } - opacity: shown ? 1 : 0 - scale: shown ? 1 : 0.8 + Component.onCompleted: shown = true + StackView.onActivating: shown = true + StackView.onDeactivating: shown = false + StackView.onRemoved: destroy() - Component.onCompleted: shown = true - StackView.onActivating: shown = true - StackView.onDeactivating: shown = false - StackView.onRemoved: destroy() + QsMenuOpener { + id: menuOpener - Behavior on opacity { - Anim {} - } + menu: menu.handle + } - Behavior on scale { - Anim {} - } + Repeater { + model: menuOpener.children - QsMenuOpener { - id: menuOpener + CustomRect { + id: item - menu: menu.handle - } + required property QsMenuEntry modelData - Repeater { - model: menuOpener.children + color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" + implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight + implicitWidth: root.biggestWidth + radius: 4 - CustomRect { - id: item + Loader { + id: children - required property QsMenuEntry modelData + active: !item.modelData.isSeparator + anchors.left: parent.left + anchors.right: parent.right + asynchronous: true - implicitWidth: root.biggestWidth - implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight + sourceComponent: Item { + implicitHeight: 30 - radius: 4 - color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" + StateLayer { + function onClicked(): void { + const entry = item.modelData; + if (entry.hasChildren) { + root.rootWidth = root.biggestWidth; + root.biggestWidth = 0; + root.push(subMenuComp.createObject(null, { + handle: entry, + isSubMenu: true + })); + } else { + item.modelData.triggered(); + root.popouts.hasCurrent = false; + } + } - Loader { - id: children + disabled: !item.modelData.enabled + radius: item.radius + } - anchors.left: parent.left - anchors.right: parent.right + Loader { + id: icon - active: !item.modelData.isSeparator - asynchronous: true + active: item.modelData.icon !== "" + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.verticalCenter: parent.verticalCenter + asynchronous: true - sourceComponent: Item { - implicitHeight: 30 + sourceComponent: Item { + implicitHeight: label.implicitHeight + implicitWidth: label.implicitHeight - StateLayer { - radius: item.radius - disabled: !item.modelData.enabled + IconImage { + id: iconImage - function onClicked(): void { - const entry = item.modelData; - if (entry.hasChildren) { - root.rootWidth = root.biggestWidth; - root.biggestWidth = 0; - root.push(subMenuComp.createObject(null, { - handle: entry, - isSubMenu: true - })); - } else { - item.modelData.triggered(); - root.popouts.hasCurrent = false; - } - } - } + implicitSize: parent.implicitHeight + source: item.modelData.icon + visible: false + } - Loader { - id: icon + MultiEffect { + anchors.fill: iconImage + colorization: 1.0 + colorizationColor: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + source: iconImage + } + } + } - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: 10 + CustomText { + id: label - active: item.modelData.icon !== "" - asynchronous: true + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + text: labelMetrics.elidedText + } - sourceComponent: Item { - implicitHeight: label.implicitHeight - implicitWidth: label.implicitHeight - IconImage { - id: iconImage - implicitSize: parent.implicitHeight - source: item.modelData.icon - visible: false - } + TextMetrics { + id: labelMetrics - MultiEffect { - anchors.fill: iconImage - source: iconImage - colorization: 1.0 - colorizationColor: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - } - } - } + font.family: label.font.family + font.pointSize: label.font.pointSize + text: item.modelData.text - CustomText { - id: label + Component.onCompleted: { + var biggestWidth = root.biggestWidth; + var currentWidth = labelMetrics.width + (item.modelData.icon ?? "" ? 30 : 0) + (item.modelData.hasChildren ? 30 : 0) + 20; + if (currentWidth > biggestWidth) { + root.biggestWidth = currentWidth; + } + } + } - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 10 + Loader { + id: expand - text: labelMetrics.elidedText - color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - } + active: item.modelData.hasChildren + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + asynchronous: true - TextMetrics { - id: labelMetrics + sourceComponent: MaterialIcon { + color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline + text: "chevron_right" + } + } + } + } + } + } - text: item.modelData.text - font.pointSize: label.font.pointSize - font.family: label.font.family + Loader { + active: menu.isSubMenu + asynchronous: true - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = labelMetrics.width + (item.modelData.icon ?? "" ? 30 : 0) + (item.modelData.hasChildren ? 30 : 0) + 20; - if ( currentWidth > biggestWidth ) { - root.biggestWidth = currentWidth; - } - } - } + sourceComponent: Item { + implicitHeight: back.implicitHeight + 2 / 2 + implicitWidth: back.implicitWidth - Loader { - id: expand + Item { + anchors.bottom: parent.bottom + implicitHeight: back.implicitHeight + implicitWidth: back.implicitWidth + 10 - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right + CustomRect { + anchors.fill: parent + color: DynamicColors.palette.m3secondaryContainer + radius: 4 - active: item.modelData.hasChildren - asynchronous: true + StateLayer { + function onClicked(): void { + root.pop(); + root.biggestWidth = root.rootWidth; + } - sourceComponent: MaterialIcon { - text: "chevron_right" - color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - } - } - } - } - } - } + color: DynamicColors.palette.m3onSecondaryContainer + radius: parent.radius + } + } - Loader { - active: menu.isSubMenu - asynchronous: true + Row { + id: back - sourceComponent: Item { - implicitWidth: back.implicitWidth - implicitHeight: back.implicitHeight + 2 / 2 + anchors.verticalCenter: parent.verticalCenter - Item { - anchors.bottom: parent.bottom - implicitWidth: back.implicitWidth + 10 - implicitHeight: back.implicitHeight + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onSecondaryContainer + text: "chevron_left" + } - CustomRect { - anchors.fill: parent - radius: 4 - color: DynamicColors.palette.m3secondaryContainer - - StateLayer { - radius: parent.radius - color: DynamicColors.palette.m3onSecondaryContainer - - function onClicked(): void { - root.pop(); - root.biggestWidth = root.rootWidth; - } - } - } - - Row { - id: back - - anchors.verticalCenter: parent.verticalCenter - - MaterialIcon { - anchors.verticalCenter: parent.verticalCenter - text: "chevron_left" - color: DynamicColors.palette.m3onSecondaryContainer - } - - CustomText { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Back") - color: DynamicColors.palette.m3onSecondaryContainer - } - } - } - } - } - } - - Component { - id: subMenuComp - - SubMenu {} - } + CustomText { + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3onSecondaryContainer + text: qsTr("Back") + } + } + } + } + } + } } diff --git a/Modules/TrayWidget.qml b/Modules/TrayWidget.qml index 4648b80..07e2e02 100644 --- a/Modules/TrayWidget.qml +++ b/Modules/TrayWidget.qml @@ -4,34 +4,56 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Services.SystemTray +import qs.Components +import qs.Config -Row { - id: root +Item { + id: root - anchors.top: parent.top - anchors.bottom: parent.bottom - - required property PanelWindow bar - required property Wrapper popouts + required property PanelWindow bar + readonly property alias items: repeater required property RowLayout loader - readonly property alias items: repeater + required property Wrapper popouts - spacing: 0 + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitHeight: 34 + implicitWidth: row.width + Appearance.padding.small * 2 - Repeater { - id: repeater - model: SystemTray.items - TrayItem { - id: trayItem - required property SystemTrayItem modelData - required property int index - ind: index - popouts: root.popouts - loader: root.loader - implicitHeight: 34 - implicitWidth: 34 - item: modelData - bar: root.bar - } - } + CustomClippingRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + radius: height / 2 + + Row { + id: row + + anchors.centerIn: parent + spacing: 0 + + Repeater { + id: repeater + + model: SystemTray.items + + TrayItem { + id: trayItem + + required property int index + required property SystemTrayItem modelData + + bar: root.bar + implicitHeight: 34 + implicitWidth: 34 + ind: index + item: modelData + loader: root.loader + popouts: root.popouts + } + } + } + } } diff --git a/Modules/UPower/UPowerPopout.qml b/Modules/UPower/UPowerPopout.qml index ea1aa92..8ab4b16 100644 --- a/Modules/UPower/UPowerPopout.qml +++ b/Modules/UPower/UPowerPopout.qml @@ -11,173 +11,169 @@ Item { required property var wrapper - implicitWidth: profiles.implicitWidth implicitHeight: profiles.implicitHeight + implicitWidth: profiles.implicitWidth - CustomRect { - id: profiles + CustomRect { + id: profiles - property string current: { - const p = PowerProfiles.profile; - if (p === PowerProfile.PowerSaver) - return saver.icon; - if (p === PowerProfile.Performance) - return perf.icon; - return balance.icon; - } + property string current: { + const p = PowerProfiles.profile; + if (p === PowerProfile.PowerSaver) + return saver.icon; + if (p === PowerProfile.Performance) + return perf.icon; + return balance.icon; + } - anchors.horizontalCenter: parent.horizontalCenter - - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight - - color: DynamicColors.tPalette.m3surfaceContainer + anchors.horizontalCenter: parent.horizontalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth // color: "transparent" - radius: 6 + radius: 6 - CustomRect { - id: indicator + CustomRect { + id: indicator - color: DynamicColors.palette.m3primary - radius: 1000 - state: profiles.current + color: DynamicColors.palette.m3primary + radius: 1000 + state: profiles.current - states: [ - State { - name: saver.icon + states: [ + State { + name: saver.icon - Fill { - item: saver - } - }, - State { - name: balance.icon + Fill { + item: saver + } + }, + State { + name: balance.icon - Fill { - item: balance - } - }, - State { - name: perf.icon + Fill { + item: balance + } + }, + State { + name: perf.icon - Fill { - item: perf - } - } - ] - - transitions: Transition { - AnchorAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects + Fill { + item: perf + } + } + ] + transitions: Transition { + AnchorAnimation { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects easing.type: Easing.BezierSpline - } - } - } + } + } + } - Profile { - id: saver + Profile { + id: saver - anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: 25 + anchors.top: parent.top anchors.topMargin: 8 - anchors.left: parent.left - anchors.leftMargin: 25 - + icon: "nest_eco_leaf" + profile: PowerProfile.PowerSaver text: "Power Saver" - profile: PowerProfile.PowerSaver - icon: "nest_eco_leaf" - } + } CustomText { id: saverLabel - anchors.top: saver.bottom + anchors.horizontalCenter: saver.horizontalCenter + anchors.top: saver.bottom font.bold: true text: saver.text } - Profile { - id: balance + Profile { + id: balance - anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - + icon: "power_settings_new" + profile: PowerProfile.Balanced text: "Balanced" - profile: PowerProfile.Balanced - icon: "power_settings_new" - } + } CustomText { id: balanceLabel - anchors.top: balance.bottom + anchors.horizontalCenter: balance.horizontalCenter + anchors.top: balance.bottom font.bold: true text: balance.text } - Profile { - id: perf + Profile { + id: perf - anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: 25 + anchors.top: parent.top anchors.topMargin: 8 - anchors.right: parent.right - anchors.rightMargin: 25 - + icon: "bolt" + profile: PowerProfile.Performance text: "Performance" - profile: PowerProfile.Performance - icon: "bolt" - } + } CustomText { id: perfLabel - anchors.top: perf.bottom + anchors.horizontalCenter: perf.horizontalCenter + anchors.top: perf.bottom font.bold: true text: perf.text } - } + } - component Fill: AnchorChanges { - required property Item item - - target: indicator - anchors.left: item.left - anchors.right: item.right - anchors.top: item.top - anchors.bottom: item.bottom - } + component Fill: AnchorChanges { + required property Item item + anchors.bottom: item.bottom + anchors.left: item.left + anchors.right: item.right + anchors.top: item.top + target: indicator + } component Profile: Item { - required property string icon - required property int profile + required property string icon + required property int profile required property string text - implicitWidth: icon.implicitHeight + 5 * 2 - implicitHeight: icon.implicitHeight + 5 * 2 + implicitHeight: icon.implicitHeight + 5 * 2 + implicitWidth: icon.implicitHeight + 5 * 2 - StateLayer { - radius: 1000 - color: profiles.current === parent.icon ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + StateLayer { + function onClicked(): void { + PowerProfiles.profile = parent.profile; + } - function onClicked(): void { - PowerProfiles.profile = parent.profile; - } - } + color: profiles.current === parent.icon ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + radius: 1000 + } - MaterialIcon { - id: icon + MaterialIcon { + id: icon - anchors.centerIn: parent + anchors.centerIn: parent + color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface + fill: profiles.current === text ? 1 : 0 + font.pointSize: 36 + text: parent.icon - text: parent.icon - font.pointSize: 36 - color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - fill: profiles.current === text ? 1 : 0 - - Behavior on fill { - Anim {} - } - } - } + Behavior on fill { + Anim { + } + } + } + } } diff --git a/Modules/UPower/UPowerWidget.qml b/Modules/UPower/UPowerWidget.qml index c4c11e0..beec824 100644 --- a/Modules/UPower/UPowerWidget.qml +++ b/Modules/UPower/UPowerWidget.qml @@ -1,34 +1,35 @@ -import Quickshell import Quickshell.Services.UPower import QtQuick import QtQuick.Layouts import qs.Components import qs.Config import qs.Helpers as Helpers -import qs.Modules Item { id: root - implicitWidth: layout.childrenRect.width + 10 * 2 - anchors.top: parent.top anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: layout.childrenRect.width + 10 * 2 CustomRect { + anchors.bottomMargin: 4 anchors.fill: parent anchors.topMargin: 4 - anchors.bottomMargin: 4 color: DynamicColors.tPalette.m3surfaceContainer radius: 1000 } RowLayout { id: layout + anchors.centerIn: parent MaterialIcon { - animate: true Layout.alignment: Qt.AlignVCenter + animate: true + color: !Helpers.UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3error + fill: 1 text: { if (!Helpers.UPower.displayDevice.isLaptopBattery) { if (PowerProfiles.profile === PowerProfile.PowerSaver) @@ -47,8 +48,6 @@ Item { level--; return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } - color: !Helpers.UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3error - fill: 1 } CustomText { diff --git a/Modules/Updates.qml b/Modules/Updates.qml index 89a67b7..b84aeee 100644 --- a/Modules/Updates.qml +++ b/Modules/Updates.qml @@ -7,29 +7,31 @@ import Quickshell.Io import qs.Modules Singleton { - property int availableUpdates: 0 + property int availableUpdates: 0 - Timer { - interval: 1 - running: true - repeat: true - onTriggered: { - updatesProc.running = true - interval = 5000 - } - } + Timer { + interval: 1 + repeat: true + running: true - Process { - id: updatesProc - running: false + onTriggered: { + updatesProc.running = true; + interval = 5000; + } + } - command: ["checkupdates"] - stdout: StdioCollector { - onStreamFinished: { - const output = this.text - const lines = output.trim().split("\n").filter(line => line.length > 0) - availableUpdates = lines.length - } - } - } + 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/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index 6fce70a..13bdc54 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -1,61 +1,43 @@ import QtQuick import QtQuick.Layouts +import qs.Components import qs.Modules import qs.Config Item { - id: root - property int countUpdates: Updates.availableUpdates - implicitWidth: textMetrics.width + contentRow.spacing + 30 - anchors.top: parent.top - anchors.bottom: parent.bottom - property color textColor: DynamicColors.palette.m3onSurface + id: root - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: 22 - radius: height / 2 - color: DynamicColors.tPalette.m3surfaceContainer - Behavior on color { - CAnim {} - } - } + property int countUpdates: Updates.availableUpdates + property color textColor: DynamicColors.palette.m3onSurface - RowLayout { - id: contentRow - spacing: 10 - anchors { - fill: parent - leftMargin: 5 - rightMargin: 5 - } + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: contentRow.childrenRect.width + Appearance.spacing.smaller - Text { - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - font.family: "Material Symbols Rounded" - font.pixelSize: 18 - text: "\uf569" - color: root.textColor - Behavior on color { - CAnim {} - } - } + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + radius: height / 2 + } - TextMetrics { - id: textMetrics - font.pixelSize: 16 - text: root.countUpdates - } + RowLayout { + id: contentRow - Text { - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - text: textMetrics.text - color: root.textColor - Behavior on color { - CAnim {} - } - } - } + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + font.pointSize: 14 + text: "package_2" + } + + CustomText { + color: root.textColor + font.pointSize: 12 + text: root.countUpdates + } + } } diff --git a/Modules/WSOverview/OverviewPopout.qml b/Modules/WSOverview/OverviewPopout.qml index 7ce7349..636beaa 100644 --- a/Modules/WSOverview/OverviewPopout.qml +++ b/Modules/WSOverview/OverviewPopout.qml @@ -11,16 +11,16 @@ import qs.Helpers Item { id: root - required property Item wrapper required property ShellScreen screen + required property Item wrapper - implicitWidth: layout.implicitWidth + 16 implicitHeight: layout.implicitHeight + 16 + implicitWidth: layout.implicitWidth + 16 GridLayout { id: layout - anchors.centerIn: parent + anchors.centerIn: parent columnSpacing: 8 rowSpacing: 8 @@ -29,23 +29,21 @@ Item { CustomRect { id: workspacePreview + required property HyprlandWorkspace modelData + Layout.preferredHeight: 180 + 10 + Layout.preferredWidth: 320 + 10 border.color: "white" border.width: 1 radius: 8 - Layout.preferredWidth: 320 + 10 - Layout.preferredHeight: 180 + 10 Repeater { model: workspacePreview.modelData.toplevels Item { id: preview - anchors.fill: parent - anchors.margins: 5 - required property HyprlandToplevel modelData property rect appPosition: { let { at: [cx, cy], @@ -55,23 +53,26 @@ Item { cx -= modelData.monitor.x; cy -= modelData.monitor.y; - return Qt.rect( (cx / 8), (cy / 8), (cw / 8), (ch / 8) ) + return Qt.rect((cx / 8), (cy / 8), (cw / 8), (ch / 8)); } + required property HyprlandToplevel modelData + + anchors.fill: parent + anchors.margins: 5 CustomRect { border.color: DynamicColors.tPalette.m3outline border.width: 1 - radius: 4 - implicitWidth: preview.appPosition.width implicitHeight: preview.appPosition.height - + implicitWidth: preview.appPosition.width + radius: 4 x: preview.appPosition.x y: preview.appPosition.y - 3.4 ScreencopyView { id: previewCopy - anchors.fill: parent + anchors.fill: parent captureSource: preview.modelData.wayland live: true } diff --git a/Modules/WallBackground.qml b/Modules/WallBackground.qml deleted file mode 100644 index a6aacfb..0000000 --- a/Modules/WallBackground.qml +++ /dev/null @@ -1,79 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Helpers -import qs.Config - -Item { - id: root - - property string source: Wallpapers.current - property Image current: one - - anchors.fill: parent - - onSourceChanged: { - if (!source) { - current = null; - } else if (current === one) { - two.update(); - } else { - one.update(); - } - } - - Component.onCompleted: { - console.log(root.source) - if (source) - Qt.callLater(() => one.update()); - } - - Img { - id: one - } - - Img { - id: two - } - - component Img: CachingImage { - id: img - - function update(): void { - if (path === root.source) { - root.current = this; - } else { - path = root.source; - } - } - - anchors.fill: parent - - opacity: 0 - scale: Wallpapers.showPreview ? 1 : 0.8 - asynchronous: true - onStatusChanged: { - if (status === Image.Ready) { - root.current = this; - } - } - - states: State { - name: "visible" - when: root.current === img - - PropertyChanges { - img.opacity: 1 - img.scale: 1 - } - } - - transitions: Transition { - Anim { - target: img - properties: "opacity,scale" - duration: Config.background.wallFadeDuration - } - } - } -} diff --git a/Modules/Wallpaper/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml new file mode 100644 index 0000000..ad0ffc2 --- /dev/null +++ b/Modules/Wallpaper/WallBackground.qml @@ -0,0 +1,79 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Components +import qs.Helpers +import qs.Config + +Item { + id: root + + property Image current: one + property string source: Wallpapers.current + + anchors.fill: parent + + Component.onCompleted: { + if (source) + Qt.callLater(() => one.update()); + } + onSourceChanged: { + if (!source) { + current = null; + } else if (current === one) { + two.update(); + } else { + one.update(); + } + } + + Img { + id: one + + } + + Img { + id: two + + } + + component Img: CachingImage { + id: img + + function update(): void { + if (path === root.source) { + root.current = this; + } else { + path = root.source; + } + } + + anchors.fill: parent + asynchronous: true + opacity: 0 + scale: Wallpapers.showPreview ? 1 : 0.8 + + states: State { + name: "visible" + when: root.current === img + + PropertyChanges { + img.opacity: 1 + img.scale: 1 + } + } + transitions: Transition { + Anim { + duration: Config.background.wallFadeDuration + properties: "opacity,scale" + target: img + } + } + + onStatusChanged: { + if (status === Image.Ready) { + root.current = this; + } + } + } +} diff --git a/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml similarity index 83% rename from Wallpaper.qml rename to Modules/Wallpaper/Wallpaper.qml index a0368b0..fa77580 100644 --- a/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -1,33 +1,35 @@ import Quickshell import QtQuick import Quickshell.Wayland -import qs.Helpers -import qs.Modules import qs.Config Loader { - - asynchronous: true active: Config.background.enabled + asynchronous: true sourceComponent: Variants { model: Quickshell.screens + PanelWindow { id: root + required property var modelData - screen: modelData - WlrLayershell.namespace: "ZShell-Wallpaper" + WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Bottom + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.namespace: "ZShell-Wallpaper" color: "transparent" + screen: modelData anchors { - top: true + bottom: true left: true right: true - bottom: true + top: true + } + + WallBackground { } - WallBackground {} } } } diff --git a/Modules/Wallust.qml b/Modules/Wallust.qml deleted file mode 100644 index dad85dd..0000000 --- a/Modules/Wallust.qml +++ /dev/null @@ -1,25 +0,0 @@ -pragma Singleton - -import Quickshell -import Quickshell.Io -import QtQuick -import qs.Config - -Singleton { - id: root - - property var args - readonly property string mode: Config.general.color.mode - readonly property string threshold: mode === "dark" ? "--threshold=9" : "--dynamic-threshold" - - function generateColors(wallpaperPath) { - root.args = wallpaperPath; - wallustProc.running = true; - } - - Process { - id: wallustProc - command: ["wallust", "run", root.args, `--palette=${root.mode}`, "--ignore-sequence=cursor", `${root.threshold}` ] - running: false - } -} diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index ae30bda..68de9db 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -6,80 +6,74 @@ import qs.Config import qs.Helpers Item { - id: root + id: root - required property var bar - required property Brightness.Monitor monitor - property color colour: DynamicColors.palette.m3primary + required property var bar + property color colour: DynamicColors.palette.m3primary + property Title current: text1 + // readonly property int maxWidth: 300 + readonly property int maxWidth: { + const otherModules = bar.children.filter(c => c.enabled && c.id && c.item !== this && c.id !== "spacer"); + const otherWidth = otherModules.reduce((acc, curr) => { + return acc + (curr.item?.nonAnimWidth ?? curr.width ?? 0); + }, 0); + return bar.width - otherWidth - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; + } + required property Brightness.Monitor monitor - readonly property int maxHeight: { - const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); - const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); - // Length - 2 cause repeater counts as a child - return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; - } - property Title current: text1 + clip: true + implicitHeight: current.implicitHeight + implicitWidth: Math.min(current.implicitWidth, root.maxWidth) - clip: true - implicitWidth: current.implicitWidth + current.anchors.leftMargin - implicitHeight: current.implicitHeight + Behavior on implicitWidth { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } - // MaterialIcon { - // id: icon - // - // anchors.verticalCenter: parent.verticalCenter - // - // animate: true - // text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows") - // color: root.colour - // } + Title { + id: text1 - Title { - id: text1 - } + } - Title { - id: text2 - } + Title { + id: text2 - TextMetrics { - id: metrics + } - text: Hypr.activeToplevel?.title ?? qsTr("Desktop") - font.pointSize: 12 - font.family: "Rubik" + TextMetrics { + id: metrics - onTextChanged: { - const next = root.current === text1 ? text2 : text1; - next.text = elidedText; - root.current = next; - } - onElideWidthChanged: root.current.text = elidedText - } + elide: Qt.ElideRight + elideWidth: root.maxWidth + font.family: "Rubik" + font.pointSize: 12 + text: Hypr.activeToplevel?.title ?? qsTr("Desktop") - Behavior on implicitWidth { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + onElideWidthChanged: root.current.text = elidedText + onTextChanged: { + const next = root.current === text1 ? text2 : text1; + next.text = elidedText; + root.current = next; + } + } - component Title: CustomText { - id: text + component Title: CustomText { + id: text + anchors.leftMargin: 7 anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 7 + color: root.colour + font.family: metrics.font.family + font.pointSize: metrics.font.pointSize + height: implicitHeight + opacity: root.current === this ? 1 : 0 + width: implicitWidth - font.pointSize: metrics.font.pointSize - font.family: metrics.font.family - color: root.colour - opacity: root.current === this ? 1 : 0 - - width: implicitWidth - height: implicitHeight - - Behavior on opacity { - Anim {} - } - } + Behavior on opacity { + Anim { + } + } + } } diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index 1d1c084..e444508 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls -import QtQuick.Layouts import QtQuick.Effects import Quickshell import Quickshell.Hyprland @@ -10,119 +9,189 @@ import qs.Config import qs.Components Item { - id: itemRoot - required property PanelWindow bar - anchors.top: parent.top - anchors.bottom: parent.bottom - implicitWidth: workspacesRow.implicitWidth + 10 + id: root - Behavior on implicitWidth { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } + property real activeWorkspaceMargin: Math.ceil(Appearance.padding.small / 2) + required property PanelWindow bar + readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.bar.screen) + property int workspaceButtonWidth: bgRect.implicitHeight - root.activeWorkspaceMargin * 2 + property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown + readonly property list workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) + readonly property int workspacesShown: workspaces.length - Rectangle { - id: root + anchors.bottom: parent.bottom + anchors.top: parent.top + implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2 - property HyprlandMonitor monitor: Hyprland.monitorFor( itemRoot.bar?.screen ) + Behavior on implicitWidth { + Anim { + } + } - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: 22 + CustomRect { + id: bgRect - function shouldShow(monitor) { - Hyprland.refreshWorkspaces(); - Hyprland.refreshMonitors(); - if ( monitor === root.monitor ) { - return true; - } else { - return false; - } - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + radius: height / 2 - color: DynamicColors.tPalette.m3surfaceContainer - radius: height / 2 + CustomRect { + id: indicator - Behavior on color { - CAnim {} - } + property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin + property real indicatorThickness: root.workspaceButtonWidth - RowLayout { - id: workspacesRow - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 6 - spacing: 8 + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + implicitHeight: indicatorThickness + implicitWidth: indicatorLength + radius: Appearance.rounding.full + x: indicatorPosition + z: 2 - Repeater { - model: Hyprland.workspaces + AnimatedTabIndexPair { + id: idxPair - RowLayout { - id: workspaceIndicator - required property var modelData - visible: root.shouldShow( workspaceIndicator.modelData.monitor ) - CustomText { - text: workspaceIndicator.modelData.name - font.pointSize: 12 - color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant - visible: true - } + index: root.workspaces.findIndex(w => w.active) + } + } - Rectangle { + Grid { + anchors.fill: parent + anchors.margins: root.activeWorkspaceMargin + columnSpacing: 0 + columns: root.workspacesShown + rowSpacing: 0 + rows: 1 + z: 3 - implicitWidth: 14 - implicitHeight: 14 - radius: height / 2 - border.width: 1 + Repeater { + model: root.workspaces - color: "transparent" - border.color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurfaceVariant + Button { + id: button + required property int index + required property HyprlandWorkspace modelData - scale: 1.0 - opacity: 1.0 + implicitHeight: indicator.indicatorThickness + implicitWidth: indicator.indicatorThickness + width: root.workspaceButtonWidth - CustomRect { - anchors.centerIn: parent - implicitWidth: 8 - implicitHeight: 8 + background: Item { + id: workspaceButtonBackground - radius: implicitHeight / 2 - color: workspaceIndicator.modelData.id === Hyprland.focusedWorkspace.id ? DynamicColors.palette.m3primary : "transparent" - } + implicitHeight: root.workspaceButtonWidth + implicitWidth: root.workspaceButtonWidth - Behavior on border.color { - ColorAnimation { - duration: 150 - easing.type: Easing.InOutQuad - } - } + CustomText { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSecondaryContainer + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + text: button.modelData.name + verticalAlignment: Text.AlignVCenter + z: 3 + } + } - NumberAnimation on scale { - from: 0.0 - to: 1.0 - duration: 300 - easing.type: Easing.OutBack - } - - NumberAnimation on opacity { - from: 0.0 - to: 1.0 - duration: 200 - } + onPressed: { + Hyprland.dispatch(`workspace ${button.modelData.name}`); + } + } + } + } - MouseArea { - anchors.fill: parent - onClicked: { - workspaceIndicator.modelData.activate(); - } - } - } - } - } - } - } + Item { + id: activeTextSource + + anchors.fill: parent + anchors.margins: root.activeWorkspaceMargin + layer.enabled: true + visible: false + z: 4 + + Grid { + anchors.fill: parent + columnSpacing: 0 + columns: root.workspacesShown + rowSpacing: 0 + rows: 1 + + Repeater { + model: root.workspaces + + Item { + id: activeWorkspace + + required property int index + required property HyprlandWorkspace modelData + + implicitHeight: indicator.indicatorThickness + implicitWidth: indicator.indicatorThickness + width: root.workspaceButtonWidth + + CustomText { + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + text: activeWorkspace.modelData.name + verticalAlignment: Text.AlignVCenter + } + } + } + } + } + + ShaderEffectSource { + id: activeTextTex + + anchors.fill: bgRect + anchors.margins: root.activeWorkspaceMargin + hideSource: true + live: true + recursive: true + sourceItem: activeTextSource + } + + Item { + id: indicatorMask + + anchors.fill: bgRect + visible: false + + CustomRect { + color: "white" + height: indicator.height + radius: indicator.radius + width: indicator.width + x: indicator.x + y: indicator.y + } + } + + ShaderEffectSource { + id: indicatorMaskEffect + + anchors.fill: activeTextSource + live: true + sourceItem: indicatorMask + visible: false + } + + MultiEffect { + anchors.fill: activeTextSource + maskEnabled: true + maskInverted: false + maskSource: indicatorMaskEffect + source: activeTextTex + z: 5 + } + } } diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index a42c9ef..337522d 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -2,186 +2,181 @@ import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick +import qs.Components import qs.Config -import qs.Helpers Item { - id: root + id: root - required property ShellScreen screen + property list animCurve: MaterialEasing.emphasized + property int animLength: MaterialEasing.emphasizedDecelTime + readonly property Item current: content.item?.current ?? null + property real currentCenter + property string currentName + property string detachedMode + property bool hasCurrent + readonly property bool isDetached: detachedMode.length > 0 + readonly property real nonAnimHeight: hasCurrent ? children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight : 0 + readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth + property string queuedMode + required property ShellScreen screen - readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth - readonly property real nonAnimHeight: hasCurrent ? children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight : 0 - readonly property Item current: content.item?.current ?? null + function close(): void { + hasCurrent = false; + animCurve = MaterialEasing.emphasizedDecel; + animLength = MaterialEasing.emphasizedDecelTime; + detachedMode = ""; + animCurve = MaterialEasing.emphasized; + } - property string currentName - property real currentCenter - property bool hasCurrent + function detach(mode: string): void { + animLength = 600; + if (mode === "winfo") { + detachedMode = mode; + } else { + detachedMode = "any"; + queuedMode = mode; + } + focus = true; + } - property string detachedMode - property string queuedMode - readonly property bool isDetached: detachedMode.length > 0 + clip: true + implicitHeight: nonAnimHeight + implicitWidth: nonAnimWidth + visible: width > 0 && height > 0 - property int animLength: MaterialEasing.emphasizedDecelTime - property list animCurve: MaterialEasing.emphasized + Behavior on implicitHeight { + Anim { + duration: root.animLength + easing.bezierCurve: root.animCurve + } + } + Behavior on implicitWidth { + enabled: root.implicitHeight > 0 - function detach(mode: string): void { - animLength = 600; - if (mode === "winfo") { - detachedMode = mode; - } else { - detachedMode = "any"; - queuedMode = mode; - } - focus = true; - } + Anim { + duration: root.animLength + easing.bezierCurve: root.animCurve + } + } - function close(): void { - hasCurrent = false; - animCurve = MaterialEasing.emphasizedDecel; - animLength = MaterialEasing.emphasizedDecelTime; - detachedMode = ""; - animCurve = MaterialEasing.emphasized; - } + // Comp { + // shouldBeActive: root.detachedMode === "winfo" + // asynchronous: true + // anchors.centerIn: parent + // + // sourceComponent: WindowInfo { + // screen: root.screen + // client: Hypr.activeToplevel + // } + // } - visible: width > 0 && height > 0 - clip: true + // Comp { + // shouldBeActive: root.detachedMode === "any" + // asynchronous: true + // anchors.centerIn: parent + // + // sourceComponent: ControlCenter { + // screen: root.screen + // active: root.queuedMode + // + // function close(): void { + // root.close(); + // } + // } + // } - implicitWidth: nonAnimWidth - implicitHeight: nonAnimHeight + Behavior on x { + enabled: root.implicitHeight > 0 - Keys.onEscapePressed: close() + Anim { + duration: root.animLength + easing.bezierCurve: root.animCurve + } + } + Behavior on y { + Anim { + duration: root.animLength + easing.bezierCurve: root.animCurve + } + } - HyprlandFocusGrab { - active: root.isDetached - windows: [QsWindow.window] - onCleared: root.close() - } + Keys.onEscapePressed: close() - Binding { - when: root.isDetached + HyprlandFocusGrab { + active: root.isDetached + windows: [QsWindow.window] - target: QsWindow.window - property: "WlrLayershell.keyboardFocus" - value: WlrKeyboardFocus.OnDemand - } + onCleared: root.close() + } - Comp { - id: content + Binding { + property: "WlrLayershell.keyboardFocus" + target: QsWindow.window + value: WlrKeyboardFocus.OnDemand + when: root.isDetached + } - shouldBeActive: root.hasCurrent - asynchronous: true + Comp { + id: content - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + asynchronous: true + shouldBeActive: root.hasCurrent - sourceComponent: Content { - wrapper: root - } - } + sourceComponent: Content { + wrapper: root + } + } - // Comp { - // shouldBeActive: root.detachedMode === "winfo" - // asynchronous: true - // anchors.centerIn: parent - // - // sourceComponent: WindowInfo { - // screen: root.screen - // client: Hypr.activeToplevel - // } - // } + component Comp: Loader { + id: comp - // Comp { - // shouldBeActive: root.detachedMode === "any" - // asynchronous: true - // anchors.centerIn: parent - // - // sourceComponent: ControlCenter { - // screen: root.screen - // active: root.queuedMode - // - // function close(): void { - // root.close(); - // } - // } - // } + property bool shouldBeActive - Behavior on x { - enabled: root.implicitHeight > 0 - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } + active: false + asynchronous: true + opacity: 0 - Behavior on y { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } + states: State { + name: "active" + when: comp.shouldBeActive - Behavior on implicitWidth { - enabled: root.implicitHeight > 0 - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } + PropertyChanges { + comp.active: true + comp.opacity: 1 + } + } + transitions: [ + Transition { + from: "" + to: "active" - Behavior on implicitHeight { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } + SequentialAnimation { + PropertyAction { + property: "active" + } - component Comp: Loader { - id: comp + Anim { + property: "opacity" + } + } + }, + Transition { + from: "active" + to: "" - property bool shouldBeActive + SequentialAnimation { + Anim { + property: "opacity" + } - asynchronous: true - active: false - opacity: 0 - - states: State { - name: "active" - when: comp.shouldBeActive - - PropertyChanges { - comp.opacity: 1 - comp.active: true - } - } - - transitions: [ - Transition { - from: "" - to: "active" - - SequentialAnimation { - PropertyAction { - property: "active" - } - Anim { - property: "opacity" - } - } - }, - Transition { - from: "active" - to: "" - - SequentialAnimation { - Anim { - property: "opacity" - } - PropertyAction { - property: "active" - } - } - } - ] - } + PropertyAction { + property: "active" + } + } + } + ] + } } diff --git a/Paths/Paths.qml b/Paths/Paths.qml index 373c7f6..fe33f82 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -5,33 +5,31 @@ import Quickshell import qs.Config Singleton { - id: root + id: root - readonly property string home: Quickshell.env("HOME") - readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures` - readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` + readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` + readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` + readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` + readonly property string home: Quickshell.env("HOME") + readonly property string imagecache: `${cache}/imagecache` + readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" + readonly property string notifimagecache: `${imagecache}/notifs` + readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures` + readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings` + readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell` + readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` + readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.wallpaperPath) - readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` - readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell` - readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` - readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` + function absolutePath(path: string): string { + return toLocalFile(path.replace("~", home)); + } - readonly property string imagecache: `${cache}/imagecache` - readonly property string notifimagecache: `${imagecache}/notifs` - readonly property string wallsdir: Quickshell.env("ZSHELL_WALLPAPERS_DIR") || absolutePath(Config.wallpaperPath) - readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings` - readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" + function shortenHome(path: string): string { + return path.replace(home, "~"); + } - function toLocalFile(path: url): string { - path = Qt.resolvedUrl(path); - return path.toString() ? ZShellIo.toLocalFile(path) : ""; - } - - function absolutePath(path: string): string { - return toLocalFile(path.replace("~", home)); - } - - function shortenHome(path: string): string { - return path.replace(home, "~"); - } + function toLocalFile(path: url): string { + path = Qt.resolvedUrl(path); + return path.toString() ? ZShellIo.toLocalFile(path) : ""; + } } diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index bde2f00..9b0b751 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -43,11 +43,13 @@ qml_module(ZShell imageanalyser.hpp imageanalyser.cpp requests.hpp requests.cpp toaster.hpp toaster.cpp + qalculator.hpp qalculator.cpp LIBRARIES Qt::Gui Qt::Quick Qt::Concurrent Qt::Sql + PkgConfig::Qalculate ) add_subdirectory(Models) 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/Plugins/ZShell/qalculator.cpp b/Plugins/ZShell/qalculator.cpp new file mode 100644 index 0000000..c58962f --- /dev/null +++ b/Plugins/ZShell/qalculator.cpp @@ -0,0 +1,52 @@ +#include "qalculator.hpp" + +#include + +namespace ZShell { + +Qalculator::Qalculator(QObject* parent) + : QObject(parent) { + if (!CALCULATOR) { + new Calculator(); + CALCULATOR->loadExchangeRates(); + CALCULATOR->loadGlobalDefinitions(); + CALCULATOR->loadLocalDefinitions(); + } +} + +QString Qalculator::eval(const QString& expr, bool printExpr) const { + if (expr.isEmpty()) { + return QString(); + } + + EvaluationOptions eo; + PrintOptions po; + + std::string parsed; + std::string result = CALCULATOR->calculateAndPrint( + CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); + + std::string error; + while (CALCULATOR->message()) { + if (!CALCULATOR->message()->message().empty()) { + if (CALCULATOR->message()->type() == MESSAGE_ERROR) { + error += "error: "; + } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { + error += "warning: "; + } + error += CALCULATOR->message()->message(); + } + CALCULATOR->nextMessage(); + } + if (!error.empty()) { + return QString::fromStdString(error); + } + + if (printExpr) { + return QString("%1 = %2").arg(parsed).arg(result); + } + + return QString::fromStdString(result); +} + +} // namespace ZShell diff --git a/Plugins/ZShell/qalculator.hpp b/Plugins/ZShell/qalculator.hpp new file mode 100644 index 0000000..348a4b7 --- /dev/null +++ b/Plugins/ZShell/qalculator.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace ZShell { + +class Qalculator : public QObject { +Q_OBJECT +QML_ELEMENT +QML_SINGLETON + +public: +explicit Qalculator(QObject* parent = nullptr); + +Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; +}; + +} // namespace ZShell diff --git a/assets/shaders/opacitymask.frag.qsb b/assets/shaders/opacitymask.frag.qsb index 4aedc35..7bf97c2 100644 Binary files a/assets/shaders/opacitymask.frag.qsb and b/assets/shaders/opacitymask.frag.qsb differ diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index c1db4e1..e2073ad 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -1,7 +1,10 @@ -from typing import Annotated, Optional import typer import json +import shutil +import os +from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined +from typing import Any, Optional, Tuple from zshell.utils.schemepalettes import PRESETS from pathlib import Path from PIL import Image @@ -9,6 +12,7 @@ from materialyoucolor.quantize import QuantizeCelebi from materialyoucolor.score.score import Score from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors from materialyoucolor.hct.hct import Hct +from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double app = typer.Typer() @@ -18,26 +22,35 @@ def generate( # image inputs (optional - used for image mode) image_path: Optional[Path] = typer.Option( None, help="Path to source image. Required for image mode."), - thumbnail_path: Optional[Path] = typer.Option( - Path("thumb.jpg"), help="Path to temporary thumbnail (image mode)."), scheme: Optional[str] = typer.Option( - "fruit-salad", help="Color scheme algorithm to use for image mode. Ignored in preset mode."), + None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."), # preset inputs (optional - used for preset mode) preset: Optional[str] = typer.Option( None, help="Name of a premade scheme in this format: :"), - mode: str = typer.Option( - "dark", help="Mode of the preset scheme (dark or light)."), - # output (required) - output: Path = typer.Option(..., help="Output JSON path.") + mode: Optional[str] = typer.Option( + None, help="Mode of the preset scheme (dark or light)."), ): - if preset is None and image_path is None: - raise typer.BadParameter( - "Either --image-path or --preset must be provided.") + + HOME = str(os.getenv("HOME")) + OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json") + SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt") + THUMB_PATH = Path(HOME + + "/.cache/zshell/imagecache/thumbnail.jpg") + WALL_DIR_PATH = Path(HOME + + "/.local/state/zshell/wallpaper_path.json") + + TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates") + WALL_PATH = Path() + CONFIG = Path(HOME + "/.config/zshell/config.json") if preset is not None and image_path is not None: raise typer.BadParameter( "Use either --image-path or --preset, not both.") + if scheme is None: + with CONFIG.open() as f: + scheme = json.load(f)["colors"]["schemeType"] + match scheme: case "fruit-salad": from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme @@ -60,6 +73,126 @@ def generate( case _: from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme + if mode is None: + with CONFIG.open() as f: + mode = json.load(f)["general"]["color"]["mode"] + + def hex_to_hct(hex_color: str) -> Hct: + s = hex_color.strip() + if s.startswith("#"): + s = s[1:] + if len(s) != 6: + raise ValueError(f"Expected 6-digit hex color, got: {hex_color!r}") + return Hct.from_int(int("0xFF" + s, 16)) + + LIGHT_GRUVBOX = list( + map( + hex_to_hct, + [ + "FDF9F3", + "FF6188", + "A9DC76", + "FC9867", + "FFD866", + "F47FD4", + "78DCE8", + "333034", + "121212", + "FF6188", + "A9DC76", + "FC9867", + "FFD866", + "F47FD4", + "78DCE8", + "333034", + ], + ) + ) + + DARK_GRUVBOX = list( + map( + hex_to_hct, + [ + "282828", + "CC241D", + "98971A", + "D79921", + "458588", + "B16286", + "689D6A", + "A89984", + "928374", + "FB4934", + "B8BB26", + "FABD2F", + "83A598", + "D3869B", + "8EC07C", + "EBDBB2", + ], + ) + ) + + with WALL_DIR_PATH.open() as f: + path = json.load(f)["currentWallpaperPath"] + WALL_PATH = path + + def lighten(color: Hct, amount: float) -> Hct: + diff = (100 - color.tone) * amount + tone = max(0.0, min(100.0, color.tone + diff)) + chroma = max(0.0, color.chroma + diff / 5) + return Hct.from_hct(color.hue, chroma, tone) + + def darken(color: Hct, amount: float) -> Hct: + diff = color.tone * amount + tone = max(0.0, min(100.0, color.tone - diff)) + chroma = max(0.0, color.chroma - diff / 5) + return Hct.from_hct(color.hue, chroma, tone) + + def grayscale(color: Hct, light: bool) -> Hct: + color = darken(color, 0.35) if light else lighten(color, 0.65) + color.chroma = 0 + return color + + def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct: + diff = difference_degrees(from_hct.hue, to_hct.hue) + rotation = min(diff * 0.8, 100) + output_hue = sanitize_degrees_double( + from_hct.hue + + rotation * rotation_direction(from_hct.hue, to_hct.hue) + ) + tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost))) + return Hct.from_hct(output_hue, from_hct.chroma, tone) + + def terminal_palette( + colors: dict[str, str], mode: str, variant: str + ) -> dict[str, str]: + light = mode.lower() == "light" + + key_hex = ( + colors.get("primary_paletteKeyColor") + or colors.get("primaryPaletteKeyColor") + or colors.get("primary") + or int_to_hex(seed.to_int()) + ) + key_hct = hex_to_hct(key_hex) + + base = LIGHT_GRUVBOX if light else DARK_GRUVBOX + out: dict[str, str] = {} + + is_mono = variant.lower() == "monochrome" + + for i, base_hct in enumerate(base): + if is_mono: + h = grayscale(base_hct, light) + else: + tone_boost = (0.35 if i < 8 else 0.2) * (-1 if light else 1) + h = harmonize(base_hct, key_hct, tone_boost) + + out[f"term{i}"] = int_to_hex(h.to_int()) + + return out + def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)): thumbnail_file = Path(thumbnail_path) @@ -70,6 +203,160 @@ def generate( thumbnail_file.parent.mkdir(parents=True, exist_ok=True) image.save(thumbnail_path, "JPEG") + def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None: + state_path.parent.mkdir(parents=True, exist_ok=True) + state_path.write_text(sequences, encoding="utf-8") + + pts_path = Path("/dev/pts") + if not pts_path.exists(): + return + + O_NOCTTY = getattr(os, "O_NOCTTY", 0) + + for pt in pts_path.iterdir(): + if not pt.name.isdigit(): + continue + try: + fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | O_NOCTTY) + try: + os.write(fd, sequences_tmux.encode()) + os.write(fd, sequences.encode()) + finally: + os.close(fd) + except (PermissionError, OSError, BlockingIOError): + pass + + def build_template_context( + *, + colors: dict[str, str], + seed: Hct, + mode: str, + wallpaper_path: str, + name: str, + flavor: str, + variant: str, + ) -> dict[str, Any]: + ctx: dict[str, Any] = { + "mode": mode, + "wallpaper_path": wallpaper_path, + "source_color": int_to_hex(seed.to_int()), + "name": name, + "seed": seed.to_int(), + "flavor": flavor, + "variant": variant, + "colors": colors + } + + for k, v in colors.items(): + ctx[k] = v + ctx[f"m3{k}"] = v + + term = terminal_palette(colors, mode, variant) + ctx.update(term) + ctx["term"] = [term[f"term{i}"] for i in range(16)] + + seq = make_sequences( + term=term, + foreground=ctx["m3onSurface"], + background=ctx["m3surface"], + ) + ctx["sequences"] = seq + ctx["sequences_tmux"] = tmux_wrap_sequences(seq) + + return ctx + + def make_sequences( + *, + term: dict[str, str], + foreground: str, + background: str, + ) -> str: + ESC = "\x1b" + ST = ESC + "\\" + + parts: list[str] = [] + + for i in range(16): + parts.append(f"{ESC}]4;{i};{term[f'term{i}']}{ST}") + + parts.append(f"{ESC}]10;{foreground}{ST}") + parts.append(f"{ESC}]11;{background}{ST}") + + return "".join(parts) + + def tmux_wrap_sequences(seq: str) -> str: + ESC = "\x1b" + return f"{ESC}Ptmux;{seq.replace(ESC, ESC+ESC)}{ESC}\\" + + def parse_output_directive(first_line: str) -> Optional[Path]: + s = first_line.strip() + if not s.startswith("#") or s.startswith("#!"): + return None + + target = s[1:].strip() + if not target: + return None + + expanded = os.path.expandvars(os.path.expanduser(target)) + return Path(expanded) + + def split_directive_and_body(text: str) -> Tuple[Optional[Path], str]: + lines = text.splitlines(keepends=True) + if not lines: + return None, "" + + out_path = parse_output_directive(lines[0]) + if out_path is None: + return None, text + + body = "".join(lines[1:]) + return out_path, body + + def render_all_templates( + templates_dir: Path, + context: dict[str, object], + *, + strict: bool = True, + ) -> list[Path]: + undefined_cls = StrictUndefined if strict else Undefined + env = Environment( + loader=FileSystemLoader(str(templates_dir)), + autoescape=False, + keep_trailing_newline=True, + undefined=undefined_cls, + ) + + rendered_outputs: list[Path] = [] + + for tpl_path in sorted(p for p in templates_dir.rglob("*") if p.is_file()): + rel = tpl_path.relative_to(templates_dir) + + if any(part.startswith(".") for part in rel.parts): + continue + + raw = tpl_path.read_text(encoding="utf-8") + out_path, body = split_directive_and_body(raw) + + out_path.parent.mkdir(parents=True, exist_ok=True) + + try: + template = env.from_string(body) + text = template.render(**context) + except Exception as e: + raise RuntimeError( + f"Template render failed for '{rel}': {e}") from e + + out_path.write_text(text, encoding="utf-8") + + try: + shutil.copymode(tpl_path, out_path) + except OSError: + pass + + rendered_outputs.append(out_path) + + return rendered_outputs + def seed_from_image(image_path: Path) -> Hct: image = Image.open(image_path) pixel_len = image.width * image.height @@ -115,9 +402,24 @@ def generate( seed = seed_from_preset(preset) colors = generate_color_scheme(seed, mode) name, flavor = preset.split(":") - else: - generate_thumbnail(image_path, str(thumbnail_path)) - seed = seed_from_image(thumbnail_path) + elif image_path: + generate_thumbnail(image_path, str(THUMB_PATH)) + seed = seed_from_image(THUMB_PATH) + colors = generate_color_scheme(seed, mode) + name = "dynamic" + flavor = "default" + elif mode: + generate_thumbnail(WALL_PATH, str(THUMB_PATH)) + seed = seed_from_image(THUMB_PATH) + colors = generate_color_scheme(seed, mode) + name = "dynamic" + flavor = "default" + elif scheme: + with OUTPUT.open() as f: + js = json.load(f) + seed = Hct.from_int(js["seed"]) + mode = str(js["mode"]) + colors = generate_color_scheme(seed, mode) name = "dynamic" flavor = "default" @@ -127,11 +429,34 @@ def generate( "flavor": flavor, "mode": mode, "variant": scheme, - "colors": colors + "colors": colors, + "seed": seed.to_int() } - output.parent.mkdir(parents=True, exist_ok=True) - with open(output, "w") as f: + if TEMPLATE_DIR is not None: + wp = str(WALL_PATH) + ctx = build_template_context( + colors=colors, + seed=seed, + mode=mode, + wallpaper_path=wp, + name=name, + flavor=flavor, + variant=scheme + ) + + rendered = render_all_templates( + templates_dir=TEMPLATE_DIR, + context=ctx, + ) + + apply_terms(ctx["sequences"], ctx["sequences_tmux"], SEQ_STATE) + + for p in rendered: + print(f"rendered: {p}") + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT, "w") as f: json.dump(output_dict, f, indent=4) except Exception as e: print(f"Error: {e}") diff --git a/nix/default.nix b/nix/default.nix index 514d346..83edc51 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -25,7 +25,8 @@ pkg-config, pythonEnv, zshell-cli, -}: let +}: +let version = "1.0.0"; runtimeDeps = [ @@ -81,63 +82,61 @@ ]; 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/nix/zshell-cli.nix b/nix/zshell-cli.nix index bac5af7..f3cc3b1 100644 --- a/nix/zshell-cli.nix +++ b/nix/zshell-cli.nix @@ -16,6 +16,7 @@ python3.pkgs.buildPythonApplication { dependencies = with python3.pkgs; [ materialyoucolor pillow + jinja2 typer ]; diff --git a/plans/ideas.md b/plans/ideas.md index af8e1a0..941e513 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -1,19 +1,23 @@ # Ideas/Features -- [x] Media showing; what song/media is playing? -- [x] Brightness control for Laptops. -- [x] Battery icon for Laptops. Broken? - [ ] Change volume for `$BROWSER` environment variable? Most general media source apart from separate music/video players. -- [ ] Quick toggle for BT, WiFi (modules in the tray do this too) -- [x] Auto hide unless on mouse hover. Also implement bar changes to mute/volume to show notif or show bar for a couple seconds. -- [x] Maybe already possible; have keybinds to show certain menus. I do not want to touch my mouse to see notifications for example. Not everything in the bar needs this, but some would be good to have. -- [x] Pressing ESC or some obvious button to close nc. -- [x] Another branch for development, hold off big changes so that a working bar or if there are big config changes. # 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) -- [ ] Update module: When there is 1 package it still looks extremely off -- [ ] Bluetooth device battery view +- [ ] Bluetooth device battery view -- Not planned ( Don't have a bluetooth + receiver ) + +# Completed features + +- [x] Auto hide unless on mouse hover. Also implement bar changes to mute/volume to show notif or show bar for a couple seconds. +- [x] Maybe already possible; have keybinds to show certain menus. I do not want to touch my mouse to see notifications for example. Not everything in the bar needs this, but some would be good to have. +- [x] Pressing ESC or some obvious button to close nc. +- [x] Another branch for development, hold off big changes so that a working bar or if there are big config changes. +- [x] Media showing; what song/media is playing? +- [x] Brightness control for Laptops. +- [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 diff --git a/shell.qml b/shell.qml index f65c607..3d19be0 100644 --- a/shell.qml +++ b/shell.qml @@ -3,22 +3,34 @@ //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell import qs.Modules -import qs.Modules.Lock as Lock +import qs.Modules.Wallpaper +import qs.Modules.Lock +import qs.Drawers import qs.Helpers import qs.Modules.Polkit ShellRoot { - Bar {} - Wallpaper {} - AreaPicker {} - Lock.Lock { - id: lock + Bar { } - Shortcuts {} - Lock.IdleInhibitor { + Wallpaper { + } + + AreaPicker { + } + + Lock { + id: lock + + } + + Shortcuts { + } + + IdleMonitors { lock: lock } - Polkit {} + Polkit { + } }