feat: Add image preview logic

This commit is contained in:
loner
2025-11-18 03:44:34 +08:00
parent ca89a0dc35
commit 79f79e0cff
4 changed files with 72 additions and 41 deletions

View File

@@ -13,12 +13,15 @@ import qs.Widgets
SmartPanel {
id: root
readonly property real clipListRatio: 0.6 // 60% for the list
readonly property real clipPreviewRatio: 0.4 // 40% for the preview
readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0
// Panel configuration
preferredWidth: Math.round(700 * Style.uiScaleRatio) // Base width when both are shown, scaled appropriately
// Panel configuration - use proportional widths
readonly property real clipListRatio: 0.6 // 60% for the list
readonly property real clipPreviewRatio: 0.4 // 40% for the preview
readonly property int totalBaseWidth: Math.round(600 * Style.uiScaleRatio) // Base width when no preview
readonly property int totalExpandedWidth: Math.round(900 * Style.uiScaleRatio) // Width when preview active
preferredWidth: previewActive ? totalExpandedWidth : totalBaseWidth
preferredHeight: Math.round(600 * Style.uiScaleRatio)
preferredWidthRatio: 0.3
preferredHeightRatio: 0.5
@@ -348,10 +351,9 @@ SmartPanel {
ColumnLayout {
id: leftPane
Layout.fillHeight: true
Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active
Layout.preferredWidth: root.previewActive ?
Math.round(root.clipListRatio * (root.preferredWidth - (Style.marginL * 2))) :
(root.preferredWidth - (Style.marginL * 2))
(root.preferredWidth - (Style.marginL * 2)) // Use full width when preview not active
anchors.margins: Style.marginL
spacing: Style.marginM
@@ -659,7 +661,6 @@ SmartPanel {
// --- Right Pane (Preview) ---
Item {
Layout.fillHeight: true
Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active
Layout.preferredWidth: root.previewActive ?
Math.round(root.clipPreviewRatio * (root.preferredWidth - (Style.marginL * 2))) : 0
visible: root.previewActive

View File

@@ -204,12 +204,9 @@ Item {
return results;
}
// Helper: Format image clipboard entry
function formatImageEntry(item) {
const meta = parseImageMeta(item.preview);
// The launcher's delegate will now be responsible for fetching the image data.
// This function's role is to provide the necessary metadata for that request.
return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
@@ -223,18 +220,15 @@ Item {
};
}
// Helper: Format text clipboard entry with preview
function formatTextEntry(item) {
const preview = (item.preview || "").trim();
const lines = preview.split('\n').filter(l => l.trim());
// Use first line as title, limit length
let title = lines[0] || "Empty text";
if (title.length > 60) {
title = title.substring(0, 57) + "...";
}
// Use second line or character count as description
let description = "";
if (lines.length > 1) {
description = lines[1];
@@ -257,7 +251,6 @@ Item {
};
}
// Helper: Parse image metadata from preview string
function parseImageMeta(preview) {
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i;
const match = (preview || "").match(re);
@@ -274,8 +267,6 @@ Item {
};
}
// Public method to get image data for a clipboard item
// This can be called by the launcher when rendering
function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
}

View File

@@ -3,29 +3,63 @@ import QtQuick.Layouts
import QtQuick.Controls
import qs.Commons
import qs.Widgets
import qs.Services.Keyboard // Import ClipboardService
import qs.Services.Keyboard
Item {
id: previewPanel
property var currentItem: null
property string fullContent: ""
property string imageDataUrl: ""
property bool loadingFullContent: false
property bool isImageContent: false
implicitHeight: contentColumn.implicitHeight + Style.marginL * 2
Connections {
target: previewPanel
function onCurrentItemChanged() {
fullContent = ""; // Clear previous content
fullContent = "";
imageDataUrl = "";
loadingFullContent = false;
isImageContent = currentItem && currentItem.isImage;
if (currentItem && currentItem.clipboardId) {
loadingFullContent = true;
ClipboardService.decode(currentItem.clipboardId, function(content) {
fullContent = content;
loadingFullContent = false;
});
if (isImageContent) {
imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || "";
loadingFullContent = !imageDataUrl;
if (!imageDataUrl && currentItem.mime) {
ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null);
}
} else {
loadingFullContent = true;
ClipboardService.decode(currentItem.clipboardId, function(content) {
fullContent = content;
loadingFullContent = false;
});
}
}
}
}
readonly property int _rev: ClipboardService.revision
Timer {
id: imageUpdateTimer
interval: 200
running: currentItem && currentItem.isImage && imageDataUrl === ""
repeat: currentItem && currentItem.isImage && imageDataUrl === ""
onTriggered: {
if (currentItem && currentItem.clipboardId) {
const newData = ClipboardService.getImageData(currentItem.clipboardId) || "";
if (newData !== imageDataUrl) {
imageDataUrl = newData;
if (newData) {
loadingFullContent = false;
}
}
}
}
}
@@ -55,7 +89,7 @@ Item {
Layout.fillWidth: true
}
Rectangle { // Frame around the content
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant || "#e0e0e0"
@@ -63,7 +97,6 @@ Item {
border.width: 1
radius: Style.radiusS
// Loading indicator
BusyIndicator {
anchors.centerIn: parent
running: loadingFullContent
@@ -72,18 +105,27 @@ Item {
height: width
}
ScrollView {
Layout.fillHeight: true // Explicitly fill height
Item {
anchors.fill: parent
anchors.margins: Style.marginS
clip: true
visible: !loadingFullContent // Hide scrollview while loading
TextArea {
Layout.fillHeight: true // Explicitly fill height
text: fullContent // Bind to fullContent
readOnly: true
wrapMode: Text.Wrap
NImageRounded {
anchors.fill: parent
imagePath: imageDataUrl
visible: isImageContent && !loadingFullContent && imageDataUrl !== ""
imageRadius: Style.radiusS
}
ScrollView {
anchors.fill: parent
clip: true
visible: !isImageContent && !loadingFullContent
TextArea {
text: fullContent
readOnly: true
wrapMode: Text.Wrap
}
}
}
}

View File

@@ -30,12 +30,14 @@ Rectangle {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
visible: false
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
fillMode: Image.PreserveAspectFit
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
onStatusChanged: root.statusChanged(status)
}
@@ -51,17 +53,14 @@ Rectangle {
format: ShaderEffectSource.RGBA
}
// Use custom property names to avoid conflicts with final properties
property real itemWidth: root.width
property real itemHeight: root.height
property real cornerRadius: root.radius
property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false
blending: true
// Make sure the background is transparent
Rectangle {
id: background
anchors.fill: parent
@@ -70,7 +69,6 @@ Rectangle {
}
}
// Fallback icon
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
anchors.centerIn: parent
@@ -83,7 +81,6 @@ Rectangle {
}
}
// Border
Rectangle {
anchors.fill: parent
radius: parent.radius