// Clipboard history store for managing copied code snippets // Implements issue #25 - Clipboard History feature import { invoke } from "@tauri-apps/api/core"; import { writable, derived } from "svelte/store"; export interface ClipboardEntry { id: string; content: string; language: string | null; source: string | null; timestamp: string; is_pinned: boolean; } // Create stores const entriesStore = writable([]); const searchQueryStore = writable(""); const languageFilterStore = writable(null); const isLoadingStore = writable(false); // Derived store for filtered entries const filteredEntriesStore = derived( [entriesStore, searchQueryStore, languageFilterStore], ([$entries, $searchQuery, $languageFilter]) => { let filtered = $entries; // Filter by language if ($languageFilter) { filtered = filtered.filter((e) => e.language === $languageFilter); } // Filter by search query if ($searchQuery) { const query = $searchQuery.toLowerCase(); filtered = filtered.filter( (e) => e.content.toLowerCase().includes(query) || e.language?.toLowerCase().includes(query) || e.source?.toLowerCase().includes(query) ); } return filtered; } ); // Derived store for unique languages const languagesStore = derived(entriesStore, ($entries) => { const languages = new Set(); for (const entry of $entries) { if (entry.language) { languages.add(entry.language); } } return Array.from(languages).sort(); }); // Helper function to detect language from content function detectLanguage(content: string): string | null { // Common language patterns const patterns: [RegExp, string][] = [ [/^(import|export|const|let|var|function|class|interface|type)\s/m, "typescript"], [/^(def|class|import|from|if __name__|async def)\s/m, "python"], [/^(fn|let|mut|impl|struct|enum|use|mod|pub)\s/m, "rust"], [/^(package|import|func|type|var|const)\s/m, "go"], [/<\?php/m, "php"], [/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)\s/im, "sql"], [/^ { isLoadingStore.set(true); try { const entries = await invoke("list_clipboard_entries", { language: null, }); entriesStore.set(entries); } catch (error) { console.error("Failed to load clipboard entries:", error); } finally { isLoadingStore.set(false); } } async function captureClipboard( content: string, language?: string | null, source?: string | null ): Promise { try { // Auto-detect language if not provided const detectedLanguage = language ?? detectLanguage(content); const entry = await invoke("capture_clipboard", { content, language: detectedLanguage, source: source ?? null, }); // Reload entries to get the updated list await loadEntries(); return entry; } catch (error) { console.error("Failed to capture clipboard:", error); return null; } } async function deleteEntry(id: string): Promise { try { await invoke("delete_clipboard_entry", { id }); entriesStore.update((entries) => entries.filter((e) => e.id !== id)); } catch (error) { console.error("Failed to delete clipboard entry:", error); } } async function togglePin(id: string): Promise { try { const updated = await invoke("toggle_pin_clipboard_entry", { id }); entriesStore.update((entries) => entries .map((e) => (e.id === id ? updated : e)) .sort((a, b) => { // Pinned first, then by timestamp if (a.is_pinned && !b.is_pinned) return -1; if (!a.is_pinned && b.is_pinned) return 1; return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }) ); } catch (error) { console.error("Failed to toggle pin:", error); } } async function clearHistory(): Promise { try { await invoke("clear_clipboard_history"); entriesStore.update((entries) => entries.filter((e) => e.is_pinned)); } catch (error) { console.error("Failed to clear clipboard history:", error); } } async function updateLanguage(id: string, language: string | null): Promise { try { const updated = await invoke("update_clipboard_language", { id, language, }); entriesStore.update((entries) => entries.map((e) => (e.id === id ? updated : e))); } catch (error) { console.error("Failed to update language:", error); } } function setSearchQuery(query: string): void { searchQueryStore.set(query); } function setLanguageFilter(language: string | null): void { languageFilterStore.set(language); } // Copy entry content to clipboard async function copyToClipboard(content: string): Promise { try { await navigator.clipboard.writeText(content); return true; } catch (error) { console.error("Failed to copy to clipboard:", error); return false; } } // Format timestamp for display function formatTimestamp(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } // Export the store export const clipboardStore = { subscribe: entriesStore.subscribe, entries: entriesStore, filteredEntries: filteredEntriesStore, languages: languagesStore, searchQuery: searchQueryStore, languageFilter: languageFilterStore, isLoading: isLoadingStore, loadEntries, captureClipboard, deleteEntry, togglePin, clearHistory, updateLanguage, setSearchQuery, setLanguageFilter, copyToClipboard, formatTimestamp, detectLanguage, };