Launcher: wip image preview

This commit is contained in:
LemmyCook 2025-09-03 09:22:27 -04:00
parent ded133d164
commit 132dbce3a3
3 changed files with 55 additions and 53 deletions

View file

@ -231,9 +231,6 @@ NPanel {
clip: true clip: true
cacheBuffer: resultsList.height * 2 cacheBuffer: resultsList.height * 2
//boundsBehavior: Flickable.StopAtBounds
// maximumFlickVelocity: 2500
// flickDeceleration: 2000
onCurrentIndexChanged: { onCurrentIndexChanged: {
cancelFlick() cancelFlick()
if (currentIndex >= 0) { if (currentIndex >= 0) {
@ -245,15 +242,26 @@ NPanel {
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded
} }
// Replace the delegate in Launcher.qml's ListView with this enhanced version:
delegate: Rectangle { delegate: Rectangle {
id: entry id: entry
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex) property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
property int badgeSize: Style.baseWidgetSize * 1.75 * scaling property int badgeSize: Style.baseWidgetSize * 1.75 * scaling
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !CliphistService.getImageData(currentClipboardId)) {
CliphistService.decodeToDataUrl(currentClipboardId, modelData.mime, null)
}
}
width: resultsList.width - Style.marginS * scaling width: resultsList.width - Style.marginS * scaling
height: badgeSize + Style.marginM * 2 *scaling height: badgeSize + Style.marginM * 2 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: entry.isSelected ? Color.mTertiary : Color.mSurface color: entry.isSelected ? Color.mTertiary : Color.mSurface
@ -282,8 +290,19 @@ NPanel {
id: imagePreview id: imagePreview
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 * scaling anchors.margins: 2 * scaling
visible: modelData.isImage && modelData.imageSource visible: modelData.isImage
source: modelData.imageSource || ""
// This property creates a dependency on the service's revision counter
readonly property int _rev: CliphistService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated
// when the cache is updated by the service.
source: {
_rev
return CliphistService.getImageData(modelData.clipboardId) || ""
}
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
smooth: true smooth: true
mipmap: true mipmap: true
@ -307,7 +326,6 @@ NPanel {
// Error fallback // Error fallback
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) { if (status === Image.Error) {
// Fall back to icon
iconLoader.visible = true iconLoader.visible = true
imagePreview.visible = false imagePreview.visible = false
} }
@ -319,7 +337,8 @@ NPanel {
id: iconLoader id: iconLoader
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
visible: !modelData.isImage || !modelData.imageSource || imagePreview.status === Image.Error
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible active: visible
sourceComponent: Component { sourceComponent: Component {
@ -391,23 +410,6 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
visible: text !== "" 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

@ -13,6 +13,17 @@ QtObject {
// Plugin capabilities // Plugin capabilities
property bool handleSearch: false // Don't handle regular search property bool handleSearch: false // Don't handle regular search
// Connections {
// target: CliphistService
// // Use the function syntax for on<SignalName>
// function onListCompleted() {
// // Only refresh if the clipboard plugin is active
// if (launcher && launcher.activePlugin === root) {
// launcher.updateResults()
// }
// }
// }
// Initialize plugin // Initialize plugin
function init() { function init() {
Logger.log("ClipboardPlugin", "Initialized") Logger.log("ClipboardPlugin", "Initialized")
@ -121,40 +132,22 @@ QtObject {
return results return results
} }
// Helper: Format image clipboard entry with actual image data // Helper: Format image clipboard entry
function formatImageEntry(item) { function formatImageEntry(item) {
const meta = parseImageMeta(item.preview) const meta = parseImageMeta(item.preview)
// Get the actual image data/path from the clipboard service // The launcher's delegate will now be responsible for fetching the image data.
// This assumes CliphistService provides either a path or base64 data // This function's role is to provide the necessary metadata for that request.
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 { return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image", "name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data", "description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
"icon": "image", "icon": "image",
"isImage": true, "isImage": true,
"imageSource": imageData,
"imageWidth": meta ? meta.w : 0, "imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0, "imageHeight": meta ? meta.h : 0,
"clipboardId"// Add clipboard item ID for potential async loading "clipboardId"// Provide the ID and mime type for the delegate to make an async request
: item.id : item.id,
"mime": item.mime
} }
} }
@ -185,7 +178,7 @@ QtObject {
return { return {
"name": title, "name": title,
"description": description, "description": description,
"icon": "description", "icon": "text-x-generic",
"isImage": false "isImage": false
} }
} }

View file

@ -39,6 +39,8 @@ Singleton {
property string _b64CurrentMime: "" property string _b64CurrentMime: ""
property string _b64CurrentId: "" property string _b64CurrentId: ""
signal listCompleted()
// Check if cliphist is available // Check if cliphist is available
Component.onCompleted: { Component.onCompleted: {
checkCliphistAvailability() checkCliphistAvailability()
@ -147,6 +149,9 @@ Singleton {
}) })
items = parsed items = parsed
loading = false loading = false
// Emit the signal for subscribers
root.listCompleted()
} }
} }
@ -285,10 +290,12 @@ Singleton {
} }
function getImageData(id) { function getImageData(id) {
if (id === undefined) {
return null
}
return root.imageDataById[id] return root.imageDataById[id]
} }
function _startNextB64() { function _startNextB64() {
if (root._b64Queue.length === 0 || !root.cliphistAvailable) if (root._b64Queue.length === 0 || !root.cliphistAvailable)
return return