verstak/cmd/verstak-gui/app.go

384 lines
9.2 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
"verstak/internal/core/actions"
"verstak/internal/core/files"
"verstak/internal/core/notes"
"verstak/internal/core/nodes"
"verstak/internal/core/storage"
"verstak/internal/core/worklog"
)
// App is the Wails v2 application adapter. It wraps core services.
type App struct {
ctx context.Context
db *storage.DB
nodes *nodes.Repository
files *files.Service
notes *notes.Service
actions *actions.Service
worklog *worklog.Service
vault string
}
// startup is called when the app starts. Store context.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// ============================================================
// DTOs
// ============================================================
type NodeDTO struct {
ID string `json:"id"`
ParentID string `json:"parentId"`
Title string `json:"title"`
Type string `json:"type"`
Section string `json:"section"`
Path string `json:"path"`
CreatedAt string `json:"createdAt"`
}
type SectionDTO struct {
ID string `json:"id"`
Label string `json:"label"`
}
type NoteDTO struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Format string `json:"format"`
CreatedAt string `json:"createdAt"`
}
type FileDTO struct {
ID string `json:"id"`
NodeID string `json:"nodeId"`
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
Mime string `json:"mime"`
IsDir bool `json:"isDir"`
Missing bool `json:"missing"`
}
type ActionDTO struct {
ID string `json:"id"`
NodeID string `json:"nodeId"`
Title string `json:"title"`
Type string `json:"type"`
Data string `json:"data"`
}
type WorklogDTO struct {
ID string `json:"id"`
NodeID string `json:"nodeId"`
Summary string `json:"summary"`
Minutes int `json:"minutes"`
CreatedAt string `json:"createdAt"`
}
type SearchResultDTO struct {
NodeID string `json:"nodeId"`
Title string `json:"title"`
Snippet string `json:"snippet"`
Type string `json:"type"`
}
// ============================================================
// Sections
// ============================================================
func (a *App) ListSections() []SectionDTO {
return []SectionDTO{
{ID: "today", Label: "Сегодня"},
{ID: "inbox", Label: "Неразобранное"},
{ID: "clients", Label: "Клиенты"},
{ID: "projects", Label: "Проекты"},
{ID: "recipes", Label: "Рецепты"},
{ID: "documents", Label: "Документы"},
{ID: "archive", Label: "Архив"},
}
}
// ============================================================
// Nodes
// ============================================================
func (a *App) ListNodesBySection(section string) ([]NodeDTO, error) {
list, err := a.nodes.ListRoots(false, section)
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
}
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
list, err := a.nodes.ListChildren(parentID, false)
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
}
func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
n, err := a.nodes.GetActive(nodeID)
if err != nil {
return nil, err
}
dto := toNodeDTO(n)
return &dto, nil
}
func (a *App) CreateNode(parentID, nodeType, title, section string) (*NodeDTO, error) {
n, err := a.nodes.Create(parentID, nodeType, title, section)
if err != nil {
return nil, err
}
dto := toNodeDTO(n)
return &dto, nil
}
func (a *App) DeleteNode(id string) error {
return a.nodes.SoftDelete(id)
}
// ============================================================
// Notes
// ============================================================
// ListNotes returns note-type children of a node.
func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) {
children, err := a.nodes.ListChildren(nodeID, false)
if err != nil {
return nil, err
}
var result []NodeDTO
for i := range children {
if children[i].Type == nodes.TypeNote {
result = append(result, toNodeDTO(&children[i]))
}
}
return result, nil
}
// CreateNote creates a note under a parent node.
func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
node, _, err := a.notes.Create(parentID, title, "")
if err != nil {
return nil, err
}
dto := toNodeDTO(node)
return &dto, nil
}
// ReadNote reads note content.
func (a *App) ReadNote(noteID string) (string, error) {
return a.notes.Read(noteID)
}
// SaveNote saves note content.
func (a *App) SaveNote(noteID, content string) error {
return a.notes.Save(noteID, content)
}
// ============================================================
// Files
// ============================================================
func (a *App) ListFiles(nodeID string) ([]FileDTO, error) {
records, err := a.files.ListByNode(nodeID)
if err != nil {
return nil, err
}
result := make([]FileDTO, len(records))
for i := range records {
isDir := records[i].MIME == "inode/directory"
missing := false
result[i] = FileDTO{
ID: records[i].ID,
NodeID: records[i].NodeID,
Name: records[i].Filename,
Path: records[i].Path,
Size: records[i].Size,
Mime: records[i].MIME,
IsDir: isDir,
Missing: missing,
}
}
return result, nil
}
// ============================================================
// Actions
// ============================================================
func (a *App) ListActions(nodeID string) ([]ActionDTO, error) {
list, err := a.actions.ListByNode(nodeID)
if err != nil {
return nil, err
}
result := make([]ActionDTO, len(list))
for i := range list {
data := list[i].Command
if list[i].URL != "" {
data = list[i].URL
}
result[i] = ActionDTO{
ID: list[i].ID,
NodeID: list[i].NodeID,
Title: list[i].Title,
Type: list[i].Kind,
Data: data,
}
}
return result, nil
}
func (a *App) RunAction(id string) error {
_, err := a.actions.Run(id)
return err
}
// ============================================================
// Worklog
// ============================================================
func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) {
list, err := a.worklog.ListByNode(nodeID)
if err != nil {
return nil, err
}
result := make([]WorklogDTO, len(list))
for i := range list {
mins := 0
if list[i].Minutes != nil {
mins = *list[i].Minutes
}
result[i] = WorklogDTO{
ID: list[i].ID,
NodeID: list[i].NodeID,
Summary: list[i].Summary,
Minutes: mins,
CreatedAt: list[i].CreatedAt.Format("2006-01-02T15:04:05Z"),
}
}
return result, nil
}
func (a *App) CreateWorklog(nodeID, summary string, minutes int) (*WorklogDTO, error) {
entry, err := a.worklog.Add(nodeID, summary, "", minutes, false, false)
if err != nil {
return nil, err
}
mins := 0
if entry.Minutes != nil {
mins = *entry.Minutes
}
dto := &WorklogDTO{
ID: entry.ID,
NodeID: entry.NodeID,
Summary: entry.Summary,
Minutes: mins,
CreatedAt: entry.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
return dto, nil
}
// ============================================================
// Search (stubs — search service not wired yet)
// ============================================================
func (a *App) Search(query string) ([]SearchResultDTO, error) {
if strings.TrimSpace(query) == "" {
return []SearchResultDTO{}, nil
}
return []SearchResultDTO{}, nil
}
// ============================================================
// File Dialogs (Wails v2 Runtime)
// ============================================================
func (a *App) PickFile() (string, error) {
return wailsruntime.OpenFileDialog(a.ctx, wailsruntime.OpenDialogOptions{
Title: "Выберите файл",
})
}
func (a *App) PickFiles() ([]string, error) {
return wailsruntime.OpenMultipleFilesDialog(a.ctx, wailsruntime.OpenDialogOptions{
Title: "Выберите файлы",
})
}
func (a *App) PickDirectory() (string, error) {
return wailsruntime.OpenDirectoryDialog(a.ctx, wailsruntime.OpenDialogOptions{
Title: "Выберите папку",
})
}
// ============================================================
// System helpers
// ============================================================
func (a *App) OpenFile(fileID string) error {
return fmt.Errorf("not implemented: %s", fileID)
}
func (a *App) OpenFolder(nodeID string) error {
cmd := exec.Command("xdg-open", a.vault)
return cmd.Run()
}
func (a *App) VerstakVersion() string {
return "verstak-gui/v2"
}
// ============================================================
// Helpers
// ============================================================
func toNodeDTO(n *nodes.Node) NodeDTO {
parentID := ""
if n.ParentID != nil {
parentID = *n.ParentID
}
path := ""
if n.Path != nil {
path = *n.Path
}
return NodeDTO{
ID: n.ID,
ParentID: parentID,
Title: n.Title,
Type: n.Type,
Section: n.Section,
Path: path,
CreatedAt: n.CreatedAt.Format("2006-01-02T15:04:05Z"),
}
}
func toNodeDTOs(list []nodes.Node) []NodeDTO {
result := make([]NodeDTO, len(list))
for i := range list {
result[i] = toNodeDTO(&list[i])
}
return result
}
var (
_ = os.Getenv
_ = exec.Command
)