add noctalia and fuzzel
This commit is contained in:
+457
@@ -0,0 +1,457 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.Noctalia
|
||||
|
||||
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: Settings.data.appLauncher.enableClipPreview
|
||||
property string previewComponentPath: "./ClipboardPreview.qml"
|
||||
|
||||
// Image handling - expose revision for reactive updates in delegates
|
||||
readonly property int imageRevision: ClipboardService.revision
|
||||
|
||||
// Categories
|
||||
property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : []
|
||||
property string selectedCategory: "All"
|
||||
|
||||
function selectCategory(cat) {
|
||||
if (selectedCategory !== cat) {
|
||||
selectedCategory = cat;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var categoryIcons: {
|
||||
"All": iconMode === "tabler" ? "border-all" : "view-grid",
|
||||
"Images": iconMode === "tabler" ? "photo" : "image",
|
||||
"Links": iconMode === "tabler" ? "link" : "insert-link",
|
||||
"Files": iconMode === "tabler" ? "file" : "text-x-generic",
|
||||
"Code": iconMode === "tabler" ? "code" : "text-x-script",
|
||||
"Colors": iconMode === "tabler" ? "palette" : "color-picker"
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
function onActiveChanged() {
|
||||
// When active state changes (e.g. dependency check completes), refresh results
|
||||
if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) {
|
||||
isWaitingForData = true;
|
||||
gotResults = false;
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("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" : "diodon",
|
||||
"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) {
|
||||
// If dependency check hasn't completed yet, show loading instead of disabled
|
||||
if (!ClipboardService.dependencyChecked) {
|
||||
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 () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
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" : "user-trash",
|
||||
"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();
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
const catMap = {
|
||||
"Images": "image",
|
||||
"Links": "link",
|
||||
"Files": "file",
|
||||
"Code": "code",
|
||||
"Colors": "color"
|
||||
};
|
||||
|
||||
// Filter and format results
|
||||
items.forEach(function (item) {
|
||||
// Category filter
|
||||
if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") {
|
||||
if (item.contentType !== catMap[root.selectedCategory]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const preview = (item.preview || "").toLowerCase();
|
||||
|
||||
// Skip if search term doesn't match
|
||||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSeen = ClipboardService.firstSeenById[item.id] || now;
|
||||
|
||||
// Format the result based on type
|
||||
let entry;
|
||||
if (item.isImage) {
|
||||
entry = formatImageEntry(item, firstSeen);
|
||||
} else {
|
||||
entry = formatTextEntry(item, firstSeen);
|
||||
}
|
||||
|
||||
// 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, firstSeen) {
|
||||
const meta = ClipboardService.parseImageMeta(item.preview);
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
let desc = meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data";
|
||||
if (timeStr)
|
||||
desc += ` • ${timeStr}`;
|
||||
|
||||
return {
|
||||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||||
"description": desc,
|
||||
"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, firstSeen) {
|
||||
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' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
if (timeStr)
|
||||
description += ` • ${timeStr}`;
|
||||
|
||||
let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic";
|
||||
let colorHex = "";
|
||||
if (Settings.data.appLauncher.enableClipboardSmartIcons) {
|
||||
if (item.contentType === "link")
|
||||
defaultIcon = iconMode === "tabler" ? "link" : "insert-link";
|
||||
else if (item.contentType === "file")
|
||||
defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic";
|
||||
else if (item.contentType === "code")
|
||||
defaultIcon = iconMode === "tabler" ? "code" : "text-x-script";
|
||||
else if (item.contentType === "color") {
|
||||
defaultIcon = iconMode === "tabler" ? "palette" : "color-picker";
|
||||
colorHex = preview;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": defaultIcon,
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"clipboardId": item.id,
|
||||
"preview": preview,
|
||||
"contentType": item.contentType,
|
||||
"colorHex": colorHex,
|
||||
"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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user