Files
noctalia-shell/Modules/Panels/Launcher/Providers/ClipboardProvider.qml
T
Braian A. Diez 02cf4f8db9 chore(clipboard): cleanup request
Signed-off-by: Braian A. Diez <bdiez19@gmail.com>
2026-03-12 21:12:05 -03:00

545 lines
17 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 bool showsCategories: Settings.data.appLauncher.enableClipboardChips
property string selectedCategory: "All"
function selectCategory(cat) {
if (selectedCategory !== cat) {
selectedCategory = cat;
if (launcher) {
launcher.updateResults();
}
}
}
// Date Filtering
property bool hasDateFilter: Settings.data.appLauncher.enableClipboardDateHeaders
property string dateFilter: "all"
property var availableDateFilters: [
{
get label() {
return I18n.tr("launcher.date-filter-all-time");
},
"action": "all",
get icon() {
return iconMode === "tabler" ? "calendar" : "x-office-calendar";
}
},
{
get label() {
return I18n.tr("launcher.date-filter-today");
},
"action": "today",
get icon() {
return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline";
}
},
{
get label() {
return I18n.tr("launcher.date-filter-yesterday");
},
"action": "yesterday",
get icon() {
return iconMode === "tabler" ? "calendar-time" : "view-calendar";
}
},
{
get label() {
return I18n.tr("launcher.date-filter-previous-7-days");
},
"action": "week",
get icon() {
return iconMode === "tabler" ? "calendar-week" : "view-calendar-week";
}
}
]
function selectDateFilter(filter) {
if (dateFilter !== filter) {
dateFilter = filter;
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();
// Date grouping trackers
const headersEnabled = Settings.data.appLauncher.enableClipboardDateHeaders;
const now = Date.now() / 1000;
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayStartTs = todayStart.getTime() / 1000;
const yesterdayStartTs = todayStartTs - 86400;
let currentGroup = "";
// Filter and format results
items.forEach(function (item) {
// Category filter
if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") {
const catMap = {
"Images": "image",
"Links": "link",
"Files": "file",
"Code": "code",
"Colors": "color"
};
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;
}
// Date Filter
const firstSeen = ClipboardService.firstSeenById[item.id] || now;
if (root.dateFilter !== "all") {
if (root.dateFilter === "today" && firstSeen < todayStartTs)
return;
if (root.dateFilter === "yesterday" && (firstSeen >= todayStartTs || firstSeen < yesterdayStartTs))
return;
if (root.dateFilter === "week" && (firstSeen >= yesterdayStartTs || firstSeen < (todayStartTs - (86400 * 7))))
return;
}
// Check date group logic
if (headersEnabled && !searchTerm && root.selectedCategory === "All" && root.dateFilter === "all") {
let groupName = I18n.tr("launcher.date-filter-all-time");
if (firstSeen >= todayStartTs) {
groupName = I18n.tr("launcher.date-filter-today");
} else if (firstSeen >= yesterdayStartTs) {
groupName = I18n.tr("launcher.date-filter-yesterday");
} else if (firstSeen >= todayStartTs - (86400 * 7)) {
groupName = I18n.tr("launcher.date-filter-previous-7-days");
}
if (groupName !== currentGroup) {
currentGroup = groupName;
results.push({
"name": currentGroup,
"description": "",
"icon": iconMode === "tabler" ? "calendar" : "x-office-calendar",
"isTablerIcon": true,
"isImage": false,
"hideIcon": true,
"isHeader": true,
"clipboardId": "",
"onActivate": function () {}
});
}
}
// 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' : ''}`;
}
}
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 || item.isHeader)
return null;
return {
"clipboardId": item.clipboardId,
"isImage": item.isImage,
"mime": item.mime,
"preview": item.preview
};
}
}