File-Explorer: Editor editierbar + Speichern-Button

This commit is contained in:
Meleki committed 2026-02-28 16:49:15 +01:00
1 parent f74409dd89
commit b84dff6d8c
3 files changed
+157 -8

No files matched your search

+53
View File
@@ -38,6 +38,7 @@ public static class WorkspaceEndpoint
{
app.MapGet("/api/workspace/tree", HandleTree);
app.MapGet("/api/workspace/file", HandleFile);
app.MapPut("/api/workspace/file", HandleFileSave);
app.MapGet("/api/workspace/browse", HandleBrowse);
}
@@ -158,6 +159,58 @@ public static class WorkspaceEndpoint
});
}
private static async Task HandleFileSave(HttpContext ctx)
{
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
var filePath = ctx.Request.Query["path"].FirstOrDefault();
if (string.IsNullOrEmpty(filePath))
{
ctx.Response.StatusCode = 400;
return;
}
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
var session = await db.GetSession(sessionId ?? "");
if (session == null || string.IsNullOrEmpty(session.Workspace))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" });
return;
}
var basePath = Path.GetFullPath(session.Workspace);
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
if (!targetPath.StartsWith(basePath))
{
ctx.Response.StatusCode = 403;
return;
}
if (!File.Exists(targetPath))
{
ctx.Response.StatusCode = 404;
return;
}
using var reader = new StreamReader(ctx.Request.Body);
var content = await reader.ReadToEndAsync();
await File.WriteAllTextAsync(targetPath, content);
var info = new FileInfo(targetPath);
var lineCount = content.Split('\n').Length;
await ctx.Response.WriteAsJsonAsync(new
{
path = filePath,
name = info.Name,
size = info.Length,
lineCount
});
}
private static async Task HandleBrowse(HttpContext ctx)
{
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
+25
View File
@@ -116,6 +116,15 @@
height: 100%;
}
.fe-cm-wrap > div {
display: flex;
flex: 1;
/* kein overflow:hidden – CodeMirror positioniert sein internes textarea
mit negativem Offset; overflow:hidden würde es clippen → kein Input */
position: relative;
height: 100%;
}
.fe-mde-wrap .EasyMDEContainer {
display: flex;
flex: 1;
@@ -261,3 +270,19 @@
flex: 1;
}
.fe-footer-actions {
display: flex;
gap: .5em;
flex-shrink: 0;
}
.fe-save-btn:not(:disabled) {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
.fe-save-btn:not(:disabled):hover {
opacity: 0.85;
}
+79 -8
View File
@@ -41,7 +41,10 @@ function openFileExplorer(opts = {}) {
</div>
<div class="fe-footer" id="feFooter" hidden>
<span class="fe-info" id="feInfo"></span>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
<div class="fe-footer-actions">
<button class="fe-btn fe-save-btn" id="feSaveBtn" disabled hidden>Speichern</button>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
</div>
</div>
`;
@@ -51,6 +54,7 @@ function openFileExplorer(opts = {}) {
document.getElementById('feBack').addEventListener('click', feGoBack);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
document.getElementById('feSaveBtn').addEventListener('click', feDoSave);
// ESC
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
@@ -220,6 +224,12 @@ async function feSelectFile(entry, itemEl) {
}
info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
const saveBtn = document.getElementById('feSaveBtn');
const selectBtn = document.getElementById('feSelectBtn');
saveBtn.hidden = false;
saveBtn.disabled = true;
selectBtn.hidden = !_feState.onSelect;
footer.hidden = false;
} catch (e) {
@@ -239,22 +249,25 @@ function feDestroyEditor() {
}
function feInitCodeMirror(container, data, ext) {
container.innerHTML = '<div class="fe-cm-wrap"><textarea id="feCmTextarea"></textarea></div>';
const textarea = document.getElementById('feCmTextarea');
textarea.value = data.content;
container.innerHTML = '<div class="fe-cm-wrap"><div id="feCmHost"></div></div>';
const host = document.getElementById('feCmHost');
const mode = getCmMode(ext);
const originalContent = data.content;
const cm = CodeMirror.fromTextArea(textarea, {
const cm = CodeMirror(host, {
value: data.content,
mode: mode,
theme: 'default',
lineNumbers: true,
readOnly: true,
readOnly: false,
lineWrapping: false,
tabSize: 4,
indentWithTabs: false,
extraKeys: {}
extraKeys: {
'Ctrl-S': () => feDoSave(),
'Cmd-S': () => feDoSave()
}
});
// Selektion-Tracking für Zeilen-Info
@@ -263,6 +276,15 @@ function feInitCodeMirror(container, data, ext) {
feUpdateSelectionInfo(cm, data);
});
// Änderungs-Tracking
cm.on('change', () => {
const saveBtn = document.getElementById('feSaveBtn');
if (saveBtn) saveBtn.disabled = cm.getValue() === originalContent;
});
// Refresh nötig wenn CM in einem Container ohne finale Größe initialisiert wird
requestAnimationFrame(() => cm.refresh());
_feState._editor = cm;
_feState._editorType = 'codemirror';
}
@@ -374,6 +396,55 @@ function feDoSelect() {
closeFileExplorer();
}
async function feDoSave() {
if (!_feState || !_feState.selectedFile) return;
const cm = feGetCm();
if (!cm) return;
const saveBtn = document.getElementById('feSaveBtn');
if (saveBtn && saveBtn.disabled) return;
const content = cm.getValue();
const path = _feState.selectedFile.path;
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = 'Speichern...';
}
try {
const resp = await fetch(
`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`,
{ method: 'PUT', body: content }
);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
alert('Fehler beim Speichern: ' + (err.error || resp.status));
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
return;
}
const result = await resp.json();
// State aktualisieren
_feState.selectedFile.content = content;
_feState.selectedFile.size = result.size;
_feState.selectedFile.lineCount = result.lineCount;
const info = document.getElementById('feInfo');
if (info) info.textContent = `${path} — ${result.lineCount} Zeilen, ${formatSize(result.size)}`;
if (saveBtn) { saveBtn.textContent = 'Speichern'; }
// bleibt disabled, da gespeicherter Stand == aktueller Stand
} catch (e) {
alert('Netzwerk-Fehler beim Speichern');
if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; }
}
}
// ── Folder Browser Mode ──
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.