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;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-meJOE1Ze.js"></script>
|
<script type="module" crossorigin src="/assets/main-OClTUyKu.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-es_E5H-H.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-BE1Aa3xO.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,8 @@
|
||||||
let suggestions = []
|
let suggestions = []
|
||||||
let suggestionCount = 0
|
let suggestionCount = 0
|
||||||
let inboxNodes = []
|
let inboxNodes = []
|
||||||
|
let inboxCaptureBusy = false
|
||||||
|
let inboxCaptureStatus = ''
|
||||||
let trashInfo = null
|
let trashInfo = null
|
||||||
let showCreateNode = false
|
let showCreateNode = false
|
||||||
let newNodeTitle = ''
|
let newNodeTitle = ''
|
||||||
|
|
@ -565,9 +567,16 @@
|
||||||
// ===== Keyboard =====
|
// ===== Keyboard =====
|
||||||
|
|
||||||
function handleKeydown(e) {
|
function handleKeydown(e) {
|
||||||
if (activeTab !== 'files') return
|
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') 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.ctrlKey || e.metaKey) {
|
||||||
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); copySelected() }
|
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); copySelected() }
|
||||||
else if (e.key === 'x' || e.key === 'X') { e.preventDefault(); cutSelected() }
|
else if (e.key === 'x' || e.key === 'X') { e.preventDefault(); cutSelected() }
|
||||||
|
|
@ -1382,6 +1391,39 @@
|
||||||
if (!source) return ''
|
if (!source) return ''
|
||||||
return t('capture.source.' + source)
|
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) {
|
function pluralize(n, one, few, many) {
|
||||||
n = Math.abs(n) % 100
|
n = Math.abs(n) % 100
|
||||||
if (n >= 5 && n <= 20) return many
|
if (n >= 5 && n <= 20) return many
|
||||||
|
|
@ -2012,6 +2054,14 @@
|
||||||
<h2>{t('nav.inbox')}</h2>
|
<h2>{t('nav.inbox')}</h2>
|
||||||
<p>{t('inbox.subtitle')}</p>
|
<p>{t('inbox.subtitle')}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{#if inboxNodes.length === 0}
|
{#if inboxNodes.length === 0}
|
||||||
<div class="empty-state">
|
<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 { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
|
||||||
.inbox-header h2 { margin: 0 0 6px; }
|
.inbox-header h2 { margin: 0 0 6px; }
|
||||||
.inbox-header p { margin: 0; color: #a0a0b8; font-size: 13px; }
|
.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-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 { 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; }
|
.inbox-item:hover { border-color: #3a3a5c; background: #1e1e32; }
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export default {
|
||||||
'nav.moveToRoot': 'Move to root',
|
'nav.moveToRoot': 'Move to root',
|
||||||
'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
|
'inbox.subtitle': 'Captured materials that still need to be assigned to cases',
|
||||||
'inbox.empty': 'No unprocessed items',
|
'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.text': 'Text',
|
||||||
'capture.kind.url': 'Link',
|
'capture.kind.url': 'Link',
|
||||||
'capture.kind.file': 'File',
|
'capture.kind.file': 'File',
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ export default {
|
||||||
|
|
||||||
'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
|
'inbox.subtitle': 'Захваченные материалы, которые нужно разложить по делам',
|
||||||
'inbox.empty': 'Неразобранных элементов нет',
|
'inbox.empty': 'Неразобранных элементов нет',
|
||||||
|
'inbox.pasteClipboard': 'Вставить из буфера',
|
||||||
|
'inbox.captured': 'Добавлено',
|
||||||
|
'inbox.clipboardEmpty': 'Буфер обмена пуст',
|
||||||
|
'inbox.clipboardUnavailable': 'Буфер обмена недоступен',
|
||||||
|
|
||||||
'capture.kind.text': 'Текст',
|
'capture.kind.text': 'Текст',
|
||||||
'capture.kind.url': 'Ссылка',
|
'capture.kind.url': 'Ссылка',
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,12 @@ async function runReadyScenario(cdp, url) {
|
||||||
await assertText(cdp, 'Текст', 'inbox: capture kind visible')
|
await assertText(cdp, 'Текст', 'inbox: capture kind visible')
|
||||||
await assertText(cdp, 'Буфер обмена', 'inbox: capture source 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 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 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 assertText(cdp, 'Inbox Smoke Item', 'inbox: item opens from list')
|
||||||
|
|
||||||
await clickText(cdp, '.nav-item', 'Корзина')
|
await clickText(cdp, '.nav-item', 'Корзина')
|
||||||
|
|
@ -335,6 +339,13 @@ async function setInputValue(cdp, selector, value) {
|
||||||
await sleep(100)
|
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) {
|
async function clickFolderOpenButton(cdp, name) {
|
||||||
const ok = await evalValue(cdp, `
|
const ok = await evalValue(cdp, `
|
||||||
(() => {
|
(() => {
|
||||||
|
|
@ -559,6 +570,13 @@ function wailsMockSource() {
|
||||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||||
const mode = new URL(location.href).searchParams.get('smokeMode') || 'ready';
|
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 = [
|
const templates = [
|
||||||
{ id: 'folder.default', title: 'template.folder.default', icon: 'folder', enabled: true },
|
{ id: 'folder.default', title: 'template.folder.default', icon: 'folder', enabled: true },
|
||||||
{ id: 'project.default', title: 'template.project.default', icon: 'project', 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)),
|
ListWorkspaceTree: async () => clone(state.nodes.filter((node) => node.captureInbox !== true)),
|
||||||
ListWorkspaceChildren: async (id) => clone(childrenOf(id)),
|
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 || '' }))),
|
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({
|
ListTrash: async () => clone({
|
||||||
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
|
trashPath: '/tmp/verstak-smoke-vault/.verstak/trash',
|
||||||
nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }],
|
nodes: [{ id: 'node-trash', title: 'Trash Smoke Folder', type: 'folder', fsPath: 'Trash Smoke Folder', deletedAt: now }],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue