diff --git a/Modules/AppLauncher/AppLauncher.qml b/Modules/AppLauncher/AppLauncher.qml index aef7dd9..3dc07d5 100644 --- a/Modules/AppLauncher/AppLauncher.qml +++ b/Modules/AppLauncher/AppLauncher.qml @@ -13,273 +13,270 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort import "../../Helpers/MathHelper.js" as MathHelper NLoader { - id: appLauncher - isLoaded: false - // Clipboard state is persisted in Services/Clipboard.qml - content: Component { - NPanel { - id: appLauncherPanel + id: appLauncher + isLoaded: false + // Clipboard state is persisted in Services/Clipboard.qml + content: Component { + NPanel { + id: appLauncherPanel - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - - // No local timer/processes; use persistent Clipboard service + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - // Removed local clipboard processes; handled by Clipboard service + // No local timer/processes; use persistent Clipboard service - + // Removed local clipboard processes; handled by Clipboard service - + // Copy helpers via simple exec; avoid keeping processes alive locally + function copyImageBase64(mime, base64) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) + } - // Copy helpers via simple exec; avoid keeping processes alive locally - function copyImageBase64(mime, base64) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) - } + function copyText(text) { + Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) + } - function copyText(text) { - Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) - } + function updateClipboardHistory() { + Clipboard.refresh() + } + function selectNext() { + if (filteredEntries.length > 0) { + selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1) + } + } + function selectPrev() { + if (filteredEntries.length > 0) { + selectedIndex = Math.max(selectedIndex - 1, 0) + } + } - function updateClipboardHistory() { - Clipboard.refresh(); - } + function activateSelected() { + if (filteredEntries.length === 0) + return - function selectNext() { - if (filteredEntries.length > 0) { - selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1); + var modelData = filteredEntries[selectedIndex] + if (modelData && modelData.execute) { + if (modelData.isCommand) { + modelData.execute() + return + } else { + modelData.execute() + } + appLauncherPanel.hide() + } + } + + property var desktopEntries: DesktopEntries.applications.values + property string searchText: "" + property int selectedIndex: 0 + property var filteredEntries: { + console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) + if (!desktopEntries || desktopEntries.length === 0) { + console.log("[AppLauncher] No desktop entries available") + return [] + } + + // Filter out entries that shouldn't be displayed + var visibleEntries = desktopEntries.filter(entry => { + if (!entry || entry.noDisplay) { + return false + } + return true + }) + + console.log("[AppLauncher] Visible entries:", visibleEntries.length) + + var query = searchText ? searchText.toLowerCase() : "" + var results = [] + + // Handle special commands + if (query === ">") { + results.push({ + "isCommand": true, + "name": ">calc", + "content": "Calculator - evaluate mathematical expressions", + "icon": "calculate", + "execute": function () { + searchText = ">calc " + searchInput.cursorPosition = searchText.length + } + }) + + results.push({ + "isCommand": true, + "name": ">clip", + "content": "Clipboard history - browse and restore clipboard items", + "icon": "content_paste", + "execute": function () { + searchText = ">clip " + searchInput.cursorPosition = searchText.length + } + }) + + return results + } + + // Handle clipboard history + if (query.startsWith(">clip")) { + if (!Clipboard.initialized) { + Clipboard.refresh() + } + const searchTerm = query.slice(5).trim() + + Clipboard.history.forEach(function (clip, index) { + let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip + + if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { + let entry + if (clip.type === 'image') { + entry = { + "isClipboard": true, + "name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(), + "content": "Image: " + clip.mimeType, + "icon": "image", + "type": 'image', + "data": clip.data, + "execute": function () { + const base64Data = clip.data.split(',')[1] + copyImageBase64(clip.mimeType, base64Data) + Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]) + } } - } + } else { + const textContent = clip.content || clip + let displayContent = textContent + let previewContent = "" - function selectPrev() { - if (filteredEntries.length > 0) { - selectedIndex = Math.max(selectedIndex - 1, 0); + displayContent = displayContent.replace(/\s+/g, ' ').trim() + + if (displayContent.length > 50) { + previewContent = displayContent + displayContent = displayContent.split('\n')[0].substring(0, 50) + "..." } + + entry = { + "isClipboard": true, + "name": displayContent, + "content": previewContent || textContent, + "icon": "content_paste", + "execute": function () { + Quickshell.clipboardText = String(textContent) + copyText(String(textContent)) + var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent + Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]) + } + } + } + results.push(entry) } + }) - function activateSelected() { - if (filteredEntries.length === 0) return; + if (results.length === 0) { + results.push({ + "isClipboard": true, + "name": "No clipboard history", + "content": "No matching clipboard entries found", + "icon": "content_paste_off" + }) + } - var modelData = filteredEntries[selectedIndex]; - if (modelData && modelData.execute) { - if (modelData.isCommand) { - modelData.execute(); - return; - } else { - modelData.execute(); + return results + } + + // Handle calculator + if (query.startsWith(">calc")) { + var expr = searchText.slice(5).trim() + if (expr && isMathExpression(expr)) { + var value = safeEval(expr) + if (value !== null && value !== undefined && value !== "") { + var formattedResult = MathHelper.MathHelper.formatResult(value) + results.push({ + "isCalculator": true, + "name": `Calculator: ${expr} = ${formattedResult}`, + "result": value, + "expr": expr, + "icon": "calculate", + "execute": function () { + Quickshell.clipboardText = String(formattedResult) + clipboardTextCopyProcess.copyText(String(formattedResult)) + Quickshell.execDetached( + ["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]) + } + }) + } + } + + return results + } + + // Regular app search + if (!query) { + results = results.concat(visibleEntries.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + })) + } else { + var fuzzyResults = Fuzzysort.go(query, visibleEntries, { + "keys": ["name", "comment", "genericName"] + }) + results = results.concat(fuzzyResults.map(function (r) { + return r.obj + })) + } + + console.log("[AppLauncher] Filtered entries:", results.length) + return results + } + + Component.onCompleted: { + console.log("[AppLauncher] Component completed") + console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') + if (typeof DesktopEntries !== 'undefined') { + console.log("[AppLauncher] DesktopEntries.entries:", + DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') + } + // Start clipboard refresh immediately on open + updateClipboardHistory() + } + + function isMathExpression(str) { + // Allow more characters for enhanced math functions + return /^[-+*/().0-9\s\w]+$/.test(str) + } + + function safeEval(expr) { + return MathHelper.MathHelper.evaluate(expr) + } + + // Main content container + Rectangle { + anchors.centerIn: parent + width: Math.min(700 * scaling, parent.width * 0.75) + height: Math.min(550 * scaling, parent.height * 0.8) + radius: 32 * scaling + color: Colors.backgroundPrimary + border.color: Colors.outline + border.width: Style.borderThin * scaling + + // Subtle gradient background + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(Colors.backgroundPrimary, 1.02) + } + GradientStop { + position: 1.0 + color: Qt.darker(Colors.backgroundPrimary, 1.1) + } } - appLauncherPanel.hide(); - } - } - - property var desktopEntries: DesktopEntries.applications.values - property string searchText: "" - property int selectedIndex: 0 - property var filteredEntries: { - console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0) - if (!desktopEntries || desktopEntries.length === 0) { - console.log("[AppLauncher] No desktop entries available") - return [] - } - - // Filter out entries that shouldn't be displayed - var visibleEntries = desktopEntries.filter(entry => { - if (!entry || entry.noDisplay) { - return false - } - return true - }) - - console.log("[AppLauncher] Visible entries:", visibleEntries.length) - - var query = searchText ? searchText.toLowerCase() : ""; - var results = []; - - // Handle special commands - if (query === ">") { - results.push({ - isCommand: true, - name: ">calc", - content: "Calculator - evaluate mathematical expressions", - icon: "calculate", - execute: function() { - searchText = ">calc "; - searchInput.cursorPosition = searchText.length; - } - }); - - results.push({ - isCommand: true, - name: ">clip", - content: "Clipboard history - browse and restore clipboard items", - icon: "content_paste", - execute: function() { - searchText = ">clip "; - searchInput.cursorPosition = searchText.length; - } - }); - - return results; - } - - // Handle clipboard history - if (query.startsWith(">clip")) { - if (!Clipboard.initialized) { - Clipboard.refresh(); - } - const searchTerm = query.slice(5).trim(); - - Clipboard.history.forEach(function(clip, index) { - let searchContent = clip.type === 'image' ? - clip.mimeType : - clip.content || clip; - - if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { - let entry; - if (clip.type === 'image') { - entry = { - isClipboard: true, - name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(), - content: "Image: " + clip.mimeType, - icon: "image", - type: 'image', - data: clip.data, - execute: function() { - const base64Data = clip.data.split(',')[1]; - copyImageBase64(clip.mimeType, base64Data); - Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]); - } - }; - } else { - const textContent = clip.content || clip; - let displayContent = textContent; - let previewContent = ""; - - displayContent = displayContent.replace(/\s+/g, ' ').trim(); - - if (displayContent.length > 50) { - previewContent = displayContent; - displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."; - } - - entry = { - isClipboard: true, - name: displayContent, - content: previewContent || textContent, - icon: "content_paste", - execute: function() { - Quickshell.clipboardText = String(textContent); - copyText(String(textContent)); - var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent; - Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]); - } - }; - } - results.push(entry); - } - }); - - if (results.length === 0) { - results.push({ - isClipboard: true, - name: "No clipboard history", - content: "No matching clipboard entries found", - icon: "content_paste_off" - }); - } - - return results; - } - - // Handle calculator - if (query.startsWith(">calc")) { - var expr = searchText.slice(5).trim(); - if (expr && isMathExpression(expr)) { - var value = safeEval(expr); - if (value !== null && value !== undefined && value !== "") { - var formattedResult = MathHelper.MathHelper.formatResult(value); - results.push({ - isCalculator: true, - name: `Calculator: ${expr} = ${formattedResult}`, - result: value, - expr: expr, - icon: "calculate", - execute: function() { - Quickshell.clipboardText = String(formattedResult); - clipboardTextCopyProcess.copyText(String(formattedResult)); - Quickshell.execDetached(["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]); - } - }); - } - } - - return results; - } - - // Regular app search - if (!query) { - results = results.concat(visibleEntries.sort(function (a, b) { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); - })); - } else { - var fuzzyResults = Fuzzysort.go(query, visibleEntries, { - keys: ["name", "comment", "genericName"] - }); - results = results.concat(fuzzyResults.map(function (r) { - return r.obj; - })); - } - - console.log("[AppLauncher] Filtered entries:", results.length) - return results - } - Component.onCompleted: { - console.log("[AppLauncher] Component completed") - console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') - if (typeof DesktopEntries !== 'undefined') { - console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') - } - // Start clipboard refresh immediately on open - updateClipboardHistory(); - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginLarge * scaling + spacing: Style.marginMedium * scaling - function isMathExpression(str) { - // Allow more characters for enhanced math functions - return /^[-+*/().0-9\s\w]+$/.test(str); - } - - function safeEval(expr) { - return MathHelper.MathHelper.evaluate(expr); - } - - // Main content container - Rectangle { - anchors.centerIn: parent - width: Math.min(700 * scaling, parent.width * 0.75) - height: Math.min(550 * scaling, parent.height * 0.8) - radius: 32 * scaling - color: Colors.backgroundPrimary - border.color: Colors.outline - border.width: Style.borderThin * scaling - - // Subtle gradient background - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(Colors.backgroundPrimary, 1.02) } - GradientStop { position: 1.0; color: Qt.darker(Colors.backgroundPrimary, 1.1) } - } - - - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginMedium * scaling - - - - // Search bar - Rectangle { + // Search bar + Rectangle { Layout.fillWidth: true Layout.preferredHeight: 40 * scaling Layout.bottomMargin: Style.marginMedium * scaling @@ -289,67 +286,71 @@ NLoader { border.width: searchInput.activeFocus ? 2 : 1 Row { - anchors.fill: parent - anchors.margins: 12 * scaling - spacing: 10 * scaling + anchors.fill: parent + anchors.margins: 12 * scaling + spacing: 10 * scaling - Text { - text: "search" - font.family: "Material Symbols Outlined" - font.pointSize: 16 * scaling - color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + Text { + text: "search" + font.family: "Material Symbols Outlined" + font.pointSize: 16 * scaling + color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary + } + + TextField { + id: searchInput + placeholderText: "Search applications..." + color: Colors.textPrimary + placeholderTextColor: Colors.textSecondary + background: null + font.pointSize: 13 * scaling + Layout.fillWidth: true + onTextChanged: { + searchText = text + selectedIndex = 0 // Reset selection when search changes } - - TextField { - id: searchInput - placeholderText: "Search applications..." - color: Colors.textPrimary - placeholderTextColor: Colors.textSecondary - background: null - font.pointSize: 13 * scaling - Layout.fillWidth: true - onTextChanged: { - searchText = text; - selectedIndex = 0; // Reset selection when search changes - } - selectedTextColor: Colors.textPrimary - selectionColor: Colors.accentPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 - font.bold: true - Component.onCompleted: { - contentItem.cursorColor = Colors.textPrimary - contentItem.verticalAlignment = TextInput.AlignVCenter - // Focus the search bar by default - Qt.callLater(() => { - searchInput.forceActiveFocus() - }) - } - onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary - - Keys.onDownPressed: selectNext() - Keys.onUpPressed: selectPrev() - Keys.onEnterPressed: activateSelected() - Keys.onReturnPressed: activateSelected() - Keys.onEscapePressed: appLauncherPanel.hide() + selectedTextColor: Colors.textPrimary + selectionColor: Colors.accentPrimary + padding: 0 + verticalAlignment: TextInput.AlignVCenter + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + font.bold: true + Component.onCompleted: { + contentItem.cursorColor = Colors.textPrimary + contentItem.verticalAlignment = TextInput.AlignVCenter + // Focus the search bar by default + Qt.callLater(() => { + searchInput.forceActiveFocus() + }) } + onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary + + Keys.onDownPressed: selectNext() + Keys.onUpPressed: selectPrev() + Keys.onEnterPressed: activateSelected() + Keys.onReturnPressed: activateSelected() + Keys.onEscapePressed: appLauncherPanel.hide() + } } Behavior on border.color { - ColorAnimation { duration: 120 } + ColorAnimation { + duration: 120 + } } Behavior on border.width { - NumberAnimation { duration: 120 } + NumberAnimation { + duration: 120 + } } - } + } - // Applications list - ScrollView { + // Applications list + ScrollView { Layout.fillWidth: true Layout.fillHeight: true clip: true @@ -357,163 +358,176 @@ NLoader { ScrollBar.vertical.policy: ScrollBar.AsNeeded ListView { - id: appsList - anchors.fill: parent - spacing: 4 * scaling - model: filteredEntries - currentIndex: selectedIndex + id: appsList + anchors.fill: parent + spacing: 4 * scaling + model: filteredEntries + currentIndex: selectedIndex - delegate: Rectangle { - width: appsList.width - Style.marginSmall * scaling - height: 56 * scaling - radius: 16 * scaling - property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundSecondary - border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent" - border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 - - Behavior on color { - ColorAnimation { duration: 150 } - } - - Behavior on border.color { - ColorAnimation { duration: 150 } - } - - Behavior on border.width { - NumberAnimation { duration: 150 } - } + delegate: Rectangle { + width: appsList.width - Style.marginSmall * scaling + height: 56 * scaling + radius: 16 * scaling + property bool isSelected: index === selectedIndex + color: (appCardArea.containsMouse || isSelected) ? Qt.darker( + Colors.accentPrimary, + 1.1) : Colors.backgroundSecondary + border.color: (appCardArea.containsMouse + || isSelected) ? Colors.accentPrimary : "transparent" + border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 - RowLayout { - anchors.fill: parent - anchors.margins: Style.marginMedium * scaling - spacing: Style.marginMedium * scaling - - // App icon with background - Rectangle { - Layout.preferredWidth: 40 * scaling - Layout.preferredHeight: 40 * scaling - radius: 14 * scaling - color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary - property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "") - visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode - - // Clipboard image display - Image { - id: clipboardImage - anchors.fill: parent - anchors.margins: 6 * scaling - visible: modelData.type === 'image' - source: modelData.data || "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - } - - IconImage { - id: iconImg - anchors.fill: parent - anchors.margins: 6 * scaling - asynchronous: true - source: modelData.isCalculator ? "calculate" : - modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : - modelData.isCommand ? modelData.icon : - (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") - visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image' - } - - // Fallback icon container - Rectangle { - anchors.fill: parent - anchors.margins: 6 * scaling - radius: 10 * scaling - color: Colors.accentPrimary - opacity: 0.3 - visible: !parent.iconLoaded - } - - Text { - anchors.centerIn: parent - visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand) - text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - font.pointSize: 18 * scaling - font.weight: Font.Bold - color: Colors.accentPrimary - } - - Behavior on color { - ColorAnimation { duration: 150 } - } - } - - // App info - ColumnLayout { - Layout.fillWidth: true - spacing: 2 * scaling - - NText { - text: modelData.name || "Unknown" - font.pointSize: 14 * scaling - font.weight: Font.Bold - color: Colors.textPrimary - elide: Text.ElideRight - Layout.fillWidth: true - } - - NText { - text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : - modelData.isClipboard ? modelData.content : - modelData.isCommand ? modelData.content : - (modelData.genericName || modelData.comment || "") - font.pointSize: 11 * scaling - color: (appCardArea.containsMouse || isSelected) ? Colors.textPrimary : Colors.textSecondary - elide: Text.ElideRight - Layout.fillWidth: true - visible: text !== "" - } - } - - - } - - MouseArea { - id: appCardArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - selectedIndex = index; - activateSelected(); - } - } + Behavior on color { + ColorAnimation { + duration: 150 + } } - } - } - // No results message - NText { + Behavior on border.color { + ColorAnimation { + duration: 150 + } + } + + Behavior on border.width { + NumberAnimation { + duration: 150 + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling + + // App icon with background + Rectangle { + Layout.preferredWidth: 40 * scaling + Layout.preferredHeight: 40 * scaling + radius: 14 * scaling + color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, + 1.1) : Colors.backgroundTertiary + property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) || (iconImg.status === Image.Ready + && iconImg.source !== "" + && iconImg.status !== Image.Error + && iconImg.source !== "") + visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode + + // Clipboard image display + Image { + id: clipboardImage + anchors.fill: parent + anchors.margins: 6 * scaling + visible: modelData.type === 'image' + source: modelData.data || "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + + IconImage { + id: iconImg + anchors.fill: parent + anchors.margins: 6 * scaling + asynchronous: true + source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") + visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand + || parent.iconLoaded) && modelData.type !== 'image' + } + + // Fallback icon container + Rectangle { + anchors.fill: parent + anchors.margins: 6 * scaling + radius: 10 * scaling + color: Colors.accentPrimary + opacity: 0.3 + visible: !parent.iconLoaded + } + + Text { + anchors.centerIn: parent + visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard + || modelData.isCommand) + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pointSize: 18 * scaling + font.weight: Font.Bold + color: Colors.accentPrimary + } + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + } + + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 * scaling + + NText { + text: modelData.name || "Unknown" + font.pointSize: 14 * scaling + font.weight: Font.Bold + color: Colors.textPrimary + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "") + font.pointSize: 11 * scaling + color: (appCardArea.containsMouse + || isSelected) ? Colors.textPrimary : Colors.textSecondary + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" + } + } + } + + MouseArea { + id: appCardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + selectedIndex = index + activateSelected() + } + } + } + } + } + + // No results message + NText { text: searchText.trim() !== "" ? "No applications found" : "No applications available" font.pointSize: Style.fontSizeLarge * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: filteredEntries.length === 0 - } + } - // Results count - NText { - text: searchText.startsWith(">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : - searchText.startsWith(">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : - `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` + // Results count + NText { + text: searchText.startsWith( + ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length + !== 1 ? 's' : ''}` : searchText.startsWith( + ">calc") ? `${filteredEntries.length} result${filteredEntries.length + !== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` font.pointSize: Style.fontSizeSmall * scaling color: Colors.textSecondary horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true visible: searchText.trim() !== "" + } } - } - } - } - } -} \ No newline at end of file + } + } + } + } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index c40fb48..2e0c2a6 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -9,12 +9,12 @@ import qs.Services import qs.Widgets NLoader { - isLoaded: Settings.data.general.showDock - content: Component { - Variants { - model: Quickshell.screens + isLoaded: Settings.data.general.showDock + content: Component { + Variants { + model: Quickshell.screens - Item { + Item { property var modelData readonly property real scaling: Scaling.scale(modelData) @@ -38,308 +38,321 @@ NLoader { property var contextMenuToplevel: null PanelWindow { - id: dockWindow - visible: true - screen: modelData + id: dockWindow + visible: true + screen: modelData + exclusionMode: ExclusionMode.Ignore + anchors.bottom: true + anchors.left: true + anchors.right: true + focusable: false + color: "transparent" + implicitHeight: 60 + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: if (autoHide && !dockHovered && !anyAppHovered) + hidden = true + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: hidden = false + } + + // Behavior for smooth hide/show animations + Behavior on margins.bottom { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: Easing.InOutQuad + } + } + + // Mouse area at screen bottom to detect entry and keep dock visible + MouseArea { + id: screenEdgeMouseArea + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 10 + hoverEnabled: true + propagateComposedEvents: true + + onEntered: if (autoHide && hidden) + showTimer.start() + onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) + hideTimer.start() + } + + margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + + MouseArea { + anchors.fill: parent + enabled: contextMenuVisible + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + } + } + + Rectangle { + id: dockContainer + width: dock.width + 40 + height: 50 + color: Colors.backgroundSecondary + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + topLeftRadius: 20 + topRightRadius: 20 + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + + onEntered: { + dockHovered = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + onExited: { + dockHovered = false + if (autoHide && !anyAppHovered && !contextMenuVisible) + hideTimer.start() + } + } + + Item { + id: dock + width: runningAppsRow.width + height: parent.height - 10 + anchors.centerIn: parent + + NTooltip { + id: appTooltip + visible: false + positionAbove: true + } + + function getAppIcon(toplevel: Toplevel): string { + if (!toplevel) + return "" + let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.appId, true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true) + if (!icon) + icon = Quickshell.iconPath(toplevel.title, true) + return icon || Quickshell.iconPath("application-x-executable", true) + } + + Row { + id: runningAppsRow + spacing: 8 + height: parent.height + anchors.centerIn: parent + + Repeater { + model: ToplevelManager ? ToplevelManager.toplevels : null + + delegate: Rectangle { + id: appButton + width: 36 + height: 36 + radius: 18 + color: "transparent" + + property bool isActive: ToplevelManager.activeToplevel + && ToplevelManager.activeToplevel === modelData + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property string appTitle: modelData ? modelData.title : "" + + Behavior on color { + ColorAnimation { + duration: 150 + } + } + + Image { + id: appIcon + width: 28 + height: 28 + anchors.centerIn: parent + source: dock.getAppIcon(modelData) + visible: source.toString() !== "" + smooth: false + mipmap: false + antialiasing: false + fillMode: Image.PreserveAspectFit + } + + Text { + anchors.centerIn: parent + visible: !appIcon.visible + text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary + } + + MouseArea { + id: appMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onEntered: { + anyAppHovered = true + const appName = appButton.appTitle || appButton.appId || "Unknown" + appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName + appTooltip.target = appButton + appTooltip.isVisible = true + if (autoHide) { + showTimer.stop() + hideTimer.stop() + hidden = false + } + } + + onExited: { + anyAppHovered = false + appTooltip.hide() + if (autoHide && !dockHovered && !contextMenuVisible) + hideTimer.start() + } + + onClicked: function (mouse) { + if (mouse.button === Qt.MiddleButton && modelData?.close) { + modelData.close() + } + if (mouse.button === Qt.LeftButton && modelData?.activate) { + modelData.activate() + } + if (mouse.button === Qt.RightButton) { + appTooltip.hide() + contextMenuTarget = appButton + contextMenuToplevel = modelData + contextMenuVisible = true + } + } + } + + Rectangle { + visible: isActive + width: 20 + height: 3 + color: Colors.accentPrimary + radius: 1.5 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 + } + } + } + } + } + } + + // Context Menu + PanelWindow { + id: contextMenuWindow + visible: contextMenuVisible + screen: dockWindow.screen exclusionMode: ExclusionMode.Ignore anchors.bottom: true anchors.left: true anchors.right: true - focusable: false color: "transparent" - implicitHeight: 60 - - // Timer for auto-hide delay - Timer { - id: hideTimer - interval: hideDelay - onTriggered: if (autoHide && !dockHovered && !anyAppHovered) hidden = true - } - - // Timer for show delay - Timer { - id: showTimer - interval: showDelay - onTriggered: hidden = false - } - - - - // Behavior for smooth hide/show animations - Behavior on margins.bottom { - NumberAnimation { - duration: hidden ? hideAnimationDuration : showAnimationDuration - easing.type: Easing.InOutQuad - } - } - - // Mouse area at screen bottom to detect entry and keep dock visible - MouseArea { - id: screenEdgeMouseArea - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 10 - hoverEnabled: true - propagateComposedEvents: true - - onEntered: if (autoHide && hidden) showTimer.start() - onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) hideTimer.start() - } - - margins.bottom: hidden ? -(fullHeight - peekHeight) : 0 + focusable: false MouseArea { - anchors.fill: parent - enabled: contextMenuVisible - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - } + anchors.fill: parent + onClicked: { + contextMenuVisible = false + contextMenuTarget = null + contextMenuToplevel = null + hidden = true // Hide dock when context menu closes + } } Rectangle { - id: dockContainer - width: dock.width + 40 - height: 50 - color: Colors.backgroundSecondary - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - topLeftRadius: 20 - topRightRadius: 20 + id: contextMenuContainer + width: 80 + height: 32 + radius: 8 + color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary + border.color: Colors.outline + border.width: 1 - MouseArea { - id: dockMouseArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true + x: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + let xPos = pos.x + (contextMenuTarget.width - width) / 2 + return Math.max(0, Math.min(xPos, dockWindow.width - width)) + } - onEntered: { - dockHovered = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - onExited: { - dockHovered = false - if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() - } + y: { + if (!contextMenuTarget) + return 0 + const pos = contextMenuTarget.mapToItem(null, 0, 0) + return pos.y - height + 32 + } + + Text { + anchors.centerIn: parent + text: "Close" + font.pixelSize: 14 + color: Colors.textPrimary + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (contextMenuToplevel?.close) + contextMenuToplevel.close() + contextMenuVisible = false + hidden = true } + } - Item { - id: dock - width: runningAppsRow.width - height: parent.height - 10 - anchors.centerIn: parent - - NTooltip { - id: appTooltip - visible: false - positionAbove: true - } - - function getAppIcon(toplevel: Toplevel): string { - if (!toplevel) return ""; - let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.appId, true); - if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true); - if (!icon) icon = Quickshell.iconPath(toplevel.title, true); - return icon || Quickshell.iconPath("application-x-executable", true); - } - - Row { - id: runningAppsRow - spacing: 8 - height: parent.height - anchors.centerIn: parent - - Repeater { - model: ToplevelManager ? ToplevelManager.toplevels : null - - delegate: Rectangle { - id: appButton - width: 36 - height: 36 - radius: 18 - color:"transparent" - - property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData - property bool hovered: appMouseArea.containsMouse - property string appId: modelData ? modelData.appId : "" - property string appTitle: modelData ? modelData.title : "" - - Behavior on color { ColorAnimation { duration: 150 } } - - Image { - id: appIcon - width: 28 - height: 28 - anchors.centerIn: parent - source: dock.getAppIcon(modelData) - visible: source.toString() !== "" - smooth: false - mipmap: false - antialiasing: false - fillMode: Image.PreserveAspectFit - } - - Text { - anchors.centerIn: parent - visible: !appIcon.visible - text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?" - font.pixelSize: 14 - font.bold: true - color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary - } - - MouseArea { - id: appMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - - onEntered: { - anyAppHovered = true - const appName = appButton.appTitle || appButton.appId || "Unknown" - appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName - appTooltip.target = appButton - appTooltip.isVisible = true - if (autoHide) { - showTimer.stop() - hideTimer.stop() - hidden = false - } - } - - onExited: { - anyAppHovered = false - appTooltip.hide() - if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start() - } - - onClicked: function(mouse) { - if (mouse.button === Qt.MiddleButton && modelData?.close) { - modelData.close() - } - if (mouse.button === Qt.LeftButton && modelData?.activate) { - modelData.activate() - } - if (mouse.button === Qt.RightButton) { - appTooltip.hide() - contextMenuTarget = appButton - contextMenuToplevel = modelData - contextMenuVisible = true - } - } - } - - Rectangle { - visible: isActive - width: 20 - height: 3 - color: Colors.accentPrimary - radius: 1.5 - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottomMargin: 2 - } - } - } - } - + // Animation + scale: contextMenuVisible ? 1 : 0.9 + opacity: contextMenuVisible ? 1 : 0 + transformOrigin: Item.Bottom + Behavior on scale { + NumberAnimation { + duration: 150 + easing.type: Easing.OutBack } + } + + Behavior on opacity { + NumberAnimation { + duration: 100 + } + } } - - // Context Menu - PanelWindow { - id: contextMenuWindow - visible: contextMenuVisible - screen: dockWindow.screen - exclusionMode: ExclusionMode.Ignore - anchors.bottom: true - anchors.left: true - anchors.right: true - color: "transparent" - focusable: false - - MouseArea { - anchors.fill: parent - onClicked: { - contextMenuVisible = false - contextMenuTarget = null - contextMenuToplevel = null - hidden = true // Hide dock when context menu closes - } - } - - - - Rectangle { - id: contextMenuContainer - width: 80 - height: 32 - radius: 8 - color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary - border.color: Colors.outline - border.width: 1 - - x: { - if (!contextMenuTarget) return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - let xPos = pos.x + (contextMenuTarget.width - width) / 2 - return Math.max(0, Math.min(xPos, dockWindow.width - width)) - } - - y: { - if (!contextMenuTarget) return 0 - const pos = contextMenuTarget.mapToItem(null, 0, 0) - return pos.y - height + 32 - } - - Text { - anchors.centerIn: parent - text: "Close" - font.pixelSize: 14 - color: Colors.textPrimary - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (contextMenuToplevel?.close) contextMenuToplevel.close() - contextMenuVisible = false - hidden = true - } - } - - // Animation - scale: contextMenuVisible ? 1 : 0.9 - opacity: contextMenuVisible ? 1 : 0 - transformOrigin: Item.Bottom - - Behavior on scale { - NumberAnimation { - duration: 150 - easing.type: Easing.OutBack - } - } - - Behavior on opacity { - NumberAnimation { duration: 100 } - } - } - } + } } + } } + } } -} -} diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 31f2fc9..0adb9e0 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -540,7 +540,7 @@ WlSessionLock { Text { anchors.centerIn: parent - text: lock.authenticating ? "EXECUTING..." : "EXECUTE" + text: lock.authenticating ? "EXECUTING" : "EXECUTE" color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary font.family: "DejaVu Sans Mono" font.pointSize: Style.fontSizeMedium diff --git a/Modules/Settings/Tabs/About.qml b/Modules/Settings/Tabs/About.qml index 258c7e2..bafd0d0 100644 --- a/Modules/Settings/Tabs/About.qml +++ b/Modules/Settings/Tabs/About.qml @@ -236,7 +236,8 @@ ColumnLayout { } NText { - text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") + text: (modelData.contributions || 0) + " " + ((modelData.contributions + || 0) === 1 ? "commit" : "commits") font.pointSize: Style.fontSizeSmall * scaling color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary } diff --git a/Services/Clipboard.qml b/Services/Clipboard.qml index 0d82e37..c66b3ce 100644 --- a/Services/Clipboard.qml +++ b/Services/Clipboard.qml @@ -6,134 +6,133 @@ import Quickshell.Io import qs.Services Singleton { - id: root + id: root - property var history: [] - property bool initialized: false + property var history: [] + property bool initialized: false - // Internal state - property bool _enabled: true + // Internal state + property bool _enabled: true - Timer { - interval: 1000 - repeat: true - running: root._enabled - onTriggered: root.refresh() - } + Timer { + interval: 1000 + repeat: true + running: root._enabled + onTriggered: root.refresh() + } - // Detect current clipboard types (text/image) - Process { - id: typeProcess - property bool isLoading: false - property var currentTypes: [] + // Detect current clipboard types (text/image) + Process { + id: typeProcess + property bool isLoading: false + property var currentTypes: [] - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - currentTypes = String(stdout.text).trim().split('\n').filter(t => t) + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + currentTypes = String(stdout.text).trim().split('\n').filter(t => t) - const imageType = currentTypes.find(t => t.startsWith('image/')) - if (imageType) { - imageProcess.mimeType = imageType - imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] - imageProcess.running = true - } else { - textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] - textProcess.running = true - } - } else { - typeProcess.isLoading = false - } + const imageType = currentTypes.find(t => t.startsWith('image/')) + if (imageType) { + imageProcess.mimeType = imageType + imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] + imageProcess.running = true + } else { + textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] + textProcess.running = true } - - stdout: StdioCollector {} + } else { + typeProcess.isLoading = false + } } - // Read image data - Process { - id: imageProcess - property string mimeType: "" + stdout: StdioCollector {} + } - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const base64 = stdout.text.trim() - if (base64) { - const entry = { - type: 'image', - mimeType: mimeType, - data: `data:${mimeType};base64,${base64}`, - timestamp: new Date().getTime() - } + // Read image data + Process { + id: imageProcess + property string mimeType: "" - const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) - if (!exists) { - root.history = [entry, ...root.history].slice(0, 20) - } - } - } + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const base64 = stdout.text.trim() + if (base64) { + const entry = { + "type": 'image', + "mimeType": mimeType, + "data": `data:${mimeType};base64,${base64}`, + "timestamp": new Date().getTime() + } - if (!textProcess.isLoading) { - root.initialized = true - } - typeProcess.isLoading = false + const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) + if (!exists) { + root.history = [entry, ...root.history].slice(0, 20) + } } + } - stdout: StdioCollector {} + if (!textProcess.isLoading) { + root.initialized = true + } + typeProcess.isLoading = false } - // Read text data - Process { - id: textProcess - property bool isLoading: false + stdout: StdioCollector {} + } - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text).trim() - if (content) { - const entry = { - type: 'text', - content: content, - timestamp: new Date().getTime() - } + // Read text data + Process { + id: textProcess + property bool isLoading: false - const exists = root.history.find(item => { - if (item.type === 'text') { - return item.content === content - } - return item === content - }) + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text).trim() + if (content) { + const entry = { + "type": 'text', + "content": content, + "timestamp": new Date().getTime() + } - if (!exists) { - const newHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - type: 'text', - content: item, - timestamp: new Date().getTime() - } - } - return item - }) + const exists = root.history.find(item => { + if (item.type === 'text') { + return item.content === content + } + return item === content + }) - root.history = [entry, ...newHistory].slice(0, 20) - } - } - } else { - textProcess.isLoading = false - } + if (!exists) { + const newHistory = root.history.map(item => { + if (typeof item === 'string') { + return { + "type": 'text', + "content": item, + "timestamp": new Date().getTime() + } + } + return item + }) - root.initialized = true - typeProcess.isLoading = false + root.history = [entry, ...newHistory].slice(0, 20) + } } + } else { + textProcess.isLoading = false + } - stdout: StdioCollector {} + root.initialized = true + typeProcess.isLoading = false } - function refresh() { - if (!typeProcess.isLoading && !textProcess.isLoading) { - typeProcess.isLoading = true - typeProcess.command = ["wl-paste", "-l"] - typeProcess.running = true - } + stdout: StdioCollector {} + } + + function refresh() { + if (!typeProcess.isLoading && !textProcess.isLoading) { + typeProcess.isLoading = true + typeProcess.command = ["wl-paste", "-l"] + typeProcess.running = true } + } } - diff --git a/Services/GitHub.qml b/Services/GitHub.qml index 1b74f4d..6404578 100644 --- a/Services/GitHub.qml +++ b/Services/GitHub.qml @@ -82,10 +82,10 @@ Singleton { data.timestamp = Time.timestamp console.log("[GitHub] Saving data to cache file:", githubDataFile) console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length) - + // Ensure cache directory exists Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) - + Qt.callLater(() => { // Use direct ID reference to the FileView githubDataFileView.writeAdapter() @@ -147,7 +147,8 @@ Singleton { console.log("[GitHub] Raw contributors response length:", response ? response.length : 0) if (response && response.trim()) { const data = JSON.parse(response) - console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array") + console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", + Array.isArray(data) ? data.length : "not array") root.data.contributors = data || [] root.contributors = root.data.contributors console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) diff --git a/Services/Wallpapers.qml b/Services/Wallpapers.qml index 300db58..0b3201f 100644 --- a/Services/Wallpapers.qml +++ b/Services/Wallpapers.qml @@ -160,6 +160,7 @@ Singleton { running: false stdout: StdioCollector { onStreamFinished: { + // console.log(this.text) } }