From d2023500a9e6e724b58d83ba6743677d5b627423 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 20:28:15 -0500 Subject: [PATCH 1/9] SmartPanelWindow: add a small delay in an attempt to improve cleanup --- Modules/MainScreen/SmartPanelWindow.qml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Modules/MainScreen/SmartPanelWindow.qml b/Modules/MainScreen/SmartPanelWindow.qml index de47ff29..b883e5c4 100644 --- a/Modules/MainScreen/SmartPanelWindow.qml +++ b/Modules/MainScreen/SmartPanelWindow.qml @@ -47,6 +47,9 @@ PanelWindow { property bool closeWatchdogActive: false property bool openWatchdogActive: false + // Delay before unloading content (helps with shader cleanup on older Qt versions) + property int contentUnloadDelay: 100 + // Signals signal panelOpened signal panelClosed @@ -176,13 +179,21 @@ PanelWindow { closeWatchdogTimer.stop(); root.isPanelVisible = false; + + // Delay unloading content to ensure shaders are properly cleaned up (helps with older Qt versions) + contentUnloadDelayTimer.start(); + + Logger.d("SmartPanelWindow", "Panel close finalized, delaying content unload", placeholder.panelName); + } + + function completeContentUnload() { root.isPanelOpen = false; root.isClosing = false; root.opacityFadeComplete = false; PanelService.closedPanel(root); panelClosed(); - Logger.d("SmartPanelWindow", "Panel close finalized", placeholder.panelName); + Logger.d("SmartPanelWindow", "Content unloaded", placeholder.panelName); } // Fullscreen container for click-to-close and content @@ -433,6 +444,16 @@ PanelWindow { } } + // Timer to delay content unloading after panel closes (helps with shader cleanup on older Qt) + Timer { + id: contentUnloadDelayTimer + interval: root.contentUnloadDelay + repeat: false + onTriggered: { + root.completeContentUnload(); + } + } + // Watch for placeholder size animation completion to finalize close Connections { target: placeholder.panelItem From b344e41828acc50b7e4779c124c5b922e57067b8 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 20:32:07 -0500 Subject: [PATCH 2/9] Revert "SmartPanelWindow: add a small delay in an attempt to improve cleanup" This reverts commit d2023500a9e6e724b58d83ba6743677d5b627423. --- Modules/MainScreen/SmartPanelWindow.qml | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Modules/MainScreen/SmartPanelWindow.qml b/Modules/MainScreen/SmartPanelWindow.qml index b883e5c4..de47ff29 100644 --- a/Modules/MainScreen/SmartPanelWindow.qml +++ b/Modules/MainScreen/SmartPanelWindow.qml @@ -47,9 +47,6 @@ PanelWindow { property bool closeWatchdogActive: false property bool openWatchdogActive: false - // Delay before unloading content (helps with shader cleanup on older Qt versions) - property int contentUnloadDelay: 100 - // Signals signal panelOpened signal panelClosed @@ -179,21 +176,13 @@ PanelWindow { closeWatchdogTimer.stop(); root.isPanelVisible = false; - - // Delay unloading content to ensure shaders are properly cleaned up (helps with older Qt versions) - contentUnloadDelayTimer.start(); - - Logger.d("SmartPanelWindow", "Panel close finalized, delaying content unload", placeholder.panelName); - } - - function completeContentUnload() { root.isPanelOpen = false; root.isClosing = false; root.opacityFadeComplete = false; PanelService.closedPanel(root); panelClosed(); - Logger.d("SmartPanelWindow", "Content unloaded", placeholder.panelName); + Logger.d("SmartPanelWindow", "Panel close finalized", placeholder.panelName); } // Fullscreen container for click-to-close and content @@ -444,16 +433,6 @@ PanelWindow { } } - // Timer to delay content unloading after panel closes (helps with shader cleanup on older Qt) - Timer { - id: contentUnloadDelayTimer - interval: root.contentUnloadDelay - repeat: false - onTriggered: { - root.completeContentUnload(); - } - } - // Watch for placeholder size animation completion to finalize close Connections { target: placeholder.panelItem From bee24143333395ac60d8af510fe2a84fa5f69459 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 20:38:29 -0500 Subject: [PATCH 3/9] Another attempt --- Widgets/NImageRounded.qml | 84 +++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index f93ea5a3..6ddd2d9a 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -19,30 +19,74 @@ Item { signal statusChanged(int status) - ClippingRectangle { + // Use ClippingRectangle when visible, but switch to simple Rectangle when fading out + // This prevents Qt 6.8 crashes with shaders in GridView delegates during close animations + Loader { + id: contentLoader anchors.fill: parent - color: Color.transparent - radius: root.radius - border.color: root.borderColor - border.width: root.borderWidth + sourceComponent: root.opacity > 0.05 ? clippedContent : simpleContent + } - Image { - anchors.fill: parent - visible: !showFallback - source: imagePath - mipmap: true - smooth: true - asynchronous: true - antialiasing: true - fillMode: root.imageFillMode - onStatusChanged: root.statusChanged(status) + // Normal rendering with ClippingRectangle (uses shaders) + Component { + id: clippedContent + + ClippingRectangle { + color: Color.transparent + radius: root.radius + border.color: root.borderColor + border.width: root.borderWidth + + Image { + anchors.fill: parent + visible: !root.showFallback + source: root.imagePath + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: root.imageFillMode + onStatusChanged: root.statusChanged(status) + } + + NIcon { + anchors.centerIn: parent + visible: root.showFallback + icon: root.fallbackIcon + pointSize: root.fallbackIconSize + } } + } - NIcon { - anchors.centerIn: parent - visible: showFallback - icon: fallbackIcon - pointSize: fallbackIconSize + // Fallback rendering without shaders (when fading out) + Component { + id: simpleContent + + Rectangle { + color: Color.transparent + radius: root.radius + border.color: root.borderColor + border.width: root.borderWidth + clip: true + + Image { + anchors.fill: parent + visible: !root.showFallback + source: root.imagePath + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: root.imageFillMode + onStatusChanged: root.statusChanged(status) + } + + NIcon { + anchors.centerIn: parent + visible: root.showFallback + icon: root.fallbackIcon + pointSize: root.fallbackIconSize + } } } } From 1c1232dc5bb5cf51f82fb81a4d77a94a8ae7f133 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 20:40:52 -0500 Subject: [PATCH 4/9] Revert "Another attempt" This reverts commit bee24143333395ac60d8af510fe2a84fa5f69459. --- Widgets/NImageRounded.qml | 84 ++++++++++----------------------------- 1 file changed, 20 insertions(+), 64 deletions(-) diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 6ddd2d9a..f93ea5a3 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -19,74 +19,30 @@ Item { signal statusChanged(int status) - // Use ClippingRectangle when visible, but switch to simple Rectangle when fading out - // This prevents Qt 6.8 crashes with shaders in GridView delegates during close animations - Loader { - id: contentLoader + ClippingRectangle { anchors.fill: parent - sourceComponent: root.opacity > 0.05 ? clippedContent : simpleContent - } + color: Color.transparent + radius: root.radius + border.color: root.borderColor + border.width: root.borderWidth - // Normal rendering with ClippingRectangle (uses shaders) - Component { - id: clippedContent - - ClippingRectangle { - color: Color.transparent - radius: root.radius - border.color: root.borderColor - border.width: root.borderWidth - - Image { - anchors.fill: parent - visible: !root.showFallback - source: root.imagePath - mipmap: true - smooth: true - asynchronous: true - antialiasing: true - fillMode: root.imageFillMode - onStatusChanged: root.statusChanged(status) - } - - NIcon { - anchors.centerIn: parent - visible: root.showFallback - icon: root.fallbackIcon - pointSize: root.fallbackIconSize - } + Image { + anchors.fill: parent + visible: !showFallback + source: imagePath + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: root.imageFillMode + onStatusChanged: root.statusChanged(status) } - } - // Fallback rendering without shaders (when fading out) - Component { - id: simpleContent - - Rectangle { - color: Color.transparent - radius: root.radius - border.color: root.borderColor - border.width: root.borderWidth - clip: true - - Image { - anchors.fill: parent - visible: !root.showFallback - source: root.imagePath - mipmap: true - smooth: true - asynchronous: true - antialiasing: true - fillMode: root.imageFillMode - onStatusChanged: root.statusChanged(status) - } - - NIcon { - anchors.centerIn: parent - visible: root.showFallback - icon: root.fallbackIcon - pointSize: root.fallbackIconSize - } + NIcon { + anchors.centerIn: parent + visible: showFallback + icon: fallbackIcon + pointSize: fallbackIconSize } } } From 04f5a0cbf81ec6154090c3719e007762a136e749 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 20:50:06 -0500 Subject: [PATCH 5/9] test --- Modules/Panels/Settings/Tabs/AboutTab.qml | 208 ++++++++++++++-------- 1 file changed, 137 insertions(+), 71 deletions(-) diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index f226afe6..a4bfe4ce 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -149,93 +149,159 @@ ColumnLayout { enableDescriptionRichText: true } - GridView { - id: contributorsGrid - - readonly property int columnsCount: 2 - + // Contributors flow layout (avoids GridView shader crashes on Qt 6.8) + Flow { + id: contributorsFlow Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: cellWidth * columnsCount - Layout.preferredHeight: { - if (root.contributors.length === 0) - return 0; + Layout.preferredWidth: Math.round(Style.baseWidgetSize * 14) + spacing: Style.marginM - const rows = Math.ceil(root.contributors.length / columnsCount); - return rows * cellHeight; - } - cellWidth: Math.round(Style.baseWidgetSize * 7) - cellHeight: Math.round(Style.baseWidgetSize * 2.5) - model: root.contributors + Repeater { + model: root.contributors - delegate: Rectangle { - width: contributorsGrid.cellWidth - Style.marginM - height: contributorsGrid.cellHeight - Style.marginM - radius: Style.radiusL - color: contributorArea.containsMouse ? Color.mHover : Color.transparent + delegate: Rectangle { + width: Math.round(Style.baseWidgetSize * 6.8) + height: Math.round(Style.baseWidgetSize * 2.3) + radius: Style.radiusL + color: contributorArea.containsMouse ? Color.mHover : Color.transparent + border.width: 1 + border.color: contributorArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3) - Behavior on color { - ColorAnimation { - duration: Style.animationFast + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } } - } - RowLayout { - anchors.centerIn: parent - width: parent.width - (Style.marginS * 2) - spacing: Style.marginM + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } - Item { - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: Style.baseWidgetSize * 2 * Style.uiScaleRatio - Layout.preferredHeight: Style.baseWidgetSize * 2 * Style.uiScaleRatio + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginM - NImageRounded { - anchors.fill: parent - anchors.margins: Style.marginXS - radius: width * 0.5 - imagePath: modelData.avatar_url || "" - fallbackIcon: "person" - borderColor: contributorArea.containsMouse ? Color.mOnHover : Color.mPrimary - borderWidth: Style.borderM + // Avatar container with layered border on top + Item { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Style.baseWidgetSize * 1.8 + Layout.preferredHeight: Style.baseWidgetSize * 1.8 - Behavior on borderColor { - ColorAnimation { + // Background and image container + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Qt.alpha(Color.mPrimary, 0.1) + clip: true // Enable clipping to prevent image overflow + + // Simple image without shader effects + Image { + anchors.fill: parent + source: modelData.avatar_url || "" + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: true + asynchronous: true + visible: modelData.avatar_url !== undefined && modelData.avatar_url !== "" + + // Simple opacity for loading state + opacity: status === Image.Ready ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Fallback icon + NIcon { + anchors.centerIn: parent + visible: !modelData.avatar_url || modelData.avatar_url === "" + icon: "person" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + } + + // Border overlay (on top of image) + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Color.transparent + border.width: 2 + border.color: contributorArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5) + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + // Info column + ColumnLayout { + spacing: 2 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + NText { + text: modelData.login || "Unknown" + font.weight: Style.fontWeightBold + color: contributorArea.containsMouse ? Color.mPrimary : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + pointSize: Style.fontSizeS + } + + RowLayout { + spacing: Style.marginXS + Layout.fillWidth: true + + NIcon { + icon: "git-commit" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: `${(modelData.contributions || 0).toString()} commits` + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + font.weight: Style.fontWeightMedium + } + } + } + + // Hover indicator + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: "arrow-right" + pointSize: Style.fontSizeS + color: Color.mPrimary + opacity: contributorArea.containsMouse ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { duration: Style.animationFast } } } } - ColumnLayout { - spacing: Style.marginXS - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - - NText { - text: modelData.login || "Unknown" - font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface - elide: Text.ElideRight - Layout.fillWidth: true + MouseArea { + id: contributorArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData.html_url) + Quickshell.execDetached(["xdg-open", modelData.html_url]); } - - NText { - text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") - pointSize: Style.fontSizeXS - color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant - } - } - } - - MouseArea { - id: contributorArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.html_url) - Quickshell.execDetached(["xdg-open", modelData.html_url]); } } } From 6e4f450f97b4846da24fddf45560d3cbaa53716d Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 21:04:06 -0500 Subject: [PATCH 6/9] Cleanup --- Modules/Panels/Settings/Tabs/AboutTab.qml | 37 +++++------------------ 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index a4bfe4ce..1b6cd6c4 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -162,10 +162,10 @@ ColumnLayout { delegate: Rectangle { width: Math.round(Style.baseWidgetSize * 6.8) height: Math.round(Style.baseWidgetSize * 2.3) - radius: Style.radiusL + radius: Style.radiusM color: contributorArea.containsMouse ? Color.mHover : Color.transparent border.width: 1 - border.color: contributorArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3) + border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline Behavior on color { ColorAnimation { @@ -181,23 +181,20 @@ ColumnLayout { RowLayout { anchors.fill: parent - anchors.margins: Style.marginS + anchors.margins: Style.marginM spacing: Style.marginM - // Avatar container with layered border on top + // Avatar container with rectangular design (modern, no shader issues) Item { Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: Style.baseWidgetSize * 1.8 Layout.preferredHeight: Style.baseWidgetSize * 1.8 // Background and image container - Rectangle { + Item { anchors.fill: parent - radius: Style.radiusM - color: Qt.alpha(Color.mPrimary, 0.1) - clip: true // Enable clipping to prevent image overflow - // Simple image without shader effects + // Simple image Image { anchors.fill: parent source: modelData.avatar_url || "" @@ -206,8 +203,6 @@ ColumnLayout { smooth: true asynchronous: true visible: modelData.avatar_url !== undefined && modelData.avatar_url !== "" - - // Simple opacity for loading state opacity: status === Image.Ready ? 1.0 : 0.0 Behavior on opacity { @@ -226,21 +221,6 @@ ColumnLayout { color: Color.mPrimary } } - - // Border overlay (on top of image) - Rectangle { - anchors.fill: parent - radius: Style.radiusM - color: Color.transparent - border.width: 2 - border.color: contributorArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5) - - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } - } } // Info column @@ -252,7 +232,7 @@ ColumnLayout { NText { text: modelData.login || "Unknown" font.weight: Style.fontWeightBold - color: contributorArea.containsMouse ? Color.mPrimary : Color.mOnSurface + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface elide: Text.ElideRight Layout.fillWidth: true pointSize: Style.fontSizeS @@ -271,8 +251,7 @@ ColumnLayout { NText { text: `${(modelData.contributions || 0).toString()} commits` pointSize: Style.fontSizeXS - color: Color.mOnSurfaceVariant - font.weight: Style.fontWeightMedium + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant } } } From 0c8b0cb395773d828f0d9cb1ba151bb0ac9399bf Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 21:10:09 -0500 Subject: [PATCH 7/9] Only top 20 --- Modules/Panels/Settings/Tabs/AboutTab.qml | 81 ++++++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index 1b6cd6c4..0eeb2162 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -149,15 +149,15 @@ ColumnLayout { enableDescriptionRichText: true } - // Contributors flow layout (avoids GridView shader crashes on Qt 6.8) + // Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8) Flow { - id: contributorsFlow + id: topContributorsFlow Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: Math.round(Style.baseWidgetSize * 14) spacing: Style.marginM Repeater { - model: root.contributors + model: Math.min(root.contributors.length, 20) delegate: Rectangle { width: Math.round(Style.baseWidgetSize * 6.8) @@ -197,12 +197,12 @@ ColumnLayout { // Simple image Image { anchors.fill: parent - source: modelData.avatar_url || "" + source: root.contributors[index].avatar_url || "" fillMode: Image.PreserveAspectCrop mipmap: true smooth: true asynchronous: true - visible: modelData.avatar_url !== undefined && modelData.avatar_url !== "" + visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== "" opacity: status === Image.Ready ? 1.0 : 0.0 Behavior on opacity { @@ -215,7 +215,7 @@ ColumnLayout { // Fallback icon NIcon { anchors.centerIn: parent - visible: !modelData.avatar_url || modelData.avatar_url === "" + visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === "" icon: "person" pointSize: Style.fontSizeL color: Color.mPrimary @@ -230,7 +230,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: modelData.login || "Unknown" + text: root.contributors[index].login || "Unknown" font.weight: Style.fontWeightBold color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface elide: Text.ElideRight @@ -249,7 +249,7 @@ ColumnLayout { } NText { - text: `${(modelData.contributions || 0).toString()} commits` + text: `${(root.contributors[index].contributions || 0).toString()} commits` pointSize: Style.fontSizeXS color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant } @@ -278,8 +278,69 @@ ColumnLayout { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (modelData.html_url) - Quickshell.execDetached(["xdg-open", modelData.html_url]); + if (root.contributors[index].html_url) + Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]); + } + } + } + } + } + + // Remaining contributors (simple text links) + Flow { + id: remainingContributorsFlow + visible: root.contributors.length > 20 + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.round(Style.baseWidgetSize * 14) + Layout.topMargin: Style.marginL + spacing: Style.marginS + + Repeater { + model: Math.max(0, root.contributors.length - 20) + + delegate: Rectangle { + width: nameText.implicitWidth + Style.marginM * 2 + height: nameText.implicitHeight + Style.marginS * 2 + radius: Style.radiusS + color: nameArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : Color.transparent + border.width: 1 + border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + id: nameText + anchors.centerIn: parent + text: root.contributors[index + 20].login || "Unknown" + pointSize: Style.fontSizeXS + color: nameArea.containsMouse ? Color.mPrimary : Color.mOnSurface + font.weight: Style.fontWeightMedium + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + id: nameArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.contributors[index + 20].html_url) + Quickshell.execDetached(["xdg-open", root.contributors[index + 20].html_url]); } } } From 5e833f06834b708954db832ab020ff628fd1edbe Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Wed, 26 Nov 2025 21:46:59 -0500 Subject: [PATCH 8/9] Round image with Qt. --- Assets/settings-default.json | 16 +- Modules/Panels/Settings/Tabs/AboutTab.qml | 17 +- Modules/Renderer/CircularAvatarRenderer.qml | 96 +++++++ Services/Noctalia/GitHubService.qml | 296 ++++++++++++++++++++ shell.qml | 1 + 5 files changed, 414 insertions(+), 12 deletions(-) create mode 100644 Modules/Renderer/CircularAvatarRenderer.qml diff --git a/Assets/settings-default.json b/Assets/settings-default.json index c8cf85a8..6d0a31f4 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -102,20 +102,20 @@ "calendar": { "cards": [ { - "id": "banner-card", - "enabled": true + "enabled": true, + "id": "banner-card" }, { - "id": "calendar-card", - "enabled": true + "enabled": true, + "id": "calendar-card" }, { - "id": "timer-card", - "enabled": true + "enabled": true, + "id": "timer-card" }, { - "id": "weather-card", - "enabled": true + "enabled": true, + "id": "weather-card" } ] }, diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index 0eeb2162..4834e00b 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -194,11 +194,20 @@ ColumnLayout { Item { anchors.fill: parent - // Simple image + // Simple circular image (pre-rendered, no shaders) Image { anchors.fill: parent - source: root.contributors[index].avatar_url || "" - fillMode: Image.PreserveAspectCrop + source: { + // Try cached circular version first + var username = root.contributors[index].login; + var cached = GitHubService.cachedCircularAvatars[username]; + if (cached) + return cached; + + // Fall back to original avatar URL + return root.contributors[index].avatar_url || ""; + } + fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency mipmap: true smooth: true asynchronous: true @@ -245,7 +254,7 @@ ColumnLayout { NIcon { icon: "git-commit" pointSize: Style.fontSizeXS - color: Color.mOnSurfaceVariant + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant } NText { diff --git a/Modules/Renderer/CircularAvatarRenderer.qml b/Modules/Renderer/CircularAvatarRenderer.qml new file mode 100644 index 00000000..5ab01355 --- /dev/null +++ b/Modules/Renderer/CircularAvatarRenderer.qml @@ -0,0 +1,96 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons + +/** +* CircularAvatarRenderer - Hidden window for rendering circular avatars +* +* This component safely uses ClippingRectangle in a separate hidden window to +* pre-render circular avatar images. The rendered images are saved as PNGs +* with transparent backgrounds, which can then be used in the UI without +* any shader effects (avoiding Qt 6.8 crashes). +* +* Usage: +* var renderer = component.createObject(null, { +* imagePath: "file:///path/to/avatar.png", +* outputPath: "/path/to/output_circular.png", +* username: "ItsLemmy" +* }); +* renderer.renderComplete.connect(function(success) { +* if (success) console.log("Rendered!"); +* renderer.destroy(); +* }); +*/ +PanelWindow { + id: root + + // Input properties + property string imagePath: "" + property string outputPath: "" + property string username: "" + + // Hidden window configuration + implicitWidth: 256 + implicitHeight: 256 + visible: true // Must be visible for grabToImage to work + color: "transparent" + + // Wayland configuration - hide it from user view + WlrLayershell.layer: WlrLayer.Bottom // Render below everything + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + // Position it off-screen or behind everything + anchors { + left: true + top: true + } + margins { + left: -512// Off-screen to the left + top: -512 // Off-screen to the top + } + + signal renderComplete(bool success) + + // Use ClippingRectangle safely (not in GridView, not visible) + ClippingRectangle { + id: clipper + anchors.fill: parent + radius: width * 0.5 // Make it circular + color: "transparent" + + Image { + id: sourceImage + anchors.fill: parent + source: root.imagePath + fillMode: Image.PreserveAspectCrop + smooth: true + mipmap: true + asynchronous: true + + onStatusChanged: { + if (status === Image.Ready) { + // Image loaded successfully, capture it on next frame + Qt.callLater(captureCircular); + } else if (status === Image.Error) { + Logger.e("CircularAvatarRenderer", "Failed to load image for", root.username); + root.renderComplete(false); + } + } + } + } + + function captureCircular() { + clipper.grabToImage(function (result) { + if (result.saveToFile(root.outputPath)) { + Logger.d("CircularAvatarRenderer", "Saved circular avatar for", root.username, "to", root.outputPath); + root.renderComplete(true); + } else { + Logger.e("CircularAvatarRenderer", "Failed to save circular avatar for", root.username); + root.renderComplete(false); + } + }, Qt.size(root.width, root.height)); + } +} diff --git a/Services/Noctalia/GitHubService.qml b/Services/Noctalia/GitHubService.qml index 77343272..963173cc 100644 --- a/Services/Noctalia/GitHubService.qml +++ b/Services/Noctalia/GitHubService.qml @@ -18,6 +18,17 @@ Singleton { property string latestVersion: I18n.tr("system.unknown-version") property var contributors: [] + // Avatar caching properties + property var cachedCircularAvatars: ({}) // username → file:// path + property var cacheMetadata: ({}) // Loaded from metadata.json + property var avatarQueue: [] + property bool isProcessingAvatars: false + property bool metadataLoaded: false + property bool avatarsCached: false // Track if we've already processed avatars + + readonly property string avatarCacheDir: Settings.cacheDirImages + "contributors/" + readonly property string metadataPath: avatarCacheDir + "metadata.json" + FileView { id: githubDataFileView path: githubDataFile @@ -25,6 +36,7 @@ Singleton { onFileChanged: reload() onAdapterUpdated: writeAdapter() Component.onCompleted: { + loadCacheMetadata(); reload(); } onLoaded: { @@ -48,6 +60,12 @@ Singleton { } } + // -------------------------------- + function init() { + Logger.i("GitHub", "Service started"); + loadFromCache(); + } + // -------------------------------- function loadFromCache() { const now = Time.timestamp; @@ -118,6 +136,284 @@ Singleton { fetchFromGitHub(); } + // -------------------------------- + // Avatar Caching Functions + // -------------------------------- + + function loadCacheMetadata() { + var loadProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["cat", "${metadataPath}"] + } + `, root, "LoadMetadata"); + + loadProcess.stdout = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + StdioCollector {} + `, loadProcess, "StdoutCollector"); + + loadProcess.stdout.onStreamFinished.connect(function () { + try { + var text = loadProcess.stdout.text; + if (text && text.trim()) { + cacheMetadata = JSON.parse(text); + Logger.d("GitHubService", "Loaded cache metadata:", Object.keys(cacheMetadata).length, "entries"); + + // Populate cachedCircularAvatars from metadata + for (var username in cacheMetadata) { + var entry = cacheMetadata[username]; + cachedCircularAvatars[username] = "file://" + entry.cached_path; + } + + metadataLoaded = true; + Logger.d("GitHubService", "Cache metadata loaded successfully"); + } else { + Logger.d("GitHubService", "No existing cache metadata found (empty response)"); + cacheMetadata = {}; + metadataLoaded = true; + } + } catch (e) { + Logger.w("GitHubService", "Failed to parse cache metadata:", e); + cacheMetadata = {}; + metadataLoaded = true; + } + loadProcess.destroy(); + }); + + loadProcess.exited.connect(function (exitCode) { + if (exitCode !== 0) { + // File doesn't exist, initialize empty + cacheMetadata = {}; + metadataLoaded = true; + Logger.d("GitHubService", "Initializing empty cache metadata"); + } + }); + + loadProcess.running = true; + } + + function saveCacheMetadata() { + Quickshell.execDetached(["mkdir", "-p", avatarCacheDir]); + + var jsonContent = JSON.stringify(cacheMetadata, null, 2); + + // Use printf with base64 encoding to safely handle special characters + var base64Content = Qt.btoa(jsonContent); // Base64 encode + + var saveProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", "echo '${base64Content}' | base64 -d > '${metadataPath}'"] + } + `, root, "SaveMetadata_" + Date.now()); + + saveProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.d("GitHubService", "Saved cache metadata"); + } else { + Logger.e("GitHubService", "Failed to save cache metadata, exit code:", exitCode); + } + saveProcess.destroy(); + }); + + saveProcess.running = true; + } + + function cacheTopContributorAvatars() { + if (contributors.length === 0) + return; + + // Mark that we've processed avatars for this contributor set + avatarsCached = true; + + Quickshell.execDetached(["mkdir", "-p", avatarCacheDir]); + + // Build queue of avatars that need processing + avatarQueue = []; + var currentTop20 = {}; + + for (var i = 0; i < Math.min(contributors.length, 20); i++) { + var contributor = contributors[i]; + var username = contributor.login; + var avatarUrl = contributor.avatar_url; + var circularPath = avatarCacheDir + username + "_circular.png"; + + currentTop20[username] = true; + + // Check if we need to process this avatar + var needsProcessing = false; + var reason = ""; + + if (!cacheMetadata[username]) { + // New user in top 20 + needsProcessing = true; + reason = "new user"; + } else if (cacheMetadata[username].avatar_url !== avatarUrl) { + // Avatar URL changed (user updated their GitHub avatar) + needsProcessing = true; + reason = "avatar URL changed"; + } else { + // Already cached - add to map + cachedCircularAvatars[username] = "file://" + circularPath; + } + + if (needsProcessing) { + Logger.d("GitHubService", "Queueing avatar for", username, "-", reason); + avatarQueue.push({ + username: username, + avatarUrl: avatarUrl, + circularPath: circularPath + }); + } + } + + // Cleanup: Remove metadata for users no longer in top 20 + var removedUsers = []; + for (var cachedUsername in cacheMetadata) { + if (!currentTop20[cachedUsername]) { + removedUsers.push(cachedUsername); + + // Delete cached circular file + var pathToDelete = cacheMetadata[cachedUsername].cached_path; + Quickshell.execDetached(["rm", "-f", pathToDelete]); + + delete cacheMetadata[cachedUsername]; + delete cachedCircularAvatars[cachedUsername]; + } + } + + if (removedUsers.length > 0) { + Logger.d("GitHubService", "Cleaned up avatars for users no longer in top 20:", removedUsers.join(", ")); + saveCacheMetadata(); + } + + // Start processing queue + if (avatarQueue.length > 0) { + Logger.i("GitHubService", "Processing", avatarQueue.length, "avatar(s)"); + processNextAvatar(); + } else { + Logger.d("GitHubService", "All avatars already cached"); + cachedCircularAvatarsChanged(); // Notify AboutTab + } + } + + function processNextAvatar() { + if (avatarQueue.length === 0 || isProcessingAvatars) + return; + + isProcessingAvatars = true; + var item = avatarQueue.shift(); + + Logger.d("GitHubService", "Downloading avatar for", item.username); + + // Download original avatar + var tempPath = avatarCacheDir + item.username + "_temp.png"; + downloadAvatar(item.avatarUrl, tempPath, function (success) { + if (success) { + // Render circular version + renderCircularAvatar(tempPath, item.circularPath, item.username, item.avatarUrl); + } else { + Logger.e("GitHubService", "Failed to download avatar for", item.username); + isProcessingAvatars = false; + processNextAvatar(); + } + }); + } + + function downloadAvatar(url, destPath, callback) { + var downloadCmd = `curl -L -s -o '${destPath}' '${url}' || wget -q -O '${destPath}' '${url}'`; + + var downloadProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", "${downloadCmd}"] + } + `, root, "Download_" + Date.now()); + + downloadProcess.exited.connect(function (exitCode) { + callback(exitCode === 0); + downloadProcess.destroy(); + }); + + downloadProcess.running = true; + } + + function renderCircularAvatar(inputPath, outputPath, username, avatarUrl) { + var rendererComponent = Qt.createComponent(Quickshell.shellDir + "/Modules/Renderer/CircularAvatarRenderer.qml"); + if (rendererComponent.status === Component.Ready) { + var renderer = rendererComponent.createObject(root, { + imagePath: "file://" + inputPath, + outputPath: outputPath, + username: username + }); + + renderer.renderComplete.connect(function (success) { + if (success) { + // Update cache metadata + cacheMetadata[username] = { + avatar_url: avatarUrl, + cached_path: outputPath, + cached_at: Date.now() + }; + + cachedCircularAvatars[username] = "file://" + outputPath; + cachedCircularAvatarsChanged(); + + saveCacheMetadata(); + + Logger.d("GitHubService", "Cached circular avatar for", username); + } else { + Logger.e("GitHubService", "Failed to render circular avatar for", username); + } + + renderer.destroy(); + + // Clean up temp file + Quickshell.execDetached(["rm", "-f", inputPath]); + + // Process next in queue + isProcessingAvatars = false; + processNextAvatar(); + }); + } else { + Logger.e("GitHubService", "Failed to create CircularAvatarRenderer"); + isProcessingAvatars = false; + processNextAvatar(); + } + } + + // -------------------------------- + // Hook into contributors change - only process once + onContributorsChanged: { + if (contributors.length > 0 && !avatarsCached) { + // Wait for metadata to load before processing + if (metadataLoaded) { + Qt.callLater(cacheTopContributorAvatars); + } else { + // Metadata not loaded yet, wait for it + metadataLoadedWatcher.start(); + } + } + } + + // Wait for metadata to be loaded before caching avatars + Timer { + id: metadataLoadedWatcher + interval: 100 + repeat: true + onTriggered: { + if (metadataLoaded && contributors.length > 0 && !avatarsCached) { + stop(); + Qt.callLater(cacheTopContributorAvatars); + } + } + } + Process { id: versionProcess diff --git a/shell.qml b/shell.qml index 8f750704..00fd3f15 100644 --- a/shell.qml +++ b/shell.qml @@ -97,6 +97,7 @@ ShellRoot { PowerProfileService.init(); HostService.init(); FontService.init(); + GitHubService.init(); UpdateService.init(); UpdateService.showLatestChangelog(); From e6a4db970705a7e57040a75e568db7a10825ea20 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Thu, 27 Nov 2025 08:34:15 -0500 Subject: [PATCH 9/9] Better rounding --- Modules/Panels/Settings/Tabs/AboutTab.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index 4834e00b..c6fb468f 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -186,10 +186,13 @@ ColumnLayout { // Avatar container with rectangular design (modern, no shader issues) Item { + id: wrapper Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: Style.baseWidgetSize * 1.8 Layout.preferredHeight: Style.baseWidgetSize * 1.8 + property bool isRounded: false + // Background and image container Item { anchors.fill: parent @@ -201,8 +204,10 @@ ColumnLayout { // Try cached circular version first var username = root.contributors[index].login; var cached = GitHubService.cachedCircularAvatars[username]; - if (cached) + if (cached) { + wrapper.isRounded = true; return cached; + } // Fall back to original avatar URL return root.contributors[index].avatar_url || ""; @@ -230,6 +235,15 @@ ColumnLayout { color: Color.mPrimary } } + + Rectangle { + visible: wrapper.isRounded + anchors.fill: parent + color: Color.transparent + radius: width * 0.5 + border.width: Style.borderM + border.color: Color.mPrimary + } } // Info column