mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
feat: Add image preview logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user