verstak/cmd/verstak-gui/bindings_settings.go

403 lines
9.3 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 (l.title_lower LIKE ? OR l.url_lower LIKE ? OR l.hostname_lower LIKE ? OR l.note_lower 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, &note, &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 (ac.title_lower LIKE ? OR ac.kind_lower LIKE ? OR ac.url_lower LIKE ? OR ac.command_lower 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"
}