feat: add text and url inbox capture
This commit is contained in:
parent
d6ef3a973a
commit
44d0be2649
|
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"verstak/internal/core/activity"
|
||||
"verstak/internal/core/files"
|
||||
"verstak/internal/core/nodes"
|
||||
syncsvc "verstak/internal/core/sync"
|
||||
"verstak/internal/core/util"
|
||||
)
|
||||
|
||||
func (a *App) CaptureText(text string) (*InboxNodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("text required")
|
||||
}
|
||||
title := firstLineTitle(text, "Captured text")
|
||||
content := "# " + title + "\n\n" + text + "\n"
|
||||
return a.createCaptureNote(title, content, "text", "clipboard")
|
||||
}
|
||||
|
||||
func (a *App) CaptureURL(rawURL, title string) (*InboxNodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return nil, fmt.Errorf("url required")
|
||||
}
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
title = rawURL
|
||||
}
|
||||
title = firstLineTitle(title, rawURL)
|
||||
content := "# " + title + "\n\n" + rawURL + "\n"
|
||||
return a.createCaptureNote(title, content, "url", "clipboard")
|
||||
}
|
||||
|
||||
func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNodeDTO, error) {
|
||||
node, err := a.nodes.Create(nil, nodes.TypeNote, title, 0, "", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create node: %w", err)
|
||||
}
|
||||
|
||||
inboxDir := filepath.Join(a.vault, ".verstak", "inbox")
|
||||
if err := os.MkdirAll(inboxDir, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("create inbox dir: %w", err)
|
||||
}
|
||||
filename := node.ID + ".md"
|
||||
absPath := filepath.Join(inboxDir, filename)
|
||||
if err := os.WriteFile(absPath, []byte(content), 0o640); err != nil {
|
||||
return nil, fmt.Errorf("write capture note: %w", err)
|
||||
}
|
||||
relPath, _ := filepath.Rel(a.vault, absPath)
|
||||
now := time.Now().UTC()
|
||||
fileRec := &files.Record{
|
||||
ID: util.UUID7(),
|
||||
NodeID: node.ID,
|
||||
Filename: filename,
|
||||
Path: relPath,
|
||||
StorageMode: "vault",
|
||||
Size: int64(len(content)),
|
||||
MIME: "text/markdown",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
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: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if err := a.nodes.MetaSet(node.ID, "capture.inbox", "true"); 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
|
||||
}
|
||||
|
||||
_ = a.activity.Record("", activity.TargetNote, node.ID, "", activity.TypeNoteCreated, title, `{"capture":true}`)
|
||||
_ = a.sync.RecordOp(syncsvc.EntityNote, node.ID, syncsvc.OpCreate, notePayload(node, fileRec, content))
|
||||
|
||||
return a.inboxNodeDTO(node)
|
||||
}
|
||||
|
||||
func (a *App) inboxNodeDTO(n *nodes.Node) (*InboxNodeDTO, error) {
|
||||
dto := &InboxNodeDTO{NodeDTO: toNodeDTO(n)}
|
||||
if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok {
|
||||
dto.CaptureKind = kind
|
||||
}
|
||||
if source, ok, err := a.nodes.MetaGet(n.ID, "capture.source"); err == nil && ok {
|
||||
dto.CaptureSource = source
|
||||
}
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
func firstLineTitle(text, fallback string) string {
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
if len(line) > 80 {
|
||||
return line[:80]
|
||||
}
|
||||
return line
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -16,14 +16,11 @@ func (a *App) ListInboxNodes() ([]InboxNodeDTO, error) {
|
|||
}
|
||||
dtos := make([]InboxNodeDTO, 0, len(list))
|
||||
for _, n := range list {
|
||||
dto := InboxNodeDTO{NodeDTO: toNodeDTO(&n)}
|
||||
if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok {
|
||||
dto.CaptureKind = kind
|
||||
dto, err := a.inboxNodeDTO(&n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if source, ok, err := a.nodes.MetaGet(n.ID, "capture.source"); err == nil && ok {
|
||||
dto.CaptureSource = source
|
||||
}
|
||||
dtos = append(dtos, dto)
|
||||
dtos = append(dtos, *dto)
|
||||
}
|
||||
for i := range dtos {
|
||||
n, err := a.nodes.CountChildren(dtos[i].ID, "case", "client", "project", "folder", "document", "recipe")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCaptureTextCreatesInboxArtifact(t *testing.T) {
|
||||
app, _ := setupTestApp(t)
|
||||
|
||||
dto, err := app.CaptureText("Нужно разобрать этот текст")
|
||||
if err != nil {
|
||||
t.Fatalf("CaptureText: %v", err)
|
||||
}
|
||||
if dto.ID == "" {
|
||||
t.Fatal("empty captured node id")
|
||||
}
|
||||
if dto.CaptureKind != "text" {
|
||||
t.Fatalf("CaptureKind = %q, want text", dto.CaptureKind)
|
||||
}
|
||||
if dto.CaptureSource != "clipboard" {
|
||||
t.Fatalf("CaptureSource = %q, want clipboard", dto.CaptureSource)
|
||||
}
|
||||
|
||||
content, err := app.ReadNote(dto.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadNote: %v", err)
|
||||
}
|
||||
if !strings.Contains(content, "Нужно разобрать этот текст") {
|
||||
t.Fatalf("captured content missing: %q", content)
|
||||
}
|
||||
var path string
|
||||
if err := app.db.QueryRow(`SELECT f.path FROM notes n JOIN files f ON f.id = n.file_id WHERE n.node_id = ?`, dto.ID).Scan(&path); err != nil {
|
||||
t.Fatalf("query note file path: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(path, ".verstak/inbox/") {
|
||||
t.Fatalf("path = %q, want .verstak/inbox prefix", path)
|
||||
}
|
||||
|
||||
inbox, err := app.ListInboxNodes()
|
||||
if err != nil {
|
||||
t.Fatalf("ListInboxNodes: %v", err)
|
||||
}
|
||||
var found bool
|
||||
for _, item := range inbox {
|
||||
if item.ID == dto.ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("captured text missing from inbox")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureURLCreatesInboxArtifact(t *testing.T) {
|
||||
app, _ := setupTestApp(t)
|
||||
|
||||
dto, err := app.CaptureURL("https://example.test/page", "Example Page")
|
||||
if err != nil {
|
||||
t.Fatalf("CaptureURL: %v", err)
|
||||
}
|
||||
if dto.CaptureKind != "url" {
|
||||
t.Fatalf("CaptureKind = %q, want url", dto.CaptureKind)
|
||||
}
|
||||
if dto.Title != "Example Page" {
|
||||
t.Fatalf("Title = %q, want Example Page", dto.Title)
|
||||
}
|
||||
|
||||
content, err := app.ReadNote(dto.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadNote: %v", err)
|
||||
}
|
||||
if !strings.Contains(content, "https://example.test/page") {
|
||||
t.Fatalf("captured URL missing: %q", content)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,14 @@ export function ListInboxNodes() {
|
|||
return window['go']['main']['App']['ListInboxNodes']();
|
||||
}
|
||||
|
||||
export function CaptureText(arg1) {
|
||||
return window['go']['main']['App']['CaptureText'](arg1);
|
||||
}
|
||||
|
||||
export function CaptureURL(arg1, arg2) {
|
||||
return window['go']['main']['App']['CaptureURL'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ListTrash() {
|
||||
return window['go']['main']['App']['ListTrash']();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue