generated from nhcarrigan/template
4c46d4c8fd
## Summary This PR adds a collection of productivity features and UI enhancements to improve the Hikari Desktop experience: ### New Features - **Clipboard History** (#25) - Track and manage copied code snippets with language detection, search, filtering, and pinning - **Quick Actions Panel** (#15) - Buttons for common quick actions like "Review PR", "Run tests", "Explain file", with customizable actions - **Git Integration Panel** (#24) - View current branch, changed/staged files, quick git actions (commit, push, pull), and branch management - **Session Import/Export** (#8) - Export conversations to JSON and import previously saved sessions - **Snippet Library** (#22) - Save and reuse common prompts with categories and quick insert - **Session History** (#14) - Auto-save conversations with browsable history and search - **High Contrast Mode** (#20) - Accessibility theme with improved visibility - **Minimize to System Tray** (#11) - System tray support with right-click menu ### UI Enhancements - Trans-pride gradient theme applied across UI elements - Copy button added to code blocks - Linter formatting and eslint-disable comments for cleaner code ## Closes Closes #8 Closes #11 Closes #14 Closes #15 Closes #20 Closes #22 Closes #24 Closes #25 Closes #34 Closes #35 Closes #36 Closes #37 Closes #69 Closes #70 ## Test Plan - [ ] Verify clipboard history captures code from code block copy buttons - [ ] Verify clipboard history captures manually selected text from terminal - [ ] Test snippet library CRUD operations and insertion - [ ] Test quick actions panel with default and custom actions - [ ] Test git panel shows correct status, branch, and performs git operations - [ ] Test session history auto-save and restore - [ ] Test session import/export roundtrip - [ ] Verify high contrast mode provides adequate contrast - [ ] Test minimize to tray functionality and tray menu - [ ] Verify trans-pride gradient theme displays correctly in all themes --- *✨ This PR was created with help from Hikari~ 🌸* Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #68 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
231 lines
6.4 KiB
TypeScript
231 lines
6.4 KiB
TypeScript
// 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<ClipboardEntry[]>([]);
|
|
const searchQueryStore = writable<string>("");
|
|
const languageFilterStore = writable<string | null>(null);
|
|
const isLoadingStore = writable<boolean>(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<string>();
|
|
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"],
|
|
[/^<!DOCTYPE|^<html|^<div|^<span/im, "html"],
|
|
[/^\s*\{[\s\S]*"[\w-]+":/m, "json"],
|
|
[/^---\s*\n/m, "yaml"],
|
|
[/^\s*#\s*(include|define|ifdef)/m, "c"],
|
|
[/^(public|private|protected)\s+(class|interface|static)/m, "java"],
|
|
[/^\$[\w_]+\s*=/m, "bash"],
|
|
];
|
|
|
|
for (const [pattern, lang] of patterns) {
|
|
if (pattern.test(content)) {
|
|
return lang;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Store actions
|
|
async function loadEntries(): Promise<void> {
|
|
isLoadingStore.set(true);
|
|
try {
|
|
const entries = await invoke<ClipboardEntry[]>("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<ClipboardEntry | null> {
|
|
try {
|
|
// Auto-detect language if not provided
|
|
const detectedLanguage = language ?? detectLanguage(content);
|
|
|
|
const entry = await invoke<ClipboardEntry>("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<void> {
|
|
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<void> {
|
|
try {
|
|
const updated = await invoke<ClipboardEntry>("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<void> {
|
|
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<void> {
|
|
try {
|
|
const updated = await invoke<ClipboardEntry>("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<boolean> {
|
|
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,
|
|
};
|