Merge pull request #1999 from linuxmobile/clipboard-filter

clipboard: add filters by type & date
This commit is contained in:
Lemmy
2026-03-13 22:18:08 -04:00
committed by GitHub
8 changed files with 212 additions and 8 deletions
+12 -1
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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
+1 -1
View File
@@ -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)
@@ -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)
@@ -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,
@@ -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
+24 -3
View File
@@ -114,7 +114,7 @@ Singleton {
const out = String(stdout.text);
const lines = out.split('\n').filter(l => l.length > 0);
// cliphist list default format: "<id> <preview>" or "<id>\t<preview>"
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('::') || 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|#\[|@|\/\/|\/\*|<\?|<html|<body|<!DOCTYPE)/i.test(t) || /\b(?:require\(|module\.exports)\b/i.test(t)) {
contentType = "code";
}
}
return {
"id": id,
"preview": preview,
"isImage": isImage,
"mime": mime
"mime": mime,
"contentType": contentType
};
});