403 lines
9.4 KiB
Go
403 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"verstak/internal/core/config"
|
|
"verstak/internal/core/nodes"
|
|
"verstak/internal/core/plugins"
|
|
"verstak/internal/i18n"
|
|
|
|
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// ===== Template management =====
|
|
|
|
// AllTemplates returns all registered templates with their enabled status.
|
|
type TemplateWithStatus struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Type string `json:"type"`
|
|
Icon string `json:"icon,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (a *App) AllTemplates() ([]TemplateWithStatus, error) {
|
|
if !a.IsReady() || a.templates == nil {
|
|
return nil, fmt.Errorf("vault not ready")
|
|
}
|
|
appCfg, _ := config.LoadAppConfig()
|
|
enabledSet := make(map[string]bool)
|
|
if appCfg != nil {
|
|
for _, id := range appCfg.EnabledTemplates {
|
|
enabledSet[id] = true
|
|
}
|
|
}
|
|
|
|
all := a.templates.All()
|
|
result := make([]TemplateWithStatus, len(all))
|
|
for i, t := range all {
|
|
// If config has explicit list, use it; otherwise default to true
|
|
enabled := true
|
|
if appCfg != nil && len(appCfg.EnabledTemplates) > 0 {
|
|
enabled = enabledSet[t.ID]
|
|
}
|
|
result[i] = TemplateWithStatus{
|
|
ID: t.ID,
|
|
Title: t.Title,
|
|
Type: t.Type,
|
|
Icon: t.Icon,
|
|
Enabled: enabled,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) SetTemplateEnabled(templateID string, enabled bool) error {
|
|
if !a.IsReady() || a.templates == nil {
|
|
return fmt.Errorf("vault not ready")
|
|
}
|
|
|
|
appCfg, _ := config.LoadAppConfig()
|
|
if appCfg == nil {
|
|
appCfg = config.DefaultAppConfig()
|
|
}
|
|
|
|
// Update enabled templates list
|
|
existing := make(map[string]bool)
|
|
for _, id := range appCfg.EnabledTemplates {
|
|
existing[id] = true
|
|
}
|
|
if enabled {
|
|
existing[templateID] = true
|
|
} else {
|
|
delete(existing, templateID)
|
|
}
|
|
|
|
appCfg.EnabledTemplates = make([]string, 0, len(existing))
|
|
for id := range existing {
|
|
appCfg.EnabledTemplates = append(appCfg.EnabledTemplates, id)
|
|
}
|
|
if err := config.SaveAppConfig(appCfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update in-memory registry
|
|
if enabled {
|
|
_ = a.templates.Enable(templateID)
|
|
} else {
|
|
_ = a.templates.Disable(templateID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ListTemplates() []TemplateDTO {
|
|
if !a.IsReady() {
|
|
return nil
|
|
}
|
|
templates := a.plugins.Templates()
|
|
out := make([]TemplateDTO, 0, len(templates))
|
|
for _, t := range templates {
|
|
out = append(out, TemplateDTO{
|
|
ID: t.Name,
|
|
Title: t.Name,
|
|
Type: t.RootType,
|
|
Icon: t.Icon,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (a *App) FromTemplate(parentID, nodeType, title, section, template string) (*NodeDTO, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return nil, err
|
|
}
|
|
var tmpl *plugins.TemplateDefinition
|
|
for _, t := range a.plugins.Templates() {
|
|
if t.Name == template {
|
|
tmpl = &t
|
|
break
|
|
}
|
|
}
|
|
if tmpl == nil {
|
|
return nil, nil
|
|
}
|
|
root, err := a.nodes.Create(strPtr(parentID), tmpl.RootType, title, 0, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var createTree func(parentID string, nodes []plugins.TreeNode) error
|
|
createTree = func(parentID string, nodes []plugins.TreeNode) error {
|
|
for _, tn := range nodes {
|
|
child, err := a.nodes.Create(strPtr(parentID), tn.Type, tn.Title, 0, "", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tn.Children) > 0 {
|
|
if err := createTree(child.ID, tn.Children); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := createTree(root.ID, tmpl.Tree); err != nil {
|
|
return nil, err
|
|
}
|
|
dto := toNodeDTO(root)
|
|
return &dto, nil
|
|
}
|
|
|
|
// ===== File picking =====
|
|
|
|
func (a *App) PickFile() (string, error) {
|
|
return wailsruntime.OpenFileDialog(a.ctx, wailsruntime.OpenDialogOptions{
|
|
Title: i18n.TF("ru", "file.pickSingle"),
|
|
})
|
|
}
|
|
|
|
func (a *App) PickFiles() ([]string, error) {
|
|
return wailsruntime.OpenMultipleFilesDialog(a.ctx, wailsruntime.OpenDialogOptions{
|
|
Title: i18n.TF("ru", "file.pickMultiple"),
|
|
})
|
|
}
|
|
|
|
func (a *App) PickDirectory() (string, error) {
|
|
return wailsruntime.OpenDirectoryDialog(a.ctx, wailsruntime.OpenDialogOptions{
|
|
Title: i18n.TF("ru", "file.pickDirectory"),
|
|
})
|
|
}
|
|
|
|
func (a *App) OpenFile(fileID string) error {
|
|
if err := a.requireVault(); err != nil {
|
|
return err
|
|
}
|
|
return a.files.Open(fileID)
|
|
}
|
|
|
|
func (a *App) ReadFileText(fileID string) (string, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return "", err
|
|
}
|
|
return a.files.ReadText(fileID)
|
|
}
|
|
|
|
func (a *App) GetFileBase64(fileID string) (string, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return "", err
|
|
}
|
|
return a.files.ReadBase64(fileID)
|
|
}
|
|
|
|
func (a *App) OpenFolder(nodeID string) error {
|
|
if err := a.requireVault(); err != nil {
|
|
return err
|
|
}
|
|
n, err := a.nodes.GetActive(nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var fileRecordPath string
|
|
if n.Type == nodes.TypeFile && n.FsPath == "" {
|
|
records, _ := a.files.ListByNode(nodeID)
|
|
if len(records) > 0 {
|
|
fileRecordPath = records[0].Path
|
|
}
|
|
}
|
|
target := resolveOpenFolderTarget(a.vault, n, fileRecordPath)
|
|
if _, err := os.Stat(target); os.IsNotExist(err) {
|
|
target = a.vault
|
|
}
|
|
cmd := exec.Command("xdg-open", target)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func resolveOpenFolderTarget(vault string, n *nodes.Node, fileRecordPath string) string {
|
|
if n.Type == nodes.TypeFile && n.FsPath == "" {
|
|
if fileRecordPath == "" {
|
|
return vault
|
|
}
|
|
return filepath.Dir(filepath.Join(vault, fileRecordPath))
|
|
}
|
|
target := filepath.Join(vault, n.FsPath)
|
|
if n.Type == nodes.TypeFile {
|
|
return filepath.Dir(target)
|
|
}
|
|
return target
|
|
}
|
|
|
|
func (a *App) OpenVaultFolder() error {
|
|
if !a.IsReady() {
|
|
return fmt.Errorf("vault not open")
|
|
}
|
|
cmd := exec.Command("xdg-open", a.vault)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func ensurePluginsFolder(vaultPath string) (string, error) {
|
|
path := filepath.Join(vaultPath, ".verstak", "plugins")
|
|
if err := os.MkdirAll(path, 0o750); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (a *App) OpenPluginsFolder() error {
|
|
if !a.IsReady() {
|
|
return fmt.Errorf("vault not open")
|
|
}
|
|
target, err := ensurePluginsFolder(a.vault)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command("xdg-open", target)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (a *App) Search(query string) ([]SearchResultDTO, error) {
|
|
if err := a.requireVault(); err != nil {
|
|
return nil, err
|
|
}
|
|
query = strings.TrimSpace(query)
|
|
if query == "" {
|
|
return []SearchResultDTO{}, nil
|
|
}
|
|
out := []SearchResultDTO{}
|
|
seen := map[string]bool{}
|
|
add := func(r SearchResultDTO) {
|
|
if r.Title == "" {
|
|
return
|
|
}
|
|
key := r.Type + ":" + r.NodeID + ":" + r.TargetID + ":" + r.Title
|
|
if seen[key] {
|
|
return
|
|
}
|
|
seen[key] = true
|
|
out = append(out, r)
|
|
}
|
|
results, err := a.search.Search(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range results {
|
|
path := r.Path
|
|
if path == "" && r.NodeID != "" {
|
|
path = a.nodes.Path(r.NodeID)
|
|
}
|
|
add(SearchResultDTO{
|
|
NodeID: r.NodeID,
|
|
Title: r.Title,
|
|
Snippet: r.Snippet,
|
|
Type: r.Type,
|
|
Path: path,
|
|
})
|
|
}
|
|
|
|
if len(out) < 20 {
|
|
nodes, err := a.nodes.Search(query, 20-len(out))
|
|
if err == nil {
|
|
for i := range nodes {
|
|
if nodes[i].IsDeleted() {
|
|
continue
|
|
}
|
|
add(SearchResultDTO{
|
|
NodeID: nodes[i].ID,
|
|
Title: nodes[i].Title,
|
|
Type: nodes[i].Type,
|
|
Path: a.nodes.Path(nodes[i].ID),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(out) < 20 {
|
|
rows, err := a.db.Query(
|
|
`SELECT l.id,l.node_id,l.title,l.url,l.hostname,COALESCE(l.note,''),n.deleted_at
|
|
FROM links l
|
|
LEFT JOIN nodes n ON n.id = l.node_id
|
|
WHERE n.deleted_at IS NULL
|
|
AND (LOWER(l.title) LIKE ? OR LOWER(l.url) LIKE ? OR LOWER(l.hostname) LIKE ? OR LOWER(COALESCE(l.note,'')) LIKE ?)
|
|
ORDER BY l.created_at DESC
|
|
LIMIT ?`,
|
|
likeQuery(query), likeQuery(query), likeQuery(query), likeQuery(query), 20-len(out))
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id, nodeID, title, url, hostname, note string
|
|
var deletedAt interface{}
|
|
if err := rows.Scan(&id, &nodeID, &title, &url, &hostname, ¬e, &deletedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
snippet := url
|
|
if note != "" {
|
|
snippet = note
|
|
}
|
|
add(SearchResultDTO{
|
|
NodeID: nodeID,
|
|
TargetID: id,
|
|
Title: title,
|
|
Snippet: snippet,
|
|
Type: "link",
|
|
Path: a.nodes.Path(nodeID),
|
|
URL: url,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(out) < 20 {
|
|
rows, err := a.db.Query(
|
|
`SELECT ac.id,ac.node_id,ac.title,ac.kind,COALESCE(ac.url,''),COALESCE(ac.command,''),n.deleted_at
|
|
FROM actions ac
|
|
LEFT JOIN nodes n ON n.id = ac.node_id
|
|
WHERE n.deleted_at IS NULL
|
|
AND (LOWER(ac.title) LIKE ? OR LOWER(ac.kind) LIKE ? OR LOWER(COALESCE(ac.url,'')) LIKE ? OR LOWER(COALESCE(ac.command,'')) LIKE ?)
|
|
ORDER BY ac.created_at DESC
|
|
LIMIT ?`,
|
|
likeQuery(query), likeQuery(query), likeQuery(query), likeQuery(query), 20-len(out))
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id, nodeID, title, kind, url, command string
|
|
var deletedAt interface{}
|
|
if err := rows.Scan(&id, &nodeID, &title, &kind, &url, &command, &deletedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
snippet := url
|
|
if snippet == "" {
|
|
snippet = command
|
|
}
|
|
add(SearchResultDTO{
|
|
NodeID: nodeID,
|
|
TargetID: id,
|
|
Title: title,
|
|
Snippet: snippet,
|
|
Type: "action",
|
|
Path: a.nodes.Path(nodeID),
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func likeQuery(query string) string {
|
|
return "%" + strings.ToLower(strings.TrimSpace(query)) + "%"
|
|
}
|
|
|
|
func (a *App) VerstakVersion() string {
|
|
return "verstak-gui/v2"
|
|
}
|