From ddfde344bcc8990c4704acdc148732b4ffb855df Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Sat, 29 Nov 2025 10:04:28 -0500 Subject: [PATCH] AboutTab: caching circular images --- Assets/settings-default.json | 8 +- Modules/Panels/Settings/Tabs/AboutTab.qml | 20 +++-- Modules/Renderer/CircularAvatarRenderer.qml | 96 --------------------- Services/Noctalia/GitHubService.qml | 68 ++++++++------- 4 files changed, 53 insertions(+), 139 deletions(-) delete mode 100644 Modules/Renderer/CircularAvatarRenderer.qml diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 7fccdbdd..fd76e684 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -308,7 +308,11 @@ "autoHideMs": 2000, "overlayLayer": true, "backgroundOpacity": 1, - "enabledTypes": [], + "enabledTypes": [ + 0, + 1, + 2 + ], "monitors": [] }, "audio": { @@ -354,6 +358,8 @@ "spicetify": false, "telegram": false, "cava": false, + "emacs": false, + "niri": false, "enableUserTemplates": false }, "nightLight": { diff --git a/Modules/Panels/Settings/Tabs/AboutTab.qml b/Modules/Panels/Settings/Tabs/AboutTab.qml index c6fb468f..75d843cd 100644 --- a/Modules/Panels/Settings/Tabs/AboutTab.qml +++ b/Modules/Panels/Settings/Tabs/AboutTab.qml @@ -15,6 +15,8 @@ ColumnLayout { property string currentVersion: UpdateService.currentVersion property var contributors: GitHubService.contributors + readonly property int topContributorsCount: 20 + spacing: Style.marginL NHeader { @@ -157,7 +159,7 @@ ColumnLayout { spacing: Style.marginM Repeater { - model: Math.min(root.contributors.length, 20) + model: Math.min(root.contributors.length, root.topContributorsCount) delegate: Rectangle { width: Math.round(Style.baseWidgetSize * 6.8) @@ -312,21 +314,21 @@ ColumnLayout { // Remaining contributors (simple text links) Flow { id: remainingContributorsFlow - visible: root.contributors.length > 20 + visible: root.contributors.length > root.topContributorsCount 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) + model: Math.max(0, root.contributors.length - root.topContributorsCount) 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 + color: nameArea.containsMouse ? Color.mHover : Color.transparent + border.width: Style.borderS border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline Behavior on color { @@ -344,9 +346,9 @@ ColumnLayout { NText { id: nameText anchors.centerIn: parent - text: root.contributors[index + 20].login || "Unknown" + text: root.contributors[index + root.topContributorsCount].login || "Unknown" pointSize: Style.fontSizeXS - color: nameArea.containsMouse ? Color.mPrimary : Color.mOnSurface + color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface font.weight: Style.fontWeightMedium Behavior on color { @@ -362,8 +364,8 @@ ColumnLayout { hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { - if (root.contributors[index + 20].html_url) - Quickshell.execDetached(["xdg-open", root.contributors[index + 20].html_url]); + if (root.contributors[index + root.topContributorsCount].html_url) + Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]); } } } diff --git a/Modules/Renderer/CircularAvatarRenderer.qml b/Modules/Renderer/CircularAvatarRenderer.qml deleted file mode 100644 index 5ab01355..00000000 --- a/Modules/Renderer/CircularAvatarRenderer.qml +++ /dev/null @@ -1,96 +0,0 @@ -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 963173cc..ea8d103a 100644 --- a/Services/Noctalia/GitHubService.qml +++ b/Services/Noctalia/GitHubService.qml @@ -92,7 +92,7 @@ Singleton { // -------------------------------- function fetchFromGitHub() { if (isFetchingData) { - Logger.w("GitHub", "GitHub data is still fetching"); + Logger.d("GitHub", "GitHub data is still fetching"); return; } @@ -344,47 +344,49 @@ Singleton { } 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 - }); + Logger.d("GitHubService", "Rendering circular avatar for", username); - renderer.renderComplete.connect(function (success) { - if (success) { - // Update cache metadata - cacheMetadata[username] = { - avatar_url: avatarUrl, - cached_path: outputPath, - cached_at: Date.now() - }; + // Use ImageMagick to create a circular avatar with proper alpha transparency + var convertProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["magick", "${inputPath}", "-resize", "256x256^", "-gravity", "center", "-extent", "256x256", "-alpha", "set", "(", "+clone", "-channel", "A", "-evaluate", "set", "0", "+channel", "-fill", "white", "-draw", "circle 128,128 128,0", ")", "-compose", "DstIn", "-composite", "${outputPath}"] + } + `, root, "Convert_" + Date.now()); - cachedCircularAvatars[username] = "file://" + outputPath; - cachedCircularAvatarsChanged(); + convertProcess.exited.connect(function (exitCode) { + var success = exitCode === 0; - saveCacheMetadata(); + if (success) { + // Update cache metadata + cacheMetadata[username] = { + avatar_url: avatarUrl, + cached_path: outputPath, + cached_at: Date.now() + }; - Logger.d("GitHubService", "Cached circular avatar for", username); - } else { - Logger.e("GitHubService", "Failed to render circular avatar for", username); - } + cachedCircularAvatars[username] = "file://" + outputPath; + cachedCircularAvatarsChanged(); - renderer.destroy(); + saveCacheMetadata(); - // Clean up temp file - Quickshell.execDetached(["rm", "-f", inputPath]); + Logger.d("GitHubService", "Cached circular avatar for", username); + } else { + Logger.e("GitHubService", "Failed to render circular avatar for", username); + } - // Process next in queue - isProcessingAvatars = false; - processNextAvatar(); - }); - } else { - Logger.e("GitHubService", "Failed to create CircularAvatarRenderer"); + // Clean up temp file + Quickshell.execDetached(["rm", "-f", inputPath]); + + // Process next in queue isProcessingAvatars = false; processNextAvatar(); - } + + convertProcess.destroy(); + }); + + convertProcess.running = true; } // --------------------------------