diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 81de7de..49cd82d 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -169,6 +169,20 @@ in readonly property int borderWidth: 2 // Screen frame band; sits inside hyprland's gaps_out (12) readonly property int frameWidth: 6 + + // ── Layout / metric tokens (referenced, not hardcoded) ── + // Bar band height. Drives widget heights, dropdown y-origin and + // the shader cutout geometry — change here, not in ~10 places. + readonly property int barHeight: 30 + readonly property int radius: 8 // cards, panels, dropdowns + readonly property int radiusSmall: 6 // workspace dots, pill buttons + readonly property int radiusTiny: 4 // hover rows, action chips + readonly property int cardPad: 8 // card inner padding (inset = 2×) + + // ── Animation duration tokens (ms) ── + readonly property int animMorph: 280 // panel grow / slide (OutExpo) + readonly property int animContent: 200 // content fade / highlight move + readonly property int animFade: 120 // hover colour transitions } ''; }; @@ -260,8 +274,6 @@ in import Quickshell.Widgets import Quickshell.Io import QtQuick - import QtQuick.Layouts - import QtQuick.Shapes import Qt5Compat.GraphicalEffects PanelWindow { @@ -284,7 +296,7 @@ in } implicitHeight: bar.screen.height - exclusiveZone: 30 + exclusiveZone: Theme.barHeight color: "transparent" mask: Region { @@ -320,7 +332,7 @@ in anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - height: 30 + height: Theme.barHeight } // Register the primary bar so shell.qml's IPC handler can @@ -338,6 +350,129 @@ in launcherPanel.toggle(); } + // ── Shared base text types: default the shell's two fonts so + // no widget repeats `font.family`. Size/colour/weight are + // overridable per use (these are just the common defaults). + component SText: Text { + color: Theme.base05 + font.family: Theme.fontFamily + font.pixelSize: 13 + } + component SIcon: Text { + color: Theme.base05 + font.family: Theme.iconFont + font.pixelSize: 16 + } + + // ── Card: the rounded base01 section surface used by every + // dropdown. Children flow into a padded auto-height column, + // so callers just set `width` and drop content in. + component Card: Rectangle { + default property alias cardData: _cardCol.data + property alias cardSpacing: _cardCol.spacing + radius: Theme.radius + color: Theme.base01 + implicitHeight: _cardCol.height + 2 * Theme.cardPad + Column { + id: _cardCol + anchors.top: parent.top + anchors.topMargin: Theme.cardPad + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 2 * Theme.cardPad + spacing: 8 + } + } + + // ── PillSlider: slim rounded track + fill with a full-height + // invisible hit area. `value` is 0..1; `moved(v)` fires on drag. + component PillSlider: Item { + property real value: 0 + property color fillColor: Theme.base0D + property real trackH: 6 + signal moved(real v) + height: 20 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: parent.trackH + radius: parent.trackH / 2 + color: Theme.base02 + } + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.value > 0 ? Math.max(parent.trackH, parent.value * parent.width) : 0 + height: parent.trackH + radius: parent.trackH / 2 + color: parent.fillColor + Behavior on width { NumberAnimation { duration: 80 } } + } + MouseArea { + anchors.fill: parent + function set(mouse) { parent.moved(Math.max(0, Math.min(1, mouse.x / width))); } + onPressed: (mouse) => set(mouse) + onPositionChanged: (mouse) => { if (pressed) set(mouse); } + } + } + + // ── NotifContent: summary + body + action chips for one + // notification, shared by the calendar list and the toast. + // Callers supply the container, dismiss button and sizes. + component NotifContent: Column { + id: _nc + property var notif + property int summarySize: 11 + property int bodySize: 10 + property int bodyLines: 2 + property color chipBg: Theme.base02 + property color chipBgHover: Theme.base03 + property color chipBorder: Theme.base03 + signal actionInvoked() + spacing: 2 + + SText { + width: parent.width + text: _nc.notif ? (_nc.notif.summary || _nc.notif.appName) : "" + font.pixelSize: _nc.summarySize + font.weight: Font.Medium + elide: Text.ElideRight + } + SText { + width: parent.width + text: _nc.notif ? (_nc.notif.body || "") : "" + color: Theme.base04 + font.pixelSize: _nc.bodySize + elide: Text.ElideRight + maximumLineCount: _nc.bodyLines + wrapMode: Text.Wrap + visible: text !== "" + } + Row { + spacing: 4 + visible: _nc.notif && _nc.notif.actions.length > 0 + Repeater { + model: _nc.notif ? _nc.notif.actions : [] + Rectangle { + required property var modelData + width: _at.width + 12 + height: _at.height + 6 + radius: Theme.radiusTiny + color: _ama.containsMouse ? _nc.chipBgHover : _nc.chipBg + Behavior on color { ColorAnimation { duration: Theme.animFade } } + border.width: 1 + border.color: _nc.chipBorder + SText { id: _at; anchors.centerIn: parent; text: modelData.text; font.pixelSize: 10 } + MouseArea { + id: _ama + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { modelData.invoke(); _nc.actionInvoked(); } + } + } + } + } + } + // ── Shell chrome: bar, frame, panel and toast rendered as // ONE signed-distance field (caelestia-style). Surfaces merge // via circular smooth-min, and the 2px border is the distance @@ -348,17 +483,20 @@ in anchors.fill: parent readonly property real panelLeft: chrome.x + 8 readonly property real panelRight: chrome.x + chrome.width + (chrome.flushRight ? 4 : -8) + // The panel/toast centre-y uses (barHeight − 4): the + // surface overlaps 4px up into the bar so they melt. + readonly property real surfTopY: Theme.barHeight - 4 property vector4d cutout: Qt.vector4d( bar.width / 2, - (30 + bar.height - Theme.frameWidth) / 2, + (Theme.barHeight + bar.height - Theme.frameWidth) / 2, bar.width / 2 - Theme.frameWidth, - (bar.height - Theme.frameWidth - 30) / 2) + (bar.height - Theme.frameWidth - Theme.barHeight) / 2) property vector4d panel: chrome.visible - ? Qt.vector4d((panelLeft + panelRight) / 2, 26 + chrome.height / 2, + ? Qt.vector4d((panelLeft + panelRight) / 2, surfTopY + chrome.height / 2, (panelRight - panelLeft) / 2, 4 + chrome.height / 2) : Qt.vector4d(0, 0, 0, 0) property vector4d toast: toastItem.visible && _toastRect.height > 0.5 - ? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, 26 + _toastRect.height / 2, + ? Qt.vector4d(toastItem.x + 8 + _toastRect.width / 2, surfTopY + _toastRect.height / 2, _toastRect.width / 2, 4 + _toastRect.height / 2) : Qt.vector4d(0, 0, 0, 0) readonly property real sessRight: bar.width - Theme.frameWidth + 4 @@ -374,8 +512,8 @@ in property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a) property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1) property vector2d res: Qt.vector2d(width, height) - property real cutoutR: 8 - property real panelR: 8 + property real cutoutR: Theme.radius + property real panelR: Theme.radius property real meltK: 12 property real borderW: Theme.borderWidth fragmentShader: "file://${chromeShader}" @@ -394,10 +532,10 @@ in property real openW: open ? 64 : 0 property real openH: open ? sessionCard.height + 24 : 36 Behavior on openW { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } Behavior on openH { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } readonly property var actions: [ @@ -457,7 +595,7 @@ in clip: true opacity: sessionMenu.open ? 1 : 0 Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic } } // Card backing, matching the other dropdowns @@ -468,7 +606,7 @@ in anchors.rightMargin: 8 width: 48 height: sessionCol.height + 8 - radius: 8 + radius: Theme.radius color: Theme.base01 // Sliding selection pill — same tech as the power @@ -476,7 +614,7 @@ in Rectangle { width: 40 height: 40 - radius: 8 + radius: Theme.radius color: Theme.base02 border.width: 1 border.color: Theme.base03 @@ -503,15 +641,14 @@ in width: 40 height: 40 - Text { + SIcon { anchors.centerIn: parent text: sessBtn.modelData.icon // Calendar palette: base05 icons; red only when // a destructive action is the armed selection color: sessBtn.selected && sessBtn.modelData.danger ? Theme.base08 : Theme.base05 - Behavior on color { ColorAnimation { duration: 120 } } - font.family: Theme.iconFont + Behavior on color { ColorAnimation { duration: Theme.animFade } } font.pixelSize: 20 font.weight: 600 font.variableAxes: { "FILL": sessBtn.selected ? 1.0 : 0.0 } @@ -546,10 +683,10 @@ in property real openH: open ? targetH : 0 property real openW: open ? panelW : 80 Behavior on openH { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } Behavior on openW { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } x: Math.round((bar.width - openW) / 2) @@ -615,7 +752,7 @@ in clip: true opacity: launcherPanel.open ? 1 : 0 Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic } } Column { @@ -632,7 +769,7 @@ in interactive: false model: launcherPanel.entries highlight: Rectangle { - radius: 6 + radius: Theme.radiusSmall color: Theme.base02 } highlightMoveDuration: 200 @@ -661,11 +798,10 @@ in source: Quickshell.iconPath(modelData.icon, true) } - Text { + SText { anchors.verticalCenter: parent.verticalCenter text: modelData.name color: Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 13 elide: Text.ElideRight width: 330 @@ -684,17 +820,16 @@ in Rectangle { width: parent.width height: 36 - radius: 6 + radius: Theme.radiusSmall color: Theme.base01 - Text { + SIcon { id: searchIcon anchors.left: parent.left anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter text: "search" color: Theme.base04 - font.family: Theme.iconFont font.pixelSize: 16 } @@ -719,14 +854,13 @@ in Keys.onEnterPressed: launcherPanel.activate(launcherPanel.entries[launcherList.currentIndex]) } - Text { + SText { anchors.left: searchIcon.right anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter visible: searchInput.text === "" text: "Search" color: Theme.base03 - font.family: Theme.fontFamily font.pixelSize: 13 } } @@ -798,7 +932,7 @@ in required property var modelData visible: modelData.id > 0 width: visible ? dot.width + 6 : 0 - height: 30 + height: Theme.barHeight Rectangle { id: dot @@ -811,7 +945,7 @@ in Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutExpo } } - Behavior on color { ColorAnimation { duration: 120 } } + Behavior on color { ColorAnimation { duration: Theme.animFade } } } MouseArea { @@ -831,14 +965,13 @@ in precision: SystemClock.Minutes } - Text { + SText { id: clockText anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: barBgRect.verticalCenter property date now: sysClock.date text: now.toLocaleTimeString(Qt.locale(), "HH:mm") color: Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 13 font.weight: Font.Medium @@ -871,7 +1004,7 @@ in Item { id: volWidget width: volRow.width - height: 30 + height: Theme.barHeight property PwNode sink: Pipewire.defaultAudioSink @@ -899,19 +1032,17 @@ in anchors.verticalCenter: parent.verticalCenter spacing: 3 - Text { + SIcon { anchors.verticalCenter: parent.verticalCenter text: volWidget.volIcon color: volWidget.muted ? Theme.base03 : Theme.base05 - font.family: Theme.iconFont font.pixelSize: 16 } - Text { + SText { anchors.verticalCenter: parent.verticalCenter text: volWidget.vol + "%" color: volWidget.muted ? Theme.base03 : Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 13 } } @@ -941,7 +1072,7 @@ in Item { id: netWidget width: 16 - height: 30 + height: Theme.barHeight property string netState: "disconnected" property string netConn: "" @@ -1032,11 +1163,10 @@ in } } - Text { + SIcon { anchors.centerIn: parent text: netWidget.netIcon color: Theme.base05 - font.family: Theme.iconFont font.pixelSize: 16 } @@ -1110,7 +1240,7 @@ in Item { id: batteryWidget width: batteryText.width + 4 + batteryIconText.width - height: 30 + height: Theme.barHeight // Live DBus-driven properties from the UPower service — // no polling, no /sys parsing, no subprocess spawns. @@ -1145,25 +1275,23 @@ in // Explicit vertical centering: Rows top-align by // default, and the icon font's taller line metrics // would push the text off the shared baseline. - Text { + SText { id: batteryText anchors.verticalCenter: parent.verticalCenter text: batteryWidget.batteryLevel + "%" color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 13 } - Text { + SIcon { id: batteryIconText anchors.verticalCenter: parent.verticalCenter text: batteryWidget.batteryIcon color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 - font.family: Theme.iconFont font.pixelSize: 16 } } @@ -1193,7 +1321,7 @@ in Row { id: trayArea spacing: 8 - height: 30 + height: Theme.barHeight anchors.verticalCenter: parent.verticalCenter HoverHandler { @@ -1208,7 +1336,7 @@ in Item { required property var modelData width: 24 - height: 30 + height: Theme.barHeight Image { id: trayIcon @@ -1328,7 +1456,7 @@ in x: alignRight ? bar.width - Theme.frameWidth - width : Math.round(Math.min(bar.width - Theme.frameWidth - width, Math.max(Theme.frameWidth, dropdownX - width / 2))) - y: 30 + y: Theme.barHeight width: fullWidth + (alignRight ? 8 : 16) height: fullHeight + 4 visible: false @@ -1378,7 +1506,7 @@ in clip: true opacity: dropdown.open ? 1 : 0 Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.OutCubic } + NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic } } Item { @@ -1426,7 +1554,7 @@ in } x: tX - y: 30 + y: Theme.barHeight width: tW height: openH visible: height > 0.5 @@ -1458,14 +1586,14 @@ in Behavior on tX { enabled: !chrome.snap - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } Behavior on tW { enabled: !chrome.snap - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } Behavior on openH { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } } @@ -1500,7 +1628,7 @@ in height: modelData.isSeparator ? 9 : 28 color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } + Behavior on color { ColorAnimation { duration: Theme.animFade } } radius: modelData.isSeparator ? 0 : 4 Rectangle { @@ -1511,29 +1639,32 @@ in color: Theme.base03 } - RowLayout { + Item { visible: !modelData.isSeparator anchors.fill: parent anchors.leftMargin: 10 anchors.rightMargin: 10 - spacing: 8 - Text { - Layout.fillWidth: true - text: modelData.text ?? "" - color: modelData.enabled ? Theme.base05 : Theme.base03 - font.family: Theme.fontFamily - font.pixelSize: 12 - elide: Text.ElideRight - } - - Text { + SText { + id: menuCheck + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter visible: modelData.buttonType !== QsMenuButtonType.None text: modelData.checkState === Qt.Checked ? "\u2713" : "" color: Theme.base0D - font.family: Theme.fontFamily font.pixelSize: 12 } + + SText { + anchors.left: parent.left + anchors.right: menuCheck.visible ? menuCheck.left : parent.right + anchors.rightMargin: menuCheck.visible ? 8 : 0 + anchors.verticalCenter: parent.verticalCenter + text: modelData.text ?? "" + color: modelData.enabled ? Theme.base05 : Theme.base03 + font.pixelSize: 12 + elide: Text.ElideRight + } } MouseArea { @@ -1566,253 +1697,141 @@ in spacing: 8 // Master volume card - Rectangle { + Card { width: parent.width - height: masterCardCol.height + 16 - radius: 8 - color: Theme.base01 - Column { - id: masterCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 + Row { + spacing: 6 + SIcon { anchors.verticalCenter: parent.verticalCenter; text: "volume_up" } + SText { + anchors.verticalCenter: parent.verticalCenter + text: "Master" + font.weight: Font.Medium + } + } + + Row { + width: parent.width spacing: 8 + PillSlider { + width: parent.width - masterVolLabel.width - 8 + anchors.verticalCenter: parent.verticalCenter + value: volWidget.sink && volWidget.sink.audio ? Math.min(1, volWidget.sink.audio.volume) : 0 + fillColor: volWidget.muted ? Theme.base03 : Theme.base0D + onMoved: (v) => { if (volWidget.sink && volWidget.sink.audio) volWidget.sink.audio.volume = v; } + } + + SText { + id: masterVolLabel + width: 36 + text: volWidget.vol + "%" + font.pixelSize: 11 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + + // Mute button + Rectangle { + width: parent.width + height: 28 + color: masterMuteMa.containsMouse ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } + radius: Theme.radiusTiny + Row { + anchors.centerIn: parent spacing: 6 - Text { + SIcon { anchors.verticalCenter: parent.verticalCenter - text: "volume_up" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 16 + text: volWidget.muted ? "volume_off" : "volume_up" + font.pixelSize: 15 } - Text { + SText { anchors.verticalCenter: parent.verticalCenter - text: "Master" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 13 - font.weight: Font.Medium + text: volWidget.muted ? "Unmute" : "Mute" + font.pixelSize: 12 } } - - Row { - width: parent.width - spacing: 8 - - // Slim pill slider: 6px fully-rounded track, - // 20px invisible hit area for comfy dragging - Item { - id: masterSliderBg - width: parent.width - masterVolLabel.width - 8 - height: 20 - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: 6 - radius: 3 - color: Theme.base02 - } - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: { - let v = volWidget.sink && volWidget.sink.audio - ? Math.min(1, volWidget.sink.audio.volume) : 0; - return v > 0 ? Math.max(6, v * parent.width) : 0; - } - height: 6 - radius: 3 - color: volWidget.muted ? Theme.base03 : Theme.base0D - Behavior on width { NumberAnimation { duration: 80 } } - } - - MouseArea { - anchors.fill: parent - onPressed: (mouse) => setVolume(mouse) - onPositionChanged: (mouse) => { if (pressed) setVolume(mouse); } - function setVolume(mouse) { - if (!volWidget.sink || !volWidget.sink.audio) return; - let v = Math.max(0, Math.min(1, mouse.x / width)); - volWidget.sink.audio.volume = v; - } - } - } - - Text { - id: masterVolLabel - width: 36 - text: volWidget.vol + "%" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 11 - horizontalAlignment: Text.AlignRight - anchors.verticalCenter: parent.verticalCenter - } - } - - // Mute button - Rectangle { - width: parent.width - height: 28 - color: masterMuteMa.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - radius: 4 - - Row { - anchors.centerIn: parent - spacing: 6 - Text { - anchors.verticalCenter: parent.verticalCenter - text: volWidget.muted ? "volume_off" : "volume_up" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 15 - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: volWidget.muted ? "Unmute" : "Mute" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 12 - } - } - MouseArea { - id: masterMuteMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (volWidget.sink && volWidget.sink.audio) - volWidget.sink.audio.muted = !volWidget.sink.audio.muted; - } + MouseArea { + id: masterMuteMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (volWidget.sink && volWidget.sink.audio) + volWidget.sink.audio.muted = !volWidget.sink.audio.muted; } } } } // Applications card - Rectangle { + Card { visible: appStreamsCol.childrenRect.height > 0 width: parent.width - height: appsCardCol.height + 16 - radius: 8 - color: Theme.base01 - Column { - id: appsCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - spacing: 8 - - Row { - spacing: 6 - Text { - anchors.verticalCenter: parent.verticalCenter - text: "graphic_eq" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 16 - } - Text { - anchors.verticalCenter: parent.verticalCenter - text: "Applications" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 13 - font.weight: Font.Medium - } + Row { + spacing: 6 + SIcon { anchors.verticalCenter: parent.verticalCenter; text: "graphic_eq" } + SText { + anchors.verticalCenter: parent.verticalCenter + text: "Applications" + font.weight: Font.Medium } + } - // Per-app streams - Column { - id: appStreamsCol - width: parent.width - spacing: 6 + // Per-app streams + Column { + id: appStreamsCol + width: parent.width + spacing: 6 - Repeater { - id: appStreamsRepeater - model: Pipewire.nodes + Repeater { + id: appStreamsRepeater + model: Pipewire.nodes - Column { - required property var modelData + Column { + required property var modelData + width: parent.width + spacing: 2 + visible: modelData.isStream && modelData.audio !== null + + PwObjectTracker { + objects: [modelData] + } + + SText { + text: modelData.properties["application.name"] || modelData.name || "Unknown" + color: Theme.base04 + font.pixelSize: 11 + elide: Text.ElideRight width: parent.width - spacing: 2 - visible: modelData.isStream && modelData.audio !== null + } - PwObjectTracker { - objects: [modelData] + Row { + width: parent.width + spacing: 8 + + PillSlider { + width: parent.width - appVolLabel.width - 8 + anchors.verticalCenter: parent.verticalCenter + height: 16 + trackH: 4 + value: modelData.audio ? Math.min(1, modelData.audio.volume) : 0 + fillColor: modelData.audio && modelData.audio.muted ? Theme.base03 : Theme.base0C + onMoved: (v) => { if (modelData.audio) modelData.audio.volume = v; } } - Text { - text: modelData.properties["application.name"] || modelData.name || "Unknown" + SText { + id: appVolLabel + width: 36 + text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%" color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 - elide: Text.ElideRight - width: parent.width - } - - Row { - width: parent.width - spacing: 8 - - // Slim pill slider, 16px hit area - Item { - width: parent.width - appVolLabel.width - 8 - height: 16 - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - height: 4 - radius: 2 - color: Theme.base02 - } - - Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: { - let v = modelData.audio ? Math.min(1, modelData.audio.volume) : 0; - return v > 0 ? Math.max(4, v * parent.width) : 0; - } - height: 4 - radius: 2 - color: modelData.audio && modelData.audio.muted - ? Theme.base03 : Theme.base0C - Behavior on width { NumberAnimation { duration: 80 } } - } - - MouseArea { - anchors.fill: parent - onPressed: (mouse) => setVol(mouse) - onPositionChanged: (mouse) => { if (pressed) setVol(mouse); } - function setVol(mouse) { - if (!modelData.audio) return; - let v = Math.max(0, Math.min(1, mouse.x / width)); - modelData.audio.volume = v; - } - } - } - - Text { - id: appVolLabel - width: 36 - text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 10 - horizontalAlignment: Text.AlignRight - anchors.verticalCenter: parent.verticalCenter - } + font.pixelSize: 10 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter } } } @@ -1836,168 +1855,137 @@ in spacing: 8 // Connection card - Rectangle { + Card { width: parent.width - height: connCardCol.height + 16 - radius: 8 - color: Theme.base01 + cardSpacing: 4 + Row { + width: parent.width + spacing: 6 - Column { - id: connCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - spacing: 4 - - Row { - width: parent.width - spacing: 6 - - Text { - anchors.verticalCenter: parent.verticalCenter - text: netWidget.netState === "connected" ? "wifi" : "wifi_off" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 16 - } - - Text { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 22 - text: netWidget.netState === "connected" - ? netWidget.netConn : "Not connected" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 13 - font.weight: Font.Medium - elide: Text.ElideRight - } + SIcon { + anchors.verticalCenter: parent.verticalCenter + text: netWidget.netState === "connected" ? "wifi" : "wifi_off" + color: Theme.base05 + font.pixelSize: 16 } - Rectangle { - visible: netWidget.netState === "connected" - width: parent.width - height: 28 - color: disconnectMouse.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - radius: 4 + SText { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 22 + text: netWidget.netState === "connected" + ? netWidget.netConn : "Not connected" + color: Theme.base05 + font.pixelSize: 13 + font.weight: Font.Medium + elide: Text.ElideRight + } + } - Text { - anchors.centerIn: parent - text: "Disconnect" - color: Theme.base08 - font.family: Theme.fontFamily - font.pixelSize: 12 - } + Rectangle { + visible: netWidget.netState === "connected" + width: parent.width + height: 28 + color: disconnectMouse.containsMouse ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } + radius: Theme.radiusTiny - MouseArea { - id: disconnectMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - netDisconnectProc.targetDevice = netWidget.netDevice; - netDisconnectProc.running = true; - netWidget.netState = "disconnected"; - netWidget.netConn = ""; - netWidget.netIcon = "wifi_off"; - bar.closeAllDropdowns(); - netRefreshDelay.start(); - } + SText { + anchors.centerIn: parent + text: "Disconnect" + color: Theme.base08 + font.pixelSize: 12 + } + + MouseArea { + id: disconnectMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + netDisconnectProc.targetDevice = netWidget.netDevice; + netDisconnectProc.running = true; + netWidget.netState = "disconnected"; + netWidget.netConn = ""; + netWidget.netIcon = "wifi_off"; + bar.closeAllDropdowns(); + netRefreshDelay.start(); } } } } // Available networks card - Rectangle { + Card { width: parent.width - height: netsCardCol.height + 16 - radius: 8 - color: Theme.base01 + cardSpacing: 4 + SText { + text: "Available networks" + color: Theme.base04 + font.pixelSize: 11 + } - Column { - id: netsCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - spacing: 4 + Repeater { + model: netWidget.wifiNetworks - Text { - text: "Available networks" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 - } + Rectangle { + required property var modelData + width: parent.width + height: 32 + color: netItemMouse.containsMouse ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } + radius: Theme.radiusTiny - Repeater { - model: netWidget.wifiNetworks + Row { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + spacing: 8 - Rectangle { - required property var modelData - width: netsCardCol.width - height: 32 - color: netItemMouse.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - radius: 4 - - Row { + SIcon { + text: { + let s = modelData.signal; + if (s >= 75) return "signal_wifi_4_bar"; + if (s >= 50) return "network_wifi_3_bar"; + if (s >= 25) return "network_wifi_2_bar"; + return "network_wifi_1_bar"; + } + color: modelData.active ? Theme.base0B : Theme.base04 + font.pixelSize: 16 anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 8 - anchors.right: parent.right - anchors.rightMargin: 8 - spacing: 8 - - Text { - text: { - let s = modelData.signal; - if (s >= 75) return "signal_wifi_4_bar"; - if (s >= 50) return "network_wifi_3_bar"; - if (s >= 25) return "network_wifi_2_bar"; - return "network_wifi_1_bar"; - } - color: modelData.active ? Theme.base0B : Theme.base04 - font.family: Theme.iconFont - font.pixelSize: 16 - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: modelData.ssid - color: modelData.active ? Theme.base0B : Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 12 - elide: Text.ElideRight - width: 140 - anchors.verticalCenter: parent.verticalCenter - } - - Text { - visible: modelData.security !== "" && modelData.security !== "--" - text: "lock" - color: Theme.base03 - font.family: Theme.iconFont - font.pixelSize: 13 - anchors.verticalCenter: parent.verticalCenter - } } - MouseArea { - id: netItemMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (!modelData.active) { - wifiConnectProc.targetSsid = modelData.ssid; - wifiConnectProc.running = true; - netRefreshDelay.start(); - } - bar.closeAllDropdowns(); + SText { + text: modelData.ssid + color: modelData.active ? Theme.base0B : Theme.base05 + font.pixelSize: 12 + elide: Text.ElideRight + width: 140 + anchors.verticalCenter: parent.verticalCenter + } + + SIcon { + visible: modelData.security !== "" && modelData.security !== "--" + text: "lock" + color: Theme.base03 + font.pixelSize: 13 + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: netItemMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!modelData.active) { + wifiConnectProc.targetSsid = modelData.ssid; + wifiConnectProc.running = true; + netRefreshDelay.start(); } + bar.closeAllDropdowns(); } } } @@ -2021,149 +2009,119 @@ in spacing: 8 // Battery status card - Rectangle { + Card { width: parent.width - height: battCardCol.height + 16 - radius: 8 - color: Theme.base01 + Row { + width: parent.width + spacing: 8 - Column { - id: battCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 + SIcon { + text: batteryWidget.batteryIcon + color: Theme.base05 + font.pixelSize: 22 + anchors.verticalCenter: parent.verticalCenter + } - Row { - width: parent.width - spacing: 8 - - Text { - text: batteryWidget.batteryIcon + Column { + anchors.verticalCenter: parent.verticalCenter + SText { + text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " — Charging" : "") color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 22 - anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 13 + font.weight: Font.Medium } - - Column { - anchors.verticalCenter: parent.verticalCenter - Text { - text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " — Charging" : "") - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 13 - font.weight: Font.Medium - } - Text { - text: batteryWidget.powerDraw.toFixed(1) + " W" - + (batteryWidget.timeRemaining !== "" ? " • " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "") - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 - } + SText { + text: batteryWidget.powerDraw.toFixed(1) + " W" + + (batteryWidget.timeRemaining !== "" ? " • " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "") + color: Theme.base04 + font.pixelSize: 11 } } } } // Power profile card - Rectangle { + Card { width: parent.width - height: profCardCol.height + 16 - radius: 8 - color: Theme.base01 + cardSpacing: 6 + SText { + text: "Power Profile" + color: Theme.base04 + font.pixelSize: 11 + } - Column { - id: profCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - spacing: 6 + Item { + width: parent.width + height: 36 - Text { - text: "Power Profile" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 + // Sliding selection pill — glides between + // profiles instead of each button flipping. + Rectangle { + id: profilePill + readonly property int selIdx: + batteryWidget.powerProfile === "power-saver" ? 0 + : batteryWidget.powerProfile === "performance" ? 2 + : 1 + width: (parent.width - 8) / 3 + height: 36 + radius: Theme.radiusSmall + color: Theme.base02 + border.width: 1 + border.color: Theme.base03 + x: selIdx * (width + 4) + Behavior on x { + NumberAnimation { duration: 250; easing.type: Easing.OutExpo } + } } - Item { - width: parent.width - height: 36 + Row { + anchors.fill: parent + spacing: 4 - // Sliding selection pill — glides between - // profiles instead of each button flipping. - Rectangle { - id: profilePill - readonly property int selIdx: - batteryWidget.powerProfile === "power-saver" ? 0 - : batteryWidget.powerProfile === "performance" ? 2 - : 1 - width: (parent.width - 8) / 3 - height: 36 - radius: 6 - color: Theme.base02 - border.width: 1 - border.color: Theme.base03 - x: selIdx * (width + 4) - Behavior on x { - NumberAnimation { duration: 250; easing.type: Easing.OutExpo } - } - } + Repeater { + model: [ + { name: "power-saver", profile: PowerProfile.PowerSaver, label: "energy_savings_leaf", tip: "Saver" }, + { name: "balanced", profile: PowerProfile.Balanced, label: "balance", tip: "Balanced" }, + { name: "performance", profile: PowerProfile.Performance, label: "speed", tip: "Performance" } + ] - Row { - anchors.fill: parent - spacing: 4 + Rectangle { + required property var modelData + width: (parent.width - 8) / 3 + height: 36 + radius: Theme.radiusSmall + color: profMouse.containsMouse && batteryWidget.powerProfile !== modelData.name + ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } - Repeater { - model: [ - { name: "power-saver", profile: PowerProfile.PowerSaver, label: "energy_savings_leaf", tip: "Saver" }, - { name: "balanced", profile: PowerProfile.Balanced, label: "balance", tip: "Balanced" }, - { name: "performance", profile: PowerProfile.Performance, label: "speed", tip: "Performance" } - ] - - Rectangle { - required property var modelData - width: (parent.width - 8) / 3 - height: 36 - radius: 6 - color: profMouse.containsMouse && batteryWidget.powerProfile !== modelData.name - ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - - Column { - anchors.centerIn: parent - spacing: 1 - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.label - color: batteryWidget.powerProfile === modelData.name - ? Theme.base0D : Theme.base05 - Behavior on color { ColorAnimation { duration: 200 } } - font.family: Theme.iconFont - font.pixelSize: 17 - } - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.tip - color: batteryWidget.powerProfile === modelData.name - ? Theme.base05 : Theme.base04 - Behavior on color { ColorAnimation { duration: 200 } } - font.family: Theme.fontFamily - font.pixelSize: 9 - } + Column { + anchors.centerIn: parent + spacing: 1 + SIcon { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.label + color: batteryWidget.powerProfile === modelData.name + ? Theme.base0D : Theme.base05 + Behavior on color { ColorAnimation { duration: 200 } } + font.pixelSize: 17 } - - MouseArea { - id: profMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: PowerProfiles.profile = modelData.profile + SText { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.tip + color: batteryWidget.powerProfile === modelData.name + ? Theme.base05 : Theme.base04 + Behavior on color { ColorAnimation { duration: 200 } } + font.pixelSize: 9 } } + + MouseArea { + id: profMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: PowerProfiles.profile = modelData.profile + } } } } @@ -2233,12 +2191,15 @@ in triggeredOnStart: true onTriggered: weatherProc.running = true } + // WMO weather codes → Material Symbols. Ranges per + // open-meteo: 0 clear, 1-2 partly, 3 overcast, 45-48 fog, + // 51-67 drizzle/rain, 71-77 snow, 80-82 rain showers, + // 85-86 snow showers, 95+ thunder. function weatherGlyph(code) { if (code === 0) return "clear_day"; if (code <= 2) return "partly_cloudy_day"; if (code === 3) return "cloud"; if (code <= 48) return "foggy"; - if (code <= 57) return "rainy"; if (code <= 67) return "rainy"; if (code <= 77) return "cloudy_snowing"; if (code <= 82) return "rainy"; @@ -2274,129 +2235,111 @@ in spacing: 8 // Calendar card - Rectangle { + Card { width: parent.width - height: calCardCol.height + 16 - radius: 8 - color: Theme.base01 + // Month header: ‹ [Month Year] › — label click jumps to today + Item { + width: parent.width + height: 28 - Column { - id: calCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: 7 * 32 - spacing: 8 - - // Month header: ‹ [Month Year] › — label click jumps to today - Item { - width: parent.width - height: 28 - - Rectangle { - width: 28; height: 28; radius: 6 - anchors.left: parent.left - color: calPrevMa.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - Text { - anchors.centerIn: parent - text: "chevron_left" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 18 - } - MouseArea { - id: calPrevMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: calPopup.shiftMonth(-1) - } - } - - Text { + Rectangle { + width: 28; height: 28; radius: Theme.radiusSmall + anchors.left: parent.left + color: calPrevMa.containsMouse ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } + SIcon { anchors.centerIn: parent - text: new Date(calPopup.viewYear, calPopup.viewMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") + text: "chevron_left" color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 14 - font.weight: Font.Medium - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: calPopup.resetView() - } + font.pixelSize: 18 } + MouseArea { + id: calPrevMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: calPopup.shiftMonth(-1) + } + } + SText { + anchors.centerIn: parent + text: new Date(calPopup.viewYear, calPopup.viewMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") + color: Theme.base05 + font.pixelSize: 14 + font.weight: Font.Medium + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: calPopup.resetView() + } + } + + Rectangle { + width: 28; height: 28; radius: Theme.radiusSmall + anchors.right: parent.right + color: calNextMa.containsMouse ? Theme.base02 : "transparent" + Behavior on color { ColorAnimation { duration: Theme.animFade } } + SIcon { + anchors.centerIn: parent + text: "chevron_right" + color: Theme.base05 + font.pixelSize: 18 + } + MouseArea { + id: calNextMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: calPopup.shiftMonth(1) + } + } + } + + Row { + spacing: 0 + Repeater { + model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] + SText { + required property var modelData + width: 32 + horizontalAlignment: Text.AlignHCenter + text: modelData + color: Theme.base04 + font.pixelSize: 13 + } + } + } + + Grid { + columns: 7 + spacing: 0 + Repeater { + model: 42 Rectangle { - width: 28; height: 28; radius: 6 - anchors.right: parent.right - color: calNextMa.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - Text { + required property int index + property int dayNum: { + let first = new Date(calPopup.viewYear, calPopup.viewMonth, 1); + let startDay = (first.getDay() + 6) % 7; + return index - startDay + 1; + } + property int daysInMonth: new Date(calPopup.viewYear, calPopup.viewMonth + 1, 0).getDate() + property bool isToday: dayNum === clockText.now.getDate() + && calPopup.viewMonth === clockText.now.getMonth() + && calPopup.viewYear === clockText.now.getFullYear() + width: 32 + height: 26 + radius: Theme.radiusTiny + color: isToday ? Theme.base03 : "transparent" + + SText { anchors.centerIn: parent - text: "chevron_right" - color: Theme.base05 - font.family: Theme.iconFont - font.pixelSize: 18 - } - MouseArea { - id: calNextMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: calPopup.shiftMonth(1) - } - } - } - - Row { - spacing: 0 - Repeater { - model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] - Text { - required property var modelData - width: 32 - horizontalAlignment: Text.AlignHCenter - text: modelData - color: Theme.base04 - font.family: Theme.fontFamily + text: parent.dayNum >= 1 && parent.dayNum <= parent.daysInMonth ? parent.dayNum.toString() : "" + color: parent.isToday ? Theme.base05 : Theme.base04 font.pixelSize: 13 } } } - - Grid { - columns: 7 - spacing: 0 - Repeater { - model: 42 - Rectangle { - required property int index - property int dayNum: { - let first = new Date(calPopup.viewYear, calPopup.viewMonth, 1); - let startDay = (first.getDay() + 6) % 7; - return index - startDay + 1; - } - property int daysInMonth: new Date(calPopup.viewYear, calPopup.viewMonth + 1, 0).getDate() - property bool isToday: dayNum === clockText.now.getDate() - && calPopup.viewMonth === clockText.now.getMonth() - && calPopup.viewYear === clockText.now.getFullYear() - width: 32 - height: 26 - radius: 4 - color: isToday ? Theme.base03 : "transparent" - - Text { - anchors.centerIn: parent - text: parent.dayNum >= 1 && parent.dayNum <= parent.daysInMonth ? parent.dayNum.toString() : "" - color: parent.isToday ? Theme.base05 : Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 13 - } - } - } - } } } @@ -2404,7 +2347,7 @@ in Rectangle { width: parent.width height: weatherRow.height + 16 - radius: 8 + radius: Theme.radius color: Theme.base01 visible: calPopup.weatherDays.length > 0 @@ -2419,32 +2362,28 @@ in required property var modelData width: 32 spacing: 2 - Text { + SText { anchors.horizontalCenter: parent.horizontalCenter text: modelData.day color: Theme.base04 - font.family: Theme.fontFamily font.pixelSize: 10 } - Text { + SIcon { anchors.horizontalCenter: parent.horizontalCenter text: calPopup.weatherGlyph(modelData.code) color: Theme.base0C - font.family: Theme.iconFont font.pixelSize: 16 } - Text { + SText { anchors.horizontalCenter: parent.horizontalCenter text: modelData.max + "°" color: Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 10 } - Text { + SText { anchors.horizontalCenter: parent.horizontalCenter text: modelData.min + "°" color: Theme.base03 - font.family: Theme.fontFamily font.pixelSize: 10 } } @@ -2463,7 +2402,7 @@ in Rectangle { width: parent.width height: 64 - radius: 8 + radius: Theme.radius color: Theme.base01 visible: calPopup.player !== null @@ -2474,16 +2413,15 @@ in Rectangle { width: 48; height: 48 - radius: 6 + radius: Theme.radiusSmall anchors.verticalCenter: parent.verticalCenter color: Theme.base02 clip: true - Text { + SIcon { anchors.centerIn: parent visible: albumArt.status !== Image.Ready text: "music_note" color: Theme.base04 - font.family: Theme.iconFont font.pixelSize: 22 } Image { @@ -2498,20 +2436,18 @@ in width: parent.width - 48 - 10 - 88 - 10 anchors.verticalCenter: parent.verticalCenter spacing: 2 - Text { + SText { width: parent.width text: calPopup.player ? calPopup.player.trackTitle : "" color: Theme.base05 - font.family: Theme.fontFamily font.pixelSize: 12 font.weight: Font.Medium elide: Text.ElideRight } - Text { + SText { width: parent.width text: calPopup.player ? calPopup.player.trackArtist : "" color: Theme.base04 - font.family: Theme.fontFamily font.pixelSize: 11 elide: Text.ElideRight } @@ -2531,12 +2467,11 @@ in required property var modelData width: 28; height: 28; radius: 14 color: mediaBtnMa.containsMouse ? Theme.base02 : "transparent" - Behavior on color { ColorAnimation { duration: 120 } } - Text { + Behavior on color { ColorAnimation { duration: Theme.animFade } } + SIcon { anchors.centerIn: parent text: mediaBtn.modelData.glyph color: Theme.base05 - font.family: Theme.iconFont font.pixelSize: 18 } MouseArea { @@ -2559,154 +2494,82 @@ in } // Notifications card - Rectangle { + Card { width: parent.width - height: notifCardCol.height + 16 - radius: 8 - color: Theme.base01 + cardSpacing: 6 - Column { - id: notifCardCol - anchors.top: parent.top - anchors.topMargin: 8 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - 16 - spacing: 6 + Item { + width: parent.width + height: 20 - Item { - width: parent.width - height: 20 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Notifications" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 13 - font.weight: Font.Medium - } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: bar.notifServer.trackedNotifications.values.length > 0 ? "Clear all" : "" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - let notifs = bar.notifServer.trackedNotifications.values; - for (let i = notifs.length - 1; i >= 0; i--) { - notifs[i].dismiss(); - } - } - } - } + SText { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: "Notifications" + font.weight: Font.Medium } - Text { - visible: bar.notifServer.trackedNotifications.values.length === 0 - text: "No notifications" - color: Theme.base03 - font.family: Theme.fontFamily + SText { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: bar.notifServer.trackedNotifications.values.length > 0 ? "Clear all" : "" + color: Theme.base04 font.pixelSize: 11 - anchors.horizontalCenter: parent.horizontalCenter - } - - Repeater { - model: bar.notifServer.trackedNotifications - - Rectangle { - id: notifItem - required property var modelData - width: notifCardCol.width - height: notifCol.height + 12 - radius: 6 - color: Theme.base02 - - Column { - id: notifCol - anchors.left: parent.left - anchors.right: dismissBtn.left - anchors.top: parent.top - anchors.margins: 6 - spacing: 2 - - Text { - width: parent.width - text: notifItem.modelData.summary || notifItem.modelData.appName - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 11 - font.weight: Font.Medium - elide: Text.ElideRight - } - - Text { - width: parent.width - text: notifItem.modelData.body || "" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 10 - elide: Text.ElideRight - maximumLineCount: 2 - wrapMode: Text.Wrap - visible: text !== "" - } - - Row { - spacing: 4 - visible: notifItem.modelData.actions.length > 0 - Repeater { - model: notifItem.modelData.actions - Rectangle { - required property var modelData - width: actionText.width + 12 - height: actionText.height + 4 - radius: 4 - color: actionMa.containsMouse ? Theme.base03 : Theme.base02 - Behavior on color { ColorAnimation { duration: 120 } } - border.width: 1 - border.color: Theme.base03 - Text { - id: actionText - anchors.centerIn: parent - text: modelData.text - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 10 - } - MouseArea { - id: actionMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: modelData.invoke() - } - } - } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + let notifs = bar.notifServer.trackedNotifications.values; + for (let i = notifs.length - 1; i >= 0; i--) { + notifs[i].dismiss(); } } + } + } + } - Text { - id: dismissBtn - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 6 - text: "close" - color: dismissMa.containsMouse ? Theme.base05 : Theme.base03 - font.family: Theme.iconFont - font.pixelSize: 14 - MouseArea { - id: dismissMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notifItem.modelData.dismiss() - } + SText { + visible: bar.notifServer.trackedNotifications.values.length === 0 + text: "No notifications" + color: Theme.base03 + font.pixelSize: 11 + anchors.horizontalCenter: parent.horizontalCenter + } + + Repeater { + model: bar.notifServer.trackedNotifications + + Rectangle { + id: notifItem + required property var modelData + width: parent.width + height: ncBody.height + 12 + radius: Theme.radiusSmall + color: Theme.base02 + + NotifContent { + id: ncBody + notif: notifItem.modelData + anchors.left: parent.left + anchors.right: dismissBtn.left + anchors.top: parent.top + anchors.margins: 6 + } + + SIcon { + id: dismissBtn + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: 6 + text: "close" + color: dismissMa.containsMouse ? Theme.base05 : Theme.base03 + font.pixelSize: 14 + MouseArea { + id: dismissMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: notifItem.modelData.dismiss() } } } @@ -2726,7 +2589,7 @@ in readonly property bool isPrimary: bar.screen === Quickshell.screens[0] x: Math.round(bar.width / 2 - width / 2) - y: 30 + y: Theme.barHeight width: _toastRect.width + 16 height: _toastRect.height + 4 @@ -2803,81 +2666,32 @@ in clip: true Behavior on height { - NumberAnimation { duration: 280; easing.type: Easing.OutExpo } + NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo } } - Column { + NotifContent { id: toastCol + notif: toastItem.currentNotif anchors.left: parent.left anchors.right: toastDismiss.left anchors.top: parent.top anchors.margins: 8 - spacing: 2 - - Text { - width: parent.width - text: toastItem.currentNotif ? (toastItem.currentNotif.summary || toastItem.currentNotif.appName) : "" - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 12 - font.weight: Font.Medium - elide: Text.ElideRight - } - - Text { - width: parent.width - text: toastItem.currentNotif ? (toastItem.currentNotif.body || "") : "" - color: Theme.base04 - font.family: Theme.fontFamily - font.pixelSize: 11 - elide: Text.ElideRight - maximumLineCount: 3 - wrapMode: Text.Wrap - visible: text !== "" - } - - Row { - spacing: 4 - visible: toastItem.currentNotif && toastItem.currentNotif.actions.length > 0 - Repeater { - model: toastItem.currentNotif ? toastItem.currentNotif.actions : [] - Rectangle { - required property var modelData - width: toastActionText.width + 12 - height: toastActionText.height + 6 - radius: 4 - color: toastActionMa.containsMouse ? Theme.base02 : Theme.base01 - Behavior on color { ColorAnimation { duration: 120 } } - border.width: 1 - border.color: Theme.base02 - Text { - id: toastActionText - anchors.centerIn: parent - text: modelData.text - color: Theme.base05 - font.family: Theme.fontFamily - font.pixelSize: 10 - } - MouseArea { - id: toastActionMa - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { modelData.invoke(); toastItem.dismiss(); } - } - } - } - } + summarySize: 12 + bodySize: 11 + bodyLines: 3 + chipBg: Theme.base01 + chipBgHover: Theme.base02 + chipBorder: Theme.base02 + onActionInvoked: toastItem.dismiss() } - Text { + SIcon { id: toastDismiss anchors.right: parent.right anchors.top: parent.top anchors.margins: 8 text: "close" color: toastDismissMa.containsMouse ? Theme.base05 : Theme.base03 - font.family: Theme.iconFont font.pixelSize: 15 MouseArea { id: toastDismissMa