Launcher: wip image preview

This commit is contained in:
LemmyCook 2025-09-03 08:44:10 -04:00
parent 7548ffc191
commit ded133d164
5 changed files with 159 additions and 16 deletions

View file

@ -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
// }
}
}

View file

@ -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) {

View file

@ -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 () {}
}]
}

View file

@ -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
}
}

View file

@ -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())
}
}