feat: capture files and images in inbox

This commit is contained in:
mirivlad 2026-06-05 02:06:21 +08:00
parent 326f6f283d
commit a96a316883
10 changed files with 427 additions and 21 deletions

View File

@ -1,7 +1,9 @@
package main package main
import ( import (
"encoding/base64"
"fmt" "fmt"
"mime"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -11,6 +13,7 @@ import (
"verstak/internal/core/files" "verstak/internal/core/files"
"verstak/internal/core/nodes" "verstak/internal/core/nodes"
syncsvc "verstak/internal/core/sync" syncsvc "verstak/internal/core/sync"
"verstak/internal/core/templates"
"verstak/internal/core/util" "verstak/internal/core/util"
) )
@ -44,6 +47,134 @@ func (a *App) CaptureURL(rawURL, title string) (*InboxNodeDTO, error) {
return a.createCaptureNote(title, content, "url", "clipboard") return a.createCaptureNote(title, content, "url", "clipboard")
} }
func (a *App) CapturePath(sourcePath string) (*InboxNodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
sourcePath = strings.TrimSpace(sourcePath)
if sourcePath == "" {
return nil, fmt.Errorf("path required")
}
absPath, err := filepath.Abs(sourcePath)
if err != nil {
return nil, fmt.Errorf("abs path: %w", err)
}
info, err := os.Stat(absPath)
if err != nil {
return nil, fmt.Errorf("stat: %w", err)
}
nodeType := nodes.TypeFile
kind := captureKindForFilename(absPath)
if info.IsDir() {
nodeType = nodes.TypeFolder
kind = "folder"
}
node, err := a.nodes.Create(nil, nodeType, filepath.Base(absPath), 0, "", "")
if err != nil {
return nil, fmt.Errorf("create capture node: %w", err)
}
stagingRel, _, err := a.captureStagingDir(node)
if err != nil {
return nil, err
}
if info.IsDir() {
if err := a.nodes.UpdateFsPath(node.ID, stagingRel); err != nil {
return nil, fmt.Errorf("set capture folder path: %w", err)
}
entries, err := os.ReadDir(absPath)
if err != nil {
return nil, fmt.Errorf("read source dir: %w", err)
}
for _, entry := range entries {
if _, err := a.files.AddPathCopy(node.ID, filepath.Join(absPath, entry.Name())); err != nil {
return nil, fmt.Errorf("copy capture child %s: %w", entry.Name(), err)
}
}
} else {
if _, err := a.files.CopyIntoVault(node.ID, absPath, stagingRel); err != nil {
return nil, fmt.Errorf("copy capture file: %w", err)
}
}
if err := a.setCaptureMeta(node.ID, kind, "drop"); err != nil {
return nil, err
}
target := activity.TargetFile
evType := activity.TypeFileAdded
entity := syncsvc.EntityFile
if info.IsDir() {
target = activity.TargetFolder
evType = activity.TypeFolderAdded
entity = syncsvc.EntityFolder
}
_ = a.activity.Record("", target, node.ID, "", evType, node.Title, `{"capture":true}`)
_ = a.sync.RecordOp(entity, node.ID, syncsvc.OpCreate, a.filePayload(node))
return a.inboxNodeDTO(node)
}
func (a *App) CaptureFileData(filename, dataBase64 string) (*InboxNodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
filename = filepath.Base(strings.TrimSpace(filename))
if filename == "." || filename == "" {
filename = "clipboard.bin"
}
if err := files.ValidateName(filename); err != nil {
return nil, err
}
if comma := strings.Index(dataBase64, ","); comma >= 0 {
dataBase64 = dataBase64[comma+1:]
}
data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(dataBase64))
if err != nil {
return nil, fmt.Errorf("decode file data: %w", err)
}
if len(data) == 0 {
return nil, fmt.Errorf("file data required")
}
node, err := a.nodes.Create(nil, nodes.TypeFile, filename, 0, "", "")
if err != nil {
return nil, fmt.Errorf("create capture file node: %w", err)
}
stagingRel, stagingAbs, err := a.captureStagingDir(node)
if err != nil {
return nil, err
}
absPath := filepath.Join(stagingAbs, filename)
if err := os.WriteFile(absPath, data, 0o640); err != nil {
return nil, fmt.Errorf("write capture data: %w", err)
}
relPath := filepath.Join(stagingRel, filename)
fileRec := &files.Record{
ID: util.UUID7(),
NodeID: node.ID,
Filename: filename,
Path: relPath,
StorageMode: "vault",
Size: int64(len(data)),
MIME: mimeForFilename(filename),
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if _, err := a.db.Exec(
`INSERT INTO files (id,node_id,filename,path,storage_mode,size,mime,created_at,updated_at,missing)
VALUES (?,?,?,?,?,?,?,?,?,0)`,
fileRec.ID, fileRec.NodeID, fileRec.Filename, fileRec.Path, fileRec.StorageMode,
fileRec.Size, fileRec.MIME, fileRec.CreatedAt.Format(time.RFC3339), fileRec.UpdatedAt.Format(time.RFC3339)); err != nil {
return nil, fmt.Errorf("insert capture file data: %w", err)
}
if err := a.setCaptureMeta(node.ID, captureKindForFilename(filename), "clipboard"); err != nil {
return nil, err
}
_ = a.activity.Record("", activity.TargetFile, node.ID, "", activity.TypeFileAdded, filename, `{"capture":true}`)
_ = a.sync.RecordOp(syncsvc.EntityFile, node.ID, syncsvc.OpCreate, a.filePayload(node))
return a.inboxNodeDTO(node)
}
func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNodeDTO, error) { func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNodeDTO, error) {
node, err := a.nodes.Create(nil, nodes.TypeNote, title, 0, "", "") node, err := a.nodes.Create(nil, nodes.TypeNote, title, 0, "", "")
if err != nil { if err != nil {
@ -82,16 +213,7 @@ func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNode
if _, err := a.db.Exec(`INSERT INTO notes (node_id, file_id, format) VALUES (?,?,?)`, node.ID, fileRec.ID, "markdown"); err != nil { if _, err := a.db.Exec(`INSERT INTO notes (node_id, file_id, format) VALUES (?,?,?)`, node.ID, fileRec.ID, "markdown"); err != nil {
return nil, fmt.Errorf("insert capture note: %w", err) return nil, fmt.Errorf("insert capture note: %w", err)
} }
if err := a.nodes.MetaSet(node.ID, "capture.inbox", "true"); err != nil { if err := a.setCaptureMeta(node.ID, kind, source); err != nil {
return nil, err
}
if err := a.nodes.MetaSet(node.ID, "capture.kind", kind); err != nil {
return nil, err
}
if err := a.nodes.MetaSet(node.ID, "capture.source", source); err != nil {
return nil, err
}
if err := a.nodes.MetaSet(node.ID, "capture.created_at", now.Format(time.RFC3339)); err != nil {
return nil, err return nil, err
} }
@ -101,6 +223,29 @@ func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNode
return a.inboxNodeDTO(node) return a.inboxNodeDTO(node)
} }
func (a *App) captureStagingDir(node *nodes.Node) (string, string, error) {
segment := node.ID + "_" + templates.SafeDisplayNameToPathSegment(node.Title)
rel := filepath.Join(".verstak", "inbox", segment)
abs := filepath.Join(a.vault, rel)
if err := os.MkdirAll(abs, 0o750); err != nil {
return "", "", fmt.Errorf("create inbox staging dir: %w", err)
}
return rel, abs, nil
}
func (a *App) setCaptureMeta(nodeID, kind, source string) error {
if err := a.nodes.MetaSet(nodeID, "capture.inbox", "true"); err != nil {
return err
}
if err := a.nodes.MetaSet(nodeID, "capture.kind", kind); err != nil {
return err
}
if err := a.nodes.MetaSet(nodeID, "capture.source", source); err != nil {
return err
}
return a.nodes.MetaSet(nodeID, "capture.created_at", time.Now().UTC().Format(time.RFC3339))
}
func (a *App) inboxNodeDTO(n *nodes.Node) (*InboxNodeDTO, error) { func (a *App) inboxNodeDTO(n *nodes.Node) (*InboxNodeDTO, error) {
dto := &InboxNodeDTO{NodeDTO: toNodeDTO(n)} dto := &InboxNodeDTO{NodeDTO: toNodeDTO(n)}
if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok { if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok {
@ -112,6 +257,34 @@ func (a *App) inboxNodeDTO(n *nodes.Node) (*InboxNodeDTO, error) {
return dto, nil return dto, nil
} }
func captureKindForFilename(filename string) string {
if strings.HasPrefix(mimeForFilename(filename), "image/") {
return "image"
}
return "file"
}
func mimeForFilename(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
}
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
if semi := strings.Index(mimeType, ";"); semi >= 0 {
return mimeType[:semi]
}
return mimeType
}
return "application/octet-stream"
}
func firstLineTitle(text, fallback string) string { func firstLineTitle(text, fallback string) string {
for _, line := range strings.Split(text, "\n") { for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)

View File

@ -1,6 +1,9 @@
package main package main
import ( import (
"encoding/base64"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
) )
@ -74,3 +77,87 @@ func TestCaptureURLCreatesInboxArtifact(t *testing.T) {
t.Fatalf("captured URL missing: %q", content) t.Fatalf("captured URL missing: %q", content)
} }
} }
func TestCapturePathCopiesFileIntoInbox(t *testing.T) {
app, vaultRoot := setupTestApp(t)
sourceDir := t.TempDir()
source := filepath.Join(sourceDir, "brief.pdf")
if err := os.WriteFile(source, []byte("pdf content"), 0o640); err != nil {
t.Fatalf("write source: %v", err)
}
dto, err := app.CapturePath(source)
if err != nil {
t.Fatalf("CapturePath: %v", err)
}
if dto.CaptureKind != "file" {
t.Fatalf("CaptureKind = %q, want file", dto.CaptureKind)
}
records, err := app.files.ListByNode(dto.ID)
if err != nil {
t.Fatalf("ListByNode: %v", err)
}
if len(records) != 1 {
t.Fatalf("records = %d, want 1", len(records))
}
if !strings.HasPrefix(records[0].Path, ".verstak/inbox/") {
t.Fatalf("path = %q, want .verstak/inbox prefix", records[0].Path)
}
if _, err := os.Stat(filepath.Join(vaultRoot, records[0].Path)); err != nil {
t.Fatalf("captured file missing in vault: %v", err)
}
}
func TestCapturePathCopiesDirectoryIntoInbox(t *testing.T) {
app, _ := setupTestApp(t)
source := t.TempDir()
if err := os.MkdirAll(filepath.Join(source, "nested"), 0o750); err != nil {
t.Fatalf("mkdir nested: %v", err)
}
if err := os.WriteFile(filepath.Join(source, "nested", "note.txt"), []byte("nested"), 0o640); err != nil {
t.Fatalf("write nested file: %v", err)
}
dto, err := app.CapturePath(source)
if err != nil {
t.Fatalf("CapturePath: %v", err)
}
if dto.CaptureKind != "folder" {
t.Fatalf("CaptureKind = %q, want folder", dto.CaptureKind)
}
items, err := app.ListItems(dto.ID)
if err != nil {
t.Fatalf("ListItems: %v", err)
}
var foundNested bool
for _, item := range items {
if item.Name == "nested" && item.Type == "folder" {
foundNested = true
}
}
if !foundNested {
t.Fatalf("captured folder children missing: %+v", items)
}
}
func TestCaptureFileDataCreatesImageInboxArtifact(t *testing.T) {
app, _ := setupTestApp(t)
data := base64.StdEncoding.EncodeToString([]byte("fake image bytes"))
dto, err := app.CaptureFileData("pasted.png", data)
if err != nil {
t.Fatalf("CaptureFileData: %v", err)
}
if dto.CaptureKind != "image" {
t.Fatalf("CaptureKind = %q, want image", dto.CaptureKind)
}
records, err := app.files.ListByNode(dto.ID)
if err != nil {
t.Fatalf("ListByNode: %v", err)
}
if len(records) != 1 || records[0].MIME != "image/png" {
t.Fatalf("records = %+v, want one png image", records)
}
}

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

View File

@ -16,8 +16,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-OClTUyKu.js"></script> <script type="module" crossorigin src="/assets/main-D-CBbIRx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BE1Aa3xO.css"> <link rel="stylesheet" crossorigin href="/assets/main-BgORPqqS.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -137,6 +137,7 @@
let selectedIds = [] let selectedIds = []
let dragIds = [] let dragIds = []
let dropRootValid = false let dropRootValid = false
let inboxDropValid = false
let showConfirm = false let showConfirm = false
let confirmTitle = '' let confirmTitle = ''
@ -1335,6 +1336,10 @@
// ===== Drag-and-drop ===== // ===== Drag-and-drop =====
async function onFilesDropped(paths) { async function onFilesDropped(paths) {
if (!paths || paths.length === 0) return if (!paths || paths.length === 0) return
if (selectedSection === 'inbox') {
await captureDroppedPaths(paths)
return
}
if (!selectedNode) { if (!selectedNode) {
error = t('error.selectCaseFirst') error = t('error.selectCaseFirst')
return return
@ -1391,6 +1396,10 @@
if (!source) return '' if (!source) return ''
return t('capture.source.' + source) return t('capture.source.' + source)
} }
function addInboxCapture(item) {
if (!item || !item.id) return
inboxNodes = [item, ...inboxNodes.filter(existing => existing.id !== item.id)]
}
function looksLikeURL(value) { function looksLikeURL(value) {
try { try {
const parsed = new URL(value) const parsed = new URL(value)
@ -1399,6 +1408,43 @@
return false return false
} }
} }
function extensionForMime(type) {
const map = {
'image/png': 'png',
'image/jpeg': 'jpg',
'image/gif': 'gif',
'image/webp': 'webp',
}
return map[type] || 'bin'
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const value = String(reader.result || '')
const comma = value.indexOf(',')
resolve(comma >= 0 ? value.slice(comma + 1) : value)
}
reader.onerror = () => reject(reader.error)
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
}
return false
}
async function captureClipboard() { async function captureClipboard() {
if (inboxCaptureBusy) return if (inboxCaptureBusy) return
inboxCaptureStatus = '' inboxCaptureStatus = ''
@ -1408,6 +1454,7 @@
} }
inboxCaptureBusy = true inboxCaptureBusy = true
try { try {
if (await captureClipboardFile()) return
const text = (await navigator.clipboard.readText()).trim() const text = (await navigator.clipboard.readText()).trim()
if (!text) { if (!text) {
inboxCaptureStatus = t('inbox.clipboardEmpty') inboxCaptureStatus = t('inbox.clipboardEmpty')
@ -1416,7 +1463,7 @@
const item = looksLikeURL(text) const item = looksLikeURL(text)
? await wailsCall('CaptureURL', text, '') ? await wailsCall('CaptureURL', text, '')
: await wailsCall('CaptureText', text) : await wailsCall('CaptureText', text)
inboxNodes = [item, ...inboxNodes.filter(existing => existing.id !== item.id)] addInboxCapture(item)
inboxCaptureStatus = t('inbox.captured') inboxCaptureStatus = t('inbox.captured')
} catch (e) { } catch (e) {
error = String(e) error = String(e)
@ -1424,6 +1471,39 @@
inboxCaptureBusy = false inboxCaptureBusy = false
} }
} }
async function captureDroppedPaths(paths) {
if (inboxCaptureBusy) return
inboxCaptureBusy = true
inboxCaptureStatus = ''
try {
for (const path of paths) {
const item = await wailsCall('CapturePath', path)
addInboxCapture(item)
}
inboxCaptureStatus = t('inbox.captured')
} catch (e) {
error = String(e)
} finally {
inboxCaptureBusy = false
inboxDropValid = false
}
}
function handleInboxDragOver(e) {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
inboxDropValid = true
}
function handleInboxDragLeave() {
inboxDropValid = false
}
async function handleInboxDrop(e) {
e.preventDefault()
inboxDropValid = false
const paths = Array.from(e.dataTransfer?.files || [])
.map(file => file.path || file.webkitRelativePath || '')
.filter(Boolean)
if (paths.length > 0) await captureDroppedPaths(paths)
}
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
@ -2048,7 +2128,13 @@
</div> </div>
{:else if selectedSection === 'inbox'} {:else if selectedSection === 'inbox'}
<div class="inbox-screen"> <div class="inbox-screen"
class:drop-valid={inboxDropValid}
role="region"
aria-label={t('nav.inbox')}
on:dragover={handleInboxDragOver}
on:dragleave={handleInboxDragLeave}
on:drop={handleInboxDrop}>
<div class="inbox-header"> <div class="inbox-header">
<div> <div>
<h2>{t('nav.inbox')}</h2> <h2>{t('nav.inbox')}</h2>
@ -2848,6 +2934,7 @@
/* Inbox screen */ /* Inbox screen */
.inbox-screen { padding: 24px; overflow-y: auto; flex: 1; } .inbox-screen { padding: 24px; overflow-y: auto; flex: 1; }
.inbox-screen.drop-valid { outline: 2px dashed #4ade80; outline-offset: -4px; background: rgba(74, 222, 128, 0.04); }
.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; }

View File

@ -90,6 +90,14 @@ export function CaptureURL(arg1, arg2) {
return window['go']['main']['App']['CaptureURL'](arg1, arg2); return window['go']['main']['App']['CaptureURL'](arg1, arg2);
} }
export function CapturePath(arg1) {
return window['go']['main']['App']['CapturePath'](arg1);
}
export function CaptureFileData(arg1, arg2) {
return window['go']['main']['App']['CaptureFileData'](arg1, arg2);
}
export function ListTrash() { export function ListTrash() {
return window['go']['main']['App']['ListTrash'](); return window['go']['main']['App']['ListTrash']();
} }

View File

@ -144,6 +144,13 @@ async function runReadyScenario(cdp, url) {
await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера') await clickText(cdp, '.inbox-header .btn', 'Вставить из буфера')
await assertText(cdp, 'https://example.test/from-clipboard', 'inbox: clipboard URL captured') await assertText(cdp, 'https://example.test/from-clipboard', 'inbox: clipboard URL captured')
await assertText(cdp, 'Ссылка', 'inbox: clipboard URL kind visible') 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 assertText(cdp, 'pasted-smoke.png', 'inbox: clipboard image captured')
await assertText(cdp, 'Изображение', 'inbox: clipboard image kind visible')
await screenshot(cdp, 'inbox.png') await screenshot(cdp, 'inbox.png')
await clickText(cdp, '.inbox-item', 'Inbox Smoke Item') 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')
@ -346,6 +353,29 @@ async function setClipboardText(cdp, value) {
}) })
} }
async function setClipboardImage(cdp, name, type, base64) {
await cdp.send('Runtime.evaluate', {
expression: `
window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ = '';
window.__VERSTAK_GUI_SMOKE_CLIPBOARD_ITEMS__ = [{
types: [${JSON.stringify(type)}],
getType: async () => new File([Uint8Array.from(atob(${JSON.stringify(base64)}), c => c.charCodeAt(0))], ${JSON.stringify(name)}, { type: ${JSON.stringify(type)} }),
}];
`,
awaitPromise: true,
returnByValue: true,
})
}
async function emitDroppedFiles(cdp, paths) {
await cdp.send('Runtime.evaluate', {
expression: `window.__VERSTAK_GUI_SMOKE__.dropFiles(${JSON.stringify(paths)})`,
awaitPromise: true,
returnByValue: true,
})
await sleep(300)
}
async function clickFolderOpenButton(cdp, name) { async function clickFolderOpenButton(cdp, name) {
const ok = await evalValue(cdp, ` const ok = await evalValue(cdp, `
(() => { (() => {
@ -574,6 +604,7 @@ function wailsMockSource() {
configurable: true, configurable: true,
value: { value: {
readText: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '', readText: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD__ || '',
read: async () => window.__VERSTAK_GUI_SMOKE_CLIPBOARD_ITEMS__ || [],
}, },
}); });
@ -719,6 +750,18 @@ function wailsMockSource() {
state.nodes.push(node); state.nodes.push(node);
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource }); return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
}, },
CapturePath: async (sourcePath) => {
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: [] };
state.nodes.push(node);
return clone({ ...node, captureKind: node.captureKind, captureSource: node.captureSource });
},
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: [] };
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 }],
@ -854,11 +897,19 @@ function wailsMockSource() {
}; };
window.go = { main: { App } }; window.go = { main: { App } };
const runtimeHandlers = {};
window.runtime = { window.runtime = {
EventsOn: () => {}, EventsOn: (name, handler) => { runtimeHandlers[name] = handler; },
EventsOff: () => {}, EventsOff: (name) => { delete runtimeHandlers[name]; },
};
window.__VERSTAK_GUI_SMOKE__ = {
mode,
state,
events,
dropFiles: async (paths) => {
if (runtimeHandlers['files-dropped']) await runtimeHandlers['files-dropped'](paths);
},
}; };
window.__VERSTAK_GUI_SMOKE__ = { mode, state, events };
})(); })();
` `
} }