Skip to content

Commit 402d2d7

Browse files
committed
feat(WebServer): add text editor for device files
- Double-click text files in device file browser to open in Ace Editor tab - Support common text formats (.txt, .json, .c, .py, .yaml, .xml, etc.) - Right-click 'Open as Text' to force-open any file as text - Large file (>100KB) two-step confirm: download / open anyway / cancel - Ctrl+S save back to device (full overwrite via existing upload flow) - VS Code-style dirty indicator (dot on tab, close button morphs) - Confirm discard on closing dirty tab - Toolbar with save-to-device and download-to-PC buttons - Image preview unchanged (priority: dir > image > text) - i18n: en, zh-CN, zh-TW translations - Tests: 1709 frontend tests passed, 80.10% coverage - No backend changes
1 parent d394951 commit 402d2d7

11 files changed

Lines changed: 1628 additions & 0 deletions

File tree

Tools/WebServer/docs/text-editor-design.md

Lines changed: 541 additions & 0 deletions
Large diffs are not rendered by default.

Tools/WebServer/static/css/workbench.css

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,69 @@ body.resizing-panel .sash {
809809
background: var(--vscode-list-hover);
810810
}
811811

812+
/* Tab dirty indicator (unsaved changes) */
813+
.tab-dirty-indicator {
814+
color: var(--vscode-foreground);
815+
opacity: 0.8;
816+
font-size: 10px;
817+
margin-right: 2px;
818+
flex-shrink: 0;
819+
}
820+
821+
/* VS Code style: dirty tab close button shows dot, hover restores × */
822+
.tab .tab-close.dirty .codicon-close::before {
823+
content: "\eab8";
824+
}
825+
826+
.tab .tab-close.dirty {
827+
opacity: 0.8;
828+
}
829+
830+
.tab:hover .tab-close.dirty .codicon-close::before {
831+
content: "\ea76";
832+
}
833+
834+
/* Text file editor toolbar */
835+
.textfile-toolbar {
836+
display: flex;
837+
align-items: center;
838+
justify-content: space-between;
839+
padding: 4px 12px;
840+
background: var(--vscode-titlebar-bg);
841+
border-bottom: 1px solid var(--vscode-panel-border);
842+
font-size: 11px;
843+
flex-shrink: 0;
844+
}
845+
846+
.textfile-path {
847+
opacity: 0.7;
848+
overflow: hidden;
849+
text-overflow: ellipsis;
850+
white-space: nowrap;
851+
font-family: "Consolas", "Courier New", monospace;
852+
}
853+
854+
.textfile-actions {
855+
display: flex;
856+
gap: 4px;
857+
flex-shrink: 0;
858+
}
859+
860+
.textfile-actions button {
861+
background: none;
862+
border: none;
863+
color: var(--vscode-foreground);
864+
cursor: pointer;
865+
padding: 2px 6px;
866+
border-radius: 3px;
867+
opacity: 0.7;
868+
}
869+
870+
.textfile-actions button:hover {
871+
opacity: 1;
872+
background: var(--vscode-list-hover);
873+
}
874+
812875
.editor-toolbar {
813876
display: flex;
814877
align-items: flex-end;

Tools/WebServer/static/js/app.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,18 @@ document.addEventListener('DOMContentLoaded', () => {
4848
watchRestoreFromStorage();
4949
}
5050
});
51+
52+
/* Global Ctrl+S handler for text file tabs */
53+
document.addEventListener('keydown', (e) => {
54+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
55+
const state = window.FPBState;
56+
if (!state) return;
57+
const activeTab = state.editorTabs.find(
58+
(tab) => tab.id === state.activeEditorTab,
59+
);
60+
if (activeTab && activeTab.type === 'textfile') {
61+
e.preventDefault();
62+
saveDeviceTextFile(activeTab.id);
63+
}
64+
}
65+
});

Tools/WebServer/static/js/features/editor.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ function closeTab(tabId, event) {
111111
const tabInfo = state.editorTabs.find((t) => t.id === tabId);
112112
if (!tabInfo || !tabInfo.closable) return;
113113

114+
// Confirm before closing dirty text file tabs
115+
if (tabInfo.type === 'textfile' && tabInfo.dirty) {
116+
const discard = confirm(
117+
t(
118+
'transfer.unsaved_changes',
119+
'File {{name}} has unsaved changes. Discard?',
120+
{
121+
name: tabInfo.title,
122+
},
123+
),
124+
);
125+
if (!discard) return;
126+
}
127+
114128
const { aceEditors } = state;
115129
if (aceEditors.has(tabId)) {
116130
const editor = aceEditors.get(tabId);

0 commit comments

Comments
 (0)