Compare commits
10 Commits
600b67bc1e
...
2487d3bbaa
| Author | SHA1 | Date |
|---|---|---|
|
|
2487d3bbaa | |
|
|
645d8878cc | |
|
|
b4010a5a24 | |
|
|
4a8f4e3319 | |
|
|
ee503c338f | |
|
|
3e07e611dd | |
|
|
77a7918569 | |
|
|
c65187f656 | |
|
|
2e50e95b68 | |
|
|
ae970e5bca |
Binary file not shown.
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd frontend && npm run build && cd ..
|
||||||
|
rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist
|
||||||
|
go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
|
|
@ -0,0 +1,513 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"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/search"
|
||||||
|
"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
|
||||||
|
search *search.Service
|
||||||
|
vault string
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the app starts. Store context and wire drag-and-drop.
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
wailsruntime.OnFileDrop(ctx, func(x, y int, paths []string) {
|
||||||
|
if len(paths) > 0 {
|
||||||
|
wailsruntime.EventsEmit(ctx, "files-dropped", paths)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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 FileTreeItemDTO struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` // "folder" | "file"
|
||||||
|
FileID string `json:"fileId,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Mime string `json:"mime,omitempty"`
|
||||||
|
HasKids bool `json:"hasKids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ListFiles returns file records directly linked to a node (non-recursive).
|
||||||
|
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 {
|
||||||
|
rec := &records[i]
|
||||||
|
result[i] = FileDTO{
|
||||||
|
ID: rec.ID,
|
||||||
|
NodeID: rec.NodeID,
|
||||||
|
Name: rec.Filename,
|
||||||
|
Path: rec.Path,
|
||||||
|
Size: rec.Size,
|
||||||
|
Mime: rec.MIME,
|
||||||
|
IsDir: rec.MIME == "inode/directory",
|
||||||
|
Missing: rec.Missing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListItems returns children of a node for the file tree view.
|
||||||
|
// Folders can be expanded; files include their file record info.
|
||||||
|
func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) {
|
||||||
|
children, err := a.nodes.ListChildren(nodeID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]FileTreeItemDTO, 0, len(children))
|
||||||
|
for i := range children {
|
||||||
|
if children[i].Type != nodes.TypeFolder && children[i].Type != nodes.TypeFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := FileTreeItemDTO{
|
||||||
|
ID: children[i].ID,
|
||||||
|
Name: children[i].Title,
|
||||||
|
Type: children[i].Type,
|
||||||
|
}
|
||||||
|
if children[i].Type == nodes.TypeFolder {
|
||||||
|
// Check if this folder has children
|
||||||
|
kids, _ := a.nodes.ListChildren(children[i].ID, false)
|
||||||
|
item.HasKids = len(kids) > 0
|
||||||
|
} else if children[i].Type == nodes.TypeFile {
|
||||||
|
records, _ := a.files.ListByNode(children[i].ID)
|
||||||
|
if len(records) > 0 {
|
||||||
|
item.FileID = records[0].ID
|
||||||
|
item.Size = records[0].Size
|
||||||
|
item.Mime = records[0].MIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) {
|
||||||
|
nodes, err := a.files.AddPathCopy(nodeID, sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toNodeDTOs(nodes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) {
|
||||||
|
nodes, err := a.files.AddPathLink(nodeID, sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toNodeDTOs(nodes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DeleteFileOrFolder(nodeID string) error {
|
||||||
|
return a.files.DeleteNodeAndChildren(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) {
|
||||||
|
node, err := a.files.CreateEmptyFile(parentID, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := toNodeDTO(node)
|
||||||
|
return &dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DuplicateNode(nodeID string) (*NodeDTO, error) {
|
||||||
|
node, err := a.files.Duplicate(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := toNodeDTO(node)
|
||||||
|
return &dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RenameNode(nodeID, newTitle string) error {
|
||||||
|
return a.nodes.UpdateTitle(nodeID, newTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) MoveNode(nodeID, newParentID string) error {
|
||||||
|
return a.nodes.Move(nodeID, newParentID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) {
|
||||||
|
return a.files.PreviewImport(sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
func (a *App) Search(query string) ([]SearchResultDTO, error) {
|
||||||
|
if strings.TrimSpace(query) == "" {
|
||||||
|
return []SearchResultDTO{}, nil
|
||||||
|
}
|
||||||
|
results, err := a.search.Search(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]SearchResultDTO, len(results))
|
||||||
|
for i, r := range results {
|
||||||
|
out[i] = SearchResultDTO{
|
||||||
|
NodeID: r.NodeID,
|
||||||
|
Title: r.Title,
|
||||||
|
Snippet: r.Snippet,
|
||||||
|
Type: r.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, 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 a.files.Open(fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ReadFileText(fileID string) (string, error) {
|
||||||
|
return a.files.ReadText(fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetFileBase64(fileID string) (string, error) {
|
||||||
|
return a.files.ReadBase64(fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenFolder(nodeID string) error {
|
||||||
|
n, err := a.nodes.GetActive(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get node: %w", err)
|
||||||
|
}
|
||||||
|
dir := filepath.Join(a.vault, "spaces", n.Slug)
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
dir = a.vault
|
||||||
|
}
|
||||||
|
cmd := exec.Command("xdg-open", dir)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
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
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
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Верстак</title>
|
||||||
|
<style>
|
||||||
|
/* Critical reset — no white borders, full viewport */
|
||||||
|
html, body, #app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #13131f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module" crossorigin src="/assets/main-a-M2pafQ.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/main-6cuAgDnH.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
:root {
|
||||||
|
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: rgba(27, 38, 54, 1);
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inter";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(""),
|
||||||
|
url("./Inter-Medium.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 3em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 60px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
margin: 0 0 0 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #e80000aa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo.vanilla:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #f7df1eaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.input-box .btn:hover {
|
||||||
|
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input {
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(240, 240, 240, 1);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input:hover {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box .input:focus {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -0,0 +1,84 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"verstak/internal/core/actions"
|
||||||
|
"verstak/internal/core/files"
|
||||||
|
"verstak/internal/core/notes"
|
||||||
|
"verstak/internal/core/nodes"
|
||||||
|
"verstak/internal/core/plugins"
|
||||||
|
"verstak/internal/core/search"
|
||||||
|
"verstak/internal/core/storage"
|
||||||
|
"verstak/internal/core/worklog"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend-dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
vaultPath := "."
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
vaultPath = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
abs, err := filepath.Abs(vaultPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
||||||
|
db, err := storage.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Open vault: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Init core services
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
fileSvc := files.NewService(db, abs, nodeRepo)
|
||||||
|
noteSvc := notes.NewService(db, abs, nodeRepo, fileSvc)
|
||||||
|
actionSvc := actions.NewService(db)
|
||||||
|
worklogSvc := worklog.NewService(db)
|
||||||
|
searchSvc := search.NewService(db)
|
||||||
|
plugins.NewManager(abs).Discover()
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
db: db,
|
||||||
|
nodes: nodeRepo,
|
||||||
|
files: fileSvc,
|
||||||
|
notes: noteSvc,
|
||||||
|
actions: actionSvc,
|
||||||
|
worklog: worklogSvc,
|
||||||
|
search: searchSvc,
|
||||||
|
vault: abs,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wails.Run(&options.App{
|
||||||
|
Title: "Верстак",
|
||||||
|
Width: 1280,
|
||||||
|
Height: 800,
|
||||||
|
MinWidth: 800,
|
||||||
|
MinHeight: 600,
|
||||||
|
BackgroundColour: &options.RGBA{R: 19, G: 19, B: 31, A: 1},
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
OnStartup: app.startup,
|
||||||
|
DragAndDrop: &options.DragAndDrop{
|
||||||
|
EnableFileDrop: true,
|
||||||
|
},
|
||||||
|
Bind: []interface{}{app},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
docs/PLAN.md
60
docs/PLAN.md
|
|
@ -21,20 +21,62 @@
|
||||||
| 8 | FTS5 Search | ✅ выполнен |
|
| 8 | FTS5 Search | ✅ выполнен |
|
||||||
| 9 | Section assignment + Sidebar filtering | ✅ выполнен |
|
| 9 | Section assignment + Sidebar filtering | ✅ выполнен |
|
||||||
| 10 | Plugin Manager (discovery + templates) | ✅ выполнен |
|
| 10 | Plugin Manager (discovery + templates) | ✅ выполнен |
|
||||||
| 11 | **Wails Desktop GUI** | ⬜ не начат |
|
| 11 | **Wails Desktop GUI** | 🔄 Wails v2 vertical MVP |
|
||||||
| 12 | **Files/Folders full workflow** | ⬜ не начат |
|
| 12 | **Files/Folders full workflow** | ⬜ следующий этап после vertical MVP |
|
||||||
| 13 | **Drag-and-drop** | ⬜ не начат |
|
| 13 | **Drag-and-drop** | ⬜ не начат |
|
||||||
| 14 | **MVP stabilization** | ⬜ не начат |
|
| 14 | **MVP stabilization** | ⬜ не начат |
|
||||||
| 15 | Sync Server Skeleton | 🔒 приостановлен |
|
| 15 | Sync Server Skeleton | 🔒 PAUSED |
|
||||||
| 16 | Sync Client MVP | 🔒 приостановлен |
|
| 16 | Sync Client MVP | 🔒 PAUSED |
|
||||||
| 17 | Activity + File Scanner/Watcher | 🔒 приостановлен |
|
| 17 | Activity + File Scanner/Watcher | 🔒 PAUSED |
|
||||||
| 18 | TUI MVP (Bubble Tea) | 🔒 приостановлен |
|
| 18 | TUI MVP (Bubble Tea) | 🔒 PAUSED |
|
||||||
| 19 | Integrity Check + Repair | 🔒 приостановлен |
|
| 19 | Integrity Check + Repair | 🔒 PAUSED |
|
||||||
| 20 | Plugins: Lua runtime | 🔒 приостановлен |
|
| 20 | Plugins: Lua runtime | 🔒 PAUSED |
|
||||||
| 21 | DokuWiki Importer (plugin) | 🔒 приостановлен |
|
| 21 | DokuWiki Importer (plugin) | 🔒 PAUSED |
|
||||||
|
| 22 | Calendar/Kanban | 🔒 PAUSED |
|
||||||
|
| 23 | New templates/integrations | 🔒 PAUSED |
|
||||||
|
|
||||||
> 🔒 = **PAUSED** — не начинать до завершения шага 14 (MVP stabilization).
|
> 🔒 = **PAUSED** — не начинать до завершения шага 14 (MVP stabilization).
|
||||||
|
|
||||||
|
> **Wails v3 → v2 migration:** Wails v3 alpha.96 показал SIGSEGV на Linux desktop (GTK/X11). Wails v2 stable выбран как GUI base для MVP. Миграция в процессе (ветка `gui/migrate-wails-v2`).
|
||||||
|
|
||||||
|
**GUI Build (Wails v2):**
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run build && cd ..
|
||||||
|
rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist
|
||||||
|
go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
|
./verstak-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
**GUI Build (Wails v2):**
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run build && cd ..
|
||||||
|
rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist
|
||||||
|
go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
|
./verstak-gui
|
||||||
|
```
|
||||||
|
|
||||||
|
Или для dev режима: `wails dev` (требует Wails v2 CLI)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущий этап: Wails v2 Vertical MVP
|
||||||
|
|
||||||
|
**Цель:** базовый рабочий desktop GUI для разделов → дел → заметок.
|
||||||
|
|
||||||
|
**Прогресс:**
|
||||||
|
- ✅ Wails v2 shell (window opens, no SIGSEGV)
|
||||||
|
- ✅ Layout fix (full viewport, dark theme, sidebar+main)
|
||||||
|
- 🔄 Notes bindings + UI
|
||||||
|
- 🔄 Tabs (Overview/Notes/Files/Actions/Worklog/Activity)
|
||||||
|
- 🔄 Node creation
|
||||||
|
- 🔄 Section filtering
|
||||||
|
|
||||||
|
**Пауза (не начинать до завершения vertical MVP):**
|
||||||
|
- Файлы/папки workflow
|
||||||
|
- Drag-and-drop
|
||||||
|
- Sync, plugins, Lua, browser extension, TUI
|
||||||
|
- Новые шаблоны, DokuWiki importer
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Выполненные шаги (1-10)
|
## Выполненные шаги (1-10)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<title>Верстак</title>
|
||||||
<title>Wails + Svelte</title>
|
<style>
|
||||||
</head>
|
/* Critical reset — no white borders, full viewport */
|
||||||
<body>
|
html, body, #app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #13131f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,16 +5,13 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build:dev": "vite build --minify false --mode development",
|
|
||||||
"build": "vite build --mode production",
|
"build": "vite build --mode production",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"@wailsio/runtime": "latest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"svelte": "^5.46.4",
|
"svelte": "^4.2.19",
|
||||||
"vite": "^8.0.5"
|
"vite": "^5.4.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,346 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import FileIcon from './lib/FileIcon.svelte'
|
||||||
|
import { formatFileSize, formatMimeType, getFileKind } from './lib/fileUtils.js'
|
||||||
|
|
||||||
|
export let item
|
||||||
|
export let selected = false
|
||||||
|
export let onDragStart
|
||||||
|
export let onDragOver
|
||||||
|
export let onDrop
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const kind = getFileKind(item)
|
||||||
|
const isFolder = item.type === 'folder'
|
||||||
|
let menuOpen = false
|
||||||
|
let clickTimer = null
|
||||||
|
|
||||||
|
function handleClick(e) {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
dispatch('toggleSelect', item.id)
|
||||||
|
} else if (e.shiftKey) {
|
||||||
|
dispatch('rangeSelect', item.id)
|
||||||
|
} else {
|
||||||
|
if (clickTimer) {
|
||||||
|
clearTimeout(clickTimer)
|
||||||
|
clickTimer = null
|
||||||
|
// Double click
|
||||||
|
if (isFolder) {
|
||||||
|
dispatch('navigate', item.id)
|
||||||
|
} else {
|
||||||
|
dispatch('preview', item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clickTimer = setTimeout(() => {
|
||||||
|
clickTimer = null
|
||||||
|
// Single click: select
|
||||||
|
if (selected) {
|
||||||
|
// Already selected: navigate/preview
|
||||||
|
if (isFolder) {
|
||||||
|
dispatch('navigate', item.id)
|
||||||
|
} else {
|
||||||
|
dispatch('preview', item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch('selectOne', item.id)
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (isFolder) {
|
||||||
|
dispatch('navigate', item.id)
|
||||||
|
} else {
|
||||||
|
dispatch('preview', item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenExternal() {
|
||||||
|
dispatch('openExternal', item.fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
dispatch('delete', { id: item.id, type: item.type })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRename() {
|
||||||
|
menuOpen = false
|
||||||
|
dispatch('rename', { id: item.id, name: item.name })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDuplicate() {
|
||||||
|
menuOpen = false
|
||||||
|
dispatch('duplicate', item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCut() {
|
||||||
|
menuOpen = false
|
||||||
|
dispatch('cut', item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
menuOpen = false
|
||||||
|
dispatch('copy', item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
menuOpen = !menuOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
menuOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(e) {
|
||||||
|
if (onDragStart) onDragStart(e, item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e) {
|
||||||
|
if (onDragOver && isFolder) onDragOver(e, item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
if (onDrop && isFolder) onDrop(e, item.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeMenu}/>
|
||||||
|
|
||||||
|
<div class="file-row"
|
||||||
|
class:file-row--selected={selected}
|
||||||
|
class:file-row--dragover={false}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
draggable="true"
|
||||||
|
on:click={handleClick}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
|
on:dragstart={handleDragStart}
|
||||||
|
on:dragover={handleDragOver}
|
||||||
|
on:drop={handleDrop}
|
||||||
|
aria-label={isFolder ? `Folder ${item.name}` : `File ${item.name}`}>
|
||||||
|
<div class="file-row-icon">
|
||||||
|
<FileIcon {kind} size={22}/>
|
||||||
|
</div>
|
||||||
|
<div class="file-row-body">
|
||||||
|
<div class="file-row-name" title={item.name}>{item.name}</div>
|
||||||
|
<div class="file-row-meta">
|
||||||
|
{#if isFolder}
|
||||||
|
<span>Folder</span>
|
||||||
|
{:else}
|
||||||
|
<span>{formatFileSize(item.size)}</span>
|
||||||
|
{#if item.mime}
|
||||||
|
<span class="meta-sep">·</span>
|
||||||
|
<span>{formatMimeType(item.mime)}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-row-actions">
|
||||||
|
{#if !isFolder}
|
||||||
|
<button class="action-btn" on:click|stopPropagation={() => dispatch('preview', item)} title="Preview" aria-label="Preview">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn" on:click|stopPropagation={handleOpenExternal} title="Open in external program" aria-label="Open externally">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="action-btn" on:click|stopPropagation={() => dispatch('navigate', item.id)} title="Open folder" aria-label="Open folder">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
<line x1="9" y1="14" x2="15" y2="14"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button class="action-btn" on:click|stopPropagation={toggleMenu} title="More actions" aria-label="More actions" aria-expanded={menuOpen}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<circle cx="12" cy="5" r="2"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
<circle cx="12" cy="19" r="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn action-btn-danger" on:click|stopPropagation={handleDelete} title="Delete" aria-label="Delete">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if menuOpen}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="menu-backdrop" on:click|stopPropagation={closeMenu} role="presentation"></div>
|
||||||
|
<div class="menu" on:click|stopPropagation role="menu">
|
||||||
|
<button class="menu-item" on:click={handleRename} role="menuitem">Rename</button>
|
||||||
|
<button class="menu-item" on:click={handleDuplicate} role="menuitem">Duplicate</button>
|
||||||
|
<button class="menu-item" on:click={handleCut} role="menuitem">Cut</button>
|
||||||
|
<button class="menu-item" on:click={handleCopy} role="menuitem">Copy</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: default;
|
||||||
|
transition: background 0.12s;
|
||||||
|
min-height: 52px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row:hover {
|
||||||
|
background: #1e1e30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row--selected {
|
||||||
|
background: #1e1e3a;
|
||||||
|
outline: 1px solid #3a3a6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row--selected:hover {
|
||||||
|
background: #252545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ddd;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-sep {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row:hover .file-row-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #2a2a3c;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-danger:hover {
|
||||||
|
background: #3a2222;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: #1a1a28;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 140px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: #2a2a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Wails v2 API wrapper — uses generated bindings from wailsjs/go/main/App.js
|
||||||
|
import * as App from '../wailsjs/go/main/App.js'
|
||||||
|
|
||||||
|
// Re-export all methods
|
||||||
|
export const listSections = () => App.ListSections()
|
||||||
|
export const listNodesBySection = (section) => App.ListNodesBySection(section)
|
||||||
|
export const listChildren = (parentID) => App.ListChildren(parentID)
|
||||||
|
export const getNodeDetail = (id) => App.GetNodeDetail(id)
|
||||||
|
export const createNode = (parentID, type, title, section) => App.CreateNode(parentID, type, title, section)
|
||||||
|
export const deleteNode = (id) => App.DeleteNode(id)
|
||||||
|
|
||||||
|
export const listNotes = (nodeID) => App.ListNotes(nodeID)
|
||||||
|
export const createNote = (parentID, title) => App.CreateNote(parentID, title)
|
||||||
|
export const readNote = (noteID) => App.ReadNote(noteID)
|
||||||
|
export const saveNote = (noteID, content) => App.SaveNote(noteID, content)
|
||||||
|
|
||||||
|
export const listFiles = (nodeID) => App.ListFiles(nodeID)
|
||||||
|
export const listItems = (nodeID) => App.ListItems(nodeID)
|
||||||
|
export const addPathCopy = (nodeID, sourcePath) => App.AddPathCopy(nodeID, sourcePath)
|
||||||
|
export const addPathLink = (nodeID, sourcePath) => App.AddPathLink(nodeID, sourcePath)
|
||||||
|
export const deleteFileOrFolder = (nodeID) => App.DeleteFileOrFolder(nodeID)
|
||||||
|
export const previewImport = (sourcePath) => App.PreviewImport(sourcePath)
|
||||||
|
export const pickFile = () => App.PickFile()
|
||||||
|
export const pickFiles = () => App.PickFiles()
|
||||||
|
export const pickDirectory = () => App.PickDirectory()
|
||||||
|
export const openFile = (id) => App.OpenFile(id)
|
||||||
|
export const readFileText = (id) => App.ReadFileText(id)
|
||||||
|
export const getFileBase64 = (id) => App.GetFileBase64(id)
|
||||||
|
export const createEmptyFile = (parentID, filename) => App.CreateEmptyFile(parentID, filename)
|
||||||
|
export const duplicateNode = (nodeID) => App.DuplicateNode(nodeID)
|
||||||
|
export const renameNode = (nodeID, newTitle) => App.RenameNode(nodeID, newTitle)
|
||||||
|
export const moveNode = (nodeID, newParentID) => App.MoveNode(nodeID, newParentID)
|
||||||
|
export const openFolder = (nodeID) => App.OpenFolder(nodeID)
|
||||||
|
|
||||||
|
export const listActions = (nodeID) => App.ListActions(nodeID)
|
||||||
|
export const runAction = (id) => App.RunAction(id)
|
||||||
|
|
||||||
|
export const listWorklog = (nodeID) => App.ListWorklog(nodeID)
|
||||||
|
export const createWorklog = (nodeID, summary, minutes) => App.CreateWorklog(nodeID, summary, minutes)
|
||||||
|
|
||||||
|
export const search = (query) => App.Search(query)
|
||||||
|
|
||||||
|
export const verstakVersion = () => App.VerstakVersion()
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<script>
|
||||||
|
export let title = 'Подтверждение'
|
||||||
|
export let message = ''
|
||||||
|
export let confirmText = 'Удалить'
|
||||||
|
export let cancelText = 'Отмена'
|
||||||
|
export let danger = false
|
||||||
|
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overlay" on:click|self={() => dispatch('cancel')} role="dialog" aria-modal="true" aria-label={title}>
|
||||||
|
<div class="modal">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p class="message">{message}</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn {danger ? 'btn-danger' : 'btn-primary'}" on:click={() => dispatch('confirm')}>{confirmText}</button>
|
||||||
|
<button class="btn" on:click={() => dispatch('cancel')}>{cancelText}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #1a1a28;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 360px;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #e4e4ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
background: #1a1a28;
|
||||||
|
color: #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #222233;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #6366f1;
|
||||||
|
border-color: #6366f1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc2626;
|
||||||
|
border-color: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let isFolder = false
|
||||||
|
export let fileId = ''
|
||||||
|
export let nodeId = ''
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
function handleOpen() {
|
||||||
|
if (isFolder) {
|
||||||
|
dispatch('openFolder', nodeId)
|
||||||
|
} else {
|
||||||
|
dispatch('open', fileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
dispatch('delete', nodeId)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="file-actions">
|
||||||
|
<button class="action-btn" on:click={handleOpen} title={isFolder ? 'Open folder' : 'Open file'} aria-label={isFolder ? 'Open folder' : 'Open file'}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
{#if isFolder}
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
<line x1="9" y1="14" x2="15" y2="14"/>
|
||||||
|
{:else}
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn action-btn-danger" on:click={handleDelete} title="Delete" aria-label="Delete">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.file-row:hover) .file-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #2a2a3c;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-danger:hover {
|
||||||
|
background: #3a2222;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let crumbs = []
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
function navigateTo(index) {
|
||||||
|
dispatch('navigate', index)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="breadcrumbs">
|
||||||
|
{#each crumbs as crumb, i}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="sep">/</span>
|
||||||
|
{/if}
|
||||||
|
{#if i === crumbs.length - 1}
|
||||||
|
<span class="crumb crumb--current">{crumb.name}</span>
|
||||||
|
{:else}
|
||||||
|
<button class="crumb crumb--link" on:click={() => navigateTo(i)}>{crumb.name}</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb--current {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb--link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: color 0.12s, background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb--link:hover {
|
||||||
|
color: #ccc;
|
||||||
|
background: #1e1e30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crumb--link:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script>
|
||||||
|
export let kind = 'generic'
|
||||||
|
export let size = 20
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
{#if kind === 'folder'}
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
{:else if kind === 'image'}
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
|
{:else if kind === 'video'}
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||||
|
<polyline points="10 9 16 12 10 15 10 9"/>
|
||||||
|
{:else if kind === 'audio'}
|
||||||
|
<path d="M9 18V5l12-2v13"/>
|
||||||
|
<circle cx="6" cy="18" r="3"/>
|
||||||
|
<circle cx="18" cy="16" r="3"/>
|
||||||
|
{:else if kind === 'pdf'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||||
|
<line x1="8" y1="16" x2="16" y2="16"/>
|
||||||
|
<line x1="8" y1="14" x2="12" y2="14"/>
|
||||||
|
{:else if kind === 'document'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
{:else if kind === 'spreadsheet'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="8" y1="12" x2="16" y2="12"/>
|
||||||
|
<line x1="8" y1="16" x2="16" y2="16"/>
|
||||||
|
<line x1="8" y1="14" x2="12" y2="14"/>
|
||||||
|
<line x1="12" y1="12" x2="12" y2="18"/>
|
||||||
|
{:else if kind === 'presentation'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="9" y1="12" x2="15" y2="12"/>
|
||||||
|
<line x1="9" y1="15" x2="13" y2="15"/>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="18"/>
|
||||||
|
{:else if kind === 'archive'}
|
||||||
|
<path d="M21 8v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8"/>
|
||||||
|
<polyline points="7 3 12 8 17 3"/>
|
||||||
|
<line x1="3" y1="8" x2="21" y2="8"/>
|
||||||
|
<rect x="10" y="12" width="4" height="4" rx="1"/>
|
||||||
|
{:else if kind === 'code'}
|
||||||
|
<polyline points="16 18 22 12 16 6"/>
|
||||||
|
<polyline points="8 6 2 12 8 18"/>
|
||||||
|
{:else if kind === 'text'}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
<polyline points="10 9 9 9 8 9"/>
|
||||||
|
{:else}
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher, onMount, onDestroy } from 'svelte'
|
||||||
|
import FileIcon from './FileIcon.svelte'
|
||||||
|
import { formatFileSize, formatMimeType, getFileKind, isImageFile, isTextFile, isPdfFile, isMarkdownFile } from './fileUtils.js'
|
||||||
|
|
||||||
|
export let item
|
||||||
|
export let content = '' // base64 data URI or text content
|
||||||
|
export let loading = false
|
||||||
|
export let error = ''
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const kind = getFileKind(item)
|
||||||
|
$: showImage = isImageFile(item) && content && content.startsWith('data:')
|
||||||
|
$: showText = isTextFile(item) || isMarkdownFile(item)
|
||||||
|
$: showPdf = isPdfFile(item)
|
||||||
|
|
||||||
|
function handleKeydown(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenExternal() {
|
||||||
|
dispatch('openExternal', item.fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overlay" on:click|self={() => dispatch('close')} role="dialog" aria-modal="true" aria-label={`Preview: ${item.name}`}>
|
||||||
|
<div class="modal">
|
||||||
|
<header class="preview-header">
|
||||||
|
<div class="preview-title">
|
||||||
|
<FileIcon {kind} size={18}/>
|
||||||
|
<span class="preview-name" title={item.name}>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-meta">{formatFileSize(item.size)} · {formatMimeType(item.mime)}</div>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<button class="action-btn" on:click={handleOpenExternal} title="Open in external program" aria-label="Open externally">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||||
|
<polyline points="15 3 21 3 21 9"/>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn action-btn-close" on:click={() => dispatch('close')} title="Close" aria-label="Close preview">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="preview-body">
|
||||||
|
{#if loading}
|
||||||
|
<div class="preview-status"><p>Loading preview...</p></div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="preview-status">
|
||||||
|
<p>{error}</p>
|
||||||
|
<button class="btn btn-sm" on:click={handleOpenExternal}>Open in external program</button>
|
||||||
|
</div>
|
||||||
|
{:else if showImage && content}
|
||||||
|
<div class="preview-image-container">
|
||||||
|
<img src={content} alt={item.name} class="preview-image"/>
|
||||||
|
</div>
|
||||||
|
{:else if showText && content}
|
||||||
|
<pre class="preview-text"><code>{content}</code></pre>
|
||||||
|
{:else if showPdf}
|
||||||
|
{#if content && content.startsWith('data:')}
|
||||||
|
<div class="preview-pdf-container">
|
||||||
|
<embed src={content} type="application/pdf" class="preview-pdf"/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="preview-status">
|
||||||
|
<p>PDF preview is not available in this environment.</p>
|
||||||
|
<button class="btn btn-sm" on:click={handleOpenExternal}>Open in external program</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="preview-status">
|
||||||
|
<p>Preview is not available for this file type.</p>
|
||||||
|
<button class="btn btn-sm" on:click={handleOpenExternal}>Open in external program</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #14141f;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 900px;
|
||||||
|
height: 85vh;
|
||||||
|
max-height: 700px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #2a2a3c;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: #2a2a3c;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:focus-visible {
|
||||||
|
outline: 2px solid #5588ff;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-close {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-close:hover {
|
||||||
|
background: #3a2222;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
background: #0e0e18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(85vh - 100px);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #ccc;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pdf-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pdf {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
background: #1a1a28;
|
||||||
|
color: #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:hover {
|
||||||
|
background: #222233;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
export function formatFileSize(bytes) {
|
||||||
|
if (bytes == null || bytes < 0) return '—'
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||||
|
const val = bytes / Math.pow(1024, i)
|
||||||
|
return (i === 0 ? val.toFixed(0) : val.toFixed(1)) + ' ' + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeLabels = {
|
||||||
|
'image/jpeg': 'JPEG image',
|
||||||
|
'image/png': 'PNG image',
|
||||||
|
'image/gif': 'GIF image',
|
||||||
|
'image/webp': 'WebP image',
|
||||||
|
'image/svg+xml': 'SVG image',
|
||||||
|
'image/bmp': 'BMP image',
|
||||||
|
'image/tiff': 'TIFF image',
|
||||||
|
'image/avif': 'AVIF image',
|
||||||
|
'application/pdf': 'PDF document',
|
||||||
|
'application/msword': 'Word document',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word document',
|
||||||
|
'application/vnd.ms-excel': 'Excel spreadsheet',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel spreadsheet',
|
||||||
|
'application/vnd.ms-powerpoint': 'PowerPoint presentation',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint presentation',
|
||||||
|
'application/zip': 'ZIP archive',
|
||||||
|
'application/gzip': 'GZIP archive',
|
||||||
|
'application/x-tar': 'TAR archive',
|
||||||
|
'application/x-7z-compressed': '7z archive',
|
||||||
|
'application/x-rar-compressed': 'RAR archive',
|
||||||
|
'text/plain': 'Text file',
|
||||||
|
'text/html': 'HTML file',
|
||||||
|
'text/css': 'CSS file',
|
||||||
|
'text/javascript': 'JavaScript file',
|
||||||
|
'application/json': 'JSON file',
|
||||||
|
'application/xml': 'XML file',
|
||||||
|
'application/x-yaml': 'YAML file',
|
||||||
|
'application/octet-stream': 'Binary file',
|
||||||
|
'application/x-msdos-program': 'Executable',
|
||||||
|
'inode/directory': 'Folder',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMimeType(mime) {
|
||||||
|
if (!mime) return 'Unknown'
|
||||||
|
return mimeLabels[mime] || mime
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileKind(item) {
|
||||||
|
if (item.type === 'folder') return 'folder'
|
||||||
|
const mime = (item.mime || '').toLowerCase()
|
||||||
|
if (mime.startsWith('image/')) return 'image'
|
||||||
|
if (mime.startsWith('video/')) return 'video'
|
||||||
|
if (mime.startsWith('audio/')) return 'audio'
|
||||||
|
if (mime.startsWith('text/')) return 'text'
|
||||||
|
if (mime.includes('pdf')) return 'pdf'
|
||||||
|
if (mime.includes('word') || mime.includes('document')) return 'document'
|
||||||
|
if (mime.includes('spreadsheet') || mime.includes('excel')) return 'spreadsheet'
|
||||||
|
if (mime.includes('presentation') || mime.includes('powerpoint')) return 'presentation'
|
||||||
|
if (mime.includes('zip') || mime.includes('tar') || mime.includes('gzip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compress')) return 'archive'
|
||||||
|
if (mime.includes('json') || mime.includes('xml') || mime.includes('yaml') || mime.includes('javascript') || mime.includes('css') || mime.includes('html')) return 'code'
|
||||||
|
const name = (item.name || '').toLowerCase()
|
||||||
|
const ext = name.split('.').pop()
|
||||||
|
const codeExts = ['js','ts','jsx','tsx','vue','svelte','py','rs','go','c','cpp','h','hpp','java','kt','swift','rb','php','pl','sh','bash','zsh','fish','yml','yaml','json','xml','toml','ini','cfg','conf','md','markdown','css','scss','less','sass','sql','graphql','proto','gradle','cmake','makefile','dockerfile','env','gitignore']
|
||||||
|
if (codeExts.includes(ext)) return 'code'
|
||||||
|
return 'generic'
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageMimes = ['image/jpeg','image/png','image/gif','image/webp','image/bmp','image/tiff','image/avif','image/svg+xml']
|
||||||
|
const textMimes = ['text/plain','text/html','text/css','text/javascript','application/json','application/xml','application/x-yaml','text/x-shellscript']
|
||||||
|
const codeNames = ['txt','log','conf','ini','yaml','yml','json','xml','csv','sh','py','js','ts','css','html','md','markdown','cfg']
|
||||||
|
|
||||||
|
const imageExts = ['jpg','jpeg','png','gif','webp','bmp','tiff','tif','avif','svg']
|
||||||
|
|
||||||
|
export function canPreviewFile(item) {
|
||||||
|
if (item.type === 'folder') return false
|
||||||
|
const mime = (item.mime || '').toLowerCase()
|
||||||
|
const name = (item.name || '').toLowerCase()
|
||||||
|
const ext = name.split('.').pop()
|
||||||
|
if (imageMimes.includes(mime) || imageExts.includes(ext)) return true
|
||||||
|
if (mime.includes('pdf')) return true
|
||||||
|
if (textMimes.includes(mime) || codeNames.includes(ext)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isImageFile(item) {
|
||||||
|
const mime = (item.mime || '').toLowerCase()
|
||||||
|
const name = (item.name || '').toLowerCase()
|
||||||
|
const ext = name.split('.').pop()
|
||||||
|
return imageMimes.includes(mime) || imageExts.includes(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTextFile(item) {
|
||||||
|
const mime = (item.mime || '').toLowerCase()
|
||||||
|
const name = (item.name || '').toLowerCase()
|
||||||
|
const ext = name.split('.').pop()
|
||||||
|
return textMimes.includes(mime) || (codeNames.includes(ext) && ext !== 'md' && ext !== 'markdown')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPdfFile(item) {
|
||||||
|
return (item.mime || '').toLowerCase().includes('pdf')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMarkdownFile(item) {
|
||||||
|
const name = (item.name || '').toLowerCase()
|
||||||
|
return name.endsWith('.md') || name.endsWith('.markdown')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsBase64Preview(item) {
|
||||||
|
return isImageFile(item) || isPdfFile(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsTextPreview(item) {
|
||||||
|
return isTextFile(item) || isMarkdownFile(item)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { mount } from 'svelte'
|
|
||||||
import App from './App.svelte'
|
import App from './App.svelte'
|
||||||
|
|
||||||
mount(App, { target: document.getElementById('app') })
|
new App({
|
||||||
|
target: document.getElementById('app')
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
// @ts-check
|
||||||
|
// Wails v2 generated bindings — auto-generated by wails build
|
||||||
|
// Manual version for go build -tags gui
|
||||||
|
|
||||||
|
export function ListSections() {
|
||||||
|
return window['go']['main']['App']['ListSections']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListNodesBySection(arg1) {
|
||||||
|
return window['go']['main']['App']['ListNodesBySection'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListChildren(arg1) {
|
||||||
|
return window['go']['main']['App']['ListChildren'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetNodeDetail(arg1) {
|
||||||
|
return window['go']['main']['App']['GetNodeDetail'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateNode(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['CreateNode'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteNode(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteNode'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListNotes(arg1) {
|
||||||
|
return window['go']['main']['App']['ListNotes'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateNote(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['CreateNote'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadNote(arg1) {
|
||||||
|
return window['go']['main']['App']['ReadNote'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveNote(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['SaveNote'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListFiles(arg1) {
|
||||||
|
return window['go']['main']['App']['ListFiles'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddPathCopy(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['AddPathCopy'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddPathLink(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['AddPathLink'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteFileOrFolder(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteFileOrFolder'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PreviewImport(arg1) {
|
||||||
|
return window['go']['main']['App']['PreviewImport'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListActions(arg1) {
|
||||||
|
return window['go']['main']['App']['ListActions'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RunAction(arg1) {
|
||||||
|
return window['go']['main']['App']['RunAction'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListWorklog(arg1) {
|
||||||
|
return window['go']['main']['App']['ListWorklog'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateWorklog(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['CreateWorklog'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Search(arg1) {
|
||||||
|
return window['go']['main']['App']['Search'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PickFile() {
|
||||||
|
return window['go']['main']['App']['PickFile']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PickFiles() {
|
||||||
|
return window['go']['main']['App']['PickFiles']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PickDirectory() {
|
||||||
|
return window['go']['main']['App']['PickDirectory']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenFile(arg1) {
|
||||||
|
return window['go']['main']['App']['OpenFile'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenFolder(arg1) {
|
||||||
|
return window['go']['main']['App']['OpenFolder'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerstakVersion() {
|
||||||
|
return window['go']['main']['App']['VerstakVersion']();
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
import wails from "@wailsio/runtime/plugins/vite";
|
import { resolve } from "path";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
server: {
|
server: {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: Number(process.env.WAILS_VITE_PORT) || 9245,
|
port: 3001,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
},
|
},
|
||||||
plugins: [svelte(), wails("./bindings")],
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publicDir: "public",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
76
go.mod
76
go.mod
|
|
@ -3,46 +3,38 @@ module verstak
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
github.com/mattn/go-sqlite3 v1.14.44
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/wailsapp/wails/v2 v2.12.0
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
github.com/adrg/xdg v0.5.3 // indirect
|
)
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
require (
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/leaanthony/u v1.1.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/lmittmann/tint v1.1.2 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.96 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
github.com/wailsapp/wails/webview2 v1.0.24 // indirect
|
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
|
||||||
golang.org/x/net v0.53.0 // indirect
|
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
|
||||||
golang.org/x/text v0.37.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
140
go.sum
140
go.sum
|
|
@ -1,116 +1,92 @@
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
|
||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
|
||||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
|
||||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
|
||||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
|
||||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
|
||||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
|
||||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
|
||||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
|
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||||
|
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||||
|
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||||
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
|
||||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
|
||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.96 h1:FmZdo4LiUkFUvz8rZO8f4nGLm5af9J+D3sr3Flset2g=
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.96/go.mod h1:p1MUZwFPPQyx81cgejDvKIwU1+x/hndR+4z1uG5bw6s=
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/wails/webview2 v1.0.24 h1:uULnjCSaRfMlU84mS3kjLgPsRosEOIusVK1nFOHZHzs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/wails/webview2 v1.0.24/go.mod h1:sdf+s0nAdxlzVWf9SCxC15XaxnQPJeY+uU1Ucn3jHQM=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
73
guimain.go
73
guimain.go
|
|
@ -1,73 +0,0 @@
|
||||||
// This file is only compiled with -tags gui.
|
|
||||||
//go:build gui
|
|
||||||
// +build gui
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
_ "embed"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"verstak/internal/core/storage"
|
|
||||||
"verstak/internal/core/nodes"
|
|
||||||
"verstak/internal/core/files"
|
|
||||||
"verstak/internal/core/notes"
|
|
||||||
"verstak/internal/core/actions"
|
|
||||||
"verstak/internal/core/worklog"
|
|
||||||
"verstak/internal/core/search"
|
|
||||||
"verstak/internal/core/plugins"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
|
||||||
var assets embed.FS
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
vaultPath := "."
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
vaultPath = os.Args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
abs, err := filepath.Abs(vaultPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath := filepath.Join(abs, ".verstak", "index.db")
|
|
||||||
db, err := storage.Open(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Open vault: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Initialize core services (registered for Wails bindings).
|
|
||||||
_ = nodes.NewRepository(db)
|
|
||||||
_ = files.NewService(db, abs)
|
|
||||||
_ = notes.NewService(db, abs, nil, nil)
|
|
||||||
_ = actions.NewService(db)
|
|
||||||
_ = worklog.NewService(db)
|
|
||||||
_ = search.NewService(db)
|
|
||||||
plugins.NewManager(abs).Discover()
|
|
||||||
|
|
||||||
app := application.New(application.Options{
|
|
||||||
Name: "verstak",
|
|
||||||
Description: "Verstak — local-first working vault",
|
|
||||||
Assets: application.AssetOptions{
|
|
||||||
Handler: application.AssetFileServerFS(assets),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Window.NewWithOptions(application.WebviewWindowOptions{
|
|
||||||
Title: "Верстак",
|
|
||||||
Width: 1200,
|
|
||||||
Height: 800,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ package files
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -12,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"verstak/internal/core/nodes"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
"verstak/internal/core/util"
|
"verstak/internal/core/util"
|
||||||
)
|
)
|
||||||
|
|
@ -32,15 +34,25 @@ type Record struct {
|
||||||
Missing bool `json:"missing"`
|
Missing bool `json:"missing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportSummary describes a scanned directory before import.
|
||||||
|
type ImportSummary struct {
|
||||||
|
Files int `json:"files"`
|
||||||
|
Folders int `json:"folders"`
|
||||||
|
TotalBytes int64 `json:"totalBytes"`
|
||||||
|
IsDangerous bool `json:"isDangerous"`
|
||||||
|
DangerReason string `json:"dangerReason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Service provides file operations inside a vault.
|
// Service provides file operations inside a vault.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db *storage.DB
|
db *storage.DB
|
||||||
vaultRoot string
|
vaultRoot string
|
||||||
|
nodes *nodes.Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a file service bound to a vault.
|
// NewService creates a file service bound to a vault.
|
||||||
func NewService(db *storage.DB, vaultRoot string) *Service {
|
func NewService(db *storage.DB, vaultRoot string, nodeRepo *nodes.Repository) *Service {
|
||||||
return &Service{db: db, vaultRoot: vaultRoot}
|
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB returns the underlying storage.
|
// DB returns the underlying storage.
|
||||||
|
|
@ -166,6 +178,280 @@ func (s *Service) Open(id string) error {
|
||||||
return openWithSystem(abs)
|
return openWithSystem(abs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxPreviewSize is the maximum file size (5 MB) for inline preview.
|
||||||
|
const maxPreviewSize = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
// ReadText reads a file's content as text, up to maxPreviewSize.
|
||||||
|
func (s *Service) ReadText(id string) (string, error) {
|
||||||
|
rec, err := s.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rec.Size > maxPreviewSize {
|
||||||
|
return "", fmt.Errorf("file too large for preview (%d bytes)", rec.Size)
|
||||||
|
}
|
||||||
|
var abs string
|
||||||
|
if rec.StorageMode == "vault" {
|
||||||
|
abs = filepath.Join(s.vaultRoot, rec.Path)
|
||||||
|
} else {
|
||||||
|
abs = rec.Path
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(abs)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read: %w", err)
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBase64 reads a file and returns a data URI (base64-encoded).
|
||||||
|
func (s *Service) ReadBase64(id string) (string, error) {
|
||||||
|
rec, err := s.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rec.Size > maxPreviewSize {
|
||||||
|
return "", fmt.Errorf("file too large for preview (%d bytes)", rec.Size)
|
||||||
|
}
|
||||||
|
var abs string
|
||||||
|
if rec.StorageMode == "vault" {
|
||||||
|
abs = filepath.Join(s.vaultRoot, rec.Path)
|
||||||
|
} else {
|
||||||
|
abs = rec.Path
|
||||||
|
}
|
||||||
|
b, err := os.ReadFile(abs)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read: %w", err)
|
||||||
|
}
|
||||||
|
mime := rec.MIME
|
||||||
|
if mime == "" {
|
||||||
|
mime = "application/octet-stream"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(b)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEmptyFile creates a file node and an empty vault file.
|
||||||
|
func (s *Service) CreateEmptyFile(parentID, filename string) (*nodes.Node, error) {
|
||||||
|
filename = s.uniqueTitle(parentID, filename)
|
||||||
|
node, err := s.nodes.Create(parentID, nodes.TypeFile, filename, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
||||||
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
|
return nil, fmt.Errorf("mkdir: %w", err)
|
||||||
|
}
|
||||||
|
dest := filepath.Join(dir, filename)
|
||||||
|
f, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create file: %w", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
relPath, _ := filepath.Rel(s.vaultRoot, dest)
|
||||||
|
_, err = s.insertRecord(node.ID, filename, relPath, "vault", 0, "")
|
||||||
|
return node, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate creates a copy of a node and its file record under the same parent.
|
||||||
|
func (s *Service) Duplicate(nodeID string) (*nodes.Node, error) {
|
||||||
|
original, err := s.nodes.GetActive(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parentID := ""
|
||||||
|
if original.ParentID != nil {
|
||||||
|
parentID = *original.ParentID
|
||||||
|
}
|
||||||
|
newName := s.uniqueTitle(parentID, original.Title)
|
||||||
|
node, err := s.nodes.Create(parentID, original.Type, newName, original.Section)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if original.Type == nodes.TypeFile {
|
||||||
|
records, _ := s.ListByNode(original.ID)
|
||||||
|
if len(records) > 0 {
|
||||||
|
src := &records[0]
|
||||||
|
if src.StorageMode == "vault" {
|
||||||
|
srcPath := filepath.Join(s.vaultRoot, src.Path)
|
||||||
|
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
||||||
|
os.MkdirAll(dir, 0o750)
|
||||||
|
dst := filepath.Join(dir, newName)
|
||||||
|
hash, err := copyAndHash(srcPath, dst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("copy file: %w", err)
|
||||||
|
}
|
||||||
|
relPath, _ := filepath.Rel(s.vaultRoot, dst)
|
||||||
|
_, err = s.insertRecord(node.ID, newName, relPath, "vault", src.Size, hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// External file: create a new record pointing to the same absolute path.
|
||||||
|
_, err = s.insertRecord(node.ID, newName, src.Path, "external", src.Size, src.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPathCopy copies sourcePath (file or directory) into the vault under nodeID.
|
||||||
|
func (s *Service) AddPathCopy(nodeID, sourcePath string) ([]nodes.Node, error) {
|
||||||
|
return s.importPath(nodeID, sourcePath, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPathLink links sourcePath (file or directory) without copying into vault.
|
||||||
|
func (s *Service) AddPathLink(nodeID, sourcePath string) ([]nodes.Node, error) {
|
||||||
|
return s.importPath(nodeID, sourcePath, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviewImport scans sourcePath and returns a summary without importing.
|
||||||
|
func (s *Service) PreviewImport(sourcePath string) (*ImportSummary, error) {
|
||||||
|
info, err := os.Stat(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return &ImportSummary{Files: 1, TotalBytes: info.Size()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum ImportSummary
|
||||||
|
err = filepath.Walk(sourcePath, func(path string, fi os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
if fi.IsDir() {
|
||||||
|
sum.Folders++
|
||||||
|
name := strings.ToLower(fi.Name())
|
||||||
|
if name == ".git" || name == "node_modules" || name == ".cache" {
|
||||||
|
sum.IsDangerous = true
|
||||||
|
sum.DangerReason = fmt.Sprintf("содержит %s", fi.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sum.Files++
|
||||||
|
sum.TotalBytes += fi.Size()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if sum.Files > 1000 && !sum.IsDangerous {
|
||||||
|
sum.IsDangerous = true
|
||||||
|
sum.DangerReason = "более 1000 файлов"
|
||||||
|
}
|
||||||
|
if sum.TotalBytes > 1<<30 && !sum.IsDangerous {
|
||||||
|
sum.IsDangerous = true
|
||||||
|
sum.DangerReason = "более 1 GB"
|
||||||
|
}
|
||||||
|
return &sum, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNodeAndChildren soft-deletes a node and all descendants,
|
||||||
|
// moving vault files to trash.
|
||||||
|
func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||||
|
children, _ := s.nodes.ListChildren(nodeID, false)
|
||||||
|
for i := range children {
|
||||||
|
if err := s.DeleteNodeAndChildren(children[i].ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = s.deleteFileRecords(nodeID)
|
||||||
|
return s.nodes.SoftDelete(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) deleteFileRecords(nodeID string) error {
|
||||||
|
records, err := s.ListByNode(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, r := range records {
|
||||||
|
_ = s.DeleteToTrash(r.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) importPath(parentID, sourcePath string, copyMode bool) ([]nodes.Node, error) {
|
||||||
|
info, err := os.Stat(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
title := s.uniqueTitle(parentID, filepath.Base(sourcePath))
|
||||||
|
node, err := s.nodes.Create(parentID, nodes.TypeFile, title, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if copyMode {
|
||||||
|
_, err = s.CopyIntoVault(node.ID, sourcePath, node.Slug)
|
||||||
|
} else {
|
||||||
|
_, err = s.AddExternal(node.ID, sourcePath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []nodes.Node{*node}, nil
|
||||||
|
}
|
||||||
|
return s.importDir(parentID, sourcePath, info.Name(), copyMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool) ([]nodes.Node, error) {
|
||||||
|
dirName = s.uniqueTitle(parentID, dirName)
|
||||||
|
folderNode, err := s.nodes.Create(parentID, nodes.TypeFolder, dirName, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var all []nodes.Node
|
||||||
|
all = append(all, *folderNode)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
childPath := filepath.Join(sourcePath, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
|
children, err := s.importDir(folderNode.ID, childPath, entry.Name(), copyMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, children...)
|
||||||
|
} else {
|
||||||
|
childNode, err := s.nodes.Create(folderNode.ID, nodes.TypeFile, entry.Name(), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if copyMode {
|
||||||
|
_, err = s.CopyIntoVault(childNode.ID, childPath, childNode.Slug)
|
||||||
|
} else {
|
||||||
|
_, err = s.AddExternal(childNode.ID, childPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, *childNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) uniqueTitle(parentID, desired string) string {
|
||||||
|
children, _ := s.nodes.ListChildren(parentID, false)
|
||||||
|
used := make(map[string]bool, len(children))
|
||||||
|
for i := range children {
|
||||||
|
used[children[i].Title] = true
|
||||||
|
}
|
||||||
|
if !used[desired] {
|
||||||
|
return desired
|
||||||
|
}
|
||||||
|
for n := 2; ; n++ {
|
||||||
|
c := fmt.Sprintf("%s (%d)", desired, n)
|
||||||
|
if !used[c] {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- implementation details ---
|
// --- implementation details ---
|
||||||
|
|
||||||
func (s *Service) insertRecord(nodeID, filename, path, mode string, size int64, sha string) (*Record, error) {
|
func (s *Service) insertRecord(nodeID, filename, path, mode string, size int64, sha string) (*Record, error) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"verstak/internal/core/nodes"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,7 +24,7 @@ func TestAddExternal(t *testing.T) {
|
||||||
db := openTestDB(t)
|
db := openTestDB(t)
|
||||||
// Run migration 002 manually since storage.Open already applied it.
|
// Run migration 002 manually since storage.Open already applied it.
|
||||||
// We can verify the table exists by inserting.
|
// We can verify the table exists by inserting.
|
||||||
filesSvc := NewService(db, t.TempDir())
|
filesSvc := NewService(db, t.TempDir(), nodes.NewRepository(db))
|
||||||
|
|
||||||
// Create a real temp file to register.
|
// Create a real temp file to register.
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
@ -62,7 +63,7 @@ func TestAddExternal(t *testing.T) {
|
||||||
func TestCopyIntoVault(t *testing.T) {
|
func TestCopyIntoVault(t *testing.T) {
|
||||||
db := openTestDB(t)
|
db := openTestDB(t)
|
||||||
vaultRoot := t.TempDir()
|
vaultRoot := t.TempDir()
|
||||||
svc := NewService(db, vaultRoot)
|
svc := NewService(db, vaultRoot, nodes.NewRepository(db))
|
||||||
|
|
||||||
// Source file.
|
// Source file.
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
|
|
@ -88,7 +89,7 @@ func TestCopyIntoVault(t *testing.T) {
|
||||||
|
|
||||||
func TestListByNode(t *testing.T) {
|
func TestListByNode(t *testing.T) {
|
||||||
db := openTestDB(t)
|
db := openTestDB(t)
|
||||||
svc := NewService(db, t.TempDir())
|
svc := NewService(db, t.TempDir(), nodes.NewRepository(db))
|
||||||
|
|
||||||
os.WriteFile(filepath.Join(t.TempDir(), "a.txt"), []byte("a"), 0o640)
|
os.WriteFile(filepath.Join(t.TempDir(), "a.txt"), []byte("a"), 0o640)
|
||||||
f1 := filepath.Join(t.TempDir(), "a1.txt")
|
f1 := filepath.Join(t.TempDir(), "a1.txt")
|
||||||
|
|
@ -111,7 +112,7 @@ func TestListByNode(t *testing.T) {
|
||||||
func TestDeleteToTrash(t *testing.T) {
|
func TestDeleteToTrash(t *testing.T) {
|
||||||
db := openTestDB(t)
|
db := openTestDB(t)
|
||||||
vaultRoot := t.TempDir()
|
vaultRoot := t.TempDir()
|
||||||
svc := NewService(db, vaultRoot)
|
svc := NewService(db, vaultRoot, nodes.NewRepository(db))
|
||||||
|
|
||||||
src := filepath.Join(t.TempDir(), "important.pdf")
|
src := filepath.Join(t.TempDir(), "important.pdf")
|
||||||
os.WriteFile(src, []byte("important data"), 0o640)
|
os.WriteFile(src, []byte("important data"), 0o640)
|
||||||
|
|
@ -140,6 +141,170 @@ func TestDeleteToTrash(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddPathCopySingleFile(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
svc := NewService(db, vaultRoot, nodeRepo)
|
||||||
|
|
||||||
|
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||||
|
src := filepath.Join(t.TempDir(), "doc.pdf")
|
||||||
|
os.WriteFile(src, []byte("file content"), 0o640)
|
||||||
|
|
||||||
|
nodes, err := svc.AddPathCopy(parent.ID, src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddPathCopy: %v", err)
|
||||||
|
}
|
||||||
|
if len(nodes) != 1 {
|
||||||
|
t.Fatalf("got %d nodes, want 1", len(nodes))
|
||||||
|
}
|
||||||
|
if nodes[0].Type != "file" {
|
||||||
|
t.Errorf("type = %q", nodes[0].Type)
|
||||||
|
}
|
||||||
|
// Source intact.
|
||||||
|
if _, err := os.Stat(src); err != nil {
|
||||||
|
t.Error("source should remain intact")
|
||||||
|
}
|
||||||
|
// File record created.
|
||||||
|
records, _ := svc.ListByNode(nodes[0].ID)
|
||||||
|
if len(records) != 1 {
|
||||||
|
t.Errorf("file records = %d", len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPathLinkSingleFile(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
svc := NewService(db, vaultRoot, nodeRepo)
|
||||||
|
|
||||||
|
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||||
|
src := filepath.Join(t.TempDir(), "linked.pdf")
|
||||||
|
os.WriteFile(src, []byte("linked"), 0o640)
|
||||||
|
|
||||||
|
nodes, err := svc.AddPathLink(parent.ID, src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddPathLink: %v", err)
|
||||||
|
}
|
||||||
|
if len(nodes) != 1 {
|
||||||
|
t.Fatalf("got %d nodes, want 1", len(nodes))
|
||||||
|
}
|
||||||
|
// File record should have external storage mode.
|
||||||
|
records, _ := svc.ListByNode(nodes[0].ID)
|
||||||
|
if len(records) != 1 {
|
||||||
|
t.Fatalf("file records = %d", len(records))
|
||||||
|
}
|
||||||
|
if records[0].StorageMode != "external" {
|
||||||
|
t.Errorf("storage mode = %q, want external", records[0].StorageMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPathCopyDirectory(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
svc := NewService(db, vaultRoot, nodeRepo)
|
||||||
|
|
||||||
|
parent, _ := nodeRepo.Create("", "case", "Test Case", "")
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, "sub"), 0o750)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("a"), 0o640)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "sub", "b.txt"), []byte("bb"), 0o640)
|
||||||
|
|
||||||
|
nodes, err := svc.AddPathCopy(parent.ID, srcDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddPathCopy dir: %v", err)
|
||||||
|
}
|
||||||
|
// Should create: folder node + file node + sub folder node + file node in sub.
|
||||||
|
if len(nodes) < 3 {
|
||||||
|
t.Errorf("expected 3+ nodes, got %d", len(nodes))
|
||||||
|
}
|
||||||
|
// Verify structure: root folder + children.
|
||||||
|
var folders, files int
|
||||||
|
for i := range nodes {
|
||||||
|
if nodes[i].Type == "folder" {
|
||||||
|
folders++
|
||||||
|
} else {
|
||||||
|
files++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if folders < 1 {
|
||||||
|
t.Error("expected at least 1 folder")
|
||||||
|
}
|
||||||
|
if files < 1 {
|
||||||
|
t.Error("expected at least 1 file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteNodeAndChildren(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
svc := NewService(db, vaultRoot, nodeRepo)
|
||||||
|
|
||||||
|
parent, _ := nodeRepo.Create("", "case", "To Delete", "")
|
||||||
|
child, _ := nodeRepo.Create(parent.ID, "file", "child.txt", "")
|
||||||
|
// Add file record to child.
|
||||||
|
src := filepath.Join(t.TempDir(), "child.txt")
|
||||||
|
os.WriteFile(src, []byte("data"), 0o640)
|
||||||
|
svc.CopyIntoVault(child.ID, src, child.Slug)
|
||||||
|
|
||||||
|
if err := svc.DeleteNodeAndChildren(parent.ID); err != nil {
|
||||||
|
t.Fatalf("DeleteNodeAndChildren: %v", err)
|
||||||
|
}
|
||||||
|
// Parent should be soft-deleted.
|
||||||
|
if _, err := nodeRepo.GetActive(parent.ID); err == nil {
|
||||||
|
t.Error("parent should be deleted")
|
||||||
|
}
|
||||||
|
// Child should be soft-deleted.
|
||||||
|
if _, err := nodeRepo.GetActive(child.ID); err == nil {
|
||||||
|
t.Error("child should be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameConflict(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
svc := NewService(db, vaultRoot, nodeRepo)
|
||||||
|
|
||||||
|
parent, _ := nodeRepo.Create("", "case", "Test", "")
|
||||||
|
src := filepath.Join(t.TempDir(), "conflict.pdf")
|
||||||
|
os.WriteFile(src, []byte("data"), 0o640)
|
||||||
|
|
||||||
|
// Import twice with same filename.
|
||||||
|
n1, _ := svc.AddPathCopy(parent.ID, src)
|
||||||
|
n2, _ := svc.AddPathCopy(parent.ID, src)
|
||||||
|
if n1[0].Title == n2[0].Title {
|
||||||
|
t.Error("expected unique name on conflict")
|
||||||
|
}
|
||||||
|
if n2[0].Title == "conflict.pdf" {
|
||||||
|
t.Errorf("title unchanged = %q", n2[0].Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreviewImportDir(t *testing.T) {
|
||||||
|
db := openTestDB(t)
|
||||||
|
vaultRoot := t.TempDir()
|
||||||
|
svc := NewService(db, vaultRoot, nodes.NewRepository(db))
|
||||||
|
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, "sub"), 0o750)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "f1.txt"), []byte("hello"), 0o640)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "f2.txt"), []byte("world"), 0o640)
|
||||||
|
|
||||||
|
sum, err := svc.PreviewImport(srcDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PreviewImport: %v", err)
|
||||||
|
}
|
||||||
|
if sum.Files != 2 {
|
||||||
|
t.Errorf("files = %d, want 2", sum.Files)
|
||||||
|
}
|
||||||
|
if sum.Folders != 2 { // root + sub
|
||||||
|
t.Errorf("folders = %d, want 2", sum.Folders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGuessMIME(t *testing.T) {
|
func TestGuessMIME(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"a.md": "text/plain",
|
"a.md": "text/plain",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func setupService(t *testing.T) (*Service, *nodes.Repository, string) {
|
||||||
t.Cleanup(func() { db.Close() })
|
t.Cleanup(func() { db.Close() })
|
||||||
|
|
||||||
nodeRepo := nodes.NewRepository(db)
|
nodeRepo := nodes.NewRepository(db)
|
||||||
fileSvc := files.NewService(db, dir)
|
fileSvc := files.NewService(db, dir, nodeRepo)
|
||||||
svc := NewService(db, dir, nodeRepo, fileSvc)
|
svc := NewService(db, dir, nodeRepo, fileSvc)
|
||||||
return svc, nodeRepo, dir
|
return svc, nodeRepo, dir
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ type Server struct {
|
||||||
// NewServer creates a GUI server for the given vault.
|
// NewServer creates a GUI server for the given vault.
|
||||||
func NewServer(db *storage.DB, vaultRoot string) *Server {
|
func NewServer(db *storage.DB, vaultRoot string) *Server {
|
||||||
nodeRepo := nodes.NewRepository(db)
|
nodeRepo := nodes.NewRepository(db)
|
||||||
fileSvc := files.NewService(db, vaultRoot)
|
fileSvc := files.NewService(db, vaultRoot, nodeRepo)
|
||||||
noteSvc := notes.NewService(db, vaultRoot, nodeRepo, fileSvc)
|
noteSvc := notes.NewService(db, vaultRoot, nodeRepo, fileSvc)
|
||||||
actionSvc := actions.NewService(db)
|
actionSvc := actions.NewService(db)
|
||||||
workSvc := worklog.NewService(db)
|
workSvc := worklog.NewService(db)
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"verstak/internal/core/nodes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WailsService exposes core methods to the Wails frontend.
|
|
||||||
type WailsService struct{}
|
|
||||||
|
|
||||||
// ListRootNodes returns root-level nodes.
|
|
||||||
func (s *WailsService) ListRootNodes(ctx context.Context) string {
|
|
||||||
// This is a stub — actual implementation will use injected DB
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListChildren returns children of a node.
|
|
||||||
func (s *WailsService) ListChildren(ctx context.Context, parentID string) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateNode creates a new node.
|
|
||||||
func (s *WailsService) CreateNode(ctx context.Context, parentID, nodeType, title, section string) string {
|
|
||||||
return "{}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListFiles returns files for a node.
|
|
||||||
func (s *WailsService) ListFiles(ctx context.Context, nodeID string) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListActions returns actions for a node.
|
|
||||||
func (s *WailsService) ListActions(ctx context.Context, nodeID string) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListWorklog returns worklog entries for a node.
|
|
||||||
func (s *WailsService) ListWorklog(ctx context.Context, nodeID string) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search performs a search query.
|
|
||||||
func (s *WailsService) Search(ctx context.Context, query string) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTemplates returns available templates.
|
|
||||||
func (s *WailsService) ListTemplates(ctx context.Context) string {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSections returns available sections.
|
|
||||||
func (s *WailsService) ListSections(ctx context.Context) string {
|
|
||||||
sections := []map[string]string{
|
|
||||||
{"id": "today", "label": "Сегодня"},
|
|
||||||
{"id": "inbox", "label": "Неразобранное"},
|
|
||||||
{"id": "clients", "label": "Клиенты"},
|
|
||||||
{"id": "projects", "label": "Проекты"},
|
|
||||||
{"id": "recipes", "label": "Рецепты"},
|
|
||||||
{"id": "documents", "label": "Документы"},
|
|
||||||
{"id": "archive", "label": "Архив"},
|
|
||||||
}
|
|
||||||
data, _ := json.Marshal(sections)
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentSelection returns current selection state.
|
|
||||||
func (s *WailsService) GetCurrentSelection(ctx context.Context) string {
|
|
||||||
sel := map[string]string{"kind": "section", "section": "today"}
|
|
||||||
data, _ := json.Marshal(sel)
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenFile opens a file with the system application.
|
|
||||||
func (s *WailsService) OpenFile(ctx context.Context, fileID string) string {
|
|
||||||
return fmt.Sprintf("opened file %s", fileID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenFolder opens a folder in the system file manager.
|
|
||||||
func (s *WailsService) OpenFolder(ctx context.Context, nodeID string) string {
|
|
||||||
return fmt.Sprintf("opened folder %s", nodeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Silence unused import.
|
|
||||||
var _ = nodes.TypeCase
|
|
||||||
Loading…
Reference in New Issue