Compare commits

..

No commits in common. "rescue/broken-app-split" and "master" have entirely different histories.

27 changed files with 731 additions and 1481 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,8 +19,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-C0D__Sxo.js"></script> <script type="module" crossorigin src="/assets/main-CzfuqGWF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-Bl-yCbt2.css"> <link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -1,161 +0,0 @@
# Frontend Architecture
## Overview
Verstak frontend is a Svelte 3 application running inside Wails v2 (Go bridge).
The app manages a hierarchical vault of nodes (folders/cases, notes, files, links, actions)
with sync capabilities, worklog/journal, and activity tracking.
## Technology Stack
- **UI Framework:** Svelte 3 (plain JS, no TypeScript in components)
- **Desktop Bridge:** Wails v2 (`window.go.main.App.*`)
- **Bundler:** Vite (via Wails)
- **Markdown:** Custom renderer in `lib/markdown/`
- **i18n:** Custom lightweight system in `lib/i18n/`
- **Styling:** Scoped CSS in Svelte components, dark theme
## Directory Structure
```
frontend/src/
├── App.svelte # Root component (being modularised)
├── TreeNode.svelte # Tree node for sidebar (inline)
├── FileTreeRow.svelte # File row in file tab (inline)
├── wailsjs/go/main/App.js # Auto-generated Wails bindings
├── lib/
│ ├── components/ # Reusable UI components
│ │ └── notes/
│ │ ├── NoteEditorPanel.svelte
│ │ ├── MarkdownEditor.svelte
│ │ ├── MarkdownPreview.svelte
│ │ ├── InternalLinkPicker.svelte
│ │ └── ObjectPickerModal.svelte
│ ├── services/ # API/Data access layer
│ │ ├── wails.js # Base Wails call helper
│ │ ├── notes.js # Notes API
│ │ ├── files.js # Files API
│ │ ├── search.js # Search API
│ │ ├── inbox.js # Inbox API
│ │ ├── trash.js # Trash API
│ │ ├── sync.js # Sync API
│ │ ├── journal.js # Journal/Worklog API
│ │ ├── actions.js # Actions API
│ │ ├── links.js # Links API
│ │ └── activity.js # Activity API
│ ├── state/ # State management (planned)
│ │ ├── navigation.js # Navigation state
│ │ └── uiState.js # UI state
│ ├── markdown/ # Markdown processing
│ │ ├── markdown.ts
│ │ └── internalLinks.ts
│ ├── i18n/ # Internationalisation
│ │ ├── index.js
│ │ └── locales/
│ │ ├── en.js
│ │ └── ru.js
│ ├── util/ # Utilities
│ │ ├── keyboardLayout.ts
│ │ └── markdown.test.js
│ ├── AppHeader.svelte
│ ├── GlobalSearch.svelte
│ ├── FileBreadcrumbs.svelte
│ ├── FileIcon.svelte
│ ├── FilePreviewModal.svelte
│ ├── ConfirmModal.svelte
│ ├── TodayScreen.svelte
│ ├── BrowserEvents.svelte
│ ├── FirstRun.svelte
│ ├── VaultRecovery.svelte
│ ├── SyncStatus.svelte
│ ├── TemplateIcon.svelte
│ ├── CalendarPluginPage.svelte
│ ├── SettingsWindow.svelte
│ ├── SettingsSidebar.svelte
│ ├── SettingsGeneral.svelte
│ ├── SettingsSync.svelte
│ ├── SettingsPlugins.svelte
│ ├── SettingsBrowserBridge.svelte
│ ├── SettingsWorkspace.svelte
│ ├── SettingsTemplates.svelte
│ ├── SettingsFiles.svelte
│ ├── SettingsBackup.svelte
│ ├── SettingsActivity.svelte
│ ├── actionIcons.js
│ └── fileUtils.js
```
## Wails Bridge
All backend calls go through `window.go.main.App[method](...)`.
The `wailsCall()` helper in `lib/services/wails.js` provides error handling.
## Planned Components (to extract from App.svelte)
### Layout
- `AppShell.svelte` — root layout wrapper
- `Sidebar.svelte` — navigation sidebar
- `MainWorkspace.svelte` — main content area
### Pages/Tab Content
- `OverviewTab.svelte` — node overview with meta and quick actions
- `NotesTab.svelte` — notes list and creation
- `FilesTab.svelte` — file browser with breadcrumbs
- `InboxContent.svelte` + `InboxFullScreen.svelte`
- `LinksTab.svelte`
- `ActionsTab.svelte`
- `WorklogTab.svelte`
- `ActivityTabContent.svelte`
- `TrashContent.svelte`
- `JournalScreen.svelte`
- `ActivityFeedScreen.svelte`
- `WelcomeScreen.svelte`
### Modals
- `CreateNodeModal.svelte`
- `WorklogModal.svelte`
- `CreateActionModal.svelte`
- `ImportModal.svelte`
- `RenameModal.svelte`
- `AssignInboxModal.svelte`
- `EditLinkModal.svelte`
- `LinkInsertModal.svelte`
- `NoteRenameModal.svelte`
- `ContextMenu.svelte`
## Data Flow
1. User interacts with UI component
2. Component calls a service function (e.g., `notesApi.createNote(...)`)
3. Service calls `wailsCall('CreateNote', ...)`
4. Wails bridge forwards to Go backend
5. Go backend returns result → Wails → service → component updates state
## State Management
Currently all state lives in App.svelte as local variables.
Target: extract into `lib/state/navigation.js` and `lib/state/uiState.js`.
### Files Flow
- **Component:** `lib/components/files/FilesTab.svelte` — self-contained file browser
- **API services:** `lib/services/files.js`, `lib/services/nodes.js`
- **Events emitted:**
- `on:openNote` — when a .md file linked to a note is opened
- `on:refreshParent` — after file operations that modify the tree
- `on:error` — on operation failures
- `on:rename` — requests parent to show rename modal
- `on:confirm` — requests parent to show confirm dialog
- **Public methods:**
- `resetToNode(nodeId)` — reset state when selected node changes
- `filesHandleKeydown(e)` — keyboard handler for files tab
- **.md → note editor flow:** Handled inside FilesTab via `CheckFileAction` Wails call. If action is 'note', emits `openNote`. If 'external', opens in system viewer. Otherwise shows built-in preview.
- **File preview:** `FilePreviewModal.svelte` (already existed), invoked by FilesTab
- **Import dialog:** Inline in FilesTab template (moved from App.svelte)
## Build & Verification
- `npm run build` in `frontend/` directory
- `go test ./...` from project root
- `bash scripts/build.sh gui` for full GUI binary
- Manual smoke testing via Wails dev server

View File

@ -1,107 +0,0 @@
# Frontend Change Map
## Purpose
This document tracks the refactoring of `App.svelte` from a 4794-line monolith
into a modular frontend architecture. Each step preserves behaviour exactly.
## Phase 1: Documentation & Foundation
- [x] Audit App.svelte (all 4794 lines read and mapped)
- [x] Create `docs/frontend-architecture.md`
- [x] Create `docs/frontend-change-map.md`
## Phase 2: API Layer
Extract all Wails calls into service modules.
- [x] Create `lib/services/wails.js` — base `wailsCall` helper
- [x] Create `lib/services/notes.js``listNotes`, `createNote`, `readNote`, `saveNote`, `renameNote`, `deleteNote`
- [x] Create `lib/services/files.js``loadFolder`, `addFile`, `deleteFile`, etc.
- [x] Create `lib/services/search.js``searchNodes`, `getNodeDetail`, `searchWorkspace`
- [x] Create `lib/services/inbox.js` — inbox capture and management
- [x] Create `lib/services/trash.js` — trash operations
- [x] Create `lib/services/sync.js` — sync status and trigger
- [x] Create `lib/services/journal.js` — worklog CRUD and reports
- [x] Create `lib/services/actions.js` — actions CRUD
- [x] Create `lib/services/links.js` — links CRUD
- [x] Create `lib/services/activity.js` — activity feed
- [x] Create `lib/services/nodes.js` — tree, node CRUD, system views
- [x] Create `lib/services/suggestions.js` — worklog suggestions
- [x] Create `lib/services/today.js` — today dashboard
- [x] Create `lib/services/browserEvents.js` — browser extension events
- [x] Create `lib/services/search.js``searchNodes`
- [x] Create `lib/services/inbox.js``listInbox`, `captureClipboard`, etc.
- [x] Create `lib/services/trash.js``loadTrash`, `restore`, `purge`
- [x] Create `lib/services/sync.js``loadSyncStatus`, `runSync`
- [x] Create `lib/services/journal.js``loadJournal`, `worklog CRUD`
- [x] Create `lib/services/actions.js``listActions`, `createAction`, `deleteAction`
- [x] Create `lib/services/links.js``listLinks`, `updateLink`, `deleteLink`
- [x] Create `lib/services/activity.js``loadActivityFeed`, `loadCaseActivity`
## Phase 3: State Extraction
- [ ] Create `lib/state/navigation.js``selectedSection`, `selectedNode`, `activeTab`, `navHistory`
- [ ] Create `lib/state/uiState.js` — modals state, confirm state, rename state, drag state
## Phase 4: Component Extraction — Layout
- [ ] Extract `Sidebar.svelte` — brand, nav items, workspace tree, footer
- [ ] Extract `MainWorkspace.svelte` — content routing
- [ ] Create `AppShell.svelte` — root layout wrapper
## Phase 5: Component Extraction — Tab Content
- [x] Extract `OverviewTab.svelte`
- [x] Extract `NotesTab.svelte`
- [x] Extract `FilesTab.svelte`
- [ ] Extract `LinksTab.svelte`
- [ ] Extract `ActionsTab.svelte`
- [ ] Extract `WorklogTab.svelte`
- [ ] Extract `ActivityTabContent.svelte`
- [ ] Extract `InboxContent.svelte`
- [ ] Extract `InboxFullScreen.svelte`
- [ ] Extract `TrashContent.svelte`
- [ ] Extract `JournalScreen.svelte`
- [ ] Extract `ActivityFeedScreen.svelte`
- [ ] Extract `WelcomeScreen.svelte`
## Phase 6: Component Extraction — Modals
- [ ] Extract `CreateNodeModal.svelte`
- [ ] Extract `WorklogModal.svelte`
- [ ] Extract `CreateActionModal.svelte`
- [ ] Extract `ImportModal.svelte`
- [ ] Extract `RenameModal.svelte`
- [ ] Extract `AssignInboxModal.svelte`
- [ ] Extract `EditLinkModal.svelte`
- [ ] Extract `ContextMenu.svelte`
## Phase 7: Extract Inline Components
- [ ] Extract `NoteEditorHeader.svelte` (note editor header with rename)
- [ ] Extract `ErrorBanner.svelte`
- [ ] Extract `CaptureDropOverlay.svelte`
- [ ] Extract `SidebarFooter.svelte`
## Phase 8: Verification
- [ ] `npm run build` passes
- [ ] `go test ./...` passes
- [ ] Smoke checklist:
1. Sidebar renders with system views
2. Workspace tree loads and is expandable
3. Selecting a node shows tabs
4. Overview tab shows metadata and quick actions
5. Notes tab — create, rename, delete notes
6. Note editor — edit, preview, save, internal links, external links
7. Files tab — browse, add file/folder, navigate breadcrumbs
8. File preview — open, close
9. Inbox — list, sort, group, assign, delete
10. Trash — browse, restore, purge
11. Journal — filter, export, worklog CRUD
12. Activity feed — load and open events
13. Today screen — dashboard, suggestions, browser events
14. Settings — open/close, sections
15. Context menu on workspace tree
16. Create node modal — templates

View File

@ -18,14 +18,6 @@
import { t } from './lib/i18n' import { t } from './lib/i18n'
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte' import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte' import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
import ErrorBanner from './lib/components/ErrorBanner.svelte'
import CaptureDropOverlay from './lib/components/CaptureDropOverlay.svelte'
import OverviewTab from './lib/components/OverviewTab.svelte'
import NotesTab from './lib/components/notes/NotesTab.svelte'
import FilesTab from './lib/components/files/FilesTab.svelte'
// Component refs
let filesTabRef = null
// ===== Wails v2 API call helper ===== // ===== Wails v2 API call helper =====
function wailsCall(method, ...args) { function wailsCall(method, ...args) {
@ -84,19 +76,6 @@
let selectedSection = '' let selectedSection = ''
let selectedNode = null let selectedNode = null
let activeTab = 'overview' let activeTab = 'overview'
// Trash preview state (kept in App.svelte because trash lives outside FilesTab)
let previewItem = null
let previewContent = ''
let previewLoading = false
let previewError = ''
// Capture/drag-drop overlay state (kept in App.svelte; used by inbox capture flow)
let captureDropActive = false
let captureDropLabel = ''
let captureDragDepth = 0
let lastCaptureDragOverAt = 0
let captureDragResetTimer = null
let dropRootValid = false
let inboxDropValid = false
let notes = [] let notes = []
let noteEditor = null let noteEditor = null
let noteEditorPanel = undefined; // bind:this ref for NoteEditorPanel let noteEditorPanel = undefined; // bind:this ref for NoteEditorPanel
@ -179,6 +158,33 @@
{ id: 'launch_app', label: t('action.launchApp') }, { id: 'launch_app', label: t('action.launchApp') },
] ]
let loading = true let loading = true
let importing = false
let importSummary = null
let showImportDialog = false
let pendingImportPath = ''
let pendingImportParent = ''
let pendingImportMode = 'copy'
let treeItems = []
let expanded = {}
let childrenMap = {}
let loadingFiles = false
let currentFolderId = null
let folderStack = []
let fileItems = []
let previewItem = null
let previewContent = ''
let previewLoading = false
let previewError = ''
let clipboard = { items: [], mode: 'copy' }
let selectedIds = []
let dragIds = []
let dropRootValid = false
let inboxDropValid = false
let captureDropActive = false
let captureDropLabel = ''
let captureDragDepth = 0
let lastCaptureDragOverAt = 0
let captureDragResetTimer = null
let showConfirm = false let showConfirm = false
let confirmTitle = '' let confirmTitle = ''
@ -296,10 +302,12 @@
function closeTopModalForBack() { function closeTopModalForBack() {
if (showConfirm) { closeConfirm(); return true } if (showConfirm) { closeConfirm(); return true }
if (showSettings) { closeSettings(); return true } if (showSettings) { closeSettings(); return true }
if (previewItem) { closePreview(); return true }
if (assignInboxItem) { closeAssignInbox(); return true } if (assignInboxItem) { closeAssignInbox(); return true }
if (editingLink) { closeEditLink(); return true } if (editingLink) { closeEditLink(); return true }
if (showRename) { showRename = false; return true } if (showRename) { showRename = false; return true }
if (showWorklogModal) { closeWorklogModal(); return true } if (showWorklogModal) { closeWorklogModal(); return true }
if (showImportDialog) { cancelImport(); return true }
if (showCreateAction) { cancelCreateAction(); return true } if (showCreateAction) { cancelCreateAction(); return true }
if (showCreateNote) { cancelCreateNote(); return true } if (showCreateNote) { cancelCreateNote(); return true }
if (showCreateNode) { cancelCreateNode(); return true } if (showCreateNode) { cancelCreateNode(); return true }
@ -318,6 +326,9 @@
} }
await selectNode(node) await selectNode(node)
activeTab = snapshot.tab || 'overview' activeTab = snapshot.tab || 'overview'
if (activeTab === 'files' && fileItems.length === 0) {
await loadFolder(node.id)
}
return true return true
} else if (snapshot.section) { } else if (snapshot.section) {
if (!systemViews.some(view => view.id === snapshot.section)) { if (!systemViews.some(view => view.id === snapshot.section)) {
@ -362,6 +373,7 @@
if (activeTab === tabId) return if (activeTab === tabId) return
rememberNavigation() rememberNavigation()
activeTab = tabId activeTab = tabId
if (tabId === 'files' && selectedNode && fileItems.length === 0 && !currentFolderId) loadFolder(selectedNode.id)
} }
// ===== Lifecycle ===== // ===== Lifecycle =====
@ -509,13 +521,19 @@
actions = [] actions = []
worklog = [] worklog = []
suggestions = [] suggestions = []
fileItems = []
folderStack = []
currentFolderId = null
previewItem = null
previewContent = ''
selectedIds = []
dragIds = []
resetTrashBrowser() resetTrashBrowser()
noteEditor = null noteEditor = null
showCreateNode = false showCreateNode = false
showCreateNote = false showCreateNote = false
error = '' error = ''
caseActivity = [] caseActivity = []
if (filesTabRef) filesTabRef.resetToNode(node.id)
await loadTabData(node.id) await loadTabData(node.id)
} }
@ -533,6 +551,291 @@
try { caseActivity = await wailsCall('ListActivityByNode', nodeID, 50, 0) || [] } catch(e) {} try { caseActivity = await wailsCall('ListActivityByNode', nodeID, 50, 0) || [] } catch(e) {}
} }
async function loadTree(nodeID) {
loadingFiles = true
try {
treeItems = await wailsCall('ListItems', nodeID) || []
} catch (e) {
treeItems = []
}
loadingFiles = false
}
// ===== Folder navigation =====
async function loadFolder(folderId) {
loadingFiles = true
try {
let items = await wailsCall('ListItems', folderId) || []
items.sort((a, b) => {
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
return (a.name || '').localeCompare(b.name || '')
})
fileItems = items
} catch (e) {
fileItems = []
}
loadingFiles = false
}
async function navigateToFolder(folderId) {
if (!selectedNode) return
rememberNavigation()
try {
const node = await wailsCall('GetNodeDetail', folderId)
if (node) {
folderStack = [...folderStack, { id: folderId, name: node.title }]
}
} catch (e) {
folderStack = [...folderStack, { id: folderId, name: '...' }]
}
currentFolderId = folderId
expanded = { ...expanded, [folderId]: true }
const children = await wailsCall('ListWorkspaceChildren', folderId) || []
setNodeChildren(workspaceTree, folderId, children)
workspaceTree = [...workspaceTree]
await loadFolder(folderId)
}
function navigateBack() {
rememberNavigation()
if (folderStack.length < 2) {
// Go back to root
folderStack = []
currentFolderId = null
loadFolder(selectedNode.id)
} else {
const target = folderStack[folderStack.length - 2]
folderStack = folderStack.slice(0, -1)
currentFolderId = target.id
loadFolder(target.id)
}
}
function navigateToBreadcrumb(index) {
const target = folderStack[index]
folderStack = folderStack.slice(0, index + 1)
currentFolderId = target.id
loadFolder(target.id)
}
// ===== File preview =====
async function openPreview(item) {
// For .md files: check if linked to a note, open note editor instead of preview modal
if (item && item.fileId && isMarkdownFile(item)) {
try {
const action = await wailsCall('CheckFileAction', item.fileId)
if (action.action === 'note') {
await openNote({ id: action.noteId, title: action.noteTitle })
return
}
if (action.action === 'external') {
await wailsCall('OpenFile', item.fileId)
return
}
// 'preview' → fall through to normal preview
} catch (e) {
console.warn('CheckFileAction failed, falling back to preview:', e)
}
}
previewItem = item
previewContent = ''
previewError = ''
previewLoading = true
try {
if (needsBase64Preview(item)) {
previewContent = await wailsCall('GetFileBase64', item.fileId) || ''
} else if (needsTextPreview(item)) {
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
}
} catch (e) {
previewError = String(e)
}
previewLoading = false
}
function closePreview() {
previewItem = null
previewContent = ''
previewError = ''
}
// ===== File operations =====
async function createFile() {
const name = prompt(t('file.namePrompt'))
if (!name || !name.trim()) return
try {
const parentId = currentFolderId || selectedNode.id
await wailsCall('CreateEmptyFile', parentId, name.trim())
await loadFolder(parentId)
await refreshParentNode(parentId)
} catch (e) {
error = String(e)
}
}
async function duplicateItem(id) {
try {
await wailsCall('DuplicateNode', id)
const parentId = currentFolderId || selectedNode.id
await loadFolder(parentId)
await refreshParentNode(parentId)
} catch (e) {
error = String(e)
}
}
function renameItem(id) {
const item = fileItems.find(x => x.id === id)
if (item) openRename(item.id, item.name)
}
function cutItem(id) {
clipboard = { items: [id], mode: 'cut' }
}
function copyItem(id) {
clipboard = { items: [id], mode: 'copy' }
}
async function pasteItem() {
if (clipboard.items.length === 0) return
const targetId = currentFolderId || selectedNode.id
try {
if (clipboard.mode === 'copy') {
for (const id of clipboard.items) {
await wailsCall('DuplicateNode', id)
}
} else {
for (const id of clipboard.items) {
await wailsCall('MoveNode', id, targetId)
}
}
clipboard = { items: [], mode: 'copy' }
await loadFolder(targetId)
} catch (e) {
error = String(e)
}
}
// ===== Selection =====
function toggleSelection(id) {
if (selectedIds.includes(id)) {
selectedIds = selectedIds.filter(x => x !== id)
} else {
selectedIds = [...selectedIds, id]
}
}
function selectOne(id) {
selectedIds = [id]
}
function selectAll() {
selectedIds = fileItems.map(x => x.id)
}
function rangeSelect(id) {
if (fileItems.length === 0) return
const lastId = selectedIds.length > 0 ? selectedIds[selectedIds.length - 1] : fileItems[0].id
const lastIdx = fileItems.findIndex(x => x.id === lastId)
const curIdx = fileItems.findIndex(x => x.id === id)
if (lastIdx === -1 || curIdx === -1) return
const start = Math.min(lastIdx, curIdx)
const end = Math.max(lastIdx, curIdx)
const range = fileItems.slice(start, end + 1).map(x => x.id)
const set = new Set(selectedIds)
range.forEach(x => set.add(x))
selectedIds = [...set]
}
function clearSelection() {
selectedIds = []
}
function getTargetIds(ids) {
return ids.length > 0 ? ids : fileItems.map(x => x.id)
}
async function deleteSelected() {
const ids = getTargetIds(selectedIds)
const item = fileItems.find(x => x.id === ids[0])
let label
if (ids.length === 1 && item?.type === 'folder') {
label = t('delete.folder')
} else if (ids.length === 1) {
label = t('delete.file')
} else {
label = t('delete.files', { count: ids.length })
}
openConfirm({
title: t('delete.confirmTitle'),
message: t('delete.confirmMessage') + ' ' + label + '?',
confirmText: t('common.delete'),
danger: true,
onConfirm: async () => {
for (const id of ids) {
try {
await wailsCall('DeleteFileOrFolder', id)
} catch (e) { error = String(e) }
}
selectedIds = []
const reloadId = currentFolderId || selectedNode.id
await loadFolder(reloadId)
}
})
}
function cutSelected() {
const ids = getTargetIds(selectedIds)
clipboard = { items: ids, mode: 'cut' }
selectedIds = []
}
function copySelected() {
const ids = getTargetIds(selectedIds)
clipboard = { items: ids, mode: 'copy' }
selectedIds = []
}
// ===== Drag-and-drop =====
function onDragStart(e, id) {
e.stopPropagation()
const ids = selectedIds.includes(id) ? selectedIds : [id]
dragIds = ids
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', ids.join(','))
}
function onDragOver(e, folderId) {
const item = fileItems.find(x => x.id === folderId)
if (item && item.type === 'folder') {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
}
}
async function onDrop(e, folderId) {
e.preventDefault()
e.stopPropagation()
if (dragIds.length === 0) return
for (const id of dragIds) {
try {
await wailsCall('MoveNode', id, folderId)
} catch (e) { error = String(e) }
}
dragIds = []
selectedIds = []
await loadFolder(currentFolderId || selectedNode.id)
}
// ===== Keyboard ===== // ===== Keyboard =====
function isEditableTarget(target) { function isEditableTarget(target) {
@ -556,9 +859,47 @@
if (activeTab !== 'files') return if (activeTab !== 'files') return
if (filesTabRef) { if (e.ctrlKey || e.metaKey) {
filesTabRef.filesHandleKeydown(e) if (e.key === 'c' || e.key === 'C') { e.preventDefault(); copySelected() }
return else if (e.key === 'x' || e.key === 'X') { e.preventDefault(); cutSelected() }
else if (e.key === 'v' || e.key === 'V') { e.preventDefault(); pasteItem() }
else if (e.key === 'a' || e.key === 'A') { e.preventDefault(); selectAll() }
else if (e.key === 'o' || e.key === 'O') { e.preventDefault(); openSelectedExternal() }
else if (e.key === 'Enter') { e.preventDefault(); openSelected() }
} else if (e.key === 'Enter') {
e.preventDefault()
openSelected()
} else if (e.key === 'Delete') {
if (previewItem) { e.preventDefault(); closePreview(); return }
if (selectedIds.length > 0) { e.preventDefault(); deleteSelected(); return }
} else if (e.key === 'Escape') {
if (previewItem) { closePreview(); return }
if (selectedIds.length > 0) { clearSelection(); return }
} else if (e.key === 'F2') {
e.preventDefault()
openRenameForSelection()
}
}
function openSelected() {
if (selectedIds.length === 1) {
const item = fileItems.find(x => x.id === selectedIds[0])
if (item) {
if (item.type === 'folder') {
navigateToFolder(item.id)
} else {
openPreview(item)
}
}
}
}
function openSelectedExternal() {
if (selectedIds.length === 1) {
const item = fileItems.find(x => x.id === selectedIds[0])
if (item && item.fileId) {
wailsCall('OpenFile', item.fileId)
}
} }
} }
@ -1638,7 +1979,80 @@
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
// ===== Drag-and-drop ===== // ===== Files =====
async function addFile() {
const path = await wailsCall('PickFile')
if (!path) return
const parentId = currentFolderId || selectedNode.id
await startImport(parentId, path)
}
async function addFolder() {
const path = await wailsCall('PickDirectory')
if (!path) return
const parentId = currentFolderId || selectedNode.id
await startImport(parentId, path)
}
async function startImport(parentID, sourcePath) {
importing = true
try {
const summary = await wailsCall('PreviewImport', sourcePath)
importSummary = summary
pendingImportPath = sourcePath
pendingImportParent = parentID
showImportDialog = true
} catch (e) {
error = String(e)
}
importing = false
}
async function confirmImport(mode) {
try {
const parentId = pendingImportParent || selectedNode.id
const result = mode === 'copy'
? await wailsCall('AddPathCopy', parentId, pendingImportPath)
: await wailsCall('AddPathLink', parentId, pendingImportPath)
showImportDialog = false
importSummary = null
folderStack = []
currentFolderId = null
await Promise.all([
loadTabData(parentId),
loadFolder(parentId),
refreshParentNode(parentId),
])
} catch (e) {
error = String(e)
}
}
function cancelImport() {
showImportDialog = false
importSummary = null
}
async function deleteFile({ id, type }) {
const label = type === 'folder' ? t('delete.folder') : t('delete.file')
openConfirm({
title: t('delete.confirmTitle'),
message: t('delete.confirmMessage') + ' ' + label + '?',
confirmText: t('common.delete'),
danger: true,
onConfirm: async () => {
try {
await wailsCall('DeleteFileOrFolder', id)
files = files.filter(f => f.nodeId !== id)
const reloadId = currentFolderId || selectedNode.id
await loadFolder(reloadId)
} catch (e) {
error = String(e)
}
}
})
}
async function openSelectedFile(fileID) { async function openSelectedFile(fileID) {
try { try {
await wailsCall('OpenFile', fileID) await wailsCall('OpenFile', fileID)
@ -2431,7 +2845,11 @@
<VaultRecovery vaultPath={startupStatus?.vaultPath || ''} onComplete={onRecoveryComplete} /> <VaultRecovery vaultPath={startupStatus?.vaultPath || ''} onComplete={onRecoveryComplete} />
{:else} {:else}
<div class="app"> <div class="app">
<CaptureDropOverlay show={captureDropActive} label={captureDropLabel} /> {#if captureDropActive}
<div class="capture-drop-overlay">
<div class="capture-drop-box">{captureDropLabel}</div>
</div>
{/if}
<!-- Sidebar --> <!-- Sidebar -->
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-brand"> <div class="sidebar-brand">
@ -2522,7 +2940,16 @@
</div> </div>
</AppHeader> </AppHeader>
<ErrorBanner {error} onDismiss={() => error = ''} /> {#if error}
<div class="error-banner" role="button" tabindex="0" on:click={() => error = ''} on:keydown={onKeyActivate(() => error = '')}>
{translateError(error)}
<button class="dismiss-btn" on:click|stopPropagation={() => error = ''} aria-label="Dismiss">
<svg width="14" height="14" 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>
{/if}
{#if noteEditor} {#if noteEditor}
<!-- Note editor with markdown preview --> <!-- Note editor with markdown preview -->
@ -2577,42 +3004,172 @@
</div> </div>
<div class="tab-content"> <div class="tab-content">
{#if activeTab === 'overview'} {#if activeTab === 'overview'}
<OverviewTab <div class="overview">
node={selectedNode} <h2>{selectedNode.title}</h2>
{notes} <div class="meta-grid">
{worklog} <div class="meta-item"><span class="meta-label">{t('overview.type')}</span><span>{nodeKindLabel(selectedNode.type)}</span></div>
{nodeKindLabel} <div class="meta-item"><span class="meta-label">{t('overview.section')}</span><span>{selectedNode.section || '—'}</span></div>
{formatDate} <div class="meta-item"><span class="meta-label">{t('overview.created')}</span><span>{formatDate(selectedNode.createdAt)}</span></div>
on:goTab={(e) => setActiveTab(e.detail)} </div>
on:openNote={(e) => openNote(e.detail.note)} <div class="quick-actions">
on:createNote={() => { setActiveTab('notes'); openCreateNote() }} <button class="qa-btn" on:click={() => { setActiveTab('notes'); openCreateNote() }}>
on:addFile={() => { setActiveTab('files'); addFile() }} <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="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
on:createAction={openCreateAction} {t('overview.newNote')}
/> </button>
<button class="qa-btn" on:click={() => { setActiveTab('files'); addFile() }}>
<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="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
{t('overview.addFile')}
</button>
<button class="qa-btn" on:click={openCreateAction}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
{t('overview.addAction')}
</button>
<button class="qa-btn" on:click={() => setActiveTab('worklog')}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
{t('overview.logTime')}
</button>
</div>
{#if notes.length > 0}
<div class="recent-section">
<h3>{t('overview.recentNotes')}</h3>
{#each notes.slice(0, 5) as note}
<div class="recent-note" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span>
</div>
{/each}
</div>
{/if}
{#if worklog.length > 0}
<div class="recent-section">
<h3>{t('overview.recentEntries')}</h3>
{#each worklog.slice(0, 3) as e}
<div class="recent-entry">{e.summary} ({e.minutes} {t('worklog.min')})</div>
{/each}
</div>
{/if}
</div>
{:else if activeTab === 'notes'} {:else if activeTab === 'notes'}
<NotesTab <div class="notes-tab">
{notes} <div class="tab-toolbar">
showCreateNote={showCreateNote} <button class="btn btn-primary" on:click={openCreateNote}>{t('note.add')}</button>
{formatDate} </div>
on:createNote={openCreateNote} {#if showCreateNote}
on:submitCreateNote={(e) => { newNoteTitle = e.detail.title; submitCreateNote() }} <div class="create-form">
on:cancelCreateNote={cancelCreateNote} <input type="text" placeholder={t('note.title')} bind:value={newNoteTitle}
on:openNote={(e) => openNote(e.detail.note)} on:keydown={(e) => e.key === 'Enter' && submitCreateNote()} />
on:startRename={(e) => startRenameNote(e.detail.note.id, e.detail.note.title)} <div class="form-actions">
on:delete={(e) => deleteNote(e.detail.note)} <button class="btn btn-primary" on:click={submitCreateNote}>{t('common.create')}</button>
/> <button class="btn" on:click={cancelCreateNote}>{t('common.cancel')}</button>
</div>
</div>
{/if}
{#if notes.length === 0 && !showCreateNote}
<div class="empty-state"><p>{t('note.noNotes')}</p><p class="hint">{t('note.createFirst')}</p></div>
{:else}
<div class="notes-list">
{#each notes as note}
<div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<div class="note-card-info">
<div class="note-card-title">{note.title}</div>
<div class="note-card-date">{formatDate(note.createdAt)}</div>
</div>
<div class="note-card-actions" on:click|stopPropagation>
<button class="note-action-btn" on:click={() => startRenameNote(note.id, note.title)} title={t('common.rename')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
</button>
<button class="note-action-btn note-action-danger" on:click={() => deleteNote(note)} title={t('common.delete')}>
<svg width="12" height="12" 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>
{/each}
</div>
{/if}
</div>
{:else if activeTab === 'files'} {:else if activeTab === 'files'}
<FilesTab <!-- External file drops are handled by the unified capture flow and land in the selected case inbox. -->
bind:this={filesTabRef} <div class="files-tab">
{selectedNode} <div class="tab-toolbar">
on:openNote={(e) => openNote(e.detail.note)} <button class="btn btn-primary" on:click={addFile} disabled={importing}>{t('file.addFile')}</button>
on:refreshParent={(e) => refreshParentNode(e.detail.nodeId)} <button class="btn" on:click={addFolder} disabled={importing}>{t('file.addFolder')}</button>
on:error={(e) => { error = e.detail.message }} <button class="btn" on:click={createFile}>{t('file.newFile')}</button>
on:rename={(e) => openRename(e.detail.id, e.detail.name)} {#if clipboard.items.length > 0}
on:confirm={(e) => openConfirm(e.detail)} <button class="btn" on:click={pasteItem}>{t('common.paste')} {clipboard.items.length}</button>
/> {/if}
</div>
{#if loadingFiles}
<div class="empty-state">
<p>{t('common.loading')}</p>
</div>
{:else}
{#if folderStack.length > 0}
<FileBreadcrumbs crumbs={[{ name: t('file.root') }, ...folderStack]} on:navigate={(e) => {
const i = e.detail
if (i === 0) {
folderStack = []
currentFolderId = null
loadFolder(selectedNode.id)
} else {
navigateToBreadcrumb(i - 1)
}
}}/>
<button class="btn btn-sm back-btn" on:click={navigateBack}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
{t('common.backLabel')}
</button>
{:else}
<FileBreadcrumbs crumbs={[{ name: t('file.root') }]}/>
{/if}
{#if fileItems.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<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"/>
</svg>
</div>
<p>{folderStack.length > 0 ? t('file.noFiles') : t('file.noFilesCase')}</p>
<p class="hint">{t('file.hint')}</p>
<div class="empty-actions">
<button class="btn btn-primary" on:click={addFile}>{t('file.addFileSimple')}</button>
<button class="btn" on:click={addFolder}>{t('file.addFolderSimple')}</button>
</div>
</div>
{:else}
<div class="file-list">
{#each fileItems as item (item.id)}
<FileTreeRow
{item}
selected={selectedIds.includes(item.id)}
{onDragStart}
{onDragOver}
{onDrop}
on:navigate={(e) => navigateToFolder(e.detail)}
on:preview={(e) => openPreview(e.detail)}
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
on:delete={(e) => deleteFile(e.detail)}
on:rename={(e) => renameItem(e.detail.id)}
on:duplicate={(e) => duplicateItem(e.detail)}
on:cut={(e) => cutItem(e.detail)}
on:copy={(e) => copyItem(e.detail)}
on:selectOne={(e) => selectOne(e.detail)}
on:toggleSelect={(e) => toggleSelection(e.detail)}
on:rangeSelect={(e) => rangeSelect(e.detail)}
/>
{/each}
</div>
{/if}
{/if}
{#if importing && !showImportDialog}
<div class="empty-state"><p>{t('file.scanning')}</p></div>
{/if}
</div>
{:else if activeTab === 'inbox'} {:else if activeTab === 'inbox'}
<div class="inbox-tab"> <div class="inbox-tab">
@ -3478,6 +4035,30 @@
</div> </div>
{/if} {/if}
{#if showImportDialog && importSummary}
<div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelImport} on:keydown={onKeyActivate(cancelImport)}>
<div class="modal">
<h3>{t('file.importTitle')} «{selectedNode ? selectedNode.title : ''}»</h3>
<div class="import-summary">
<div class="summary-row"><span>{t('file.importFiles')}</span><span>{importSummary.files}</span></div>
<div class="summary-row"><span>{t('file.importFolders')}</span><span>{importSummary.folders}</span></div>
<div class="summary-row"><span>{t('file.importSize')}</span><span>{(importSummary.totalBytes / 1024).toFixed(1)} KB</span></div>
{#if importSummary.isDangerous}
<div class="summary-warn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
{importSummary.dangerReason}
</div>
{/if}
</div>
<div class="modal-actions">
<button class="btn btn-primary" on:click={() => confirmImport('copy')}>{t('file.importCopy')}</button>
<button class="btn" on:click={() => confirmImport('link')}>{t('file.importLink')}</button>
<button class="btn" on:click={cancelImport}>{t('common.cancel')}</button>
</div>
</div>
</div>
{/if}
{#if showRename} {#if showRename}
<div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelRename} on:keydown={onKeyActivate(cancelRename)}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelRename} on:keydown={onKeyActivate(cancelRename)}>
<div class="modal"> <div class="modal">

View File

@ -1,15 +0,0 @@
<script>
export let show = false
export let label = ''
</script>
{#if show}
<div class="capture-drop-overlay">
<div class="capture-drop-box">{label}</div>
</div>
{/if}
<style>
.capture-drop-overlay { position: fixed; inset: 0; z-index: 120; pointer-events: none; display: flex; align-items: center; justify-content: center; background: rgba(19, 19, 31, 0.42); border: 2px dashed #818cf8; }
.capture-drop-box { max-width: min(520px, calc(100vw - 48px)); padding: 14px 18px; border: 1px solid #3a3a5c; border-radius: 8px; background: #1a1a28; color: #e4e4ef; font-size: 14px; font-weight: 600; box-shadow: 0 12px 32px rgba(0,0,0,0.35); text-align: center; }
</style>

View File

@ -1,37 +0,0 @@
<script>
import { t } from '../i18n'
export let error = ''
export let onDismiss = () => {}
function translateError(msg) {
const map = {
'vault not open': t('error.vaultNotOpen'),
}
return map[msg] || msg
}
function handleKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onDismiss()
}
}
</script>
{#if error}
<div class="error-banner" role="button" tabindex="0" on:click={onDismiss} on:keydown={handleKeydown}>
{translateError(error)}
<button class="dismiss-btn" on:click|stopPropagation={onDismiss} aria-label="Dismiss">
<svg width="14" height="14" 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>
{/if}
<style>
.error-banner { background: #3a2222; color: #ff8888; padding: 8px 24px; font-size: 12px; border-bottom: 1px solid #4a2222; flex-shrink: 0; cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
.dismiss-btn { background: none; border: none; color: #ff6666; cursor: pointer; padding: 2px; display: flex; align-items: center; border-radius: 2px; }
.dismiss-btn:hover { color: #ff4444; }
</style>

View File

@ -1,98 +0,0 @@
<script>
import { t } from '../i18n'
import { actionIcon } from '../actionIcons'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let node = null
export let notes = []
export let worklog = []
export let nodeKindLabel = (type) => type || ''
export let formatDate = (d) => d || ''
function goNotesCreate() {
dispatch('goTab', 'notes')
dispatch('createNote')
}
function goFilesAdd() {
dispatch('goTab', 'files')
dispatch('addFile')
}
function onKeyActivate(fn) {
return (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
fn()
}
}
}
</script>
<div class="overview">
<h2 class="node-title">{node.title}</h2>
<div class="meta-grid">
<div class="meta-item"><span class="meta-label">{t('overview.type')}</span><span>{nodeKindLabel(node.type)}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.section')}</span><span>{node.section || '—'}</span></div>
<div class="meta-item"><span class="meta-label">{t('overview.created')}</span><span>{formatDate(node.createdAt)}</span></div>
</div>
<div class="quick-actions">
<button class="qa-btn" on:click={goNotesCreate}>
<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="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
{t('overview.newNote')}
</button>
<button class="qa-btn" on:click={goFilesAdd}>
<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="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
{t('overview.addFile')}
</button>
<button class="qa-btn" on:click={() => dispatch('createAction')}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
{t('overview.addAction')}
</button>
<button class="qa-btn" on:click={() => dispatch('goTab', 'worklog')}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
{t('overview.logTime')}
</button>
</div>
{#if notes.length > 0}
<div class="recent-section">
<h3>{t('overview.recentNotes')}</h3>
{#each notes.slice(0, 5) as note}
<div class="recent-note" role="button" tabindex="0" on:click={() => dispatch('openNote', { note })} on:keydown={onKeyActivate(() => dispatch('openNote', { note }))}>
<span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span>
</div>
{/each}
</div>
{/if}
{#if worklog.length > 0}
<div class="recent-section">
<h3>{t('overview.recentEntries')}</h3>
{#each worklog.slice(0, 3) as e}
<div class="recent-entry">{e.summary} ({e.minutes} {t('worklog.min')})</div>
{/each}
</div>
{/if}
</div>
<style>
.overview { padding: 24px; }
.node-title { font-size: 24px; margin-bottom: 16px; color: #e4e4ef; }
.meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
.meta-item { background: #1a1a28; padding: 12px 16px; border-radius: 8px; }
.meta-label { display: block; font-size: 11px; color: #666; margin-bottom: 4px; text-transform: uppercase; }
.quick-actions { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.qa-btn { padding: 10px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
.qa-btn:hover { background: #222233; }
.qa-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.recent-section { margin-bottom: 24px; }
.recent-section h3 { font-size: 13px; color: #666; text-transform: uppercase; margin-bottom: 8px; }
.recent-note { padding: 8px 12px; border-radius: 6px; cursor: pointer; display: flex; justify-content: space-between; }
.recent-note:hover { background: #1a1a28; }
.recent-date { font-size: 11px; color: #555; }
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
</style>

View File

@ -1,616 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte'
import { t } from '../../i18n'
import { isMarkdownFile } from '../../fileUtils.js'
import FileTreeRow from '../../../FileTreeRow.svelte'
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
import FilePreviewModal from '../../FilePreviewModal.svelte'
const dispatch = createEventDispatcher()
// Props
export let selectedNode = null
// Internal state
let loadingFiles = false
let currentFolderId = null
let folderStack = []
let fileItems = []
let previewItem = null
let previewContent = ''
let previewLoading = false
let previewError = ''
let clipboard = { items: [], mode: 'copy' }
let selectedIds = []
let dragIds = []
let importing = false
let importSummary = null
let showImportDialog = false
let pendingImportPath = ''
let pendingImportParent = ''
// ===== Wails call helper =====
function wailsCall(method, ...args) {
try {
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
const fn = window['go']['main']['App'][method]
if (typeof fn === 'function') {
return fn(...args)
}
}
} catch (e) {
console.error('Wails call error:', method, e)
}
return Promise.reject(new Error('Wails not connected: ' + method))
}
// ===== Public API for keyboard handler =====
export function filesHandleKeydown(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'c' || e.key === 'C') { e.preventDefault(); copySelected() }
else if (e.key === 'x' || e.key === 'X') { e.preventDefault(); cutSelected() }
else if (e.key === 'v' || e.key === 'V') { e.preventDefault(); pasteItem() }
else if (e.key === 'a' || e.key === 'A') { e.preventDefault(); selectAll() }
else if (e.key === 'o' || e.key === 'O') { e.preventDefault(); openSelectedExternal() }
else if (e.key === 'Enter') { e.preventDefault(); openSelected() }
} else if (e.key === 'Enter') {
e.preventDefault()
openSelected()
} else if (e.key === 'Delete') {
if (previewItem) { e.preventDefault(); closePreview(); return }
if (selectedIds.length > 0) { e.preventDefault(); deleteSelected(); return }
} else if (e.key === 'Escape') {
if (previewItem) { closePreview(); return }
if (selectedIds.length > 0) { clearSelection(); return }
} else if (e.key === 'F2') {
e.preventDefault()
openRenameForSelection()
}
}
// ===== Folder navigation =====
export async function loadFolder(folderId) {
loadingFiles = true
try {
let items = await wailsCall('ListItems', folderId) || []
items.sort((a, b) => {
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
return (a.name || '').localeCompare(b.name || '')
})
fileItems = items
} catch (e) {
fileItems = []
}
loadingFiles = false
}
async function navigateToFolder(folderId) {
if (!selectedNode) return
try {
const node = await wailsCall('GetNodeDetail', folderId)
if (node) {
folderStack = [...folderStack, { id: folderId, name: node.title }]
}
} catch (e) {
folderStack = [...folderStack, { id: folderId, name: '...' }]
}
currentFolderId = folderId
await loadFolder(folderId)
}
function navigateBack() {
if (folderStack.length < 2) {
folderStack = []
currentFolderId = null
if (selectedNode) loadFolder(selectedNode.id)
} else {
const target = folderStack[folderStack.length - 2]
folderStack = folderStack.slice(0, -1)
currentFolderId = target.id
loadFolder(target.id)
}
}
function navigateToBreadcrumb(index) {
const target = folderStack[index]
folderStack = folderStack.slice(0, index + 1)
currentFolderId = target.id
loadFolder(target.id)
}
// ===== File preview =====
async function openPreview(item) {
if (item && item.fileId && isMarkdownFile(item)) {
try {
const action = await wailsCall('CheckFileAction', item.fileId)
if (action.action === 'note') {
dispatch('openNote', { note: { id: action.noteId, title: action.noteTitle } })
return
}
if (action.action === 'external') {
await wailsCall('OpenFile', item.fileId)
return
}
} catch (e) {
console.warn('CheckFileAction failed, falling back to preview:', e)
}
}
previewItem = item
previewContent = ''
previewError = ''
previewLoading = true
try {
if (needsBase64Preview(item)) {
previewContent = await wailsCall('GetFileBase64', item.fileId) || ''
} else if (needsTextPreview(item)) {
previewContent = await wailsCall('ReadFileText', item.fileId) || ''
}
} catch (e) {
previewError = String(e)
}
previewLoading = false
}
function closePreview() {
previewItem = null
previewContent = ''
previewError = ''
}
// ===== Helpers for preview =====
function needsBase64Preview(item) {
if (!item) return false
const ext = (item.name || '').toLowerCase()
return ext.endsWith('.png') || ext.endsWith('.jpg') || ext.endsWith('.jpeg') ||
ext.endsWith('.gif') || ext.endsWith('.webp') || ext.endsWith('.svg') ||
ext.endsWith('.bmp') || ext.endsWith('.ico') || ext.endsWith('.pdf')
}
function needsTextPreview(item) {
if (!item) return false
const ext = (item.name || '').toLowerCase()
return ext.endsWith('.txt') || ext.endsWith('.log') || ext.endsWith('.json') ||
ext.endsWith('.xml') || ext.endsWith('.csv') || ext.endsWith('.md') ||
ext.endsWith('.js') || ext.endsWith('.ts') || ext.endsWith('.css') ||
ext.endsWith('.html') || ext.endsWith('.yaml') || ext.endsWith('.yml')
}
// ===== File operations =====
async function createFile() {
const name = prompt(t('file.namePrompt'))
if (!name || !name.trim()) return
try {
const parentId = currentFolderId || selectedNode.id
await wailsCall('CreateEmptyFile', parentId, name.trim())
await loadFolder(parentId)
dispatch('refreshParent', { nodeId: parentId })
} catch (e) {
dispatch('error', { message: String(e) })
}
}
async function duplicateItem(id) {
try {
await wailsCall('DuplicateNode', id)
const parentId = currentFolderId || selectedNode.id
await loadFolder(parentId)
dispatch('refreshParent', { nodeId: parentId })
} catch (e) {
dispatch('error', { message: String(e) })
}
}
function renameItem(id) {
const item = fileItems.find(x => x.id === id)
if (item) {
dispatch('rename', { id, name: item.name })
}
}
function cutItem(id) {
clipboard = { items: [id], mode: 'cut' }
}
function copyItem(id) {
clipboard = { items: [id], mode: 'copy' }
}
async function pasteItem() {
if (clipboard.items.length === 0) return
const targetId = currentFolderId || selectedNode.id
try {
if (clipboard.mode === 'copy') {
for (const id of clipboard.items) {
await wailsCall('DuplicateNode', id)
}
} else {
for (const id of clipboard.items) {
await wailsCall('MoveNode', id, targetId)
}
}
clipboard = { items: [], mode: 'copy' }
await loadFolder(targetId)
} catch (e) {
dispatch('error', { message: String(e) })
}
}
// ===== Selection =====
function getTargetIds(ids) {
return ids.length > 0 ? ids : fileItems.map(x => x.id)
}
function toggleSelection(id) {
if (selectedIds.includes(id)) {
selectedIds = selectedIds.filter(x => x !== id)
} else {
selectedIds = [...selectedIds, id]
}
}
function selectOne(id) {
selectedIds = [id]
}
function selectAll() {
selectedIds = fileItems.map(x => x.id)
}
function clearSelection() {
selectedIds = []
}
function rangeSelect(id) {
if (fileItems.length === 0) return
const lastId = selectedIds.length > 0 ? selectedIds[selectedIds.length - 1] : fileItems[0].id
const lastIdx = fileItems.findIndex(x => x.id === lastId)
const curIdx = fileItems.findIndex(x => x.id === id)
if (lastIdx === -1 || curIdx === -1) return
const start = Math.min(lastIdx, curIdx)
const end = Math.max(lastIdx, curIdx)
const range = fileItems.slice(start, end + 1).map(x => x.id)
const set = new Set(selectedIds)
range.forEach(x => set.add(x))
selectedIds = [...set]
}
function cutSelected() {
const ids = getTargetIds(selectedIds)
clipboard = { items: ids, mode: 'cut' }
selectedIds = []
}
function copySelected() {
const ids = getTargetIds(selectedIds)
clipboard = { items: ids, mode: 'copy' }
selectedIds = []
}
function openSelected() {
if (selectedIds.length === 1) {
const item = fileItems.find(x => x.id === selectedIds[0])
if (item) {
if (item.type === 'folder') {
navigateToFolder(item.id)
} else {
openPreview(item)
}
}
}
}
function openSelectedExternal() {
if (selectedIds.length === 1) {
const item = fileItems.find(x => x.id === selectedIds[0])
if (item && item.fileId) {
wailsCall('OpenFile', item.fileId)
}
}
}
function openRenameForSelection() {
if (selectedIds.length === 1) {
const item = fileItems.find(x => x.id === selectedIds[0])
if (item) {
dispatch('rename', { id: item.id, name: item.name })
}
}
}
async function deleteSelected() {
const ids = getTargetIds(selectedIds)
const item = fileItems.find(x => x.id === ids[0])
let label
if (ids.length === 1 && item?.type === 'folder') {
label = t('delete.folder')
} else if (ids.length === 1) {
label = t('delete.file')
} else {
label = t('delete.files', { count: ids.length })
}
dispatch('confirm', {
title: t('delete.confirmTitle'),
message: t('delete.confirmMessage') + ' ' + label + '?',
confirmText: t('common.delete'),
danger: true,
onConfirm: async () => {
for (const id of ids) {
try {
await wailsCall('DeleteFileOrFolder', id)
} catch (e) {
dispatch('error', { message: String(e) })
}
}
selectedIds = []
const reloadId = currentFolderId || selectedNode.id
await loadFolder(reloadId)
}
})
}
// ===== Drag-and-drop =====
function onDragStart(e, id) {
e.stopPropagation()
const ids = selectedIds.includes(id) ? selectedIds : [id]
dragIds = ids
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', ids.join(','))
}
function onDragOver(e, folderId) {
const item = fileItems.find(x => x.id === folderId)
if (item && item.type === 'folder') {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'move'
}
}
async function onDrop(e, folderId) {
e.preventDefault()
e.stopPropagation()
if (dragIds.length === 0) return
for (const id of dragIds) {
try {
await wailsCall('MoveNode', id, folderId)
} catch (e) {
dispatch('error', { message: String(e) })
}
}
dragIds = []
selectedIds = []
await loadFolder(currentFolderId || selectedNode.id)
}
// ===== Import =====
async function addFile() {
const path = await wailsCall('PickFile')
if (!path) return
const parentId = currentFolderId || selectedNode.id
await startImport(parentId, path)
}
async function addFolder() {
const path = await wailsCall('PickDirectory')
if (!path) return
const parentId = currentFolderId || selectedNode.id
await startImport(parentId, path)
}
async function startImport(parentID, sourcePath) {
importing = true
try {
const summary = await wailsCall('PreviewImport', sourcePath)
importSummary = summary
pendingImportPath = sourcePath
pendingImportParent = parentID
showImportDialog = true
} catch (e) {
dispatch('error', { message: String(e) })
}
importing = false
}
async function confirmImport(mode) {
try {
const parentId = pendingImportParent || selectedNode.id
const result = mode === 'copy'
? await wailsCall('AddPathCopy', parentId, pendingImportPath)
: await wailsCall('AddPathLink', parentId, pendingImportPath)
showImportDialog = false
importSummary = null
folderStack = []
currentFolderId = null
await loadFolder(parentId)
dispatch('refreshParent', { nodeId: parentId })
} catch (e) {
dispatch('error', { message: String(e) })
}
}
function cancelImport() {
showImportDialog = false
importSummary = null
}
async function deleteFile({ id, type }) {
const label = type === 'folder' ? t('delete.folder') : t('delete.file')
dispatch('confirm', {
title: t('delete.confirmTitle'),
message: t('delete.confirmMessage') + ' ' + label + '?',
confirmText: t('common.delete'),
danger: true,
onConfirm: async () => {
try {
await wailsCall('DeleteFileOrFolder', id)
const reloadId = currentFolderId || selectedNode.id
await loadFolder(reloadId)
} catch (e) {
dispatch('error', { message: String(e) })
}
}
})
}
// ===== Public API: called from parent when node changes =====
export function resetToNode(nodeId) {
folderStack = []
currentFolderId = null
selectedIds = []
dragIds = []
clipboard = { items: [], mode: 'copy' }
fileItems = []
loadingFiles = false
previewItem = null
previewContent = ''
previewError = ''
importing = false
showImportDialog = false
importSummary = null
if (nodeId) loadFolder(nodeId)
}
// Load folder when selectedNode changes
$: if (selectedNode && selectedNode.id && !currentFolderId && fileItems.length === 0 && !loadingFiles) {
loadFolder(selectedNode.id)
}
</script>
<div class="files-tab">
<div class="tab-toolbar">
<button class="btn btn-primary" on:click={addFile} disabled={importing}>{t('file.addFile')}</button>
<button class="btn" on:click={addFolder} disabled={importing}>{t('file.addFolder')}</button>
<button class="btn" on:click={createFile}>{t('file.newFile')}</button>
{#if clipboard.items.length > 0}
<button class="btn" on:click={pasteItem}>{t('common.paste')} {clipboard.items.length}</button>
{/if}
</div>
{#if loadingFiles}
<div class="empty-state">
<p>{t('common.loading')}</p>
</div>
{:else}
{#if folderStack.length > 0}
<FileBreadcrumbs crumbs={[{ name: t('file.root') }, ...folderStack]} on:navigate={(e) => {
const i = e.detail
if (i === 0) {
folderStack = []
currentFolderId = null
if (selectedNode) loadFolder(selectedNode.id)
} else {
navigateToBreadcrumb(i - 1)
}
}}/>
<button class="btn btn-sm back-btn" on:click={navigateBack}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
{t('common.backLabel')}
</button>
{:else}
<FileBreadcrumbs crumbs={[{ name: t('file.root') }]}/>
{/if}
{#if fileItems.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<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"/>
</svg>
</div>
<p>{folderStack.length > 0 ? t('file.noFiles') : t('file.noFilesCase')}</p>
<p class="hint">{t('file.hint')}</p>
<div class="empty-actions">
<button class="btn btn-primary" on:click={addFile}>{t('file.addFileSimple')}</button>
<button class="btn" on:click={addFolder}>{t('file.addFolderSimple')}</button>
</div>
</div>
{:else}
<div class="file-list">
{#each fileItems as item (item.id)}
<FileTreeRow
{item}
selected={selectedIds.includes(item.id)}
{onDragStart}
{onDragOver}
{onDrop}
on:navigate={(e) => navigateToFolder(e.detail)}
on:preview={(e) => openPreview(e.detail)}
on:openExternal={(e) => wailsCall('OpenFile', e.detail)}
on:showInFolder={(e) => wailsCall('OpenFolder', e.detail)}
on:delete={(e) => deleteFile(e.detail)}
on:rename={(e) => renameItem(e.detail.id)}
on:duplicate={(e) => duplicateItem(e.detail)}
on:cut={(e) => cutItem(e.detail)}
on:copy={(e) => copyItem(e.detail)}
on:selectOne={(e) => selectOne(e.detail)}
on:toggleSelect={(e) => toggleSelection(e.detail)}
on:rangeSelect={(e) => rangeSelect(e.detail)}
/>
{/each}
</div>
{/if}
{/if}
{#if importing && !showImportDialog}
<div class="empty-state"><p>{t('file.scanning')}</p></div>
{/if}
<!-- Import dialog -->
{#if showImportDialog && importSummary}
<div class="modal-overlay" role="dialog" aria-modal="true" aria-label={t('file.importTitle')} on:click|self={cancelImport} on:keydown={(e) => e.key === 'Escape' && cancelImport()}>
<div class="modal-box" role="document">
<h3>{t('file.importTitle')}</h3>
<div class="import-summary">
<p><strong>{t('file.path')}:</strong> {importSummary.path || pendingImportPath}</p>
{#if importSummary.files}
<p>{t('file.files')}: {importSummary.files.length}</p>
{/if}
</div>
<div class="form-actions">
<button class="btn btn-primary" on:click={() => confirmImport('copy')}>{t('file.importCopy')}</button>
<button class="btn" on:click={() => confirmImport('link')}>{t('file.importLink')}</button>
<button class="btn" on:click={cancelImport}>{t('common.cancel')}</button>
</div>
</div>
</div>
{/if}
<!-- File preview modal -->
{#if previewItem}
<FilePreviewModal
item={previewItem}
content={previewContent}
loading={previewLoading}
error={previewError}
{wailsCall}
on:close={closePreview}
/>
{/if}
</div>
<style>
.files-tab { padding: 20px; }
.files-tab .tab-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; }
.file-list { display: flex; flex-direction: column; }
.back-btn { margin-bottom: 4px; display: inline-flex; align-items: center; gap: 4px; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
.empty-state p { margin: 0; font-size: 14px; color: #666; }
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
.empty-icon { margin-bottom: 12px; color: #333; }
.empty-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
.btn:hover { background: #222233; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
.btn-primary:hover { background: #4f46e5; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.modal-overlay { position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; }
.modal-box { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 12px; padding: 24px; min-width: 320px; max-width: 560px; width: 90vw; }
.modal-box h3 { margin: 0 0 16px; font-size: 16px; }
.import-summary { margin-bottom: 16px; font-size: 13px; }
.import-summary p { margin: 4px 0; }
.form-actions { display: flex; gap: 8px; justify-content: flex-end; }
</style>

View File

@ -1,114 +0,0 @@
<script>
import { t } from '../../i18n'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let notes = []
export let showCreateNote = false
export let formatDate = (d) => d || ''
let newNoteTitle = ''
function handleCreateNote() {
dispatch('createNote')
}
function handleSubmitCreateNote() {
if (newNoteTitle.trim()) {
dispatch('submitCreateNote', { title: newNoteTitle.trim() })
newNoteTitle = ''
}
}
function handleCancelCreateNote() {
dispatch('cancelCreateNote')
}
function handleOpenNote(note) {
dispatch('openNote', { note })
}
function handleStartRename(note) {
dispatch('startRename', { note })
}
function handleDelete(note) {
dispatch('delete', { note })
}
function onKeyActivate(fn) {
return (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
fn()
}
}
}
</script>
<div class="notes-tab">
<div class="tab-toolbar">
<button class="btn btn-primary" on:click={handleCreateNote}>{t('note.add')}</button>
</div>
{#if showCreateNote}
<div class="create-form">
<input type="text" placeholder={t('note.title')} bind:value={newNoteTitle}
on:keydown={(e) => e.key === 'Enter' && handleSubmitCreateNote()} />
<div class="form-actions">
<button class="btn btn-primary" on:click={handleSubmitCreateNote}>{t('common.create')}</button>
<button class="btn" on:click={handleCancelCreateNote}>{t('common.cancel')}</button>
</div>
</div>
{/if}
{#if notes.length === 0 && !showCreateNote}
<div class="empty-state"><p>{t('note.noNotes')}</p><p class="hint">{t('note.createFirst')}</p></div>
{:else}
<div class="notes-list">
{#each notes as note}
<div class="note-card" role="button" tabindex="0" on:click={() => handleOpenNote(note)} on:keydown={onKeyActivate(() => handleOpenNote(note))}>
<div class="note-card-info">
<div class="note-card-title">{note.title}</div>
<div class="note-card-date">{formatDate(note.createdAt)}</div>
</div>
<div class="note-card-actions" on:click|stopPropagation>
<button class="note-action-btn" on:click={() => handleStartRename(note)} title={t('common.rename')}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
</button>
<button class="note-action-btn note-action-danger" on:click={() => handleDelete(note)} title={t('common.delete')}>
<svg width="12" height="12" 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>
{/each}
</div>
{/if}
</div>
<style>
.notes-tab { padding: 24px; }
.tab-toolbar { margin-bottom: 16px; }
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; outline: none; }
.create-form input:focus { border-color: #6366f1; }
.form-actions { display: flex; gap: 8px; }
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; }
.note-card:hover { border-color: #3a3a5c; }
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.note-card-date { font-size: 11px; color: #555; }
.note-card-info { flex: 1; min-width: 0; }
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
.note-card:hover .note-card-actions { opacity: 1; }
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
.note-action-danger:hover { background: rgba(239, 68, 68, 0.15); color: #f87171; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
.empty-state p { margin: 0; font-size: 14px; color: #666; }
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
.btn { padding: 8px 16px; border: 1px solid #2a2a3c; background: #1a1a28; color: #ccc; border-radius: 6px; cursor: pointer; font-size: 13px; font-family: inherit; display: inline-flex; align-items: center; gap: 6px; }
.btn:hover { background: #222233; }
.btn-primary { background: #6366f1; border-color: #6366f1; color: #fff; }
.btn-primary:hover { background: #4f46e5; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
</style>

View File

@ -1,9 +0,0 @@
/**
* Actions API actions (quick commands) on nodes.
*/
import { createApi } from './wails.js'
export const listActions = createApi('ListActions')
export const createAction = createApi('CreateAction')
export const runAction = createApi('RunAction')
export const deleteAction = createApi('DeleteAction')

View File

@ -1,7 +0,0 @@
/**
* Activity API activity feed and per-node activity.
*/
import { createApi } from './wails.js'
export const listActivityFeed = createApi('ListActivityFeed')
export const listActivityByNode = createApi('ListActivityByNode')

View File

@ -1,9 +0,0 @@
/**
* Browser Events API browser extension events.
*/
import { createApi } from './wails.js'
export const listBrowserEvents = createApi('ListBrowserEvents')
export const acceptBrowserEvent = createApi('AcceptBrowserEvent')
export const dismissBrowserEvent = createApi('DismissBrowserEvent')
export const attachBrowserEventToNode = createApi('AttachBrowserEventToNode')

View File

@ -1,20 +0,0 @@
/**
* Files API all file/folder related Wails calls.
*/
import { createApi } from './wails.js'
export const listItems = createApi('ListItems')
export const listFiles = createApi('ListFiles')
export const createEmptyFile = createApi('CreateEmptyFile')
export const deleteFileOrFolder = createApi('DeleteFileOrFolder')
export const openFile = createApi('OpenFile')
export const openFolder = createApi('OpenFolder')
export const pickFile = createApi('PickFile')
export const pickDirectory = createApi('PickDirectory')
export const previewImport = createApi('PreviewImport')
export const addPathCopy = createApi('AddPathCopy')
export const addPathLink = createApi('AddPathLink')
export const checkFileAction = createApi('CheckFileAction')
export const getFileBase64 = createApi('GetFileBase64')
export const readFileText = createApi('ReadFileText')
export const showInFolder = createApi('OpenFolder')

View File

@ -1,14 +0,0 @@
/**
* Inbox API capture and inbox management.
*/
import { createApi } from './wails.js'
export const listInboxNodes = createApi('ListInboxNodes')
export const listInboxNodesForTarget = createApi('ListInboxNodesForTarget')
export const captureClipboardTextWithContext = createApi('CaptureClipboardTextWithContext')
export const captureTextWithContext = createApi('CaptureTextWithContext')
export const captureURLWithContext = createApi('CaptureURLWithContext')
export const capturePathWithContext = createApi('CapturePathWithContext')
export const captureFileDataWithContext = createApi('CaptureFileDataWithContext')
export const resolveInboxNode = createApi('ResolveInboxNode')
export const deleteInboxNode = createApi('DeleteInboxNode')

View File

@ -1,13 +0,0 @@
/**
* Journal & Worklog API time tracking and reporting.
*/
import { createApi } from './wails.js'
export const listWorklog = createApi('ListWorklog')
export const createWorklogFull = createApi('CreateWorklogFull')
export const updateWorklogEntry = createApi('UpdateWorklogEntry')
export const deleteWorklogEntry = createApi('DeleteWorklogEntry')
export const getWorklogEntryEvents = createApi('GetWorklogEntryEvents')
export const listWorklogReport = createApi('ListWorklogReport')
export const worklogReportSummary = createApi('WorklogReportSummary')
export const saveWorklogReport = createApi('SaveWorklogReport')

View File

@ -1,9 +0,0 @@
/**
* Links API external URL links management.
*/
import { createApi } from './wails.js'
export const listLinks = createApi('ListLinks')
export const updateLink = createApi('UpdateLink')
export const deleteLink = createApi('DeleteLink')
export const openLink = createApi('OpenLink')

View File

@ -1,20 +0,0 @@
/**
* Node & Workspace API tree, node CRUD, system views.
*/
import { createApi } from './wails.js'
export const getStartupStatus = createApi('GetStartupStatus')
export const verstakVersion = createApi('VerstakVersion')
export const listSystemViewsWithPlugins = createApi('ListSystemViewsWithPlugins')
export const listWorkspaceTree = createApi('ListWorkspaceTree')
export const listWorkspaceChildren = createApi('ListWorkspaceChildren')
export const listEnabledTemplates = createApi('ListEnabledTemplates')
export const createNodeFromTemplate = createApi('CreateNodeFromTemplate')
export const deleteNode = createApi('DeleteNode')
export const renameNode = createApi('RenameNode')
export const moveNode = createApi('MoveNode')
export const duplicateNode = createApi('DuplicateNode')
export const getNodeDetail = createApi('GetNodeDetail')
export const getSuggestions = createApi('GetSuggestions')
export const validateName = createApi('ValidateName')
export const writeDebugLog = createApi('WriteDebugLog')

View File

@ -1,11 +0,0 @@
/**
* Notes API all note-related Wails calls.
*/
import { createApi } from './wails.js'
export const listNotes = createApi('ListNotes')
export const createNote = createApi('CreateNote')
export const readNote = createApi('ReadNote')
export const saveNote = createApi('SaveNote')
export const renameNote = createApi('RenameNote')
export const deleteNoteApi = createApi('DeleteNote')

View File

@ -1,8 +0,0 @@
/**
* Search API global search and node lookup.
*/
import { createApi } from './wails.js'
export const searchNodes = createApi('SearchNodes')
export const getNodeDetail = createApi('GetNodeDetail')
export const searchWorkspace = createApi('SearchWorkspace')

View File

@ -1,9 +0,0 @@
/**
* Suggestions API worklog suggestions.
*/
import { createApi } from './wails.js'
export const getSuggestions = createApi('GetSuggestions')
export const acceptSuggestionFull = createApi('AcceptSuggestionFull')
export const dismissSuggestion = createApi('DismissSuggestion')
export const acceptSuggestionWith = createApi('AcceptSuggestionWith')

View File

@ -1,7 +0,0 @@
/**
* Sync API vault synchronisation.
*/
import { createApi } from './wails.js'
export const syncStatus = createApi('SyncStatus')
export const syncNow = createApi('SyncNow')

View File

@ -1,8 +0,0 @@
/**
* Today API today dashboard, in-progress items, captures.
*/
import { createApi } from './wails.js'
export const listTodayView = createApi('ListTodayView')
export const listTodayInProgress = createApi('ListTodayInProgress')
export const listTodayCaptures = createApi('ListTodayCaptures')

View File

@ -1,12 +0,0 @@
/**
* Trash API trash operations.
*/
import { createApi } from './wails.js'
export const listTrash = createApi('ListTrash')
export const trashCount = createApi('TrashCount')
export const readTrashFile = createApi('ReadTrashFile')
export const readTrashFileContent = createApi('ReadTrashFileContent')
export const restoreTrashNodes = createApi('RestoreTrashNodesJSON')
export const purgeTrashNodes = createApi('PurgeTrashNodesJSON')
export const emptyTrash = createApi('EmptyTrash')

View File

@ -1,27 +0,0 @@
/**
* Base Wails API call helper.
* All backend communication goes through this function.
*/
export function wailsCall(method, ...args) {
try {
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
const fn = window['go']['main']['App'][method]
if (typeof fn === 'function') {
return fn(...args)
}
}
} catch (e) {
console.error('Wails call error:', method, e)
}
return Promise.reject(new Error('Wails not connected: ' + method))
}
/**
* Create a wrapped API function with consistent error handling.
*/
export function createApi(method) {
return (...args) => {
return wailsCall(method, ...args)
}
}