From f302a1d4048c804dd3a5a6100e9347f951d1c276 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 25 Jun 2026 23:04:32 +0300 Subject: [PATCH] Added content prefill from clipboard when adding a snippet with Quick Note, with option in settings to disable it. --- src-tauri/capabilities/default.json | 1 + src/lib/components/SettingsModal.svelte | 13 +++ src/lib/stores/settings.svelte.ts | 8 ++ src/routes/quicknote/+page.svelte | 102 +++++++++++++++++++++++- 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f242dbc..5a2933b 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,6 +8,7 @@ "core:window:allow-close", "global-shortcut:allow-register", "clipboard-manager:allow-write-text", + "clipboard-manager:allow-read-text", "store:default" ] } diff --git a/src/lib/components/SettingsModal.svelte b/src/lib/components/SettingsModal.svelte index f14ef1c..2f87522 100644 --- a/src/lib/components/SettingsModal.svelte +++ b/src/lib/components/SettingsModal.svelte @@ -6,6 +6,7 @@ let draftHotkey = $state(settings.hotkey); let draftFontSize = $state(settings.fontSize); let draftStayOpen = $state(settings.quickNoteStayOpen); + let draftClipboardPrefill = $state(settings.clipboardPrefill); let draftTheme = $state(settings.theme); let saving = $state(false); let error = $state(null); @@ -18,6 +19,7 @@ draftHotkey = settings.hotkey; draftFontSize = settings.fontSize; draftStayOpen = settings.quickNoteStayOpen; + draftClipboardPrefill = settings.clipboardPrefill; draftTheme = settings.theme; error = null; setTimeout(() => overlayEl?.focus(), 0); @@ -52,6 +54,7 @@ hotkey: draftHotkey, fontSize: draftFontSize, quickNoteStayOpen: draftStayOpen, + clipboardPrefill: draftClipboardPrefill, theme: draftTheme, }); onclose(); @@ -123,6 +126,16 @@ /> +
+ + +
+
Theme
diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index 11f4eb1..84c63bc 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -5,6 +5,7 @@ const DEFAULTS = { hotkey: 'Ctrl+Shift+Space', fontSize: 13, quickNoteStayOpen: false, + clipboardPrefill: true, theme: 'dark', }; @@ -14,6 +15,7 @@ class SettingsStore { hotkey = $state(DEFAULTS.hotkey); fontSize = $state(DEFAULTS.fontSize); quickNoteStayOpen = $state(DEFAULTS.quickNoteStayOpen); + clipboardPrefill = $state(DEFAULTS.clipboardPrefill); theme = $state(DEFAULTS.theme); loaded = $state(false); @@ -22,6 +24,7 @@ class SettingsStore { this.hotkey = (await store.get('hotkey')) ?? DEFAULTS.hotkey; this.fontSize = (await store.get('fontSize')) ?? DEFAULTS.fontSize; this.quickNoteStayOpen = (await store.get('quickNoteStayOpen')) ?? DEFAULTS.quickNoteStayOpen; + this.clipboardPrefill = (await store.get('clipboardPrefill')) ?? DEFAULTS.clipboardPrefill; this.theme = (await store.get('theme')) ?? DEFAULTS.theme; this.loaded = true; this.applyFontSize(); @@ -40,6 +43,7 @@ class SettingsStore { hotkey?: string; fontSize?: number; quickNoteStayOpen?: boolean; + clipboardPrefill?: boolean; theme?: string; }): Promise { const store = await load(STORE_FILE, { autoSave: false, defaults: DEFAULTS }); @@ -57,6 +61,10 @@ class SettingsStore { this.quickNoteStayOpen = patch.quickNoteStayOpen; await store.set('quickNoteStayOpen', patch.quickNoteStayOpen); } + if (patch.clipboardPrefill !== undefined) { + this.clipboardPrefill = patch.clipboardPrefill; + await store.set('clipboardPrefill', patch.clipboardPrefill); + } if (patch.theme !== undefined) { this.theme = patch.theme; this.applyTheme(); diff --git a/src/routes/quicknote/+page.svelte b/src/routes/quicknote/+page.svelte index 6c91e3f..f30a9ee 100644 --- a/src/routes/quicknote/+page.svelte +++ b/src/routes/quicknote/+page.svelte @@ -2,11 +2,14 @@ import { onMount } from 'svelte'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { emit } from '@tauri-apps/api/event'; + import { readText } from '@tauri-apps/plugin-clipboard-manager'; import { createSnippet } from '$lib/api'; import { detectSnippet } from '$lib/highlight'; import { settings } from '$lib/stores/settings.svelte'; import type { ContentType } from '$lib/types'; + const TITLE_MAX = 60; + let title = $state(''); let content = $state(''); let contentType = $state('text'); @@ -15,7 +18,50 @@ let saved = $state(false); let saving = $state(false); let saveError = $state(null); - const canSave = $derived(!saving && (title.trim() !== '' || content.trim() !== '')); + + let contentPrefilled = $state(false); + let titleEdited = $state(false); + + const suggestedTitle = $derived.by(() => { + const firstLine = content.split('\n').find((l) => l.trim().length > 0)?.trim() ?? ''; + return firstLine.length > TITLE_MAX + ? `${firstLine.slice(0, TITLE_MAX).trimEnd()}…` + : firstLine; + }); + + const canSave = $derived( + !saving && ((titleEdited ? title.trim() : suggestedTitle) !== '' || content.trim() !== ''), + ); + + function onTitleInput(e: Event) { + titleEdited = true; + title = (e.currentTarget as HTMLInputElement).value; + } + + function clearPrefill() { + content = ''; + contentPrefilled = false; + titleEdited = false; + title = ''; + systemTags = []; + contentType = 'text'; + document.getElementById('content-area')?.focus(); + } + + async function prefillFromClipboard() { + if (!settings.loaded) await settings.load(); + if (!settings.clipboardPrefill) return; + try { + const text = await readText(); + if (text && text.trim()) { + content = text; + contentPrefilled = true; + try { runDetection(); } catch { /* detection is best-effort */ } + } + } catch { + /* clipboard empty or non-text — nothing to suggest */ + } + } function runDetection() { if (!content.trim()) { systemTags = []; return; } @@ -38,7 +84,7 @@ .map(name => ({ name, source: 'user' })); const sysTags = systemTags.map(name => ({ name, source: 'system' })); await createSnippet({ - title: title.trim() || 'Untitled', + title: (titleEdited ? title.trim() : '') || suggestedTitle || 'Untitled', content, content_type: contentType, tags: [...userTags, ...sysTags], @@ -54,6 +100,8 @@ tags = ''; systemTags = []; contentType = 'text'; + contentPrefilled = false; + titleEdited = false; saved = false; saving = false; }, 800); @@ -69,12 +117,18 @@ function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') getCurrentWindow().close(); if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave(); + // Ctrl+Shift+X clears the clipboard suggestion; only active while one is shown. + if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'x' || e.key === 'X') && contentPrefilled) { + e.preventDefault(); + clearPrefill(); + } } onMount(() => { settings.load().then(() => { document.documentElement.style.setProperty('--font-size-editor', `${settings.fontSize}px`); }); + prefillFromClipboard(); }); @@ -89,9 +143,11 @@
@@ -108,13 +164,21 @@
+ {#if contentPrefilled} +
+ Prefilled from clipboard + +
+ {/if} @@ -217,6 +281,8 @@ .title-input:hover:not(:focus) { border-bottom-color: var(--border); } .title-input:focus { border-bottom-color: var(--accent-border); } .title-input::placeholder { color: var(--text-faint); font-weight: 400; } + + .title-input.suggested:not(:focus) { color: var(--text-muted); } .tags-input { background: var(--bg-surface); @@ -276,6 +342,36 @@ border-color: var(--accent-border); } + /* Clipboard-seeded content: subtle accent edge until the user edits it. */ + .content-area.suggested { + border-color: var(--accent-border); + border-left-width: 3px; + background: var(--accent-tint); + } + + .prefill-chip { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--font-body); + font-size: 11px; + color: var(--accent); + margin-bottom: -0.05rem; + } + + .clear-btn { + background: none; + border: none; + color: var(--text-muted); + font-family: var(--font-body); + font-size: 11px; + padding: 0; + cursor: pointer; + text-decoration: underline; + } + + .clear-btn:hover { color: var(--text); } + .save-error { background: rgba(239, 68, 68, 0.12); border: 1px solid var(--danger);