diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index dc88f6024..0eeb858e5 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -625,7 +625,11 @@ "emoji-search-description": "Search and copy emojis", "settings-search-description": "Search and navigate to settings", "windows-search-description": "Search and focus open windows" - } + }, + "date-filter-all-time": "All Time", + "date-filter-today": "Today", + "date-filter-yesterday": "Yesterday", + "date-filter-previous-7-days": "Previous 7 Days" }, "lock-screen": { "authenticating": "Authenticating...", @@ -1299,8 +1303,14 @@ "settings-annotation-tool-placeholder": "e.g. 'gradia', 'satty -f -'", "settings-auto-paste-description": "Automatically paste the selected clipboard item. Requires wtype.", "settings-auto-paste-label": "Auto paste", + "settings-clip-chips-description": "Show a tab bar to filter clipboard history by type (Images, Links, Files, Code, etc).", + "settings-clip-chips-label": "Enable Category Chips", + "settings-clip-date-headers-description": "Group clipboard history chronologically with visual headers.", + "settings-clip-date-headers-label": "Enable Date grouping", "settings-clip-preview-description": "Show a preview of the clipboard content when using the >clip command.", "settings-clip-preview-label": "Enable clip preview", + "settings-clip-smart-icons-description": "Show specific icons for links, files, colors, and code instead of a generic clipboard icon.", + "settings-clip-smart-icons-label": "Enable Smart Icons", "settings-clip-wrap-text-description": "Wrap text in the clipboard list instead of truncating it.", "settings-clip-wrap-text-label": "Wrap clipboard text", "settings-clipboard-history-description": "Access previously copied items from the launcher.", @@ -1949,6 +1959,7 @@ "click-to-stop-recording": "Screen recorder (stop recording)", "collapse": "Collapse sidebar", "copy-address": "Copy address", + "date-filter": "Date filter", "delete-notification": "Delete notification", "dismiss-notification": "Dismiss notification", "do-not-disturb-enabled": "Do Not Disturb", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 9ba668aba..781c60bcd 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -231,6 +231,9 @@ "autoPasteClipboard": false, "enableClipPreview": true, "clipboardWrapText": true, + "enableClipboardSmartIcons": true, + "enableClipboardChips": true, + "enableClipboardDateHeaders": true, "clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store", "clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store", "position": "center", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 841007e92..835ea0795 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -420,6 +420,9 @@ Singleton { property bool autoPasteClipboard: false property bool enableClipPreview: true property bool clipboardWrapText: true + property bool enableClipboardSmartIcons: true + property bool enableClipboardChips: true + property bool enableClipboardDateHeaders: true property string clipboardWatchTextCommand: "wl-paste --type text --watch cliphist store" property string clipboardWatchImageCommand: "wl-paste --type image --watch cliphist store" property string position: "center" // Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 6623ce301..f71dd9da1 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -40,7 +40,7 @@ SmartPanel { return false; if (!Settings.data.appLauncher.enableClipPreview) return false; - return selectedIndex >= 0 && results && !!results[selectedIndex]; + return selectedIndex >= 0 && results && !!results[selectedIndex] && !results[selectedIndex].isHeader; } readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) diff --git a/Modules/Panels/Launcher/LauncherOverlayWindow.qml b/Modules/Panels/Launcher/LauncherOverlayWindow.qml index 3b1b2c80c..28329ce12 100644 --- a/Modules/Panels/Launcher/LauncherOverlayWindow.qml +++ b/Modules/Panels/Launcher/LauncherOverlayWindow.qml @@ -93,7 +93,7 @@ Variants { return false; if (!Settings.data.appLauncher.enableClipPreview) return false; - return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex]; + return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex] && !launcherCore.results[launcherCore.selectedIndex].isHeader; } // Dimmer background (click to close) diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index 2f746a2f8..afa090d60 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import qs.Commons import qs.Services.Keyboard +import qs.Services.Noctalia Item { id: root @@ -23,6 +24,64 @@ Item { // 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: [ + { + "label": I18n.tr("launcher.date-filter-all-time"), + "action": "all", + "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar" + }, + { + "label": I18n.tr("launcher.date-filter-today"), + "action": "today", + "icon": iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline" + }, + { + "label": I18n.tr("launcher.date-filter-yesterday"), + "action": "yesterday", + "icon": iconMode === "tabler" ? "calendar-time" : "view-calendar" + }, + { + "label": I18n.tr("launcher.date-filter-previous-7-days"), + "action": "week", + "icon": 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 @@ -199,8 +258,33 @@ Item { // 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 = ""; + + 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 @@ -208,6 +292,44 @@ Item { 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) { @@ -293,14 +415,31 @@ Item { } } + 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": iconMode === "tabler" ? "clipboard" : "text-x-generic", + "icon": defaultIcon, "isTablerIcon": true, "isImage": false, "clipboardId": item.id, "preview": preview, + "contentType": item.contentType, + "colorHex": colorHex, "provider": root }; } @@ -378,7 +517,7 @@ Item { // Get preview data for the preview panel function getPreviewData(item) { - if (!item) + if (!item || item.isHeader) return null; return { "clipboardId": item.clipboardId, diff --git a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml index 4c1a100a9..7ddb19d2e 100644 --- a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml @@ -45,6 +45,33 @@ ColumnLayout { enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable } + NToggle { + label: I18n.tr("panels.launcher.settings-clip-smart-icons-label") + description: I18n.tr("panels.launcher.settings-clip-smart-icons-description") + checked: Settings.data.appLauncher.enableClipboardSmartIcons + onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-chips-label") + description: I18n.tr("panels.launcher.settings-clip-chips-description") + checked: Settings.data.appLauncher.enableClipboardChips + onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-date-headers-label") + description: I18n.tr("panels.launcher.settings-clip-date-headers-description") + checked: Settings.data.appLauncher.enableClipboardDateHeaders + onToggled: checked => Settings.data.appLauncher.enableClipboardDateHeaders = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardDateHeaders") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + NDivider { Layout.fillWidth: true visible: Settings.data.appLauncher.enableClipboardHistory diff --git a/Services/Keyboard/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml index db646c9a2..3c04bd29b 100644 --- a/Services/Keyboard/ClipboardService.qml +++ b/Services/Keyboard/ClipboardService.qml @@ -114,7 +114,7 @@ Singleton { const out = String(stdout.text); const lines = out.split('\n').filter(l => l.length > 0); // cliphist list default format: " " or "\t" - const parsed = lines.map(l => { + const parsed = lines.map((l, i) => { let id = ""; let preview = ""; const m = l.match(/^(\d+)\s+(.+)$/); @@ -144,13 +144,34 @@ Singleton { } // Record first seen time for new ids (approximate copy time) if (!root.firstSeenById[id]) { - root.firstSeenById[id] = Time.timestamp; + const assumedAge = i * 15 * 60; + root.firstSeenById[id] = Time.timestamp - assumedAge; } + // Smart type detection + var contentType = "text"; + if (isImage) { + contentType = "image"; + } else { + const t = preview.trim(); + const tLower = t.toLowerCase(); + if (/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/.test(tLower)) { + contentType = "color"; + } else if (/^https?:\/\//i.test(t)) { + contentType = "link"; + } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { + contentType = "file"; + } else if ((t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || t.includes('') || t.includes('=>') || t.includes('===') || t.includes('!==') || t.includes('::') || t.includes('->') || + /^(?:const|let|var|function|class|struct|interface|type|enum|import|export|func|fn|pub|def|using|namespace|property|public|private|protected)\b/i.test(t) || /^(?:#include|#define|#\[|@|\/\/|\/\*|<\?|