figrecipe 0.6.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- 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 +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -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/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -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 +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- 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 +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- 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 +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- 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/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- 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 +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- 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 +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- 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 +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""File browser JavaScript for the file tree panel."""
|
|
4
|
+
|
|
5
|
+
SCRIPTS_FILES = """
|
|
6
|
+
// ==================== FILE BROWSER ====================
|
|
7
|
+
// File tree panel for browsing and switching between recipe files
|
|
8
|
+
|
|
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
|
+
}
|
|
94
|
+
|
|
95
|
+
async function loadFileList() {
|
|
96
|
+
const fileTree = document.getElementById('file-tree');
|
|
97
|
+
if (!fileTree) return;
|
|
98
|
+
|
|
99
|
+
// Load saved expanded state
|
|
100
|
+
loadExpandedState();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch('/api/files');
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
fileTree.innerHTML = '<li class="file-tree-empty"><p>No files found</p></li>';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
const tree = data.tree || [];
|
|
111
|
+
const files = data.files || [];
|
|
112
|
+
currentFilePath = data.current_file;
|
|
113
|
+
|
|
114
|
+
// Build file tree HTML
|
|
115
|
+
let treeHtml = '';
|
|
116
|
+
|
|
117
|
+
// Show unsaved figure entry when no current file path (new/unsaved figure)
|
|
118
|
+
if (!currentFilePath) {
|
|
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;
|
|
131
|
+
}
|
|
132
|
+
|
|
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
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
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
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log('[FileBrowser] Loaded', files.length, 'files in', tree.length, 'root items');
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('[FileBrowser] Error loading files:', error);
|
|
182
|
+
fileTree.innerHTML = '<li class="file-tree-empty"><p>Error loading files</p></li>';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function switchToFile(filePath) {
|
|
187
|
+
if (!filePath || filePath === currentFilePath) return;
|
|
188
|
+
|
|
189
|
+
showToast('Loading figure...', 'info');
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch('/api/switch', {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
body: JSON.stringify({ path: filePath })
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
const error = await response.json();
|
|
200
|
+
throw new Error(error.error || 'Failed to switch file');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const data = await response.json();
|
|
204
|
+
|
|
205
|
+
// Update preview image
|
|
206
|
+
const img = document.getElementById('preview-image');
|
|
207
|
+
if (img && data.image) {
|
|
208
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Update bboxes
|
|
212
|
+
if (data.bboxes) {
|
|
213
|
+
window.currentBboxes = data.bboxes;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update color map for hitmap
|
|
217
|
+
if (data.color_map) {
|
|
218
|
+
window.currentColorMap = data.color_map;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update current file path
|
|
222
|
+
currentFilePath = filePath;
|
|
223
|
+
|
|
224
|
+
// Clear selection
|
|
225
|
+
clearSelection();
|
|
226
|
+
document.getElementById('selected-element-panel')?.style.setProperty('display', 'none');
|
|
227
|
+
|
|
228
|
+
showToast('Loaded: ' + filePath, 'success');
|
|
229
|
+
console.log('[FileSwitcher] Switched to:', filePath);
|
|
230
|
+
|
|
231
|
+
// Reload file list to update selection state
|
|
232
|
+
loadFileList();
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('[FileSwitcher] Error switching file:', error);
|
|
236
|
+
showToast('Error: ' + error.message, 'error');
|
|
237
|
+
// Revert selector
|
|
238
|
+
loadFileList();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function createNewFigure() {
|
|
243
|
+
showToast('Creating new figure...', 'info');
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch('/api/new', {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: { 'Content-Type': 'application/json' }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
const error = await response.json();
|
|
253
|
+
throw new Error(error.error || 'Failed to create new figure');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = await response.json();
|
|
257
|
+
|
|
258
|
+
// Update preview image
|
|
259
|
+
const img = document.getElementById('preview-image');
|
|
260
|
+
if (img && data.image) {
|
|
261
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Update bboxes
|
|
265
|
+
if (data.bboxes) {
|
|
266
|
+
window.currentBboxes = data.bboxes;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update color map for hitmap
|
|
270
|
+
if (data.color_map) {
|
|
271
|
+
window.currentColorMap = data.color_map;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Update current file path to the new file
|
|
275
|
+
currentFilePath = data.file || null;
|
|
276
|
+
|
|
277
|
+
// Clear selection
|
|
278
|
+
if (typeof clearSelection === 'function') {
|
|
279
|
+
clearSelection();
|
|
280
|
+
}
|
|
281
|
+
const selectedPanel = document.getElementById('selected-element-panel');
|
|
282
|
+
if (selectedPanel) selectedPanel.style.display = 'none';
|
|
283
|
+
|
|
284
|
+
const fileName = data.file_name || 'new_figure';
|
|
285
|
+
showToast(`Created: ${fileName}.yaml`, 'success');
|
|
286
|
+
console.log('[FileSwitcher] Created new figure:', data.file);
|
|
287
|
+
|
|
288
|
+
// Reload file list to show (Unsaved figure)
|
|
289
|
+
loadFileList();
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('[FileSwitcher] Error creating new figure:', error);
|
|
293
|
+
showToast('Error: ' + error.message, 'error');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function toggleFileBrowser() {
|
|
298
|
+
const panel = document.getElementById('file-browser-panel');
|
|
299
|
+
const collapseBtn = document.getElementById('btn-collapse-browser');
|
|
300
|
+
if (!panel) return;
|
|
301
|
+
|
|
302
|
+
fileBrowserCollapsed = !fileBrowserCollapsed;
|
|
303
|
+
panel.classList.toggle('collapsed', fileBrowserCollapsed);
|
|
304
|
+
if (collapseBtn) {
|
|
305
|
+
collapseBtn.innerHTML = fileBrowserCollapsed ? '❯' : '❮';
|
|
306
|
+
collapseBtn.title = fileBrowserCollapsed ? 'Expand panel' : 'Collapse panel';
|
|
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');
|
|
314
|
+
|
|
315
|
+
if (newBtn) {
|
|
316
|
+
newBtn.addEventListener('click', createNewFigure);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (refreshBtn) {
|
|
320
|
+
refreshBtn.addEventListener('click', loadFileList);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (collapseBtn) {
|
|
324
|
+
collapseBtn.addEventListener('click', toggleFileBrowser);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Load file list on init
|
|
328
|
+
loadFileList();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Initialize file browser after DOM is ready
|
|
332
|
+
if (document.readyState === 'loading') {
|
|
333
|
+
document.addEventListener('DOMContentLoaded', initFileBrowser);
|
|
334
|
+
} else {
|
|
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
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log('[FileBrowser] Loaded - Use file tree to switch figures');
|
|
427
|
+
"""
|
|
428
|
+
|
|
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
|