mirror of
https://github.com/bogdancornescu/Faro.git
synced 2026-07-08 19:20:47 +02:00
Merge pull request #2 from bogdancornescu/bc_2
Content prefill from clipboard
This commit is contained in:
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user