diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 5e5d0f38c..71475b772 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Webbrowser" }, - "date-filter-all-time": "Gesamte Zeit", - "date-filter-previous-7-days": "Letzte 7 Tage", - "date-filter-today": "Heute", - "date-filter-yesterday": "Gestern", "providers": { "applications": "Anwendungen", "calculator": "Rechner", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Bildschirmrekorder (Aufnahme stoppen)", "collapse": "Seitenleiste einklappen", "copy-address": "Adresse kopieren", - "date-filter": "Datumsfilter", "delete-notification": "Benachrichtigung löschen", "dismiss-notification": "Benachrichtigung schließen", "do-not-disturb-enabled": "Nicht stören", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index a99a5f4e9..ae374c781 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Navegador web" }, - "date-filter-all-time": "Todo el tiempo", - "date-filter-previous-7-days": "Últimos 7 días", - "date-filter-today": "Hoy", - "date-filter-yesterday": "Ayer", "providers": { "applications": "Aplicaciones", "calculator": "Calculadora", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Grabadora de pantalla (detener grabación)", "collapse": "Colapsar barra lateral", "copy-address": "Copiar dirección", - "date-filter": "Filtro de fecha", "delete-notification": "Eliminar notificación", "dismiss-notification": "Descartar notificación", "do-not-disturb-enabled": "No molestar", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 3a221c91d..cc15f4f27 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -599,10 +599,6 @@ "system": "Système", "webbrowser": "Navigateur web" }, - "date-filter-all-time": "Tout le temps", - "date-filter-previous-7-days": "7 derniers jours", - "date-filter-today": "Aujourd'hui", - "date-filter-yesterday": "Hier", "providers": { "applications": "Applications", "calculator": "Calculatrice", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Enregistreur d'écran (arrêter l'enregistrement)", "collapse": "Réduire la barre latérale", "copy-address": "Copier l'adresse", - "date-filter": "Filtre de date", "delete-notification": "Supprimer la notification", "dismiss-notification": "Ignorer la notification", "do-not-disturb-enabled": "Ne pas déranger", diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index aa24195fc..8136d287b 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -599,10 +599,6 @@ "system": "Rendszer", "webbrowser": "Webböngésző" }, - "date-filter-all-time": "Minden idő", - "date-filter-previous-7-days": "Elmúlt 7 nap", - "date-filter-today": "Ma", - "date-filter-yesterday": "Tegnap", "providers": { "applications": "Alkalmazások", "calculator": "Számológép", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Képernyőfelvevő (felvétel leállítása)", "collapse": "Oldalsáv összecsukása", "copy-address": "Cím másolása", - "date-filter": "Dátum szűrő", "delete-notification": "Értesítés törlése", "dismiss-notification": "Értesítés elvetése", "do-not-disturb-enabled": "Ne zavarjanak", diff --git a/Assets/Translations/it.json b/Assets/Translations/it.json index 40fdd6882..fc99560c3 100644 --- a/Assets/Translations/it.json +++ b/Assets/Translations/it.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Browser web" }, - "date-filter-all-time": "Tutto il tempo", - "date-filter-previous-7-days": "Ultimi 7 giorni", - "date-filter-today": "Oggi", - "date-filter-yesterday": "Ieri", "providers": { "applications": "Applicazioni", "calculator": "Calcolatrice", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Registratore schermo (ferma registrazione)", "collapse": "Comprimi barra laterale", "copy-address": "Copia indirizzo", - "date-filter": "Filtro data", "delete-notification": "Elimina notifica", "dismiss-notification": "Ignora notifica", "do-not-disturb-enabled": "Non disturbare", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index 5c860c0e3..63bf6758d 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -599,10 +599,6 @@ "system": "システム", "webbrowser": "ウェブブラウザ" }, - "date-filter-all-time": "すべての期間", - "date-filter-previous-7-days": "過去 7 日間", - "date-filter-today": "今日", - "date-filter-yesterday": "昨日", "providers": { "applications": "アプリケーション", "calculator": "電卓", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "画面録画(録画停止)", "collapse": "サイドバーを折りたたむ", "copy-address": "アドレスをコピー", - "date-filter": "日付フィルター", "delete-notification": "通知を削除", "dismiss-notification": "通知を閉じる", "do-not-disturb-enabled": "おやすみモード", diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index fce4865d6..7c57dbd75 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -599,10 +599,6 @@ "system": "시스템", "webbrowser": "웹 브라우저" }, - "date-filter-all-time": "모든 시간", - "date-filter-previous-7-days": "최근 7일", - "date-filter-today": "오늘", - "date-filter-yesterday": "어제", "providers": { "applications": "애플리케이션", "calculator": "계산기", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "화면 녹화 (녹화 중지)", "collapse": "사이드바 접기", "copy-address": "주소 복사", - "date-filter": "날짜 필터", "delete-notification": "알림 삭제", "dismiss-notification": "알림 닫기", "do-not-disturb-enabled": "방해 금지 모드", diff --git a/Assets/Translations/ku.json b/Assets/Translations/ku.json index 4591edd90..49332543c 100644 --- a/Assets/Translations/ku.json +++ b/Assets/Translations/ku.json @@ -545,10 +545,6 @@ "system": "Pergal", "webbrowser": "Geroka tevnê" }, - "date-filter-all-time": "Hemû Dem", - "date-filter-previous-7-days": "7 Rojên Borî", - "date-filter-today": "Îro", - "date-filter-yesterday": "Duh", "providers": { "applications": "Sepan", "calculator": "Jimarkar", @@ -1654,7 +1650,6 @@ "click-to-stop-recording": "Tomarkarê dîmenderê (tomarkirinê rawestîne)", "collapse": "Darika kêlelê veşêre", "copy-address": "Navnîşana kopî bike", - "date-filter": "Parzûna dîrokê", "delete-notification": "Agahdariyê jê bibe", "do-not-disturb-enabled": "Dengê dernexîne", "expand": "Darika kêlekê fereh bike", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index ac9bd115e..a4d41d6cf 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -599,10 +599,6 @@ "system": "Systeem", "webbrowser": "Webbrowser" }, - "date-filter-all-time": "Alle tijd", - "date-filter-previous-7-days": "Afgelopen 7 dagen", - "date-filter-today": "Vandaag", - "date-filter-yesterday": "Gisteren", "providers": { "applications": "Applicaties", "calculator": "Rekenmachine", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Schermrecorder (opname stoppen)", "collapse": "Zijbalk inklappen", "copy-address": "Adres kopiëren", - "date-filter": "Datumfilter", "delete-notification": "Melding verwijderen", "dismiss-notification": "Melding sluiten", "do-not-disturb-enabled": "Niet storen", diff --git a/Assets/Translations/nn-HN.json b/Assets/Translations/nn-HN.json index e73a5e129..de1f34d7c 100644 --- a/Assets/Translations/nn-HN.json +++ b/Assets/Translations/nn-HN.json @@ -224,12 +224,6 @@ "brightness": "Ljosstyrke", "select-color-description": "Lita med hamlitene for tyngd." }, - "launcher": { - "date-filter-all-time": "Heile tida", - "date-filter-previous-7-days": "Siste 7 dagar", - "date-filter-today": "I dag", - "date-filter-yesterday": "I går" - }, "options": { "bar": { "density-compact": "Klembd" @@ -243,8 +237,5 @@ }, "system": { "welcome-back": "Velkomen attende," - }, - "tooltips": { - "date-filter": "Dato-filter" } } diff --git a/Assets/Translations/nn-NO.json b/Assets/Translations/nn-NO.json index 39a0a688f..84f9fe1e9 100644 --- a/Assets/Translations/nn-NO.json +++ b/Assets/Translations/nn-NO.json @@ -576,10 +576,6 @@ "system": "System", "webbrowser": "Nettlesar" }, - "date-filter-all-time": "Heile tida", - "date-filter-previous-7-days": "Siste 7 dagar", - "date-filter-today": "I dag", - "date-filter-yesterday": "I går", "providers": { "applications": "Applikasjonar", "calculator": "Kalkulator", @@ -1792,7 +1788,6 @@ "click-to-stop-recording": "Skjermopptak (stogg opptak)", "collapse": "Gøym sidestolpen", "copy-address": "Kopier adresse", - "date-filter": "Dato-filter", "delete-notification": "Slett varsel", "dismiss-notification": "Avvis varsel", "do-not-disturb-enabled": "Ikkje forstyrr", diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index 0f31bb9e3..3f10b9cc7 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Przeglądarka www" }, - "date-filter-all-time": "Cały czas", - "date-filter-previous-7-days": "Ostatnie 7 dni", - "date-filter-today": "Dzisiaj", - "date-filter-yesterday": "Wczoraj", "providers": { "applications": "Aplikacje", "calculator": "Kalkulator", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Nagrywanie ekranu (stop)", "collapse": "Zwiń pasek boczny", "copy-address": "Kopiuj adres", - "date-filter": "Filtr daty", "delete-notification": "Usuń powiadomienie", "dismiss-notification": "Odrzuć powiadomienie", "do-not-disturb-enabled": "Nie przeszkadzać", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index ba929625a..7cb2a338c 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Navegador web" }, - "date-filter-all-time": "Todo o tempo", - "date-filter-previous-7-days": "Últimos 7 dias", - "date-filter-today": "Hoje", - "date-filter-yesterday": "Ontem", "providers": { "applications": "Aplicativos", "calculator": "Calculadora", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Gravador de tela (parar gravação)", "collapse": "Recolher barra lateral", "copy-address": "Copiar endereço", - "date-filter": "Filtro de data", "delete-notification": "Excluir notificação", "dismiss-notification": "Descartar notificação", "do-not-disturb-enabled": "Não perturbe", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 8e2faac06..96b9dcdf8 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -599,10 +599,6 @@ "system": "Система", "webbrowser": "Веб-браузер" }, - "date-filter-all-time": "Все время", - "date-filter-previous-7-days": "Последние 7 дней", - "date-filter-today": "Сегодня", - "date-filter-yesterday": "Вчера", "providers": { "applications": "Приложения", "calculator": "Калькулятор", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Запись экрана (остановить запись)", "collapse": "Свернуть боковую панель", "copy-address": "Копировать адрес", - "date-filter": "Фильтр по дате", "delete-notification": "Удалить уведомление", "dismiss-notification": "Отклонить уведомление", "do-not-disturb-enabled": "Не беспокоить", diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index 537204da1..15481362b 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Webbläsare" }, - "date-filter-all-time": "Hela tiden", - "date-filter-previous-7-days": "Senaste 7 dagarna", - "date-filter-today": "Idag", - "date-filter-yesterday": "Igår", "providers": { "applications": "Applikationer", "calculator": "Kalkylator", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Skärminspelare (stoppa inspelning)", "collapse": "Dölj sidofält", "copy-address": "Kopiera adress", - "date-filter": "Datumfilter", "delete-notification": "Ta bort avisering", "dismiss-notification": "Avfärda avisering", "do-not-disturb-enabled": "Stör inte", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index b97b65ee1..9f15abad2 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -599,10 +599,6 @@ "system": "Sistem", "webbrowser": "Tarayıcı" }, - "date-filter-all-time": "Tüm Zamanlar", - "date-filter-previous-7-days": "Son 7 Gün", - "date-filter-today": "Bugün", - "date-filter-yesterday": "Dün", "providers": { "applications": "Uygulamalar", "calculator": "Hesap makinesi", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Ekran kaydedici (kaydı durdur)", "collapse": "Kenar çubuğunu daralt", "copy-address": "Adresi kopyala", - "date-filter": "Tarih filtresi", "delete-notification": "Bildiriyi sil", "dismiss-notification": "Bildirimi kapat", "do-not-disturb-enabled": "Rahatsız etme", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 98c34dbbb..1adb21bb7 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -599,10 +599,6 @@ "system": "Система", "webbrowser": "Веб-браузер" }, - "date-filter-all-time": "Весь час", - "date-filter-previous-7-days": "Останні 7 днів", - "date-filter-today": "Сьогодні", - "date-filter-yesterday": "Вчора", "providers": { "applications": "Застосунки", "calculator": "Калькулятор", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Запис екрана (зупинити запис)", "collapse": "Згорнути бічну панель", "copy-address": "Копіювати адресу", - "date-filter": "Фільтр за датою", "delete-notification": "Видалити сповіщення", "dismiss-notification": "Відхилити сповіщення", "do-not-disturb-enabled": "Не турбувати", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index e1e8854bb..9e181b10c 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -599,10 +599,6 @@ "system": "系统", "webbrowser": "网页浏览器" }, - "date-filter-all-time": "所有时间", - "date-filter-previous-7-days": "过去 7 天", - "date-filter-today": "今天", - "date-filter-yesterday": "昨天", "providers": { "applications": "应用程序", "calculator": "计算器", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "屏幕录制器(停止录制)", "collapse": "折叠侧边栏", "copy-address": "复制地址", - "date-filter": "日期过滤", "delete-notification": "删除通知", "dismiss-notification": "关闭通知", "do-not-disturb-enabled": "勿扰模式", diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index d37dff40a..f948273a5 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -599,10 +599,6 @@ "system": "系統", "webbrowser": "瀏覽器" }, - "date-filter-all-time": "所有時間", - "date-filter-previous-7-days": "過去 7 天", - "date-filter-today": "今天", - "date-filter-yesterday": "昨天", "providers": { "applications": "應用程式", "calculator": "計算機", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "螢幕錄影 (停止錄製)", "collapse": "收起側邊欄", "copy-address": "複製位址", - "date-filter": "日期過濾", "delete-notification": "刪除通知", "dismiss-notification": "關閉通知", "do-not-disturb-enabled": "勿擾模式", diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index a942f8e31..5eee31fdc 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -5,540 +5,540 @@ import qs.Services.Keyboard import qs.Services.Noctalia Item { - id: root + 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 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 + // 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" + // 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 + // 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" + // 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(); - } - } + 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(); - } - } + // 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"; + } } + ] - 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" + function selectDateFilter(filter) { + if (dateFilter !== filter) { + dateFilter = filter; + if (launcher) { + launcher.updateResults(); + } } + } - // Internal state - property bool isWaitingForData: false - property bool gotResults: false - property string lastSearchText: "" + 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" + } - // 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); - } + // 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(); } + } } - - // 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() { + 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; - lastSearchText = ""; - - // Refresh clipboard history when launcher opens - if (ClipboardService.active) { - ClipboardService.list(100); - } + ClipboardService.list(100); + } } + } - // Check if this provider handles the command - function handleCommand(searchText) { - return searchText.startsWith(">clip"); + // Initialize provider + function init() { + Logger.d("ClipboardProvider", "Initialized"); + // Pre-load clipboard data if service is active + if (ClipboardService.active) { + ClipboardService.list(100); } + } - // 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(); - } - } - ]; + // Called when launcher opens + function onOpened() { + isWaitingForData = true; + gotResults = false; + lastSearchText = ""; + + // Refresh clipboard history when launcher opens + if (ClipboardService.active) { + ClipboardService.list(100); } + } - // Get search results - function getResults(searchText) { - if (!searchText.startsWith(">clip")) { - return []; - } + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">clip"); + } - 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, + // 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, - "clipboardId": item.id, - "preview": preview, - "contentType": item.contentType, - "colorHex": colorHex, - "provider": root + "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; + } + } - function getImageForItem(clipboardId) { - return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; - } + const preview = (item.preview || "").toLowerCase(); - // ------------------------- - // Item actions for launcher delegate - function getItemActions(item) { - if (!item || !item.clipboardId) - return []; + // Skip if search term doesn't match + if (searchTerm && preview.indexOf(searchTerm) === -1) { + return; + } - var actions = []; + // 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; + } - // 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(); - } - }); + // 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"); } - // 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); - } + 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 + } + }); } - // Get image URL for item (used by delegates) - function getImageUrl(item) { - if (!item || !item.clipboardId) - return ""; - return ClipboardService.getImageData(item.clipboardId) || ""; + //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) + "..."; } - // 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 - }; + 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 + }; + } } diff --git a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml index fd7899a46..7ddb19d2e 100644 --- a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml @@ -6,94 +6,94 @@ import qs.Services.System import qs.Widgets ColumnLayout { - id: root - spacing: Style.marginL + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.launcher.settings-clipboard-history-label") + description: I18n.tr("panels.launcher.settings-clipboard-history-description") + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-preview-label") + description: I18n.tr("panels.launcher.settings-clip-preview-description") + checked: Settings.data.appLauncher.enableClipPreview + onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") + description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") + checked: Settings.data.appLauncher.clipboardWrapText + onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked + defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-auto-paste-label") + description: I18n.tr("panels.launcher.settings-auto-paste-description") + checked: Settings.data.appLauncher.autoPasteClipboard + onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked + defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") + 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 + } - NToggle { - label: I18n.tr("panels.launcher.settings-clipboard-history-label") - description: I18n.tr("panels.launcher.settings-clipboard-history-description") - checked: Settings.data.appLauncher.enableClipboardHistory - onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") - } + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchTextCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } - NToggle { - label: I18n.tr("panels.launcher.settings-clip-preview-label") - description: I18n.tr("panels.launcher.settings-clip-preview-description") - checked: Settings.data.appLauncher.enableClipPreview - onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") - description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") - checked: Settings.data.appLauncher.clipboardWrapText - onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked - defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-auto-paste-label") - description: I18n.tr("panels.launcher.settings-auto-paste-description") - checked: Settings.data.appLauncher.autoPasteClipboard - onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked - defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") - 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 - } - - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchTextCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } - - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchImageCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchImageCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } } diff --git a/Services/Keyboard/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml index 1c66ec8b2..3c04bd29b 100644 --- a/Services/Keyboard/ClipboardService.qml +++ b/Services/Keyboard/ClipboardService.qml @@ -8,534 +8,529 @@ import qs.Services.UI // Clipboard history service using cliphist + local content cache Singleton { - id: root + id: root - // Public API - property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable - property bool loading: false - property var items: [] // [{id, preview, mime, isImage}] + // Public API + property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable + property bool loading: false + property var items: [] // [{id, preview, mime, isImage}] - // Check if cliphist is available on the system - property bool cliphistAvailable: false - property bool dependencyChecked: false + // Check if cliphist is available on the system + property bool cliphistAvailable: false + property bool dependencyChecked: false - // Optional automatic watchers to feed cliphist DB - property bool autoWatch: true - property bool watchersStarted: false + // Optional automatic watchers to feed cliphist DB + property bool autoWatch: true + property bool watchersStarted: false - // Expose decoded thumbnails by id and a revision to notify bindings - property var imageDataById: ({}) - property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction - readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once - property int revision: 0 + // Expose decoded thumbnails by id and a revision to notify bindings + property var imageDataById: ({}) + property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction + readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once + property int revision: 0 - // Local content cache - stores full text content by ID - // This avoids relying on cliphist decode which can be unreliable - property var contentCache: ({}) + // Local content cache - stores full text content by ID + // This avoids relying on cliphist decode which can be unreliable + property var contentCache: ({}) - // Track the most recent clipboard content for instant access - property string _latestTextContent: "" - property string _latestTextId: "" + // Track the most recent clipboard content for instant access + property string _latestTextContent: "" + property string _latestTextId: "" - // Approximate first-seen timestamps for entries this session (seconds) - property var firstSeenById: ({}) + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) - // Internal: store callback for decode - property var _decodeCallback: null - property int _decodeRequestId: 0 + // Internal: store callback for decode + property var _decodeCallback: null + property int _decodeRequestId: 0 - // Queue for base64 decodes - property var _b64Queue: [] - property var _b64CurrentCb: null - property string _b64CurrentMime: "" - property string _b64CurrentId: "" + // Queue for base64 decodes + property var _b64Queue: [] + property var _b64CurrentCb: null + property string _b64CurrentMime: "" + property string _b64CurrentId: "" - signal listCompleted + signal listCompleted - // Check if cliphist is available - Component.onCompleted: { - checkCliphistAvailability(); - } + // Check if cliphist is available + Component.onCompleted: { + checkCliphistAvailability(); + } - // Check dependency availability - function checkCliphistAvailability() { - if (dependencyChecked) - return; - dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; - dependencyCheckProcess.running = true; - } + // Check dependency availability + function checkCliphistAvailability() { + if (dependencyChecked) + return; + dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; + dependencyCheckProcess.running = true; + } - // Process to check if cliphist is available - Process { - id: dependencyCheckProcess - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - root.dependencyChecked = true; - if (exitCode === 0) { - root.cliphistAvailable = true; - // Start watchers if feature is enabled - if (root.active) { - startWatchers(); - } - } else { - root.cliphistAvailable = false; - // Show toast notification if feature is enabled but cliphist is missing - if (Settings.data.appLauncher.enableClipboardHistory) { - ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); - } - } - } - } - - // Start/stop watchers when enabled changes - onActiveChanged: { + // Process to check if cliphist is available + Process { + id: dependencyCheckProcess + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + root.dependencyChecked = true; + if (exitCode === 0) { + root.cliphistAvailable = true; + // Start watchers if feature is enabled if (root.active) { - startWatchers(); - } else { - stopWatchers(); - loading = false; - items = []; + startWatchers(); } + } else { + root.cliphistAvailable = false; + // Show toast notification if feature is enabled but cliphist is missing + if (Settings.data.appLauncher.enableClipboardHistory) { + ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); + } + } } + } - // Fallback: periodically refresh list so UI updates even if not in clip mode - Timer { - interval: 5000 - repeat: true - running: root.active - onTriggered: list() + // Start/stop watchers when enabled changes + onActiveChanged: { + if (root.active) { + startWatchers(); + } else { + stopWatchers(); + loading = false; + items = []; } + } - // Internal process objects - Process { - id: listProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - 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, i) => { - let id = ""; - let preview = ""; - const m = l.match(/^(\d+)\s+(.+)$/); - if (m) { - id = m[1]; - preview = m[2]; - } else { - const tab = l.indexOf('\t'); - id = tab > -1 ? l.slice(0, tab) : l; - preview = tab > -1 ? l.slice(tab + 1) : ""; - } - const lower = preview.toLowerCase(); - const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); - // Best-effort mime guess from preview - var mime = "text/plain"; - if (isImage) { - if (lower.includes(" png")) - mime = "image/png"; - else if (lower.includes(" jpg") || lower.includes(" jpeg")) - mime = "image/jpeg"; - else if (lower.includes(" webp")) - mime = "image/webp"; - else if (lower.includes(" gif")) - mime = "image/gif"; - else - mime = "image/*"; - } - // Record first seen time for new ids (approximate copy time) - if (!root.firstSeenById[id]) { - 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|#\[|@|\/\/|\/\*|<\?| { + 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, i) => { + let id = ""; + let preview = ""; + const m = l.match(/^(\d+)\s+(.+)$/); + if (m) { + id = m[1]; + preview = m[2]; + } else { + const tab = l.indexOf('\t'); + id = tab > -1 ? l.slice(0, tab) : l; + preview = tab > -1 ? l.slice(tab + 1) : ""; + } + const lower = preview.toLowerCase(); + const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); + // Best-effort mime guess from preview + var mime = "text/plain"; + if (isImage) { + if (lower.includes(" png")) + mime = "image/png"; + else if (lower.includes(" jpg") || lower.includes(" jpeg")) + mime = "image/jpeg"; + else if (lower.includes(" webp")) + mime = "image/webp"; + else if (lower.includes(" gif")) + mime = "image/gif"; + else + mime = "image/*"; + } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + 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|#\[|@|\/\/|\/\*|<\?| { - if (item.isImage) - return true; - const p = item.preview; - // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact - const nullCount = (p.match(/\x00/g) || []).length; - if (nullCount > p.length * 0.2) - return false; - // Skip browser-generated HTML wrapper, firefox - if (p.toLowerCase().startsWith(" { + if (item.isImage) + return true; + const p = item.preview; + // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact + const nullCount = (p.match(/\x00/g) || []).length; + if (nullCount > p.length * 0.2) + return false; + // Skip browser-generated HTML wrapper, firefox + if (p.toLowerCase().startsWith(" root._imageDataMaxEntries) { + const evicted = root._imageDataInsertOrder.shift(); + delete root.imageDataById[evicted]; + } + root.revision += 1; + } + root._b64CurrentCb = null; + root._b64CurrentMime = ""; + root._b64CurrentId = ""; + Qt.callLater(root._startNextB64); + } + } + + // Text watcher - stores to cliphist and triggers content capture + Process { + id: watchText + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { + watchTextRestartTimer.restart(); + } + } + } + + Timer { + id: watchTextRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchText.running = true; + } + } + + // Image watcher + Process { + id: watchImage + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { + watchImageRestartTimer.restart(); + } + } + } + + Timer { + id: watchImageRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchImage.running = true; + } + } + + // Capture current clipboard text when needed + Process { + id: captureTextProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text); + if (content.length > 0) { + root._latestTextContent = content; + // Associate with newest item if we have one + if (root.items.length > 0 && !root.items[0].isImage) { + const newestId = root.items[0].id; + if (!root.contentCache[newestId]) { + root.contentCache[newestId] = content; + root.revision++; } - - root.listCompleted(); + } } + } } + } - Process { - id: decodeProc - property int requestId: 0 - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (requestId === root._decodeRequestId && root._decodeCallback) { - const out = String(stdout.text); - try { - root._decodeCallback(out); - } finally { - root._decodeCallback = null; - } - } - } - } + function startWatchers() { + if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) + return; + watchersStarted = true; - Process { - id: copyProc - stdout: StdioCollector {} - } - - Process { - id: pasteProc - stdout: StdioCollector {} - } - - Process { - id: deleteProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - revision++; - Qt.callLater(() => list()); - } - } - - // Base64 decode pipeline (queued) - Process { - id: decodeB64Proc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const b64 = String(stdout.text).trim(); - if (root._b64CurrentCb) { - const url = `data:${root._b64CurrentMime};base64,${b64}`; - try { - root._b64CurrentCb(url); - } catch (e) {} - } - if (root._b64CurrentId !== "") { - const entryId = root._b64CurrentId; - root.imageDataById[entryId] = `data:${root._b64CurrentMime};base64,${b64}`; - // Track insertion order and evict oldest entries beyond the cap - root._imageDataInsertOrder.push(entryId); - while (root._imageDataInsertOrder.length > root._imageDataMaxEntries) { - const evicted = root._imageDataInsertOrder.shift(); - delete root.imageDataById[evicted]; - } - root.revision += 1; - } - root._b64CurrentCb = null; - root._b64CurrentMime = ""; - root._b64CurrentId = ""; - Qt.callLater(root._startNextB64); - } - } - - // Text watcher - stores to cliphist and triggers content capture - Process { - id: watchText - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { - watchTextRestartTimer.restart(); - } - } - } - - Timer { - id: watchTextRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchText.running = true; - } - } + // Text watcher + watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; + watchText.running = true; // Image watcher - Process { - id: watchImage - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { - watchImageRestartTimer.restart(); - } - } + watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; + watchImage.running = true; + } + + function stopWatchers() { + if (!watchersStarted) + return; + watchText.running = false; + watchImage.running = false; + watchersStarted = false; + } + + // Capture current clipboard text and cache it + function captureCurrentClipboard() { + if (captureTextProc.running) + return; + captureTextProc.command = ["wl-paste", "--no-newline"]; + captureTextProc.running = true; + } + + function list(maxPreviewWidth) { + if (!root.active || !root.cliphistAvailable) { + return; + } + if (listProc.running) + return; + loading = true; + const width = maxPreviewWidth || 100; + listProc.command = ["cliphist", "list", "-preview-width", String(width)]; + listProc.running = true; + } + + // Get content for an ID - uses cache first, falls back to cliphist decode + function getContent(id) { + if (root.contentCache[id]) { + return root.contentCache[id]; + } + return null; + } + + // Async decode - checks cache first, then falls back to cliphist + function decode(id, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; } - Timer { - id: watchImageRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchImage.running = true; - } + // Check cache first + const cached = root.contentCache[id]; + if (cached) { + if (cb) + cb(cached); + return; } - // Capture current clipboard text when needed - Process { - id: captureTextProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text); - if (content.length > 0) { - root._latestTextContent = content; - // Associate with newest item if we have one - if (root.items.length > 0 && !root.items[0].isImage) { - const newestId = root.items[0].id; - if (!root.contentCache[newestId]) { - root.contentCache[newestId] = content; - root.revision++; - } - } - } - } - } + // Fall back to cliphist decode + if (decodeProc.running) { + decodeProc.running = false; } + root._decodeRequestId++; + decodeProc.requestId = root._decodeRequestId; + root._decodeCallback = function (content) { + // Cache the result if successful + if (content && content.trim()) { + root.contentCache[id] = content; + } + if (cb) + cb(content); + }; + const idStr = String(id); + decodeProc.command = ["cliphist", "decode", idStr]; + decodeProc.running = true; + } - function startWatchers() { - if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) - return; - watchersStarted = true; - - // Text watcher - watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; - watchText.running = true; - - // Image watcher - watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; - watchImage.running = true; + function decodeToDataUrl(id, mime, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; } - - function stopWatchers() { - if (!watchersStarted) - return; - watchText.running = false; - watchImage.running = false; - watchersStarted = false; + // If cached, return immediately + if (root.imageDataById[id]) { + if (cb) + cb(root.imageDataById[id]); + return; } - - // Capture current clipboard text and cache it - function captureCurrentClipboard() { - if (captureTextProc.running) - return; - captureTextProc.command = ["wl-paste", "--no-newline"]; - captureTextProc.running = true; + // Queue request; ensures single process handles sequentially + root._b64Queue.push({ + "id": id, + "mime": mime || "image/*", + "cb": cb + }); + if (!decodeB64Proc.running && root._b64CurrentCb === null) { + _startNextB64(); } + } - function list(maxPreviewWidth) { - if (!root.active || !root.cliphistAvailable) { - return; - } - if (listProc.running) - return; - loading = true; - const width = maxPreviewWidth || 100; - listProc.command = ["cliphist", "list", "-preview-width", String(width)]; - listProc.running = true; + function getImageData(id) { + if (id === undefined) { + return null; } + return root.imageDataById[id]; + } - // Get content for an ID - uses cache first, falls back to cliphist decode - function getContent(id) { - if (root.contentCache[id]) { - return root.contentCache[id]; - } - return null; + function _startNextB64() { + if (root._b64Queue.length === 0 || !root.cliphistAvailable) + return; + const job = root._b64Queue.shift(); + root._b64CurrentCb = job.cb; + root._b64CurrentMime = job.mime; + root._b64CurrentId = job.id; + decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; + decodeB64Proc.running = true; + } + + function copyToClipboard(id) { + if (!root.cliphistAvailable) { + return; } + copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; + copyProc.running = true; + } - // Async decode - checks cache first, then falls back to cliphist - function decode(id, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; - } - - // Check cache first - const cached = root.contentCache[id]; - if (cached) { - if (cb) - cb(cached); - return; - } - - // Fall back to cliphist decode - if (decodeProc.running) { - decodeProc.running = false; - } - root._decodeRequestId++; - decodeProc.requestId = root._decodeRequestId; - root._decodeCallback = function (content) { - // Cache the result if successful - if (content && content.trim()) { - root.contentCache[id] = content; - } - if (cb) - cb(content); - }; - const idStr = String(id); - decodeProc.command = ["cliphist", "decode", idStr]; - decodeProc.running = true; + function pasteFromClipboard(id, mime) { + if (!root.cliphistAvailable) { + return; } + const isImage = mime && mime.startsWith("image/"); + const typeArg = isImage ? ` --type ${mime}` : ""; + const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; + const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } - function decodeToDataUrl(id, mime, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; - } - // If cached, return immediately - if (root.imageDataById[id]) { - if (cb) - cb(root.imageDataById[id]); - return; - } - // Queue request; ensures single process handles sequentially - root._b64Queue.push({ - "id": id, - "mime": mime || "image/*", - "cb": cb - }); - if (!decodeB64Proc.running && root._b64CurrentCb === null) { - _startNextB64(); - } + function pasteText(text) { + if (!text) + return; + const escaped = text.replace(/'/g, "'\\''"); + const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function deleteById(id) { + if (!root.cliphistAvailable) { + return; } - - function getImageData(id) { - if (id === undefined) { - return null; - } - return root.imageDataById[id]; + if (deleteProc.running) { + return; } + const idStr = String(id).trim(); + // Remove from caches + delete root.contentCache[idStr]; + delete root.imageDataById[idStr]; + const orderIdx = root._imageDataInsertOrder.indexOf(idStr); + if (orderIdx !== -1) + root._imageDataInsertOrder.splice(orderIdx, 1); + deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; + deleteProc.running = true; + } - function _startNextB64() { - if (root._b64Queue.length === 0 || !root.cliphistAvailable) - return; - const job = root._b64Queue.shift(); - root._b64CurrentCb = job.cb; - root._b64CurrentMime = job.mime; - root._b64CurrentId = job.id; - decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; - decodeB64Proc.running = true; + function wipeAll() { + if (!root.cliphistAvailable) { + return; } + // Clear caches + root.contentCache = {}; + root.imageDataById = {}; + root._imageDataInsertOrder = []; + root._latestTextContent = ""; + root._latestTextId = ""; - function copyToClipboard(id) { - if (!root.cliphistAvailable) { - return; - } - copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; - copyProc.running = true; - } + Quickshell.execDetached(["cliphist", "wipe"]); + revision++; + Qt.callLater(() => list()); + } - function pasteFromClipboard(id, mime) { - if (!root.cliphistAvailable) { - return; - } - const isImage = mime && mime.startsWith("image/"); - const typeArg = isImage ? ` --type ${mime}` : ""; - const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; - const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - - function pasteText(text) { - if (!text) - return; - const escaped = text.replace(/'/g, "'\\''"); - const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - - function deleteById(id) { - if (!root.cliphistAvailable) { - return; - } - if (deleteProc.running) { - return; - } - const idStr = String(id).trim(); - // Remove from caches - delete root.contentCache[idStr]; - delete root.imageDataById[idStr]; - const orderIdx = root._imageDataInsertOrder.indexOf(idStr); - if (orderIdx !== -1) - root._imageDataInsertOrder.splice(orderIdx, 1); - deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; - deleteProc.running = true; - } - - function wipeAll() { - if (!root.cliphistAvailable) { - return; - } - // Clear caches - root.contentCache = {}; - root.imageDataById = {}; - root._imageDataInsertOrder = []; - root._latestTextContent = ""; - root._latestTextId = ""; - - Quickshell.execDetached(["cliphist", "wipe"]); - revision++; - Qt.callLater(() => list()); - } - - // Parse image metadata from cliphist preview string - function parseImageMeta(preview) { - const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; - const match = (preview || "").match(re); - if (!match) - return null; - return { - "size": match[1], - "fmt": (match[2] || "").toUpperCase(), - "w": Number(match[3]), - "h": Number(match[4]) - }; - } + // Parse image metadata from cliphist preview string + function parseImageMeta(preview) { + const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; + const match = (preview || "").match(re); + if (!match) + return null; + return { + "size": match[1], + "fmt": (match[2] || "").toUpperCase(), + "w": Number(match[3]), + "h": Number(match[4]) + }; + } }