diff --git a/.gitignore b/.gitignore index 0eb9971..3ad9b94 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -launcher/favorites.txt +launcher/launcher_state.json diff --git a/bar/modules/Notifications.qml b/bar/modules/Notifications.qml index 1496749..20a01f0 100644 --- a/bar/modules/Notifications.qml +++ b/bar/modules/Notifications.qml @@ -46,7 +46,7 @@ ModuleChip { signal dismissed(string toastId) - width: parent ? parent.width : 350 + width: parent ? parent.width : 400 height: toastCard.height property real progressVal: 1.0 @@ -65,12 +65,12 @@ ModuleChip { NumberAnimation { target: slideTranslate; property: "x" from: toastRoot.slideX; to: 0 - duration: 320; easing.type: Easing.OutCubic + duration: 380; easing.type: Easing.OutCubic } NumberAnimation { target: bubbleScale; property: "yScale" from: 0; to: 1 - duration: 280; easing.type: Easing.OutBack + duration: 320; easing.type: Easing.OutBack } } @@ -112,7 +112,7 @@ ModuleChip { Rectangle { id: toastCard width: parent.width - height: toastContent.implicitHeight + (toastRoot.urgency === NotificationUrgency.Low ? 30 : 24) + height: toastContent.implicitHeight + (toastRoot.urgency === NotificationUrgency.Low ? 31 : 25) radius: 16 color: cardHover.hovered ? toastRoot.t.notifToastBgHover : toastRoot.t.notifToastBg @@ -121,19 +121,46 @@ ModuleChip { : (cardHover.hovered ? toastRoot.t.notifBorderActive : toastRoot.t.notifToastBorder) border.width: Cfg.Config.popupBorderWidth - Behavior on color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } } - Behavior on border.color { ColorAnimation { duration: 150 } } + Behavior on color { ColorAnimation { duration: 180; easing.type: Easing.OutCubic } } + Behavior on border.color { ColorAnimation { duration: 180 } } + + // Subtle outer glow for critical notifications + Rectangle { + visible: toastRoot.urgency === NotificationUrgency.Critical + anchors.fill: parent + anchors.margins: -3 + radius: parent.radius + 3 + color: "transparent" + border.color: Qt.rgba(toastRoot.t.notifUrgencyCritical.r, toastRoot.t.notifUrgencyCritical.g, toastRoot.t.notifUrgencyCritical.b, 0.22) + border.width: 2 + z: -1 + } HoverHandler { id: cardHover } TapHandler { onTapped: toastRoot.triggerDismiss() } + // Left accent bar — wider with a soft glow backing + Rectangle { + id: accentGlow + width: 8 + height: Math.max(18, toastCard.height - 25) + radius: 4 + anchors { + left: parent.left + leftMargin: 7 + verticalCenter: parent.verticalCenter + } + color: Qt.rgba(toastRoot.urgencyColor(toastRoot.urgency).r, + toastRoot.urgencyColor(toastRoot.urgency).g, + toastRoot.urgencyColor(toastRoot.urgency).b, 0.18) + } Rectangle { width: 4 - height: Math.max(16, toastCard.height - 24) + height: Math.max(18, toastCard.height - 25) radius: 2 anchors { left: parent.left - leftMargin: 10 + leftMargin: 9 verticalCenter: parent.verticalCenter } color: toastRoot.urgencyColor(toastRoot.urgency) @@ -145,20 +172,20 @@ ModuleChip { left: parent.left right: parent.right top: parent.top - leftMargin: 24 - rightMargin: 12 - topMargin: 12 + leftMargin: 25 + rightMargin: 13 + topMargin: 13 } - spacing: 4 + spacing: 5 RowLayout { Layout.fillWidth: true - spacing: 6 + spacing: 7 Text { text: toastRoot.urgencyIcon(toastRoot.urgency) color: toastRoot.urgencyColor(toastRoot.urgency) - font.pixelSize: 12 + font.pixelSize: 11 font.bold: true Layout.alignment: Qt.AlignVCenter renderType: Text.NativeRendering @@ -166,10 +193,10 @@ ModuleChip { Text { text: toastRoot.appName color: toastRoot.t.notifToastAppName - font.pixelSize: 12 + font.pixelSize: 10 font.bold: true font.capitalization: Font.AllUppercase - font.letterSpacing: 0.4 + font.letterSpacing: 0.8 Layout.fillWidth: true elide: Text.ElideRight verticalAlignment: Text.AlignVCenter @@ -178,17 +205,18 @@ ModuleChip { Text { text: toastRoot.timeStr color: toastRoot.t.notifToastDim - font.pixelSize: 11 - font.bold: true + font.pixelSize: 10 verticalAlignment: Text.AlignVCenter renderType: Text.NativeRendering } Rectangle { Layout.alignment: Qt.AlignVCenter - width: 20; height: 20; radius: 8 - color: closeHover.hovered ? toastRoot.t.notifHistoryHover : "transparent" - Behavior on color { ColorAnimation { duration: 100 } } + width: 18; height: 18; radius: 10 + color: closeHover.hovered + ? Qt.rgba(1, 1, 1, 0.1) + : "transparent" + Behavior on color { ColorAnimation { duration: 120 } } Text { anchors.centerIn: parent @@ -218,16 +246,15 @@ ModuleChip { Text { text: toastRoot.body color: toastRoot.t.notifToastDim - font.pixelSize: 12 - font.bold: true + font.pixelSize: 11 Layout.fillWidth: true wrapMode: Text.WordWrap maximumLineCount: 3 elide: Text.ElideRight - textFormat: Text.PlainText + textFormat: Text.PlainText visible: toastRoot.body !== "" Layout.bottomMargin: toastRoot.urgency === NotificationUrgency.Low ? 6 : 0 - lineHeight: 1.2 + lineHeight: 1.35 renderType: Text.NativeRendering } } @@ -236,9 +263,9 @@ ModuleChip { visible: toastRoot.urgency === NotificationUrgency.Low && toastRoot.timeout > 0 anchors { left: parent.left; right: parent.right; bottom: parent.bottom - margins: 10; bottomMargin: 10 + margins: 13; bottomMargin: 10 } - height: 4 + height: 2 Rectangle { anchors.fill: parent @@ -250,6 +277,7 @@ ModuleChip { height: parent.height radius: 2 color: toastRoot.t.notifProgressFill + Behavior on width { NumberAnimation { duration: 80; easing.type: Easing.Linear } } } } } @@ -271,7 +299,7 @@ ModuleChip { signal dismissed(int itemIndex) - height: histItemContent.implicitHeight + 16 + height: histItemContent.implicitHeight + 18 radius: 10 clip: true color: histItemHover.hovered ? histRoot.t.notifHistoryHover : "transparent" @@ -282,25 +310,35 @@ ModuleChip { SequentialAnimation { id: dismissAnim ParallelAnimation { - NumberAnimation { target: histRoot; property: "opacity"; to: 0; duration: 150; easing.type: Easing.OutCubic } - NumberAnimation { target: histRoot; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.OutCubic } + NumberAnimation { target: histRoot; property: "opacity"; to: 0; duration: 180; easing.type: Easing.OutCubic } + NumberAnimation { target: histRoot; property: "scale"; to: 0.94; duration: 180; easing.type: Easing.OutCubic } } - NumberAnimation { target: histRoot; property: "height"; to: 0; duration: 150; easing.type: Easing.InOutQuad } + NumberAnimation { target: histRoot; property: "height"; to: 0; duration: 160; easing.type: Easing.InOutQuad } ScriptAction { script: histRoot.dismissed(histRoot.itemIndex) } } + // Glow backing behind accent bar + Rectangle { + width: 7 + height: Math.max(13, histRoot.height - 16) + radius: 3 + anchors { left: parent.left; leftMargin: 6; verticalCenter: parent.verticalCenter } + color: Qt.rgba(histRoot.urgencyColor(histRoot.urgency).r, + histRoot.urgencyColor(histRoot.urgency).g, + histRoot.urgencyColor(histRoot.urgency).b, 0.15) + } Rectangle { width: 3 - height: Math.max(10, histRoot.height - 16) - radius: 1.5 + height: Math.max(13, histRoot.height - 16) + radius: 2 anchors { left: parent.left; leftMargin: 8; verticalCenter: parent.verticalCenter } color: histRoot.urgencyColor(histRoot.urgency) } Rectangle { - anchors { right: parent.right; top: parent.top; margins: 6 } - width: 18; height: 18; radius: 8 - color: histCloseHover.hovered ? histRoot.t.notifHistoryHover : "transparent" + anchors { right: parent.right; top: parent.top; margins: 6 } + width: 18; height: 18; radius: 9 + color: histCloseHover.hovered ? Qt.rgba(1, 1, 1, 0.1) : "transparent" opacity: histItemHover.hovered ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 150 } } Behavior on color { ColorAnimation { duration: 150 } } @@ -311,40 +349,39 @@ ModuleChip { } Text { - anchors { right: parent.right; rightMargin: 6; top: parent.top; topMargin: 8 } + anchors { right: parent.right; rightMargin: 8; top: parent.top; topMargin: 8 } text: histRoot.timeStr color: histRoot.t.notifToastDim font.pixelSize: 10 - font.bold: true verticalAlignment: Text.AlignVCenter renderType: Text.NativeRendering - opacity: histItemHover.hovered ? 0 : 1 + opacity: histItemHover.hovered ? 0 : 0.8 Behavior on opacity { NumberAnimation { duration: 150 } } } ColumnLayout { id: histItemContent - anchors { left: parent.left; right: parent.right; top: parent.top; leftMargin: 20; rightMargin: 40; topMargin: 8 } + anchors { left: parent.left; right: parent.right; top: parent.top; leftMargin: 21; rightMargin: 39; topMargin: 8 } spacing: 2 RowLayout { Layout.fillWidth: true; spacing: 5 Text { text: histRoot.urgencyIcon(histRoot.urgency); color: histRoot.urgencyColor(histRoot.urgency); font.pixelSize: 10; font.bold: true; Layout.alignment: Qt.AlignVCenter; renderType: Text.NativeRendering } Text { - text: histRoot.appName; color: histRoot.t.notifToastAppName; font.pixelSize: 11; font.bold: true - font.capitalization: Font.AllUppercase; font.letterSpacing: 0.4; Layout.fillWidth: true; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter + text: histRoot.appName; color: histRoot.t.notifToastAppName; font.pixelSize: 10; font.bold: true + font.capitalization: Font.AllUppercase; font.letterSpacing: 0.6; Layout.fillWidth: true; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter renderType: Text.NativeRendering } } Text { - text: histRoot.summary; color: histRoot.t.notifToastText; font.pixelSize: 12; font.bold: true + text: histRoot.summary; color: histRoot.t.notifToastText; font.pixelSize: 13; font.bold: true Layout.fillWidth: true; elide: Text.ElideRight; textFormat: Text.PlainText; visible: histRoot.summary !== "" renderType: Text.NativeRendering } Text { - text: histRoot.body; color: histRoot.t.notifToastDim; font.pixelSize: 11; font.bold: true; Layout.fillWidth: true - wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight; textFormat: Text.PlainText; visible: histRoot.body !== ""; lineHeight: 1.2 + text: histRoot.body; color: histRoot.t.notifToastDim; font.pixelSize: 10; Layout.fillWidth: true + wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight; textFormat: Text.PlainText; visible: histRoot.body !== ""; lineHeight: 1.3 renderType: Text.NativeRendering } } @@ -395,9 +432,9 @@ ModuleChip { visible: root.unreadCount > 0 anchors.top: parent.top anchors.right: parent.right - width: Math.max(14, badgeLabel.implicitWidth + 6) - height: 14 - radius: 7 + width: Math.max(16, badgeLabel.implicitWidth + 8) + height: 16 + radius: 8 color: t.notifBadgeBg Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } @@ -495,7 +532,7 @@ ModuleChip { readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright" anchors { top: true; bottom: true; right: atRight; left: !atRight } - implicitWidth: 350 + root.barMargin + toastPad + implicitWidth: 344 + root.barMargin + toastPad WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "main-shell-notifications-toasts" @@ -512,8 +549,8 @@ ModuleChip { id: toastCol x: toastLayer.atRight ? root.barMargin : toastLayer.toastPad y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height - spacing: 8 - width: 350 + spacing: 10 + width: 332 Repeater { model: activeToasts @@ -566,7 +603,7 @@ ModuleChip { Item { id: histClipContainer - readonly property int cardW: 340 + readonly property int cardW: 352 readonly property int screenPad: root.barMargin + 10 x: { @@ -577,9 +614,9 @@ ModuleChip { anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom - anchors.topMargin: root.isTop ? (root.barHeight + root.barEdgePad + 10) : 0 - anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.barEdgePad + 10) - height: parent.height - root.barHeight - root.barEdgePad - 10 + anchors.topMargin: root.isTop ? (root.barHeight + root.barEdgePad + 12) : 0 + anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.barEdgePad + 12) + height: parent.height - root.barHeight - root.barEdgePad - 12 clip: true @@ -588,7 +625,7 @@ ModuleChip { popupOpen: historyPopup.popupOpen isTop: root.isTop animEnabled: historyPopup.visible - height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? histCol.implicitHeight : 90), 520) + height: Math.min(histHeader.implicitHeight + 58 + (notifHistory.count > 0 ? histCol.implicitHeight : 94), 480) color: t.notifHistoryBg radius: Cfg.Config.popupRadius @@ -598,25 +635,27 @@ ModuleChip { RowLayout { id: histHeader anchors { left: parent.left; right: parent.right; top: parent.top } - anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20 + anchors.topMargin: 16; anchors.leftMargin: 16; anchors.rightMargin: 16 spacing: 10 Text { text: "󰂚"; color: t.notifToastAppName; font.pixelSize: 18; font.bold: true; Layout.alignment: Qt.AlignVCenter; renderType: Text.NativeRendering } Text { - text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true + text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 14; font.bold: true Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter; renderType: Text.NativeRendering } Rectangle { Layout.alignment: Qt.AlignVCenter - width: dndRow.implicitWidth + 16 - height: 24 - radius: 8 + width: dndRow.implicitWidth + 19 + height: 25 + radius: 13 color: root.doNotDisturb ? Qt.rgba(t.notifDnd.r, t.notifDnd.g, t.notifDnd.b, 0.18) : (dndHover.hovered ? t.notifHistoryHover : "transparent") - border.color: root.doNotDisturb ? t.notifDnd : "transparent" - border.width: Cfg.Config.popupBorderWidth + border.color: root.doNotDisturb + ? Qt.rgba(t.notifDnd.r, t.notifDnd.g, t.notifDnd.b, 0.55) + : Qt.rgba(1, 1, 1, 0.08) + border.width: 1 Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } @@ -627,7 +666,7 @@ ModuleChip { Text { text: "󰂛" color: root.doNotDisturb ? t.notifDnd : t.notifToastDim - font.pixelSize: 12 + font.pixelSize: 11 font.bold: true anchors.verticalCenter: parent.verticalCenter Behavior on color { ColorAnimation { duration: 150 } } @@ -636,7 +675,7 @@ ModuleChip { Text { text: root.doNotDisturb ? "On" : "Off" color: root.doNotDisturb ? t.notifDnd : t.notifToastDim - font.pixelSize: 11 + font.pixelSize: 10 font.bold: true anchors.verticalCenter: parent.verticalCenter Behavior on color { ColorAnimation { duration: 150 } } @@ -651,13 +690,15 @@ ModuleChip { Rectangle { visible: notifHistory.count > 0 Layout.alignment: Qt.AlignVCenter - width: clearText.implicitWidth + 16 - height: 24 - radius: 8 + width: clearText.implicitWidth + 19 + height: 25 + radius: 13 color: clearHover.hovered ? t.notifHistoryHover : "transparent" + border.color: Qt.rgba(1, 1, 1, 0.08) + border.width: 1 Behavior on color { ColorAnimation { duration: 150 } } - Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11; font.bold: true; renderType: Text.NativeRendering } + Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 10; font.bold: true; renderType: Text.NativeRendering } HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: clearAllAnim.start() } } @@ -666,7 +707,7 @@ ModuleChip { Rectangle { id: histDivider anchors { left: parent.left; right: parent.right; top: histHeader.bottom } - anchors.topMargin: 12; height: 1; color: t.notifSeparator + anchors.topMargin: 10; height: 1; color: t.notifSeparator; opacity: 0.6 } SequentialAnimation { @@ -674,11 +715,11 @@ ModuleChip { ParallelAnimation { NumberAnimation { target: histCol; property: "opacity" - to: 0; duration: 220; easing.type: Easing.OutCubic + to: 0; duration: 240; easing.type: Easing.OutCubic } NumberAnimation { target: clearShift; property: "y" - to: -16; duration: 220; easing.type: Easing.OutCubic + to: -20; duration: 240; easing.type: Easing.OutCubic } } ScriptAction { @@ -693,7 +734,7 @@ ModuleChip { Flickable { id: histFlick anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom } - anchors.leftMargin: 2; anchors.rightMargin: 8 + anchors.leftMargin: 4; anchors.rightMargin: 6 anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true contentHeight: histCol.implicitHeight flickableDirection: Flickable.VerticalFlick @@ -703,14 +744,14 @@ ModuleChip { id: histCol opacity: 1 transform: Translate { id: clearShift; y: 0 } - width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 2; rightPadding: 8 + width: histFlick.width; spacing: 2; topPadding: 2; bottomPadding: 2; leftPadding: 4; rightPadding: 6 Item { - width: histCol.width - 16; height: 90; visible: notifHistory.count === 0 + width: histCol.width - 16; height: 94; visible: notifHistory.count === 0 ColumnLayout { anchors.centerIn: parent; spacing: 8 - Text { text: "󰂚"; color: t.notifHistoryEmpty; font.pixelSize: 30; font.bold: true; Layout.alignment: Qt.AlignHCenter; renderType: Text.NativeRendering } - Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; font.bold: true; Layout.alignment: Qt.AlignHCenter; renderType: Text.NativeRendering } + Text { text: "󰂚"; color: t.notifHistoryEmpty; font.pixelSize: 29; font.bold: true; Layout.alignment: Qt.AlignHCenter; renderType: Text.NativeRendering; opacity: 0.5 } + Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 13; font.bold: true; Layout.alignment: Qt.AlignHCenter; renderType: Text.NativeRendering; opacity: 0.5 } } } diff --git a/config/themes/aero.qml b/config/themes/aero.qml index 601e929..628377e 100644 --- a/config/themes/aero.qml +++ b/config/themes/aero.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#4481c7c7" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22cbdc3d" readonly property color launcherSelected: "#44cbdc3d" diff --git a/config/themes/aurora.qml b/config/themes/aurora.qml index cd4681a..c1ce61e 100644 --- a/config/themes/aurora.qml +++ b/config/themes/aurora.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#441e3a5f" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#2200d9ff" readonly property color launcherSelected: "#4400d9ff" diff --git a/config/themes/autumn.qml b/config/themes/autumn.qml index be4a9e8..3dca43d 100644 --- a/config/themes/autumn.qml +++ b/config/themes/autumn.qml @@ -215,6 +215,8 @@ QtObject { readonly property color launcherPill: "#444a5d6e" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#33f2b425" readonly property color launcherSelected: "#44c19375" diff --git a/config/themes/beach.qml b/config/themes/beach.qml index 5cbb3ca..1e88a69 100644 --- a/config/themes/beach.qml +++ b/config/themes/beach.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#444a5d6e" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22ebc6a0" readonly property color launcherSelected: "#44ebc6a0" diff --git a/config/themes/boat.qml b/config/themes/boat.qml index a93e21e..2504d76 100644 --- a/config/themes/boat.qml +++ b/config/themes/boat.qml @@ -215,6 +215,8 @@ QtObject { readonly property color launcherPill: "#44614d4a" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22e0533c" readonly property color launcherSelected: "#44e0533c" diff --git a/config/themes/colorful.qml b/config/themes/colorful.qml index 8fb5f19..f03a832 100644 --- a/config/themes/colorful.qml +++ b/config/themes/colorful.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#444a566e" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22f0a1ba" readonly property color launcherSelected: "#44f0a1ba" diff --git a/config/themes/cosmic.qml b/config/themes/cosmic.qml index 7d8cb41..9fb322a 100644 --- a/config/themes/cosmic.qml +++ b/config/themes/cosmic.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#443d4a6d" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22bdf0ff" readonly property color launcherSelected: "#44bdf0ff" diff --git a/config/themes/everforest.qml b/config/themes/everforest.qml index a08d135..8ffe178 100644 --- a/config/themes/everforest.qml +++ b/config/themes/everforest.qml @@ -218,6 +218,8 @@ QtObject { readonly property color launcherPill: "#44284038" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#224a8c5c" readonly property color launcherSelected: "#444a8c5c" diff --git a/config/themes/goldencity.qml b/config/themes/goldencity.qml index 131bad5..b778f02 100644 --- a/config/themes/goldencity.qml +++ b/config/themes/goldencity.qml @@ -216,6 +216,8 @@ QtObject { readonly property color launcherPill: "#444a5d6e" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22c19375" readonly property color launcherSelected: "#44c19375" diff --git a/config/themes/hummingbird.qml b/config/themes/hummingbird.qml index fd64dc8..ce29116 100644 --- a/config/themes/hummingbird.qml +++ b/config/themes/hummingbird.qml @@ -215,6 +215,8 @@ QtObject { readonly property color launcherPill: "#442e7d32" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22ff5722" readonly property color launcherSelected: "#44ff5722" diff --git a/config/themes/lowlight.qml b/config/themes/lowlight.qml index 8dc83f5..9d9704a 100644 --- a/config/themes/lowlight.qml +++ b/config/themes/lowlight.qml @@ -218,6 +218,8 @@ QtObject { readonly property color launcherPill: "#44203060" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22b068d8" readonly property color launcherSelected: "#44b068d8" diff --git a/config/themes/metropolitan.qml b/config/themes/metropolitan.qml index b966ebe..bd1aa29 100644 --- a/config/themes/metropolitan.qml +++ b/config/themes/metropolitan.qml @@ -208,6 +208,8 @@ QtObject { readonly property color launcherPill: "#44565f89" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22ffb86c" readonly property color launcherSelected: "#44ffb86c" diff --git a/config/themes/neonica.qml b/config/themes/neonica.qml index 6b24ccd..4f95e5a 100644 --- a/config/themes/neonica.qml +++ b/config/themes/neonica.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#4431748f" readonly property color launcherText: "#ffe0def4" readonly property color launcherDim: "#ff6e6a86" + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: "#ff9ccfd8" readonly property color launcherHover: "#4431748f" readonly property color launcherSelected: "#6631748f" diff --git a/config/themes/pinkie.qml b/config/themes/pinkie.qml index a848846..d1a4eaf 100644 --- a/config/themes/pinkie.qml +++ b/config/themes/pinkie.qml @@ -218,6 +218,8 @@ QtObject { readonly property color launcherPill: "#444a4068" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#224ec0b8" readonly property color launcherSelected: "#444ec0b8" diff --git a/config/themes/snowy.qml b/config/themes/snowy.qml index 802e2c8..47f4fa2 100644 --- a/config/themes/snowy.qml +++ b/config/themes/snowy.qml @@ -215,6 +215,8 @@ QtObject { readonly property color launcherPill: "#444a5270" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22c4889e" readonly property color launcherSelected: "#44c4889e" diff --git a/config/themes/supreme.qml b/config/themes/supreme.qml index dbf981b..d1a0031 100644 --- a/config/themes/supreme.qml +++ b/config/themes/supreme.qml @@ -207,6 +207,8 @@ QtObject { readonly property color launcherPill: "#44ffb7ce" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22ff7eb9" readonly property color launcherSelected: "#44ff7eb9" diff --git a/config/themes/tranquil.qml b/config/themes/tranquil.qml index d621608..c8af77c 100644 --- a/config/themes/tranquil.qml +++ b/config/themes/tranquil.qml @@ -219,6 +219,8 @@ QtObject { readonly property color launcherPill: "#444a3d6e" readonly property color launcherText: textMain readonly property color launcherDim: textDim + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#22b09de8" readonly property color launcherSelected: "#44b09de8" diff --git a/config/themes/tropicalnight.qml b/config/themes/tropicalnight.qml index c5ed47f..66b969b 100644 --- a/config/themes/tropicalnight.qml +++ b/config/themes/tropicalnight.qml @@ -216,6 +216,8 @@ QtObject { readonly property color launcherPill: "#444a5d6e" readonly property color launcherText: textMain readonly property color launcherDim: "#ff92979b" + readonly property color launcherStar: launcherDim + readonly property color launcherStarHover: accent readonly property color launcherAccent: accent readonly property color launcherHover: "#2289b4fa" readonly property color launcherSelected: "#4489b4fa" diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml index a5cc7ff..e701877 100644 --- a/launcher/Launcher.qml +++ b/launcher/Launcher.qml @@ -34,7 +34,8 @@ PanelWindow { property bool loading: false property int lastCacheUpdate: 0 property var favoriteIds: [] - readonly property string favoritesPath: Quickshell.env("HOME") + "/.config/quickshell/launcher/favorites.txt" + property var accessCounts: ({}) + readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json" readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1 @@ -48,15 +49,31 @@ PanelWindow { filtered = allItems.filter(i => i.name.toLowerCase().startsWith(q)) } const favs = filtered.filter(i => root.favoriteIds.includes(i.id)) - const rest = filtered.filter(i => !root.favoriteIds.includes(i.id)) + let rest = filtered.filter(i => !root.favoriteIds.includes(i.id)) + rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0)) const result = [] - if (favs.length > 0) - result.push(...favs.map(i => Object.assign({}, i, { section: "Favorites" }))) - if (rest.length > 0) - result.push(...rest.map(i => Object.assign({}, i, { section: "All Applications" }))) + if (favs.length > 0) { + result.push({ isHeader: true, section: "Favorites" }) + for (let i = 0; i < favs.length; i++) + result.push(Object.assign({}, favs[i], { isHeader: false })) + } + if (rest.length > 0) { + result.push({ isHeader: true, section: "All Applications" }) + for (let i = 0; i < rest.length; i++) + result.push(Object.assign({}, rest[i], { isHeader: false })) + } return result } + function skipHeader(idx, dir) { + while (idx >= 0 && idx < root.filteredItems.length) { + const item = root.filteredItems[idx] + if (item && !item.isHeader) return idx + idx += dir + } + return -1 + } + function toggle() { if (!_isOpen) { var fm = Hyprland.focusedMonitor @@ -73,8 +90,9 @@ PanelWindow { _isOpen = true searchInput.text = "" resultsList.currentIndex = root.defaultIndex + Qt.callLater(() => resultsList.positionViewAtBeginning()) loadItems() - loadFavorites() + loadState() Qt.callLater(() => searchInput.forceActiveFocus()) } @@ -147,20 +165,16 @@ PanelWindow { return args } - function loadFavorites() { - favoritesReadProc.command = ["sh", "-c", "cat " + shellQuote(favoritesPath) + " 2>/dev/null || true"] - favoritesReadProc.running = true + function loadState() { + stateReadProc.command = ["sh", "-c", "cat " + shellQuote(statePath) + " 2>/dev/null || true"] + stateReadProc.running = true } - function saveFavorites() { - if (favoriteIds.length === 0) { - favoritesWriteProc.command = ["sh", "-c", ": > " + shellQuote(favoritesPath)] - } else { - const quoted = favoriteIds.map(id => shellQuote(id)).join(" ") - favoritesWriteProc.command = ["sh", "-c", - "printf '%s\\n' " + quoted + " > " + shellQuote(favoritesPath)] - } - favoritesWriteProc.running = true + function saveState() { + const data = JSON.stringify({ favorites: favoriteIds, access: accessCounts }) + stateWriteProc.command = ["sh", "-c", + "printf '%s' " + shellQuote(data) + " > " + shellQuote(statePath)] + stateWriteProc.running = true } function toggleFavorite(itemId) { @@ -172,7 +186,14 @@ PanelWindow { arr.push(itemId) } favoriteIds = arr - saveFavorites() + saveState() + } + + function incrementAccess(itemId) { + const counts = Object.assign({}, accessCounts) + counts[itemId] = (counts[itemId] || 0) + 1 + accessCounts = counts + saveState() } function launch(item) { @@ -188,6 +209,7 @@ PanelWindow { launchProc.running = true console.log("Launching: " + cleanExec) + incrementAccess(item.id) close() } @@ -238,20 +260,28 @@ PanelWindow { Process { id: launchProc } Process { - id: favoritesReadProc + id: stateReadProc property string rawData: "" - stdout: SplitParser { onRead: data => favoritesReadProc.rawData += data + "\n" } + stdout: SplitParser { onRead: data => stateReadProc.rawData += data + "\n" } onRunningChanged: { if (!running) { if (rawData.trim()) { - root.favoriteIds = rawData.trim().split("\n").filter(l => l.length > 0) + try { + const state = JSON.parse(rawData.trim()) + if (Array.isArray(state.favorites)) + root.favoriteIds = state.favorites + if (state.access && typeof state.access === "object") + root.accessCounts = state.access + } catch (e) { + console.log("Failed to parse launcher state: " + e) + } } rawData = "" } } } - Process { id: favoritesWriteProc } + Process { id: stateWriteProc } MouseArea { @@ -282,7 +312,7 @@ PanelWindow { height: targetHeight Behavior on height { enabled: root._isOpen - NumberAnimation { duration: 154; easing.type: Easing.OutCubic } + NumberAnimation { duration: 80; easing.type: Easing.OutCubic } } radius: Cfg.Config.popupRadius @@ -328,29 +358,33 @@ PanelWindow { onTextChanged: resultsList.currentIndex = root.defaultIndex Keys.onReturnPressed: { - if (root.filteredItems.length > 0) { - root.launch(root.filteredItems[Math.max(0, resultsList.currentIndex)]) - } + const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1) + if (idx >= 0) + root.launch(root.filteredItems[idx]) } Keys.onDownPressed: { root._keyboardNav = true - if (resultsList.currentIndex < 0) { - resultsList.currentIndex = 0; - } else { - resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1) + var next = resultsList.currentIndex < 0 ? 0 : resultsList.currentIndex + 1 + if (next >= resultsList.count) next = resultsList.count - 1 + next = root.skipHeader(next, 1) + if (next < 0) next = root.skipHeader(0, 1) + if (next >= 0) { + resultsList.currentIndex = next + resultsList.positionViewAtIndex(next, ListView.Contain) } - resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain) } Keys.onUpPressed: { root._keyboardNav = true - if (resultsList.currentIndex < 0) { - resultsList.currentIndex = resultsList.count - 1; - } else { - resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0) + var next = resultsList.currentIndex < 0 ? resultsList.count - 1 : resultsList.currentIndex - 1 + if (next < 0) next = 0 + next = root.skipHeader(next, -1) + if (next < 0) next = root.skipHeader(resultsList.count - 1, -1) + if (next >= 0) { + resultsList.currentIndex = next + resultsList.positionViewAtIndex(next, ListView.Contain) } - resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain) } } } @@ -361,49 +395,77 @@ PanelWindow { Layout.fillWidth: true Layout.preferredHeight: listHeight - property int listHeight: Math.min(contentHeight, 400) - Behavior on listHeight { - NumberAnimation { duration: 154; easing.type: Easing.OutCubic } + // Avoid reacting to contentHeight changes from lazy delegate + // instantiation during scroll — only animate on model count changes. + property int listHeight: 0 + + NumberAnimation { + id: listHeightAnim + target: resultsList + property: "listHeight" + duration: 80 + easing.type: Easing.OutCubic } + function _syncListHeight() { + var h = Math.min(contentHeight, 400) + if (h === listHeight) return + listHeightAnim.stop() + listHeightAnim.from = listHeight + listHeightAnim.to = h + listHeightAnim.start() + } + + onCountChanged: Qt.callLater(_syncListHeight) + Component.onCompleted: Qt.callLater(_syncListHeight) + clip: true model: root.filteredItems currentIndex: root.defaultIndex spacing: 2 + snapMode: ListView.NoSnap + highlightRangeMode: ListView.NoHighlightRange + cacheBuffer: 400 + reuseItems: true + pixelAligned: false + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick - section.property: "section" - section.criteria: ViewSection.FullString - section.delegate: Rectangle { - width: resultsList.width - height: 28 - color: "transparent" - Text { - anchors { left: parent.left; leftMargin: 12; verticalCenter: parent.verticalCenter } - text: section - color: t.launcherDim - font.pixelSize: 11 - font.weight: Font.Bold + onModelChanged: { + currentIndex = root.defaultIndex + for (let i = 0; i < count; i++) { + const md = root.filteredItems[i] + if (md && !md.isHeader) { + currentIndex = i + break + } } + Qt.callLater(() => positionViewAtBeginning()) } - - onModelChanged: currentIndex = root.defaultIndex ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - add: Transition { - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100; easing.type: Easing.InQuad } - } - delegate: Rectangle { width: resultsList.width height: 38 - radius: 8 - property bool hovered: false - - color: hovered ? t.launcherHover + radius: modelData.isHeader ? 0 : 8 + color: modelData.isHeader ? "transparent" + : (starHovered || hovered) ? t.launcherHover : (resultsList.currentIndex === index && index !== -1 ? t.launcherSelected : "transparent") + property bool hovered: false + property bool starHovered: false + + Text { + visible: modelData.isHeader + anchors { left: parent.left; leftMargin: 12; verticalCenter: parent.verticalCenter } + text: modelData.section || "" + color: t.launcherDim + font.pixelSize: 13 + font.weight: Font.Bold + } RowLayout { + visible: !modelData.isHeader anchors.fill: parent anchors.leftMargin: 12 anchors.rightMargin: 12 @@ -417,10 +479,10 @@ PanelWindow { id: appIcon anchors.fill: parent source: modelData.icon ? "image://icon/" + modelData.icon : "" - sourceSize: Qt.size(64, 64) + sourceSize: Qt.size(64, 64) fillMode: Image.PreserveAspectFit smooth: true - mipmap: Cfg.Config.launcherMipmapIcons + mipmap: Cfg.Config.launcherMipmapIcons visible: status === Image.Ready } @@ -442,14 +504,22 @@ PanelWindow { } Item { - width: 22; height: 22 + width: 26; height: 26 Layout.alignment: Qt.AlignVCenter Text { + id: starText anchors.centerIn: parent text: root.favoriteIds.includes(modelData.id) ? "★" : "☆" - color: root.favoriteIds.includes(modelData.id) ? t.launcherAccent : t.launcherDim - font.pixelSize: 14 + font.pixelSize: 18 + + readonly property bool isFav: root.favoriteIds.includes(modelData.id) + color: starHovered ? t.launcherStarHover + : (isFav ? t.launcherAccent : t.launcherStar) + + Behavior on color { + ColorAnimation { duration: 180; easing.type: Easing.OutCubic } + } } } } @@ -458,8 +528,12 @@ PanelWindow { anchors.fill: parent hoverEnabled: true onEntered: parent.hovered = true - onExited: parent.hovered = false + onExited: { parent.hovered = false; parent.starHovered = false } + onPositionChanged: mouse => { + parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46) + } onClicked: mouse => { + if (modelData.isHeader) return if (mouse.x >= width - 46) { root.toggleFavorite(modelData.id) } else {