mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
370 lines
11 KiB
QML
370 lines
11 KiB
QML
import QtQuick
|
||
import Quickshell
|
||
import qs.Commons
|
||
import qs.Services.Keyboard
|
||
|
||
Item {
|
||
id: root
|
||
|
||
// Provider metadata
|
||
property string name: I18n.tr("launcher.providers.clipboard")
|
||
property var launcher: null
|
||
property string iconMode: Settings.data.appLauncher.iconMode
|
||
property string supportedLayouts: "list" // List view for clipboard content
|
||
property bool wrapNavigation: false // Don't wrap at end of list
|
||
|
||
// Provider capabilities
|
||
property bool handleSearch: false // Don't handle regular search
|
||
|
||
// Preview support
|
||
property bool hasPreview: true
|
||
property string previewComponentPath: "./ClipboardPreview.qml"
|
||
|
||
// Image handling - expose revision for reactive updates in delegates
|
||
readonly property int imageRevision: ClipboardService.revision
|
||
|
||
// Internal state
|
||
property bool isWaitingForData: false
|
||
property bool gotResults: false
|
||
property string lastSearchText: ""
|
||
|
||
// Listen for clipboard data updates
|
||
Connections {
|
||
target: ClipboardService
|
||
function onListCompleted() {
|
||
if (gotResults && (lastSearchText === searchText)) {
|
||
// Do not update results after the first fetch.
|
||
// This will avoid the list resetting every 2seconds when the service updates.
|
||
return;
|
||
}
|
||
// Refresh results if we're waiting for data or if clipboard plugin is active
|
||
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
|
||
isWaitingForData = false;
|
||
gotResults = true;
|
||
if (launcher) {
|
||
launcher.updateResults();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize provider
|
||
function init() {
|
||
Logger.i("ClipboardProvider", "Initialized");
|
||
// Pre-load clipboard data if service is active
|
||
if (ClipboardService.active) {
|
||
ClipboardService.list(100);
|
||
}
|
||
}
|
||
|
||
// Called when launcher opens
|
||
function onOpened() {
|
||
isWaitingForData = true;
|
||
gotResults = false;
|
||
lastSearchText = "";
|
||
|
||
// Refresh clipboard history when launcher opens
|
||
if (ClipboardService.active) {
|
||
ClipboardService.list(100);
|
||
}
|
||
}
|
||
|
||
// Check if this provider handles the command
|
||
function handleCommand(searchText) {
|
||
return searchText.startsWith(">clip");
|
||
}
|
||
|
||
// Return available commands when user types ">"
|
||
function commands() {
|
||
return [
|
||
{
|
||
"name": ">clip",
|
||
"description": I18n.tr("launcher.providers.clipboard-search-description"),
|
||
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {
|
||
launcher.setSearchText(">clip ");
|
||
}
|
||
},
|
||
{
|
||
"name": ">clip clear",
|
||
"description": I18n.tr("launcher.providers.clipboard-clear-description"),
|
||
"icon": iconMode === "tabler" ? "trash" : "user-trash",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {
|
||
ClipboardService.wipeAll();
|
||
launcher.close();
|
||
}
|
||
}
|
||
];
|
||
}
|
||
|
||
// Get search results
|
||
function getResults(searchText) {
|
||
if (!searchText.startsWith(">clip")) {
|
||
return [];
|
||
}
|
||
|
||
lastSearchText = searchText;
|
||
const results = [];
|
||
const query = searchText.slice(5).trim();
|
||
|
||
// Check if clipboard service is not active
|
||
if (!ClipboardService.active) {
|
||
return [
|
||
{
|
||
"name": I18n.tr("launcher.providers.clipboard-history-disabled"),
|
||
"description": I18n.tr("launcher.providers.clipboard-history-disabled-description"),
|
||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {}
|
||
}
|
||
];
|
||
}
|
||
|
||
// Special command: clear
|
||
if (query === "clear") {
|
||
return [
|
||
{
|
||
"name": I18n.tr("launcher.providers.clipboard-clear-history"),
|
||
"description": I18n.tr("launcher.providers.clipboard-clear-description-full"),
|
||
"icon": iconMode === "tabler" ? "trash" : "delete_sweep",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {
|
||
ClipboardService.wipeAll();
|
||
launcher.close();
|
||
}
|
||
}
|
||
];
|
||
}
|
||
|
||
// Show loading state if data is being loaded
|
||
if (ClipboardService.loading || isWaitingForData) {
|
||
return [
|
||
{
|
||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {}
|
||
}
|
||
];
|
||
}
|
||
|
||
// Get clipboard items
|
||
const items = ClipboardService.items || [];
|
||
|
||
// If no items and we haven't tried loading yet, trigger a load
|
||
if (items.count === 0 && !ClipboardService.loading) {
|
||
isWaitingForData = true;
|
||
ClipboardService.list(100);
|
||
return [
|
||
{
|
||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {}
|
||
}
|
||
];
|
||
}
|
||
|
||
// Search clipboard items
|
||
const searchTerm = query.toLowerCase();
|
||
|
||
// Filter and format results
|
||
items.forEach(function (item) {
|
||
const preview = (item.preview || "").toLowerCase();
|
||
|
||
// Skip if search term doesn't match
|
||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||
return;
|
||
}
|
||
|
||
// Format the result based on type
|
||
let entry;
|
||
if (item.isImage) {
|
||
entry = formatImageEntry(item);
|
||
} else {
|
||
entry = formatTextEntry(item);
|
||
}
|
||
|
||
// Add activation handler
|
||
entry.onActivate = function () {
|
||
if (Settings.data.appLauncher.autoPasteClipboard) {
|
||
launcher.closeImmediately();
|
||
Qt.callLater(() => {
|
||
ClipboardService.pasteFromClipboard(item.id, item.mime);
|
||
});
|
||
} else {
|
||
ClipboardService.copyToClipboard(item.id);
|
||
launcher.close();
|
||
}
|
||
};
|
||
|
||
results.push(entry);
|
||
});
|
||
|
||
// Show empty state if no results
|
||
if (results.length === 0) {
|
||
results.push({
|
||
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
|
||
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
|
||
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"onActivate": function () {// Do nothing
|
||
}
|
||
});
|
||
}
|
||
|
||
//Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
|
||
return results;
|
||
}
|
||
|
||
function formatImageEntry(item) {
|
||
const meta = ClipboardService.parseImageMeta(item.preview);
|
||
|
||
return {
|
||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||
"description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data",
|
||
"icon": iconMode === "tabler" ? "photo" : "image",
|
||
"isTablerIcon": true,
|
||
"isImage": true,
|
||
"imageWidth": meta ? meta.w : 0,
|
||
"imageHeight": meta ? meta.h : 0,
|
||
"clipboardId": item.id,
|
||
"mime": item.mime,
|
||
"preview": item.preview,
|
||
"provider": root
|
||
};
|
||
}
|
||
|
||
function formatTextEntry(item) {
|
||
const preview = (item.preview || "").trim();
|
||
const lines = preview.split('\n').filter(l => l.trim());
|
||
|
||
let title = lines[0] || "Empty text";
|
||
if (title.length > 60) {
|
||
title = title.substring(0, 57) + "...";
|
||
}
|
||
|
||
let description = "";
|
||
if (lines.length > 1) {
|
||
description = lines[1];
|
||
if (description.length > 80) {
|
||
description = description.substring(0, 77) + "...";
|
||
}
|
||
} else {
|
||
// Preview is truncated at ~100 chars, so we can't show exact count
|
||
if (preview.length >= 100) {
|
||
description = I18n.tr("toast.clipboard.long-text");
|
||
} else {
|
||
const chars = preview.length;
|
||
const words = preview.split(/\s+/).length;
|
||
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`;
|
||
}
|
||
}
|
||
|
||
return {
|
||
"name": title,
|
||
"description": description,
|
||
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
|
||
"isTablerIcon": true,
|
||
"isImage": false,
|
||
"clipboardId": item.id,
|
||
"preview": preview,
|
||
"provider": root
|
||
};
|
||
}
|
||
|
||
function getImageForItem(clipboardId) {
|
||
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
|
||
}
|
||
|
||
// -------------------------
|
||
// Item actions for launcher delegate
|
||
function getItemActions(item) {
|
||
if (!item || !item.clipboardId)
|
||
return [];
|
||
|
||
var actions = [];
|
||
|
||
// Annotation tool for images
|
||
if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") {
|
||
actions.push({
|
||
"icon": "pencil",
|
||
"tooltip": I18n.tr("tooltips.open-annotation-tool"),
|
||
"action": function () {
|
||
var tool = Settings.data.appLauncher.screenshotAnnotationTool;
|
||
Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]);
|
||
if (launcher)
|
||
launcher.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Delete action
|
||
actions.push({
|
||
"icon": "trash",
|
||
"tooltip": I18n.tr("launcher.providers.clipboard-delete"),
|
||
"action": function () {
|
||
deleteItem(item);
|
||
}
|
||
});
|
||
|
||
return actions;
|
||
}
|
||
|
||
function canDeleteItem(item) {
|
||
return item && !!item.clipboardId;
|
||
}
|
||
|
||
function deleteItem(item) {
|
||
if (!item || !item.clipboardId)
|
||
return;
|
||
|
||
// Set provider state before deletion so refresh works
|
||
gotResults = false;
|
||
isWaitingForData = true;
|
||
lastSearchText = launcher ? launcher.searchText : "";
|
||
|
||
// Delete the item
|
||
ClipboardService.deleteById(String(item.clipboardId));
|
||
}
|
||
|
||
// Prepare item for display (handles image decoding)
|
||
function prepareItem(item) {
|
||
if (item && item.isImage && item.clipboardId) {
|
||
if (!ClipboardService.getImageData(item.clipboardId)) {
|
||
ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Get image URL for item (used by delegates)
|
||
function getImageUrl(item) {
|
||
if (!item || !item.clipboardId)
|
||
return "";
|
||
return ClipboardService.getImageData(item.clipboardId) || "";
|
||
}
|
||
|
||
// Get preview data for the preview panel
|
||
function getPreviewData(item) {
|
||
if (!item)
|
||
return null;
|
||
return {
|
||
"clipboardId": item.clipboardId,
|
||
"isImage": item.isImage,
|
||
"mime": item.mime,
|
||
"preview": item.preview
|
||
};
|
||
}
|
||
}
|