Merge pull request #2 from bogdancornescu/bc_2

Content prefill from clipboard
This commit is contained in:
Bogdan
2026-06-25 23:06:59 +03:00
committed by GitHub
4 changed files with 121 additions and 3 deletions
+1
View File
@@ -8,6 +8,7 @@
"core:window:allow-close", "core:window:allow-close",
"global-shortcut:allow-register", "global-shortcut:allow-register",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"store:default" "store:default"
] ]
} }
+13
View File
@@ -6,6 +6,7 @@
let draftHotkey = $state(settings.hotkey); let draftHotkey = $state(settings.hotkey);
let draftFontSize = $state(settings.fontSize); let draftFontSize = $state(settings.fontSize);
let draftStayOpen = $state(settings.quickNoteStayOpen); let draftStayOpen = $state(settings.quickNoteStayOpen);
let draftClipboardPrefill = $state(settings.clipboardPrefill);
let draftTheme = $state(settings.theme); let draftTheme = $state(settings.theme);
let saving = $state(false); let saving = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
@@ -18,6 +19,7 @@
draftHotkey = settings.hotkey; draftHotkey = settings.hotkey;
draftFontSize = settings.fontSize; draftFontSize = settings.fontSize;
draftStayOpen = settings.quickNoteStayOpen; draftStayOpen = settings.quickNoteStayOpen;
draftClipboardPrefill = settings.clipboardPrefill;
draftTheme = settings.theme; draftTheme = settings.theme;
error = null; error = null;
setTimeout(() => overlayEl?.focus(), 0); setTimeout(() => overlayEl?.focus(), 0);
@@ -52,6 +54,7 @@
hotkey: draftHotkey, hotkey: draftHotkey,
fontSize: draftFontSize, fontSize: draftFontSize,
quickNoteStayOpen: draftStayOpen, quickNoteStayOpen: draftStayOpen,
clipboardPrefill: draftClipboardPrefill,
theme: draftTheme, theme: draftTheme,
}); });
onclose(); onclose();
@@ -123,6 +126,16 @@
/> />
</div> </div>
<div class="setting-row">
<label for="clipboard-prefill-toggle" class="setting-label">Prefill Quick Note from clipboard</label>
<input
id="clipboard-prefill-toggle"
type="checkbox"
bind:checked={draftClipboardPrefill}
class="setting-checkbox"
/>
</div>
<div class="setting-row"> <div class="setting-row">
<span class="setting-label">Theme</span> <span class="setting-label">Theme</span>
<div class="theme-buttons" role="group" aria-label="Theme"> <div class="theme-buttons" role="group" aria-label="Theme">
+8
View File
@@ -5,6 +5,7 @@ const DEFAULTS = {
hotkey: 'Ctrl+Shift+Space', hotkey: 'Ctrl+Shift+Space',
fontSize: 13, fontSize: 13,
quickNoteStayOpen: false, quickNoteStayOpen: false,
clipboardPrefill: true,
theme: 'dark', theme: 'dark',
}; };
@@ -14,6 +15,7 @@ class SettingsStore {
hotkey = $state(DEFAULTS.hotkey); hotkey = $state(DEFAULTS.hotkey);
fontSize = $state(DEFAULTS.fontSize); fontSize = $state(DEFAULTS.fontSize);
quickNoteStayOpen = $state(DEFAULTS.quickNoteStayOpen); quickNoteStayOpen = $state(DEFAULTS.quickNoteStayOpen);
clipboardPrefill = $state(DEFAULTS.clipboardPrefill);
theme = $state(DEFAULTS.theme); theme = $state(DEFAULTS.theme);
loaded = $state(false); loaded = $state(false);
@@ -22,6 +24,7 @@ class SettingsStore {
this.hotkey = (await store.get<string>('hotkey')) ?? DEFAULTS.hotkey; this.hotkey = (await store.get<string>('hotkey')) ?? DEFAULTS.hotkey;
this.fontSize = (await store.get<number>('fontSize')) ?? DEFAULTS.fontSize; this.fontSize = (await store.get<number>('fontSize')) ?? DEFAULTS.fontSize;
this.quickNoteStayOpen = (await store.get<boolean>('quickNoteStayOpen')) ?? DEFAULTS.quickNoteStayOpen; this.quickNoteStayOpen = (await store.get<boolean>('quickNoteStayOpen')) ?? DEFAULTS.quickNoteStayOpen;
this.clipboardPrefill = (await store.get<boolean>('clipboardPrefill')) ?? DEFAULTS.clipboardPrefill;
this.theme = (await store.get<string>('theme')) ?? DEFAULTS.theme; this.theme = (await store.get<string>('theme')) ?? DEFAULTS.theme;
this.loaded = true; this.loaded = true;
this.applyFontSize(); this.applyFontSize();
@@ -40,6 +43,7 @@ class SettingsStore {
hotkey?: string; hotkey?: string;
fontSize?: number; fontSize?: number;
quickNoteStayOpen?: boolean; quickNoteStayOpen?: boolean;
clipboardPrefill?: boolean;
theme?: string; theme?: string;
}): Promise<void> { }): Promise<void> {
const store = await load(STORE_FILE, { autoSave: false, defaults: DEFAULTS }); const store = await load(STORE_FILE, { autoSave: false, defaults: DEFAULTS });
@@ -57,6 +61,10 @@ class SettingsStore {
this.quickNoteStayOpen = patch.quickNoteStayOpen; this.quickNoteStayOpen = patch.quickNoteStayOpen;
await store.set('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) { if (patch.theme !== undefined) {
this.theme = patch.theme; this.theme = patch.theme;
this.applyTheme(); this.applyTheme();
+99 -3
View File
@@ -2,11 +2,14 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { readText } from '@tauri-apps/plugin-clipboard-manager';
import { createSnippet } from '$lib/api'; import { createSnippet } from '$lib/api';
import { detectSnippet } from '$lib/highlight'; import { detectSnippet } from '$lib/highlight';
import { settings } from '$lib/stores/settings.svelte'; import { settings } from '$lib/stores/settings.svelte';
import type { ContentType } from '$lib/types'; import type { ContentType } from '$lib/types';
const TITLE_MAX = 60;
let title = $state(''); let title = $state('');
let content = $state(''); let content = $state('');
let contentType = $state<ContentType>('text'); let contentType = $state<ContentType>('text');
@@ -15,7 +18,50 @@
let saved = $state(false); let saved = $state(false);
let saving = $state(false); let saving = $state(false);
let saveError = $state<string | null>(null); let saveError = $state<string | null>(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() { function runDetection() {
if (!content.trim()) { systemTags = []; return; } if (!content.trim()) { systemTags = []; return; }
@@ -38,7 +84,7 @@
.map(name => ({ name, source: 'user' })); .map(name => ({ name, source: 'user' }));
const sysTags = systemTags.map(name => ({ name, source: 'system' })); const sysTags = systemTags.map(name => ({ name, source: 'system' }));
await createSnippet({ await createSnippet({
title: title.trim() || 'Untitled', title: (titleEdited ? title.trim() : '') || suggestedTitle || 'Untitled',
content, content,
content_type: contentType, content_type: contentType,
tags: [...userTags, ...sysTags], tags: [...userTags, ...sysTags],
@@ -54,6 +100,8 @@
tags = ''; tags = '';
systemTags = []; systemTags = [];
contentType = 'text'; contentType = 'text';
contentPrefilled = false;
titleEdited = false;
saved = false; saved = false;
saving = false; saving = false;
}, 800); }, 800);
@@ -69,12 +117,18 @@
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') getCurrentWindow().close(); if (e.key === 'Escape') getCurrentWindow().close();
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave(); 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(() => { onMount(() => {
settings.load().then(() => { settings.load().then(() => {
document.documentElement.style.setProperty('--font-size-editor', `${settings.fontSize}px`); document.documentElement.style.setProperty('--font-size-editor', `${settings.fontSize}px`);
}); });
prefillFromClipboard();
}); });
</script> </script>
@@ -89,9 +143,11 @@
<input <input
id="title-input" id="title-input"
type="text" type="text"
bind:value={title} value={titleEdited ? title : suggestedTitle}
oninput={onTitleInput}
placeholder="Untitled snippet" placeholder="Untitled snippet"
class="title-input" class="title-input"
class:suggested={!titleEdited && suggestedTitle !== ''}
/> />
<div class="field"> <div class="field">
@@ -108,13 +164,21 @@
</div> </div>
<div class="field grow"> <div class="field grow">
{#if contentPrefilled}
<div class="prefill-chip">
<span>Prefilled from clipboard</span>
<button type="button" class="clear-btn" onclick={clearPrefill}>Clear (Ctrl+Shift+X)</button>
</div>
{/if}
<!-- svelte-ignore a11y_autofocus --> <!-- svelte-ignore a11y_autofocus -->
<textarea <textarea
id="content-area" id="content-area"
bind:value={content} bind:value={content}
oninput={() => { contentPrefilled = false; }}
onblur={runDetection} onblur={runDetection}
placeholder="Paste your snippet here…" placeholder="Paste your snippet here…"
class="content-area" class="content-area"
class:suggested={contentPrefilled}
spellcheck="false" spellcheck="false"
autofocus autofocus
></textarea> ></textarea>
@@ -217,6 +281,8 @@
.title-input:hover:not(:focus) { border-bottom-color: var(--border); } .title-input:hover:not(:focus) { border-bottom-color: var(--border); }
.title-input:focus { border-bottom-color: var(--accent-border); } .title-input:focus { border-bottom-color: var(--accent-border); }
.title-input::placeholder { color: var(--text-faint); font-weight: 400; } .title-input::placeholder { color: var(--text-faint); font-weight: 400; }
.title-input.suggested:not(:focus) { color: var(--text-muted); }
.tags-input { .tags-input {
background: var(--bg-surface); background: var(--bg-surface);
@@ -276,6 +342,36 @@
border-color: var(--accent-border); 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 { .save-error {
background: rgba(239, 68, 68, 0.12); background: rgba(239, 68, 68, 0.12);
border: 1px solid var(--danger); border: 1px solid var(--danger);