diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index fcf94bb..b26b38d 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -245,13 +245,15 @@ NPanel { policy: ScrollBar.AsNeeded } + // Replace the delegate in Launcher.qml's ListView with this enhanced version: delegate: Rectangle { id: entry property bool isSelected: mouseArea.containsMouse || (index === selectedIndex) + property int badgeSize: Style.baseWidgetSize * 1.75 * scaling width: resultsList.width - Style.marginS * scaling - height: 65 * scaling + height: badgeSize + Style.marginM * 2 *scaling radius: Style.radiusM * scaling color: entry.isSelected ? Color.mTertiary : Color.mSurface @@ -267,33 +269,107 @@ NPanel { anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - // Icon badge + // Icon badge or Image preview Rectangle { - Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling - Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling - radius: Style.radiusS * scaling + Layout.preferredWidth: badgeSize + Layout.preferredHeight: badgeSize + radius: Style.radiusM * scaling color: Color.mSurfaceVariant + clip: true - IconImage { + // Image preview for clipboard images + Image { + id: imagePreview anchors.fill: parent - anchors.margins: Style.marginXS * scaling - source: modelData.icon ? Icons.iconFromName(modelData.icon, "application-x-executable") : "" - visible: modelData.icon && source !== "" + anchors.margins: 2 * scaling + visible: modelData.isImage && modelData.imageSource + source: modelData.imageSource || "" + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true asynchronous: true + cache: true + + // Loading indicator + Rectangle { + anchors.fill: parent + visible: parent.status === Image.Loading + color: Color.mSurfaceVariant + + BusyIndicator { + anchors.centerIn: parent + running: true + width: Style.baseWidgetSize * 0.5 * scaling + height: width + } + } + + // Error fallback + onStatusChanged: { + if (status === Image.Error) { + // Fall back to icon + iconLoader.visible = true + imagePreview.visible = false + } + } } - // Fallback if no icon + // Icon fallback + Loader { + id: iconLoader + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + visible: !modelData.isImage || !modelData.imageSource || imagePreview.status === Image.Error + active: visible + + sourceComponent: Component { + IconImage { + anchors.fill: parent + source: modelData.icon ? Icons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" + asynchronous: true + } + } + } + + // Fallback text if no icon and no image NText { anchors.centerIn: parent - visible: !modelData.icon || parent.children[0].source === "" + visible: !imagePreview.visible && !iconLoader.visible text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Color.mOnPrimary } + + // Image type indicator overlay + Rectangle { + visible: modelData.isImage && imagePreview.visible + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 * scaling + width: formatLabel.width + 6 * scaling + height: formatLabel.height + 2 * scaling + radius: 2 * scaling + color: Qt.rgba(0, 0, 0, 0.7) + + NText { + id: formatLabel + anchors.centerIn: parent + text: { + if (!modelData.isImage) + return "" + const desc = modelData.description || "" + const parts = desc.split(" • ") + return parts[0] || "IMG" + } + font.pointSize: Style.fontSizeXXS * scaling + color: Color.mPrimary + } + } } - // Text + // Text content ColumnLayout { Layout.fillWidth: true spacing: 0 * scaling @@ -315,6 +391,23 @@ NPanel { Layout.fillWidth: true visible: text !== "" } + + // // Show text preview for text items if space allows + // NText { + // visible: !modelData.isImage && modelData.fullText && modelData.fullText.length > 100 + // text: { + // if (!modelData.fullText) return "" + // const preview = modelData.fullText.substring(0, 150).replace(/\n/g, " ") + // return preview + (modelData.fullText.length > 150 ? "..." : "") + // } + // font.pointSize: Style.fontSizeXS * scaling + // color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant + // opacity: 0.7 + // elide: Text.ElideRight + // maximumLineCount: 2 + // wrapMode: Text.WordWrap + // Layout.fillWidth: true + // } } } diff --git a/Modules/Launcher/Plugins/ApplicationsPlugin.qml b/Modules/Launcher/Plugins/ApplicationsPlugin.qml index 29df787..2bf45a9 100644 --- a/Modules/Launcher/Plugins/ApplicationsPlugin.qml +++ b/Modules/Launcher/Plugins/ApplicationsPlugin.qml @@ -80,6 +80,7 @@ QtObject { "name": app.name || "Unknown", "description": app.genericName || app.comment || "", "icon": app.icon || "application-x-executable", + "isImage": false, "onActivate": function () { Logger.log("ApplicationsPlugin", `Launching: ${app.name}`) if (app.execute) { diff --git a/Modules/Launcher/Plugins/CalculatorPlugin.qml b/Modules/Launcher/Plugins/CalculatorPlugin.qml index 281e0e5..a9578dd 100644 --- a/Modules/Launcher/Plugins/CalculatorPlugin.qml +++ b/Modules/Launcher/Plugins/CalculatorPlugin.qml @@ -17,6 +17,7 @@ QtObject { "name": ">calc", "description": "Calculator - evaluate mathematical expressions", "icon": "accessories-calculator", + "isImage": false, "onActivate": function () { launcher.setSearchText(">calc ") } @@ -39,6 +40,7 @@ QtObject { "name": "Calculator", "description": "Enter a mathematical expression", "icon": "accessories-calculator", + "isImage": false, "onActivate": function () {} }] } @@ -50,6 +52,7 @@ QtObject { "name": AdvancedMath.formatResult(result), "description": `${expression} = ${result}`, "icon": "accessories-calculator", + "isImage": false, "onActivate": function () { // Copy result to clipboard if service available // if (typeof ClipboardService !== 'undefined') { @@ -63,6 +66,7 @@ QtObject { "name": "Error", "description": error.message || "Invalid expression", "icon": "dialog-error", + "isImage": false, "onActivate": function () {} }] } diff --git a/Modules/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Launcher/Plugins/ClipboardPlugin.qml index 2bbf6aa..d0e2269 100644 --- a/Modules/Launcher/Plugins/ClipboardPlugin.qml +++ b/Modules/Launcher/Plugins/ClipboardPlugin.qml @@ -35,6 +35,7 @@ QtObject { "name": ">clip", "description": "Search clipboard history", "icon": "content_paste", + "isImage": false, "onActivate": function () { launcher.setSearchText(">clip ") } @@ -42,6 +43,7 @@ QtObject { "name": ">clip clear", "description": "Clear all clipboard history", "icon": "delete_sweep", + "isImage": false, "onActivate": function () { CliphistService.wipeAll() launcher.close() @@ -110,6 +112,7 @@ QtObject { "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", "icon": "content_paste_off", + "isImage": false, "onActivate": function () {// Do nothing } }) @@ -118,18 +121,44 @@ QtObject { return results } - // Helper: Format image clipboard entry + // Helper: Format image clipboard entry with actual image data function formatImageEntry(item) { const meta = parseImageMeta(item.preview) + // Get the actual image data/path from the clipboard service + // This assumes CliphistService provides either a path or base64 data + let imageData = null + + // Try to get image data from the service + // Method 1: If the service provides a file path + if (item.imagePath) { + imageData = "file://" + item.imagePath + } // Method 2: If the service provides base64 data + else if (item.imageData) { + imageData = ClipHistService.getImageData(item.id) + + // "data:" + (item.mime || "image/png") + ";base64," + item.imageData + } // Method 3: If we need to fetch it from the service + + // else if (item.id) { + // // Some clipboard services might require fetching the image separately + // // This would depend on your CliphistService implementation + // imageData = CliphistService.getImageData ? CliphistService.getImageData(item.id) : null + // } return { "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", - "icon": "image" + "icon": "image", + "isImage": true, + "imageSource": imageData, + "imageWidth": meta ? meta.w : 0, + "imageHeight": meta ? meta.h : 0, + "clipboardId"// Add clipboard item ID for potential async loading + : item.id } } - // Helper: Format text clipboard entry + // Helper: Format text clipboard entry with preview function formatTextEntry(item) { const preview = (item.preview || "").trim() const lines = preview.split('\n').filter(l => l.trim()) @@ -156,7 +185,8 @@ QtObject { return { "name": title, "description": description, - "icon": "description" + "icon": "description", + "isImage": false } } @@ -176,4 +206,10 @@ QtObject { "h": Number(match[4]) } } + + // Public method to get image data for a clipboard item + // This can be called by the launcher when rendering + function getImageForItem(clipboardId) { + return CliphistService.getImageData ? CliphistService.getImageData(clipboardId) : null + } } diff --git a/Services/CliphistService.qml b/Services/CliphistService.qml index 87f14f3..60b9a34 100644 --- a/Services/CliphistService.qml +++ b/Services/CliphistService.qml @@ -284,6 +284,11 @@ Singleton { } } + function getImageData(id) { + return root.imageDataById[id] + } + + function _startNextB64() { if (root._b64Queue.length === 0 || !root.cliphistAvailable) return @@ -316,7 +321,11 @@ Singleton { if (!root.cliphistAvailable) { return } + Quickshell.execDetached(["cliphist", "wipe"]) + + revision++ + Qt.callLater(() => list()) } }