feat: unify frontend capture pipeline
This commit is contained in:
parent
9e70e36f7f
commit
c1dfc456ec
|
|
@ -138,6 +138,8 @@
|
|||
let dragIds = []
|
||||
let dropRootValid = false
|
||||
let inboxDropValid = false
|
||||
let captureDropActive = false
|
||||
let captureDropLabel = ''
|
||||
|
||||
let showConfirm = false
|
||||
let confirmTitle = ''
|
||||
|
|
@ -219,6 +221,10 @@
|
|||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('paste', handleGlobalPaste)
|
||||
window.addEventListener('dragover', handleGlobalDragOver)
|
||||
window.addEventListener('dragleave', handleGlobalDragLeave)
|
||||
window.addEventListener('drop', handleGlobalDrop)
|
||||
|
||||
loading = false
|
||||
loadSyncStatus()
|
||||
|
|
@ -227,6 +233,10 @@
|
|||
onDestroy(() => {
|
||||
if (unlistenDrop) unlistenDrop()
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('paste', handleGlobalPaste)
|
||||
window.removeEventListener('dragover', handleGlobalDragOver)
|
||||
window.removeEventListener('dragleave', handleGlobalDragLeave)
|
||||
window.removeEventListener('drop', handleGlobalDrop)
|
||||
})
|
||||
|
||||
// ===== System view / Node selection =====
|
||||
|
|
@ -544,6 +554,7 @@
|
|||
// ===== Drag-and-drop =====
|
||||
|
||||
function onDragStart(e, id) {
|
||||
e.stopPropagation()
|
||||
const ids = selectedIds.includes(id) ? selectedIds : [id]
|
||||
dragIds = ids
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
|
|
@ -554,12 +565,14 @@
|
|||
const item = fileItems.find(x => x.id === folderId)
|
||||
if (item && item.type === 'folder') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrop(e, folderId) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (dragIds.length === 0) return
|
||||
for (const id of dragIds) {
|
||||
try {
|
||||
|
|
@ -573,14 +586,13 @@
|
|||
|
||||
// ===== Keyboard =====
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||
function isEditableTarget(target) {
|
||||
if (!target || !(target instanceof Element)) return false
|
||||
return !!target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')
|
||||
}
|
||||
|
||||
if (selectedSection === 'inbox' && (e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V')) {
|
||||
e.preventDefault()
|
||||
captureClipboard()
|
||||
return
|
||||
}
|
||||
function handleKeydown(e) {
|
||||
if (isEditableTarget(e.target)) return
|
||||
|
||||
if (activeTab !== 'files') return
|
||||
|
||||
|
|
@ -814,6 +826,7 @@
|
|||
|
||||
async function handleDropRoot(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
try {
|
||||
const draggedId = e.dataTransfer.getData('text/plain')
|
||||
if (!draggedId) return
|
||||
|
|
@ -1342,16 +1355,7 @@
|
|||
// ===== Drag-and-drop =====
|
||||
async function onFilesDropped(paths) {
|
||||
if (!paths || paths.length === 0) return
|
||||
if (selectedSection === 'inbox') {
|
||||
await captureDroppedPaths(paths)
|
||||
return
|
||||
}
|
||||
if (!selectedNode) {
|
||||
error = t('error.selectCaseFirst')
|
||||
return
|
||||
}
|
||||
const path = paths[0]
|
||||
await startImport(selectedNode.id, path)
|
||||
await captureDroppedPaths(paths, 'drop')
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
|
@ -1435,56 +1439,142 @@
|
|||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
async function captureClipboardFile() {
|
||||
if (!navigator.clipboard || typeof navigator.clipboard.read !== 'function') return false
|
||||
const items = await navigator.clipboard.read()
|
||||
for (const item of items || []) {
|
||||
const type = (item.types || []).find(value => value.startsWith('image/'))
|
||||
if (!type) continue
|
||||
const blob = await item.getType(type)
|
||||
const filename = blob.name || `clipboard.${extensionForMime(type)}`
|
||||
const data = await blobToBase64(blob)
|
||||
const captured = await wailsCall('CaptureFileData', filename, data)
|
||||
addInboxCapture(captured)
|
||||
inboxCaptureStatus = t('inbox.captured')
|
||||
return true
|
||||
|
||||
function resolveCaptureContext() {
|
||||
if (selectedNode && selectedNode.id) {
|
||||
return {
|
||||
contextType: 'node',
|
||||
nodeId: selectedNode.id,
|
||||
suggestedTargetNodeId: selectedNode.id,
|
||||
}
|
||||
}
|
||||
if (selectedSection) {
|
||||
return {
|
||||
contextType: 'section',
|
||||
section: selectedSection,
|
||||
}
|
||||
}
|
||||
return {
|
||||
contextType: 'global',
|
||||
section: 'root',
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function captureContextJSON() {
|
||||
return JSON.stringify(resolveCaptureContext())
|
||||
}
|
||||
|
||||
function captureContextLabel() {
|
||||
if (selectedNode && selectedNode.id) {
|
||||
return t('capture.dropOverlayNode', { title: selectedNode.title })
|
||||
}
|
||||
return t('capture.dropOverlayGlobal')
|
||||
}
|
||||
|
||||
async function refreshInboxAfterCapture(item = null) {
|
||||
if (item) addInboxCapture(item)
|
||||
if (selectedSection === 'inbox') {
|
||||
inboxNodes = await wailsCall('ListInboxNodes') || inboxNodes
|
||||
}
|
||||
}
|
||||
|
||||
async function captureTextPayload(text, source) {
|
||||
const value = String(text || '').trim()
|
||||
if (!value) return null
|
||||
const item = looksLikeURL(value)
|
||||
? await wailsCall('CaptureURLWithContext', value, '', source, captureContextJSON())
|
||||
: await wailsCall('CaptureTextWithContext', value, source, captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
return item
|
||||
}
|
||||
|
||||
async function captureUrlPayload(url, title, source) {
|
||||
const value = String(url || '').trim()
|
||||
if (!value) return null
|
||||
const item = await wailsCall('CaptureURLWithContext', value, title || '', source, captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
return item
|
||||
}
|
||||
|
||||
async function captureFilePayload(file, source) {
|
||||
if (!file) return null
|
||||
const path = file.path || file.webkitRelativePath || ''
|
||||
if (path) {
|
||||
const item = await wailsCall('CapturePathWithContext', path, source, captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
return item
|
||||
}
|
||||
const data = await blobToBase64(file)
|
||||
const item = await wailsCall('CaptureFileDataWithContext', file.name || `clipboard.${extensionForMime(file.type)}`, data, source, captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
return item
|
||||
}
|
||||
|
||||
function parseMozURL(value) {
|
||||
const lines = String(value || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean)
|
||||
if (!lines.length) return null
|
||||
return { url: lines[0], title: lines[1] || '' }
|
||||
}
|
||||
|
||||
function parseURIList(value) {
|
||||
const lines = String(value || '').split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'))
|
||||
return lines[0] || ''
|
||||
}
|
||||
|
||||
async function captureTransferData(dataTransfer, source) {
|
||||
if (!dataTransfer) return false
|
||||
let captured = false
|
||||
const filesList = Array.from(dataTransfer.files || [])
|
||||
for (const file of filesList) {
|
||||
await captureFilePayload(file, source)
|
||||
captured = true
|
||||
}
|
||||
const moz = dataTransfer.getData?.('text/x-moz-url')
|
||||
if (moz) {
|
||||
const parsed = parseMozURL(moz)
|
||||
if (parsed && looksLikeURL(parsed.url)) {
|
||||
await captureUrlPayload(parsed.url, parsed.title, source)
|
||||
return true
|
||||
}
|
||||
}
|
||||
const uri = dataTransfer.getData?.('text/uri-list')
|
||||
if (uri) {
|
||||
const url = parseURIList(uri)
|
||||
if (looksLikeURL(url)) {
|
||||
await captureUrlPayload(url, '', source)
|
||||
return true
|
||||
}
|
||||
}
|
||||
const text = dataTransfer.getData?.('text/plain')
|
||||
if (String(text || '').trim()) {
|
||||
await captureTextPayload(text, source)
|
||||
captured = true
|
||||
}
|
||||
return captured
|
||||
}
|
||||
|
||||
async function captureClipboard() {
|
||||
if (inboxCaptureBusy) return
|
||||
inboxCaptureStatus = ''
|
||||
if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') {
|
||||
error = t('inbox.clipboardUnavailable')
|
||||
return
|
||||
}
|
||||
inboxCaptureBusy = true
|
||||
try {
|
||||
if (await captureClipboardFile()) return
|
||||
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)
|
||||
addInboxCapture(item)
|
||||
const item = await wailsCall('CaptureClipboardTextWithContext', captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
error = String(e).includes('clipboard is empty') ? t('inbox.clipboardEmpty') : t('inbox.clipboardUnavailable')
|
||||
} finally {
|
||||
inboxCaptureBusy = false
|
||||
}
|
||||
}
|
||||
async function captureDroppedPaths(paths) {
|
||||
async function captureDroppedPaths(paths, source = 'drop') {
|
||||
if (inboxCaptureBusy) return
|
||||
inboxCaptureBusy = true
|
||||
inboxCaptureStatus = ''
|
||||
try {
|
||||
for (const path of paths) {
|
||||
const item = await wailsCall('CapturePath', path)
|
||||
addInboxCapture(item)
|
||||
const item = await wailsCall('CapturePathWithContext', path, source, captureContextJSON())
|
||||
await refreshInboxAfterCapture(item)
|
||||
}
|
||||
inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (e) {
|
||||
|
|
@ -1494,6 +1584,50 @@
|
|||
inboxDropValid = false
|
||||
}
|
||||
}
|
||||
async function handleGlobalPaste(e) {
|
||||
if (showFirstRun || showRecovery) return
|
||||
if (isEditableTarget(e.target)) return
|
||||
if (!e.clipboardData) return
|
||||
try {
|
||||
const captured = await captureTransferData(e.clipboardData, 'paste')
|
||||
if (captured) {
|
||||
e.preventDefault()
|
||||
inboxCaptureStatus = t('inbox.captured')
|
||||
}
|
||||
} catch (err) {
|
||||
error = String(err)
|
||||
}
|
||||
}
|
||||
function hasExternalCaptureData(dataTransfer) {
|
||||
const types = Array.from(dataTransfer?.types || [])
|
||||
return types.includes('Files') ||
|
||||
types.includes('text/uri-list') ||
|
||||
types.includes('text/x-moz-url') ||
|
||||
(types.includes('text/plain') && !types.includes('application/x-verstak-node'))
|
||||
}
|
||||
function handleGlobalDragOver(e) {
|
||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
captureDropLabel = captureContextLabel()
|
||||
captureDropActive = true
|
||||
}
|
||||
function handleGlobalDragLeave(e) {
|
||||
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||
captureDropActive = false
|
||||
}
|
||||
}
|
||||
async function handleGlobalDrop(e) {
|
||||
if (!hasExternalCaptureData(e.dataTransfer)) return
|
||||
e.preventDefault()
|
||||
captureDropActive = false
|
||||
try {
|
||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||
} catch (err) {
|
||||
error = String(err)
|
||||
}
|
||||
}
|
||||
function handleInboxDragOver(e) {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
|
|
@ -1504,11 +1638,10 @@
|
|||
}
|
||||
async function handleInboxDrop(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
inboxDropValid = false
|
||||
const paths = Array.from(e.dataTransfer?.files || [])
|
||||
.map(file => file.path || file.webkitRelativePath || '')
|
||||
.filter(Boolean)
|
||||
if (paths.length > 0) await captureDroppedPaths(paths)
|
||||
const captured = await captureTransferData(e.dataTransfer, 'drop')
|
||||
if (captured) inboxCaptureStatus = t('inbox.captured')
|
||||
}
|
||||
function openAssignInbox(item) {
|
||||
assignInboxItem = item
|
||||
|
|
@ -1772,6 +1905,11 @@
|
|||
<VaultRecovery vaultPath={startupStatus?.vaultPath || ''} onComplete={onRecoveryComplete} />
|
||||
{:else}
|
||||
<div class="app">
|
||||
{#if captureDropActive}
|
||||
<div class="capture-drop-overlay">
|
||||
<div class="capture-drop-box">{captureDropLabel}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
|
|
@ -2915,6 +3053,8 @@
|
|||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
.app { display: flex; width: 100vw; height: 100vh; overflow: hidden; background: #13131f; color: #e4e4ef; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; }
|
||||
.capture-drop-overlay { position: fixed; inset: 0; z-index: 120; pointer-events: none; display: flex; align-items: center; justify-content: center; background: rgba(19, 19, 31, 0.42); border: 2px dashed #818cf8; }
|
||||
.capture-drop-box { max-width: min(520px, calc(100vw - 48px)); padding: 14px 18px; border: 1px solid #3a3a5c; border-radius: 8px; background: #1a1a28; color: #e4e4ef; font-size: 14px; font-weight: 600; box-shadow: 0 12px 32px rgba(0,0,0,0.35); text-align: center; }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar { width: 260px; min-width: 200px; height: 100vh; display: flex; flex-direction: column; background: #1a1a28; border-right: 1px solid #2a2a3c; flex-shrink: 0; overflow: hidden; }
|
||||
|
|
|
|||
|
|
@ -42,9 +42,13 @@ export default {
|
|||
'capture.kind.folder': 'Folder',
|
||||
'capture.kind.image': 'Image',
|
||||
'capture.source.clipboard': 'Clipboard',
|
||||
'capture.source.clipboard_button': 'Clipboard button',
|
||||
'capture.source.drop': 'Drop',
|
||||
'capture.source.paste': 'Paste',
|
||||
'capture.source.browser': 'Browser',
|
||||
'capture.source.manual': 'Manual',
|
||||
'capture.dropOverlayNode': 'Will be added to Inbox for: {title}',
|
||||
'capture.dropOverlayGlobal': 'Will be added to global Inbox',
|
||||
'trash.openFolder': 'Open trash folder',
|
||||
'trash.empty': 'Trash is empty',
|
||||
'trash.deletedNodes': 'Deleted items',
|
||||
|
|
|
|||
|
|
@ -44,9 +44,13 @@ export default {
|
|||
'capture.kind.folder': 'Папка',
|
||||
'capture.kind.image': 'Изображение',
|
||||
'capture.source.clipboard': 'Буфер обмена',
|
||||
'capture.source.clipboard_button': 'Кнопка буфера',
|
||||
'capture.source.drop': 'Перетаскивание',
|
||||
'capture.source.paste': 'Вставка',
|
||||
'capture.source.browser': 'Браузер',
|
||||
'capture.source.manual': 'Вручную',
|
||||
'capture.dropOverlayNode': 'Будет добавлено в Неразобранное для: {title}',
|
||||
'capture.dropOverlayGlobal': 'Будет добавлено в глобальное Неразобранное',
|
||||
|
||||
'trash.openFolder': 'Открыть папку корзины',
|
||||
'trash.empty': 'Корзина пуста',
|
||||
|
|
|
|||
|
|
@ -142,13 +142,12 @@ async function runReadyScenario(cdp, url) {
|
|||
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, 'example.test', 'inbox: clipboard URL captured')
|
||||
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible')
|
||||
await emitDroppedFiles(cdp, ['/tmp/smoke-drop-folder'])
|
||||
await assertText(cdp, 'smoke-drop-folder', 'inbox: dropped folder captured')
|
||||
await assertText(cdp, 'Перетаскивание', 'inbox: dropped source visible')
|
||||
await setClipboardImage(cdp, 'pasted-smoke.png', 'image/png', 'c21va2UtaW1hZ2U=')
|
||||
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
|
||||
await dispatchPasteImage(cdp, 'pasted-smoke.png', 'image/png', 'c21va2UtaW1hZ2U=')
|
||||
await assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured')
|
||||
await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible')
|
||||
await clickInboxItemButton(cdp, 'smoke-drop-folder', 'Разложить')
|
||||
|
|
@ -379,6 +378,24 @@ async function setClipboardImage(cdp, name, type, base64) {
|
|||
})
|
||||
}
|
||||
|
||||
async function dispatchPasteImage(cdp, name, type, base64) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `
|
||||
(() => {
|
||||
const bytes = Uint8Array.from(atob(${JSON.stringify(base64)}), c => c.charCodeAt(0));
|
||||
const file = new File([bytes], ${JSON.stringify(name)}, { type: ${JSON.stringify(type)} });
|
||||
const data = new DataTransfer();
|
||||
data.items.add(file);
|
||||
const event = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: data });
|
||||
window.dispatchEvent(event);
|
||||
})();
|
||||
`,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
})
|
||||
await sleep(300)
|
||||
}
|
||||
|
||||
async function emitDroppedFiles(cdp, paths) {
|
||||
await cdp.send('Runtime.evaluate', {
|
||||
expression: `window.__VERSTAK_GUI_SMOKE__.dropFiles(${JSON.stringify(paths)})`,
|
||||
|
|
@ -684,6 +701,9 @@ function wailsMockSource() {
|
|||
worklog: {
|
||||
'node-project': [{ id: 'wl-1', nodeId: 'node-project', summary: 'Manual smoke entry', details: 'Smoke details', minutes: 45, billable: true, approximate: false, source: 'manual', createdAt: now, date: '2026-06-04' }],
|
||||
},
|
||||
links: {
|
||||
'node-project': [],
|
||||
},
|
||||
};
|
||||
|
||||
const fileNodeDetails = {
|
||||
|
|
@ -718,6 +738,50 @@ function wailsMockSource() {
|
|||
return node?.children || [];
|
||||
}
|
||||
|
||||
function parseCaptureContext(contextJSON) {
|
||||
let raw = {};
|
||||
try { raw = JSON.parse(contextJSON || '{}') || {}; } catch {}
|
||||
const out = {};
|
||||
if (raw.contextType === 'node' && raw.nodeId) {
|
||||
out.captureContextType = 'node';
|
||||
out.captureContextNodeId = raw.nodeId;
|
||||
out.suggestedTargetNodeId = raw.suggestedTargetNodeId || raw.nodeId;
|
||||
out.captureContextLabel = findNode(raw.nodeId)?.title || raw.nodeId;
|
||||
out.suggestedTargetLabel = findNode(out.suggestedTargetNodeId)?.title || out.suggestedTargetNodeId;
|
||||
} else if (raw.contextType === 'section') {
|
||||
out.captureContextType = 'section';
|
||||
out.captureContextSection = raw.section || 'root';
|
||||
out.captureContextLabel = out.captureContextSection;
|
||||
} else {
|
||||
out.captureContextType = 'global';
|
||||
out.captureContextSection = raw.section || 'root';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function hostnameForURL(value) {
|
||||
try { return new URL(value).hostname; } catch { return ''; }
|
||||
}
|
||||
|
||||
function inboxDTO(node) {
|
||||
return {
|
||||
...node,
|
||||
captureKind: node.captureKind || '',
|
||||
sourceKind: node.sourceKind || node.captureKind || '',
|
||||
captureSource: node.captureSource || '',
|
||||
captureStatus: node.captureStatus || 'unresolved',
|
||||
captureContextType: node.captureContextType || 'global',
|
||||
captureContextNodeId: node.captureContextNodeId || '',
|
||||
captureContextSection: node.captureContextSection || '',
|
||||
suggestedTargetNodeId: node.suggestedTargetNodeId || '',
|
||||
captureContextLabel: node.captureContextLabel || '',
|
||||
suggestedTargetLabel: node.suggestedTargetLabel || '',
|
||||
capturedAt: node.capturedAt || node.createdAt || '',
|
||||
url: node.url || '',
|
||||
hostname: node.hostname || '',
|
||||
};
|
||||
}
|
||||
|
||||
function detachNode(id, items = state.nodes) {
|
||||
const idx = items.findIndex((node) => node.id === id);
|
||||
if (idx >= 0) {
|
||||
|
|
@ -783,33 +847,55 @@ 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: [] };
|
||||
ListInboxNodes: async () => clone(state.nodes.filter((node) => !node.parent_id && node.captureInbox === true).map(inboxDTO)),
|
||||
ListInboxNodesForTarget: async (nodeId) => clone(state.nodes.filter((node) => node.captureInbox === true && (node.captureContextNodeId === nodeId || node.suggestedTargetNodeId === nodeId)).map(inboxDTO)),
|
||||
CaptureTextWithContext: async (text, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
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', sourceKind: 'text', captureSource: source || 'paste', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
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: [] };
|
||||
CaptureText: async (text) => App.CaptureTextWithContext(text, 'clipboard', '{}'),
|
||||
CaptureURLWithContext: async (url, title, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
const hostname = hostnameForURL(url);
|
||||
const node = { id: 'node-capture-url-' + Date.now(), title: title || hostname || url, type: 'link', section: '', captureInbox: true, captureKind: 'url', sourceKind: 'url', captureSource: source || 'paste', captureStatus: 'unresolved', url, hostname, createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
CapturePath: async (sourcePath) => {
|
||||
CaptureURL: async (url, title) => App.CaptureURLWithContext(url, title, 'clipboard', '{}'),
|
||||
CapturePathWithContext: async (sourcePath, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
const title = String(sourcePath || '').split('/').filter(Boolean).pop() || 'Dropped file';
|
||||
const kind = title.includes('folder') ? 'folder' : 'file';
|
||||
const node = { id: 'node-capture-path-' + Date.now(), title, type: kind === 'folder' ? 'folder' : 'file', section: '', captureInbox: true, captureKind: kind, captureSource: 'drop', createdAt: now, has_children: false, children: [] };
|
||||
const node = { id: 'node-capture-path-' + Date.now(), title, type: kind === 'folder' ? 'folder' : 'file', section: '', captureInbox: true, captureKind: kind, sourceKind: kind, captureSource: source || 'drop', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
CaptureFileData: async (filename) => {
|
||||
const node = { id: 'node-capture-data-' + Date.now(), title: filename, type: 'file', section: '', captureInbox: true, captureKind: filename.endsWith('.png') ? 'image' : 'file', captureSource: 'clipboard', createdAt: now, has_children: false, children: [] };
|
||||
CapturePath: async (sourcePath) => App.CapturePathWithContext(sourcePath, 'drop', '{}'),
|
||||
CaptureFileDataWithContext: async (filename, _dataBase64, source, contextJSON) => {
|
||||
const ctx = parseCaptureContext(contextJSON);
|
||||
const node = { id: 'node-capture-data-' + Date.now(), title: filename, type: 'file', section: '', captureInbox: true, captureKind: filename.endsWith('.png') ? 'image' : 'file', sourceKind: filename.endsWith('.png') ? 'image' : 'file', captureSource: source || 'paste', captureStatus: 'unresolved', createdAt: now, capturedAt: now, has_children: false, children: [], ...ctx };
|
||||
state.nodes.push(node);
|
||||
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
|
||||
return clone(inboxDTO(node));
|
||||
},
|
||||
AssignInboxNode: async (nodeId, targetParentId) => {
|
||||
CaptureFileData: async (filename, dataBase64) => App.CaptureFileDataWithContext(filename, dataBase64, 'clipboard', '{}'),
|
||||
ReadClipboardText: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '',
|
||||
CaptureClipboardTextWithContext: async (contextJSON) => {
|
||||
const text = String(window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '').trim();
|
||||
if (!text) throw new Error('clipboard is empty');
|
||||
if (/^https?:\\/\\//.test(text)) return App.CaptureURLWithContext(text, '', 'clipboard_button', contextJSON);
|
||||
return App.CaptureTextWithContext(text, 'clipboard_button', contextJSON);
|
||||
},
|
||||
ResolveInboxNode: async (nodeId, targetParentId) => {
|
||||
const node = detachNode(nodeId);
|
||||
const parent = findNode(targetParentId);
|
||||
if (!node || !parent) throw new Error('assign target not found');
|
||||
if (node.sourceKind === 'url' || node.captureKind === 'url') {
|
||||
state.links[targetParentId] = state.links[targetParentId] || [];
|
||||
state.links[targetParentId].push({ id: 'link-' + Date.now(), nodeId: targetParentId, title: node.title, url: node.url, hostname: node.hostname || hostnameForURL(node.url), note: '', source: node.captureSource, capturedAt: node.capturedAt || now, createdAt: now, updatedAt: now });
|
||||
return clone(parent);
|
||||
}
|
||||
node.captureInbox = false;
|
||||
node.captureKind = '';
|
||||
node.captureSource = '';
|
||||
|
|
@ -819,11 +905,32 @@ function wailsMockSource() {
|
|||
parent.has_children = true;
|
||||
return clone(node);
|
||||
},
|
||||
ResolveInboxNodeHere: async (nodeId) => {
|
||||
const node = findNode(nodeId);
|
||||
if (!node?.suggestedTargetNodeId) throw new Error('suggested target is required');
|
||||
return App.ResolveInboxNode(nodeId, node.suggestedTargetNodeId);
|
||||
},
|
||||
AssignInboxNode: async (nodeId, targetParentId) => App.ResolveInboxNode(nodeId, targetParentId),
|
||||
DeleteInboxNode: async (nodeId) => {
|
||||
const node = detachNode(nodeId);
|
||||
if (!node) throw new Error('inbox node not found');
|
||||
return true;
|
||||
},
|
||||
ListLinks: async (nodeId) => clone(state.links[nodeId] || []),
|
||||
UpdateLink: async (id, title, url, note) => {
|
||||
const list = Object.values(state.links).flat();
|
||||
const link = list.find((item) => item.id === id);
|
||||
if (!link) throw new Error('link not found');
|
||||
Object.assign(link, { title, url, note, hostname: hostnameForURL(url), updatedAt: now });
|
||||
return clone(link);
|
||||
},
|
||||
DeleteLink: async (id) => {
|
||||
for (const key of Object.keys(state.links)) {
|
||||
state.links[key] = state.links[key].filter((link) => link.id !== id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
OpenLink: async () => true,
|
||||
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