feat: file edit added

This commit is contained in:
kodi
2026-03-11 14:09:44 +01:00
parent ba6a369f78
commit b93cb01879
18 changed files with 701 additions and 16 deletions
+134
View File
@@ -23,6 +23,11 @@ let state = {
};
const ROW_JUMP_STEP = 10;
let wildcardDialogMode = "select";
let editorState = {
path: null,
originalContent: "",
modified: null,
};
function paneState(pane) {
return state.panes[pane];
@@ -70,6 +75,20 @@ function viewerElements() {
};
}
function editorElements() {
return {
overlay: document.getElementById("editor-modal"),
title: document.getElementById("editor-title"),
fileName: document.getElementById("editor-file-name"),
filePath: document.getElementById("editor-file-path"),
error: document.getElementById("editor-error"),
content: document.getElementById("editor-content"),
closeButton: document.getElementById("editor-close-btn"),
saveButton: document.getElementById("editor-save-btn"),
cancelButton: document.getElementById("editor-cancel-btn"),
};
}
async function apiRequest(method, url, body) {
const options = { method, headers: {} };
if (body !== undefined) {
@@ -158,12 +177,25 @@ function updateActionButtons() {
const exactlyOne = count === 1;
const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
document.getElementById("view-btn").disabled = !exactlyOne || !allFiles;
document.getElementById("edit-btn").disabled = !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
document.getElementById("rename-btn").disabled = !exactlyOne;
document.getElementById("delete-btn").disabled = !hasSelection;
document.getElementById("copy-btn").disabled = !allFiles;
document.getElementById("move-btn").disabled = !allFiles;
}
function isEditableSelection(item) {
if (!item || item.kind !== "file") {
return false;
}
const name = item.name || "";
const lower = name.toLowerCase();
if (lower === "dockerfile" || lower === "containerfile") {
return true;
}
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
}
function currentParentPath(path) {
if (!path.includes("/")) {
return null;
@@ -650,6 +682,10 @@ function isViewerOpen() {
return !viewerElements().overlay.classList.contains("hidden");
}
function isEditorOpen() {
return !editorElements().overlay.classList.contains("hidden");
}
function escapeRegExp(text) {
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}
@@ -739,6 +775,33 @@ function closeViewer() {
viewer.content.textContent = "";
}
function editorIsDirty() {
return editorElements().content.value !== editorState.originalContent;
}
function resetEditorState() {
editorState = {
path: null,
originalContent: "",
modified: null,
};
}
function attemptCloseEditor() {
if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) {
return;
}
closeEditor();
}
function closeEditor() {
const editor = editorElements();
editor.overlay.classList.add("hidden");
editor.error.textContent = "";
editor.content.value = "";
resetEditorState();
}
async function openViewer() {
const selectedItems = activePaneState().selectedItems;
if (selectedItems.length !== 1 || selectedItems[0].kind !== "file") {
@@ -766,6 +829,59 @@ async function openViewer() {
}
}
async function openEditor() {
const selectedItems = activePaneState().selectedItems;
if (selectedItems.length !== 1 || !isEditableSelection(selectedItems[0])) {
return;
}
const selected = selectedItems[0];
const editor = editorElements();
editor.overlay.classList.remove("hidden");
editor.title.textContent = "Edit";
editor.fileName.textContent = selected.name;
editor.filePath.textContent = selected.path;
editor.error.textContent = "";
editor.content.value = "";
editor.content.disabled = true;
editor.saveButton.disabled = true;
try {
const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path, for_edit: "true" }).toString()}`);
editor.fileName.textContent = data.name;
editor.filePath.textContent = data.path;
editor.content.value = data.content;
editor.content.disabled = false;
editor.saveButton.disabled = false;
editorState.path = data.path;
editorState.originalContent = data.content;
editorState.modified = data.modified;
editor.content.focus();
} catch (err) {
editor.error.textContent = err.message;
}
}
async function saveEditor() {
if (!editorState.path) {
return;
}
const editor = editorElements();
editor.error.textContent = "";
try {
const response = await apiRequest("POST", "/api/files/save", {
path: editorState.path,
content: editor.content.value,
expected_modified: editorState.modified,
});
editorState.originalContent = editor.content.value;
editorState.modified = response.modified;
setStatus(`Saved ${response.path}`);
closeEditor();
await loadBrowsePane(state.activePane);
} catch (err) {
editor.error.textContent = err.message;
}
}
function moveCurrentRow(delta) {
const pane = state.activePane;
const model = paneState(pane);
@@ -821,6 +937,13 @@ function clearSelectionForActivePane() {
}
function handleKeyboardShortcuts(event) {
if (isEditorOpen()) {
if (event.key === "Escape") {
event.preventDefault();
attemptCloseEditor();
}
return;
}
if (isViewerOpen()) {
if (event.key === "Escape") {
event.preventDefault();
@@ -910,6 +1033,7 @@ function setupEvents() {
setupPaneEvents("right");
document.addEventListener("keydown", handleKeyboardShortcuts);
document.getElementById("view-btn").onclick = openViewer;
document.getElementById("edit-btn").onclick = openEditor;
document.getElementById("rename-btn").onclick = renameSelected;
document.getElementById("delete-btn").onclick = deleteSelected;
document.getElementById("copy-btn").onclick = startCopySelected;
@@ -943,6 +1067,16 @@ function setupEvents() {
closeViewer();
}
};
const editor = editorElements();
editor.closeButton.onclick = attemptCloseEditor;
editor.cancelButton.onclick = attemptCloseEditor;
editor.saveButton.onclick = saveEditor;
editor.overlay.onclick = (event) => {
if (event.target === editor.overlay) {
attemptCloseEditor();
}
};
}
async function init() {