figrecipe 0.7.4__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +2 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,53 +1,185 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
"""File
|
|
3
|
+
"""File browser JavaScript for the file tree panel."""
|
|
4
4
|
|
|
5
5
|
SCRIPTS_FILES = """
|
|
6
|
-
// ==================== FILE
|
|
7
|
-
//
|
|
6
|
+
// ==================== FILE BROWSER ====================
|
|
7
|
+
// File tree panel for browsing and switching between recipe files
|
|
8
8
|
|
|
9
9
|
let currentFilePath = null;
|
|
10
|
+
let fileBrowserCollapsed = false;
|
|
11
|
+
let expandedFolders = new Set();
|
|
12
|
+
|
|
13
|
+
// Load expanded state from localStorage
|
|
14
|
+
function loadExpandedState() {
|
|
15
|
+
try {
|
|
16
|
+
const saved = localStorage.getItem('figrecipe-expanded-folders');
|
|
17
|
+
if (saved) {
|
|
18
|
+
expandedFolders = new Set(JSON.parse(saved));
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.warn('[FileBrowser] Failed to load expanded state:', e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Save expanded state to localStorage
|
|
26
|
+
function saveExpandedState() {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem('figrecipe-expanded-folders', JSON.stringify([...expandedFolders]));
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn('[FileBrowser] Failed to save expanded state:', e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Toggle folder expand/collapse
|
|
35
|
+
function toggleFolder(folderPath) {
|
|
36
|
+
if (expandedFolders.has(folderPath)) {
|
|
37
|
+
expandedFolders.delete(folderPath);
|
|
38
|
+
} else {
|
|
39
|
+
expandedFolders.add(folderPath);
|
|
40
|
+
}
|
|
41
|
+
saveExpandedState();
|
|
42
|
+
|
|
43
|
+
// Update DOM
|
|
44
|
+
const folderEl = document.querySelector(`.file-tree-folder[data-path="${folderPath}"]`);
|
|
45
|
+
if (folderEl) {
|
|
46
|
+
folderEl.classList.toggle('expanded', expandedFolders.has(folderPath));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Render a tree item (file or folder)
|
|
51
|
+
function renderTreeItem(item, level = 0) {
|
|
52
|
+
const indent = level * 16; // 16px per level
|
|
53
|
+
|
|
54
|
+
if (item.type === 'directory') {
|
|
55
|
+
const isExpanded = expandedFolders.has(item.path);
|
|
56
|
+
const expandedClass = isExpanded ? ' expanded' : '';
|
|
57
|
+
|
|
58
|
+
let childrenHtml = '';
|
|
59
|
+
if (item.children && item.children.length > 0) {
|
|
60
|
+
childrenHtml = item.children.map(child => renderTreeItem(child, level + 1)).join('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `<li class="file-tree-folder${expandedClass}" data-path="${item.path}">
|
|
64
|
+
<div class="file-tree-entry" data-path="${item.path}" data-type="folder" style="padding-left: ${12 + indent}px;">
|
|
65
|
+
<span class="file-tree-icon">📁</span>
|
|
66
|
+
<span class="file-tree-name">${item.name}</span>
|
|
67
|
+
<span class="file-tree-badge folder-badge">${item.children ? item.children.length : 0}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<ul class="file-tree-children">
|
|
70
|
+
${childrenHtml}
|
|
71
|
+
</ul>
|
|
72
|
+
</li>`;
|
|
73
|
+
} else {
|
|
74
|
+
// File item
|
|
75
|
+
const isCurrent = item.is_current;
|
|
76
|
+
const currentClass = isCurrent ? ' current' : '';
|
|
77
|
+
const hasImageClass = item.has_image ? ' has-image' : '';
|
|
78
|
+
const icon = item.has_image ? '📊' : '📄';
|
|
79
|
+
const badge = item.has_image ? '<span class="file-tree-badge">PNG</span>' : '';
|
|
80
|
+
|
|
81
|
+
return `<li class="file-tree-item">
|
|
82
|
+
<div class="file-tree-entry${currentClass}${hasImageClass}" data-path="${item.path}" data-type="file" style="padding-left: ${12 + indent}px;">
|
|
83
|
+
<span class="file-tree-icon">${icon}</span>
|
|
84
|
+
<span class="file-tree-name">${item.name}</span>
|
|
85
|
+
${badge}
|
|
86
|
+
<span class="file-tree-actions">
|
|
87
|
+
<button class="file-action-btn btn-rename" data-path="${item.path}" title="Rename">✏️</button>
|
|
88
|
+
<button class="file-action-btn btn-delete" data-path="${item.path}" title="Delete">🗑️</button>
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
</li>`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
10
94
|
|
|
11
95
|
async function loadFileList() {
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
96
|
+
const fileTree = document.getElementById('file-tree');
|
|
97
|
+
if (!fileTree) return;
|
|
98
|
+
|
|
99
|
+
// Load saved expanded state
|
|
100
|
+
loadExpandedState();
|
|
14
101
|
|
|
15
102
|
try {
|
|
16
103
|
const response = await fetch('/api/files');
|
|
17
104
|
if (!response.ok) {
|
|
18
|
-
|
|
105
|
+
fileTree.innerHTML = '<li class="file-tree-empty"><p>No files found</p></li>';
|
|
19
106
|
return;
|
|
20
107
|
}
|
|
21
108
|
|
|
22
109
|
const data = await response.json();
|
|
110
|
+
const tree = data.tree || [];
|
|
23
111
|
const files = data.files || [];
|
|
24
112
|
currentFilePath = data.current_file;
|
|
25
113
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
114
|
+
// Build file tree HTML
|
|
115
|
+
let treeHtml = '';
|
|
30
116
|
|
|
31
|
-
//
|
|
32
|
-
let optionsHtml = '';
|
|
117
|
+
// Show unsaved figure entry when no current file path (new/unsaved figure)
|
|
33
118
|
if (!currentFilePath) {
|
|
34
|
-
|
|
119
|
+
treeHtml += `<li class="file-tree-item">
|
|
120
|
+
<div class="file-tree-entry current" data-path="" data-type="file">
|
|
121
|
+
<span class="file-tree-icon">✨</span>
|
|
122
|
+
<span class="file-tree-name">(Unsaved figure)</span>
|
|
123
|
+
</div>
|
|
124
|
+
</li>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Show empty state only if no unsaved figure AND no files
|
|
128
|
+
if (tree.length === 0 && files.length === 0 && currentFilePath !== null) {
|
|
129
|
+
fileTree.innerHTML = '<li class="file-tree-empty"><p>No recipe files</p><p>Create one with figrecipe.subplots()</p></li>';
|
|
130
|
+
return;
|
|
35
131
|
}
|
|
36
132
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
133
|
+
// Render tree structure
|
|
134
|
+
tree.forEach(item => {
|
|
135
|
+
treeHtml += renderTreeItem(item, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
fileTree.innerHTML = treeHtml;
|
|
139
|
+
|
|
140
|
+
// Add click handlers for folder entries (expand/collapse)
|
|
141
|
+
fileTree.querySelectorAll('.file-tree-folder > .file-tree-entry').forEach(entry => {
|
|
142
|
+
entry.addEventListener('click', (e) => {
|
|
143
|
+
const folderPath = entry.dataset.path;
|
|
144
|
+
if (folderPath !== undefined) {
|
|
145
|
+
toggleFolder(folderPath);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Add click handlers for file entries
|
|
151
|
+
fileTree.querySelectorAll('.file-tree-entry[data-type="file"]').forEach(entry => {
|
|
152
|
+
entry.addEventListener('click', (e) => {
|
|
153
|
+
// Don't switch if clicking action buttons
|
|
154
|
+
if (e.target.closest('.file-action-btn')) return;
|
|
155
|
+
const path = entry.dataset.path;
|
|
156
|
+
if (path) {
|
|
157
|
+
switchToFile(path);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Add click handlers for rename buttons
|
|
163
|
+
fileTree.querySelectorAll('.btn-rename').forEach(btn => {
|
|
164
|
+
btn.addEventListener('click', (e) => {
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
renameFile(btn.dataset.path);
|
|
167
|
+
});
|
|
42
168
|
});
|
|
43
169
|
|
|
44
|
-
|
|
170
|
+
// Add click handlers for delete buttons
|
|
171
|
+
fileTree.querySelectorAll('.btn-delete').forEach(btn => {
|
|
172
|
+
btn.addEventListener('click', (e) => {
|
|
173
|
+
e.stopPropagation();
|
|
174
|
+
deleteFile(btn.dataset.path);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
45
177
|
|
|
46
|
-
console.log('[
|
|
178
|
+
console.log('[FileBrowser] Loaded', files.length, 'files in', tree.length, 'root items');
|
|
47
179
|
|
|
48
180
|
} catch (error) {
|
|
49
|
-
console.error('[
|
|
50
|
-
|
|
181
|
+
console.error('[FileBrowser] Error loading files:', error);
|
|
182
|
+
fileTree.innerHTML = '<li class="file-tree-empty"><p>Error loading files</p></li>';
|
|
51
183
|
}
|
|
52
184
|
}
|
|
53
185
|
|
|
@@ -139,8 +271,8 @@ async function createNewFigure() {
|
|
|
139
271
|
window.currentColorMap = data.color_map;
|
|
140
272
|
}
|
|
141
273
|
|
|
142
|
-
//
|
|
143
|
-
currentFilePath = null;
|
|
274
|
+
// Update current file path to the new file
|
|
275
|
+
currentFilePath = data.file || null;
|
|
144
276
|
|
|
145
277
|
// Clear selection
|
|
146
278
|
if (typeof clearSelection === 'function') {
|
|
@@ -149,8 +281,9 @@ async function createNewFigure() {
|
|
|
149
281
|
const selectedPanel = document.getElementById('selected-element-panel');
|
|
150
282
|
if (selectedPanel) selectedPanel.style.display = 'none';
|
|
151
283
|
|
|
152
|
-
|
|
153
|
-
|
|
284
|
+
const fileName = data.file_name || 'new_figure';
|
|
285
|
+
showToast(`Created: ${fileName}.yaml`, 'success');
|
|
286
|
+
console.log('[FileSwitcher] Created new figure:', data.file);
|
|
154
287
|
|
|
155
288
|
// Reload file list to show (Unsaved figure)
|
|
156
289
|
loadFileList();
|
|
@@ -161,35 +294,136 @@ async function createNewFigure() {
|
|
|
161
294
|
}
|
|
162
295
|
}
|
|
163
296
|
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
const
|
|
297
|
+
function toggleFileBrowser() {
|
|
298
|
+
const panel = document.getElementById('file-browser-panel');
|
|
299
|
+
const collapseBtn = document.getElementById('btn-collapse-browser');
|
|
300
|
+
if (!panel) return;
|
|
167
301
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
});
|
|
302
|
+
fileBrowserCollapsed = !fileBrowserCollapsed;
|
|
303
|
+
panel.classList.toggle('collapsed', fileBrowserCollapsed);
|
|
304
|
+
if (collapseBtn) {
|
|
305
|
+
collapseBtn.innerHTML = fileBrowserCollapsed ? '❯' : '❮';
|
|
306
|
+
collapseBtn.title = fileBrowserCollapsed ? 'Expand panel' : 'Collapse panel';
|
|
175
307
|
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function initFileBrowser() {
|
|
311
|
+
const newBtn = document.getElementById('btn-new-file');
|
|
312
|
+
const refreshBtn = document.getElementById('btn-refresh-files');
|
|
313
|
+
const collapseBtn = document.getElementById('btn-collapse-browser');
|
|
176
314
|
|
|
177
315
|
if (newBtn) {
|
|
178
316
|
newBtn.addEventListener('click', createNewFigure);
|
|
179
317
|
}
|
|
180
318
|
|
|
319
|
+
if (refreshBtn) {
|
|
320
|
+
refreshBtn.addEventListener('click', loadFileList);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (collapseBtn) {
|
|
324
|
+
collapseBtn.addEventListener('click', toggleFileBrowser);
|
|
325
|
+
}
|
|
326
|
+
|
|
181
327
|
// Load file list on init
|
|
182
328
|
loadFileList();
|
|
183
329
|
}
|
|
184
330
|
|
|
185
|
-
// Initialize file
|
|
331
|
+
// Initialize file browser after DOM is ready
|
|
186
332
|
if (document.readyState === 'loading') {
|
|
187
|
-
document.addEventListener('DOMContentLoaded',
|
|
333
|
+
document.addEventListener('DOMContentLoaded', initFileBrowser);
|
|
188
334
|
} else {
|
|
189
|
-
|
|
335
|
+
initFileBrowser();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function deleteFile(filePath) {
|
|
339
|
+
if (!filePath) return;
|
|
340
|
+
|
|
341
|
+
const fileName = filePath.split('/').pop().replace('.yaml', '');
|
|
342
|
+
if (!confirm(`Delete "${fileName}" and its associated files (.yaml, .png)?`)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
showToast('Deleting...', 'info');
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetch('/api/delete', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
headers: { 'Content-Type': 'application/json' },
|
|
352
|
+
body: JSON.stringify({ path: filePath })
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const data = await response.json();
|
|
356
|
+
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
throw new Error(data.error || 'Failed to delete');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
showToast(`Deleted: ${data.deleted.join(', ')}`, 'success');
|
|
362
|
+
console.log('[FileBrowser] Deleted:', data.deleted);
|
|
363
|
+
|
|
364
|
+
// If we deleted the current file, switch to another or create new
|
|
365
|
+
if (data.was_current) {
|
|
366
|
+
if (data.switch_to) {
|
|
367
|
+
// Switch to another existing file
|
|
368
|
+
console.log('[FileBrowser] Switching to:', data.switch_to);
|
|
369
|
+
await switchToFile(data.switch_to);
|
|
370
|
+
} else {
|
|
371
|
+
// No other files, create a new one
|
|
372
|
+
console.log('[FileBrowser] No files left, creating new figure');
|
|
373
|
+
await createNewFigure();
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// Just reload file list
|
|
377
|
+
loadFileList();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error('[FileBrowser] Delete error:', error);
|
|
382
|
+
showToast('Error: ' + error.message, 'error');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function renameFile(filePath) {
|
|
387
|
+
if (!filePath) return;
|
|
388
|
+
|
|
389
|
+
const oldName = filePath.split('/').pop().replace('.yaml', '');
|
|
390
|
+
const newName = prompt(`Rename "${oldName}" to:`, oldName);
|
|
391
|
+
|
|
392
|
+
if (!newName || newName === oldName) return;
|
|
393
|
+
|
|
394
|
+
showToast('Renaming...', 'info');
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const response = await fetch('/api/rename', {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
400
|
+
body: JSON.stringify({ path: filePath, new_name: newName })
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const data = await response.json();
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error(data.error || 'Failed to rename');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
showToast(`Renamed to: ${data.new_name}`, 'success');
|
|
410
|
+
console.log('[FileBrowser] Renamed:', data.renamed);
|
|
411
|
+
|
|
412
|
+
// Update current file path if it was the renamed file
|
|
413
|
+
if (currentFilePath === filePath) {
|
|
414
|
+
currentFilePath = data.new_name + '.yaml';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Reload file list
|
|
418
|
+
loadFileList();
|
|
419
|
+
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('[FileBrowser] Rename error:', error);
|
|
422
|
+
showToast('Error: ' + error.message, 'error');
|
|
423
|
+
}
|
|
190
424
|
}
|
|
191
425
|
|
|
192
|
-
console.log('[
|
|
426
|
+
console.log('[FileBrowser] Loaded - Use file tree to switch figures');
|
|
193
427
|
"""
|
|
194
428
|
|
|
195
429
|
__all__ = ["SCRIPTS_FILES"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Files tree right-click context menu JavaScript."""
|
|
4
|
+
|
|
5
|
+
JS_FILES_CONTEXT_MENU = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Files Tree Context Menu (Right-Click Menu)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
let filesContextMenu = null;
|
|
10
|
+
let filesContextTarget = null; // Track which file was right-clicked
|
|
11
|
+
|
|
12
|
+
function createFilesContextMenu() {
|
|
13
|
+
if (filesContextMenu) return;
|
|
14
|
+
|
|
15
|
+
filesContextMenu = document.createElement('div');
|
|
16
|
+
filesContextMenu.className = 'files-context-menu';
|
|
17
|
+
filesContextMenu.style.display = 'none';
|
|
18
|
+
filesContextMenu.innerHTML = `
|
|
19
|
+
<div class="context-menu-item" data-action="open">
|
|
20
|
+
Open
|
|
21
|
+
</div>
|
|
22
|
+
<div class="context-menu-item" data-action="rename">
|
|
23
|
+
Rename
|
|
24
|
+
</div>
|
|
25
|
+
<div class="context-menu-divider"></div>
|
|
26
|
+
<div class="context-menu-item" data-action="duplicate">
|
|
27
|
+
Duplicate
|
|
28
|
+
</div>
|
|
29
|
+
<div class="context-menu-item" data-action="download">
|
|
30
|
+
Download
|
|
31
|
+
</div>
|
|
32
|
+
<div class="context-menu-divider"></div>
|
|
33
|
+
<div class="context-menu-item context-menu-danger" data-action="delete">
|
|
34
|
+
Delete
|
|
35
|
+
</div>
|
|
36
|
+
<div class="context-menu-divider"></div>
|
|
37
|
+
<div class="context-menu-item" data-action="new-file">
|
|
38
|
+
New figure
|
|
39
|
+
</div>
|
|
40
|
+
<div class="context-menu-item" data-action="refresh">
|
|
41
|
+
Refresh list
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
document.body.appendChild(filesContextMenu);
|
|
45
|
+
setupFilesContextMenuListeners();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setupFilesContextMenuListeners() {
|
|
49
|
+
if (!filesContextMenu) return;
|
|
50
|
+
|
|
51
|
+
// Click on menu items
|
|
52
|
+
filesContextMenu.querySelectorAll('.context-menu-item').forEach(item => {
|
|
53
|
+
item.addEventListener('click', (e) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
const action = item.dataset.action;
|
|
56
|
+
handleFilesContextMenuAction(action);
|
|
57
|
+
hideFilesContextMenu();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Hide on click outside
|
|
62
|
+
document.addEventListener('click', hideFilesContextMenu);
|
|
63
|
+
document.addEventListener('scroll', hideFilesContextMenu, true);
|
|
64
|
+
document.addEventListener('keydown', (e) => {
|
|
65
|
+
if (e.key === 'Escape') hideFilesContextMenu();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleFilesContextMenuAction(action) {
|
|
70
|
+
switch (action) {
|
|
71
|
+
case 'open':
|
|
72
|
+
if (filesContextTarget) {
|
|
73
|
+
loadFile(filesContextTarget);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'rename':
|
|
77
|
+
if (filesContextTarget) {
|
|
78
|
+
const newName = prompt('Enter new name:', filesContextTarget.split('/').pop());
|
|
79
|
+
if (newName && newName !== filesContextTarget.split('/').pop()) {
|
|
80
|
+
renameFile(filesContextTarget, newName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'duplicate':
|
|
85
|
+
if (filesContextTarget) {
|
|
86
|
+
duplicateFile(filesContextTarget);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'download':
|
|
90
|
+
if (filesContextTarget) {
|
|
91
|
+
downloadFile(filesContextTarget);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 'delete':
|
|
95
|
+
if (filesContextTarget) {
|
|
96
|
+
if (confirm(`Delete "${filesContextTarget.split('/').pop()}"?`)) {
|
|
97
|
+
deleteFile(filesContextTarget);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case 'new-file':
|
|
102
|
+
document.getElementById('btn-new-file')?.click();
|
|
103
|
+
break;
|
|
104
|
+
case 'refresh':
|
|
105
|
+
document.getElementById('btn-refresh-files')?.click();
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
filesContextTarget = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function renameFile(filePath, newName) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch('/api/rename', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ path: filePath, new_name: newName })
|
|
117
|
+
});
|
|
118
|
+
const result = await response.json();
|
|
119
|
+
if (result.success) {
|
|
120
|
+
refreshFileList();
|
|
121
|
+
showToast('File renamed');
|
|
122
|
+
} else {
|
|
123
|
+
showToast(result.error || 'Failed to rename', 'error');
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error('Rename error:', err);
|
|
127
|
+
showToast('Failed to rename', 'error');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function duplicateFile(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch('/api/duplicate', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ path: filePath })
|
|
137
|
+
});
|
|
138
|
+
const result = await response.json();
|
|
139
|
+
if (result.success) {
|
|
140
|
+
refreshFileList();
|
|
141
|
+
showToast('File duplicated');
|
|
142
|
+
} else {
|
|
143
|
+
showToast(result.error || 'Failed to duplicate', 'error');
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Duplicate error:', err);
|
|
147
|
+
showToast('Failed to duplicate', 'error');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function downloadFile(filePath) {
|
|
152
|
+
window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function deleteFile(filePath) {
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch('/api/delete', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({ path: filePath })
|
|
161
|
+
});
|
|
162
|
+
const result = await response.json();
|
|
163
|
+
if (result.success) {
|
|
164
|
+
refreshFileList();
|
|
165
|
+
showToast('File deleted');
|
|
166
|
+
} else {
|
|
167
|
+
showToast(result.error || 'Failed to delete', 'error');
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error('Delete error:', err);
|
|
171
|
+
showToast('Failed to delete', 'error');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function showFilesContextMenu(e, filePath) {
|
|
176
|
+
if (!filesContextMenu) createFilesContextMenu();
|
|
177
|
+
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
|
|
181
|
+
filesContextTarget = filePath;
|
|
182
|
+
|
|
183
|
+
const x = e.clientX;
|
|
184
|
+
const y = e.clientY;
|
|
185
|
+
|
|
186
|
+
// Position off-screen to measure
|
|
187
|
+
filesContextMenu.style.left = '-9999px';
|
|
188
|
+
filesContextMenu.style.top = '-9999px';
|
|
189
|
+
filesContextMenu.style.display = 'block';
|
|
190
|
+
|
|
191
|
+
const menuWidth = filesContextMenu.offsetWidth;
|
|
192
|
+
const menuHeight = filesContextMenu.offsetHeight;
|
|
193
|
+
|
|
194
|
+
// Adjust position to fit in viewport
|
|
195
|
+
let left = x;
|
|
196
|
+
let top = y;
|
|
197
|
+
if (x + menuWidth > window.innerWidth - 10) {
|
|
198
|
+
left = x - menuWidth;
|
|
199
|
+
}
|
|
200
|
+
if (y + menuHeight > window.innerHeight - 10) {
|
|
201
|
+
top = y - menuHeight;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
filesContextMenu.style.left = `${Math.max(10, left)}px`;
|
|
205
|
+
filesContextMenu.style.top = `${Math.max(10, top)}px`;
|
|
206
|
+
|
|
207
|
+
// Update menu based on context
|
|
208
|
+
const isFile = filePath && !filePath.endsWith('/');
|
|
209
|
+
filesContextMenu.querySelectorAll('[data-action="open"], [data-action="rename"], [data-action="duplicate"], [data-action="download"], [data-action="delete"]').forEach(item => {
|
|
210
|
+
item.style.display = isFile ? 'flex' : 'none';
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function hideFilesContextMenu() {
|
|
215
|
+
if (filesContextMenu) {
|
|
216
|
+
filesContextMenu.style.display = 'none';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Initialize files context menu
|
|
221
|
+
function initializeFilesContextMenu() {
|
|
222
|
+
const fileTree = document.getElementById('file-tree');
|
|
223
|
+
if (fileTree) {
|
|
224
|
+
fileTree.addEventListener('contextmenu', (e) => {
|
|
225
|
+
const fileEntry = e.target.closest('.file-tree-entry');
|
|
226
|
+
if (fileEntry) {
|
|
227
|
+
const filePath = fileEntry.dataset.path;
|
|
228
|
+
showFilesContextMenu(e, filePath);
|
|
229
|
+
} else {
|
|
230
|
+
// Right-click on empty area
|
|
231
|
+
showFilesContextMenu(e, null);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
__all__ = ["JS_FILES_CONTEXT_MENU"]
|
|
239
|
+
|
|
240
|
+
# EOF
|