feat: capture clipboard links in inbox gui
This commit is contained in:
parent
44d0be2649
commit
326f6f283d
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,8 +16,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-meJOE1Ze.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-es_E5H-H.css">
|
||||
<script type="module" crossorigin src="/assets/main-OClTUyKu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BE1Aa3xO.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@
|
|||
let suggestions = []
|
||||
let suggestionCount = 0
|
||||
let inboxNodes = []
|
||||
let inboxCaptureBusy = false
|
||||
let inboxCaptureStatus = ''
|
||||
let trashInfo = null
|
||||
let showCreateNode = false
|
||||
let newNodeTitle = ''
|
||||
|
|
@ -565,9 +567,16 @@
|
|||
// ===== Keyboard =====
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (activeTab !== 'files') return
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||
|
||||
if (selectedSection === 'inbox' && (e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V')) {
|
||||
e.preventDefault()
|
||||
captureClipboard()
|
||||
return
|
||||
}
|
||||
|
||||
if (activeTab !== 'files') return
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); copySelected() }
|
||||
else if (e.key === 'x' || e.key === 'X') { e.preventDefault(); cutSelected() }
|
||||
|
|
@ -1382,6 +1391,39 @@
|
|||
if (!source) return ''
|
||||
return t('capture.source.' + source)
|
||||
}
|
||||
function looksLikeURL(value) {
|
||||
try {
|
||||
const parsed = new URL(value)
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
async function captureClipboard() {
|
||||
if (inboxCaptureBusy) return
|
||||
inboxCaptureStatus = ''
|
||||
if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') {
|
||||
error = t('inbox.clipboardUnavailable')
|
||||
return
|
||||
}
|
||||
inboxCaptureBusy = true
|
||||
try {
|
||||
const text = (await navigator.clipboard.readText()).trim()
|
||||
if (!text) {
|
||||
inboxCaptureStatus = t('inbox.clipboardEmpty')
|
||||
return
|
||||
}
|
||||
const item = looksLikeURL(text)
|
||||
? await wailsCall('CaptureURL', text, '')
|
||||
: await wailsCall('CaptureText', text)
|
||||
inboxNodes = [item, ...inboxNodes.filter(existing => existing.id !== item.id)]
|
||||
inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
} finally {
|
||||
inboxCaptureBusy = false
|
||||
}
|
||||
}
|
||||
function pluralize(n, one, few, many) {
|
||||
n = Math.abs(n) % 100
|
||||
if (n >= 5 && n <= 20) return many
|
||||
|
|
@ -2012,6 +2054,14 @@
|
|||
<h2>{t('nav.inbox')}</h2>
|
||||
<p>{t('inbox.subtitle')}</p>
|
||||
</div>
|
||||
<div class="inbox-header-actions">
|
||||
<button class="btn btn-primary" on:click={captureClipboard} disabled={inboxCaptureBusy}>
|
||||
{inboxCaptureBusy ? t('common.loading') : t('inbox.pasteClipboard')}
|
||||
</button>
|
||||
{#if inboxCaptureStatus}
|
||||
<span class="inbox-capture-status">{inboxCaptureStatus}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if inboxNodes.length === 0}
|
||||
<div class="empty-state">
|
||||
|
|
@ -2801,6 +2851,8 @@
|
|||
.inbox-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||
.inbox-header h2 { margin: 0 0 6px; }
|
||||
.inbox-header p { margin: 0; color: #a0a0b8; font-size: 13px; }
|
||||
.inbox-header-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.inbox-capture-status { color: #8ee6b1; font-size: 12px; }
|
||||
.inbox-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.inbox-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #2a2a3c; border-radius: 8px; background: #1a1a28; cursor: pointer; }
|
||||
.inbox-item:hover { border-color: #3a3a5c; background: #1e1e32; }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export default {
|
|||
'nav.moveToRoot': 'Move to root',
|
||||
'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
|
||||
'inbox.empty': 'No unprocessed items',
|
||||
'inbox.pasteClipboard': 'Paste from clipboard',
|
||||
'inbox.captured': 'Added',
|
||||
'inbox.clipboardEmpty': 'Clipboard is empty',
|
||||
'inbox.clipboardUnavailable': 'Clipboard is unavailable',
|
||||
'capture.kind.text': 'Text',
|
||||
'capture.kind.url': 'Link',
|
||||
'capture.kind.file': 'File',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export default {
|
|||
|
||||
'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
|
||||
'inbox.empty': 'Неразобранных элементов нет',
|
||||
'inbox.pasteClipboard': 'Вставить из буфера',
|
||||
'inbox.captured': 'Добавлено',
|
||||
'inbox.clipboardEmpty': 'Буфер обмена пуст',
|
||||
'inbox.clipboardUnavailable': 'Буфер обмена недоступен',
|
||||
|
||||
'capture.kind.text': 'Текст',
|
||||
'capture.kind.url': 'Ссылка',
|
||||
|
|
|
|||
|
|
@ -140,8 +140,12 @@ async function runReadyScenario(cdp, url) {
|
|||
await assertText(cdp, 'Текст', 'inbox: capture kind visible')
|
||||
await assertText(cdp, 'Буфер обмена', 'inbox: capture source visible')
|
||||
await assertEval(cdp, `!document.querySelector('.inbox-screen')?.innerText.includes('Manual Root Item')`, 'inbox: manual root is hidden')
|
||||
await setClipboardText(cdp, 'https://example.test/from-clipboard')
|
||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||
await assertText(cdp, 'https://example.test/from-clipboard', 'inbox: clipboard URL captured')
|
||||
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
|
||||
await screenshot(cdp, 'inbox.png')
|
||||
await clickText(cdp, '.inbox-item-actions .btn', 'Открыть')
|
||||
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item')
|
||||
await assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
||||
|
||||
await clickText(cdp, '.nav-item', 'Корзина')
|
||||
|
|
@ -335,6 +339,13 @@ async function setInputValue(cdp, selector, value) {
|
|||
await sleep(100)
|
||||
}
|
||||
|
||||
async function setClipboardText(cdp, value) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ = ${JSON.stringify(value)}`,
|
||||
returnByValue: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function clickFolderOpenButton(cdp, name) {
|
||||
const ok = await evalValue(cdp, `
|
||||
(() => {
|
||||
|
|
@ -559,6 +570,13 @@ function wailsMockSource() {
|
|||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
const mode = new URL(location.href).searchParams.get('smokeMode') || 'ready';
|
||||
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: {
|
||||
readText: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '',
|
||||
},
|
||||
});
|
||||
|
||||
const templates = [
|
||||
{ id: 'folder.default', title: 'template.folder.default', icon: 'folder', enabled: true },
|
||||
{ id: 'project.default', title: 'template.project.default', icon: 'project', enabled: true },
|
||||
|
|
@ -691,6 +709,16 @@ function wailsMockSource() {
|
|||
ListWorkspaceTree: async () => clone(state.nodes.filter((node) => node.captureInbox !== true)),
|
||||
ListWorkspaceChildren: async (id) => clone(childrenOf(id)),
|
||||
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true).map((node) => ({ ...node, captureKind: node.captureKind || '', captureSource: node.captureSource || '' }))),
|
||||
CaptureText: async (text) => {
|
||||
const node = { id: 'node-capture-text-' + Date.now(), title: String(text || '').trim().split('\\n').find(Boolean) || 'Captured text', type: 'note', section: '', captureInbox: true, captureKind: 'text', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
},
|
||||
CaptureURL: async (url, title) => {
|
||||
const node = { id: 'node-capture-url-' + Date.now(), title: title || url, type: 'note', section: '', captureInbox: true, captureKind: 'url', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
},
|
||||
ListTrash: async () => clone({
|
||||
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
|
||||
nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }],
|
||||
|
|
|
|||
Loading…
Reference in New Issue