mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #1999 from linuxmobile/clipboard-filter
clipboard: add filters by type & date
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user