mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
02cf4f8db9
Signed-off-by: Braian A. Diez <bdiez19@gmail.com>
545 lines
17 KiB
QML
545 lines
17 KiB
QML
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
|
||
};
|
||
}
|
||
}
|