feat: monaco editor toegevoegd
This commit is contained in:
Binary file not shown.
@@ -76,7 +76,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('id="settings-logs-list"', body)
|
||||
self.assertIn('id="viewer-content"', body)
|
||||
self.assertIn('id="editor-modal"', body)
|
||||
self.assertIn('id="editor-content"', body)
|
||||
self.assertIn('id="editor-host"', body)
|
||||
self.assertIn('id="editor-save-btn"', body)
|
||||
self.assertIn('id="editor-cancel-btn"', body)
|
||||
self.assertIn('id="move-popup"', body)
|
||||
@@ -158,6 +158,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('return triggerActionButton("rename-btn");', app_js)
|
||||
self.assertIn('function openVideoViewer()', app_js)
|
||||
self.assertIn('function openPdfViewer()', app_js)
|
||||
self.assertIn('async function loadMonacoModule()', app_js)
|
||||
self.assertIn('async function ensureMonacoEditor(path, content)', app_js)
|
||||
self.assertIn('function disposeMonacoEditor()', app_js)
|
||||
self.assertIn('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm', app_js)
|
||||
self.assertIn('document.getElementById("pdf-modal")', app_js)
|
||||
self.assertIn("`/api/files/pdf?", app_js)
|
||||
self.assertIn('if (isPdfSelection(selected)) {', app_js)
|
||||
@@ -186,6 +190,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('.entry-media-icon.pdf', style_css)
|
||||
self.assertIn('.entry-media-svg', style_css)
|
||||
self.assertIn('.entry-media-icon.file', style_css)
|
||||
self.assertIn('.editor-card', style_css)
|
||||
self.assertIn('.editor-host', style_css)
|
||||
self.assertNotIn('.select-marker', style_css)
|
||||
|
||||
app_js_url = app.url_path_for("ui", path="/app.js")
|
||||
|
||||
+137
-10
@@ -32,6 +32,13 @@ let editorState = {
|
||||
originalContent: "",
|
||||
modified: null,
|
||||
};
|
||||
let monacoState = {
|
||||
module: null,
|
||||
loadPromise: null,
|
||||
editor: null,
|
||||
model: null,
|
||||
resizeHandler: null,
|
||||
};
|
||||
let moveState = {
|
||||
source: null,
|
||||
destination: "",
|
||||
@@ -81,6 +88,9 @@ function applyTheme(theme) {
|
||||
button.setAttribute("aria-label", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
|
||||
button.setAttribute("title", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
|
||||
}
|
||||
if (monacoState.module) {
|
||||
monacoState.module.editor.setTheme(nextTheme === "light" ? "vs" : "vs-dark");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -143,7 +153,7 @@ function editorElements() {
|
||||
fileName: document.getElementById("editor-file-name"),
|
||||
filePath: document.getElementById("editor-file-path"),
|
||||
error: document.getElementById("editor-error"),
|
||||
content: document.getElementById("editor-content"),
|
||||
host: document.getElementById("editor-host"),
|
||||
closeButton: document.getElementById("editor-close-btn"),
|
||||
saveButton: document.getElementById("editor-save-btn"),
|
||||
cancelButton: document.getElementById("editor-cancel-btn"),
|
||||
@@ -555,6 +565,47 @@ function isEditableSelection(item) {
|
||||
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
|
||||
}
|
||||
|
||||
function monacoLanguageForName(name) {
|
||||
const lower = (name || "").toLowerCase();
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
return "dockerfile";
|
||||
}
|
||||
if ([".js", ".mjs", ".cjs"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "javascript";
|
||||
}
|
||||
if ([".ts", ".tsx"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "typescript";
|
||||
}
|
||||
if (lower.endsWith(".json")) {
|
||||
return "json";
|
||||
}
|
||||
if (lower.endsWith(".css")) {
|
||||
return "css";
|
||||
}
|
||||
if ([".html", ".htm"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "html";
|
||||
}
|
||||
if (lower.endsWith(".xml")) {
|
||||
return "xml";
|
||||
}
|
||||
if ([".yml", ".yaml"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "yaml";
|
||||
}
|
||||
if (lower.endsWith(".py")) {
|
||||
return "python";
|
||||
}
|
||||
if ([".sh", ".bash", ".zsh", ".fish"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "shell";
|
||||
}
|
||||
if ([".md", ".markdown"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "markdown";
|
||||
}
|
||||
if ([".txt", ".log", ".ini", ".cfg", ".conf"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "plaintext";
|
||||
}
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
function isVideoSelection(item) {
|
||||
if (!item || item.kind !== "file") {
|
||||
return false;
|
||||
@@ -1264,6 +1315,82 @@ function isEditorOpen() {
|
||||
return !editorElements().overlay.classList.contains("hidden");
|
||||
}
|
||||
|
||||
async function loadMonacoModule() {
|
||||
if (monacoState.module) {
|
||||
return monacoState.module;
|
||||
}
|
||||
if (!monacoState.loadPromise) {
|
||||
monacoState.loadPromise = import("https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm")
|
||||
.then((module) => {
|
||||
monacoState.module = module;
|
||||
module.editor.setTheme(document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark");
|
||||
return module;
|
||||
});
|
||||
}
|
||||
return monacoState.loadPromise;
|
||||
}
|
||||
|
||||
function disposeMonacoEditor() {
|
||||
if (typeof monacoState.resizeHandler === "function") {
|
||||
window.removeEventListener("resize", monacoState.resizeHandler);
|
||||
monacoState.resizeHandler = null;
|
||||
}
|
||||
if (monacoState.editor) {
|
||||
monacoState.editor.dispose();
|
||||
monacoState.editor = null;
|
||||
}
|
||||
if (monacoState.model) {
|
||||
monacoState.model.dispose();
|
||||
monacoState.model = null;
|
||||
}
|
||||
}
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||
}
|
||||
|
||||
async function ensureMonacoEditor(path, content) {
|
||||
const editor = editorElements();
|
||||
const monaco = await loadMonacoModule();
|
||||
await nextAnimationFrame();
|
||||
await nextAnimationFrame();
|
||||
disposeMonacoEditor();
|
||||
editor.host.textContent = "";
|
||||
const model = monaco.editor.createModel(content, monacoLanguageForName(baseName(path)));
|
||||
const instance = monaco.editor.create(editor.host, {
|
||||
model,
|
||||
theme: document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark",
|
||||
language: monacoLanguageForName(baseName(path)),
|
||||
automaticLayout: false,
|
||||
lineNumbers: "on",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: "line",
|
||||
wordWrap: "on",
|
||||
fontSize: 13,
|
||||
roundedSelection: false,
|
||||
readOnly: false,
|
||||
});
|
||||
const resizeHandler = () => {
|
||||
if (monacoState.editor) {
|
||||
monacoState.editor.layout();
|
||||
}
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
monacoState.model = model;
|
||||
monacoState.editor = instance;
|
||||
monacoState.resizeHandler = resizeHandler;
|
||||
instance.layout();
|
||||
instance.focus();
|
||||
}
|
||||
|
||||
function currentEditorContent() {
|
||||
if (monacoState.editor) {
|
||||
return monacoState.editor.getValue();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||
}
|
||||
@@ -1854,7 +1981,7 @@ async function openSettings(tab = "general") {
|
||||
}
|
||||
|
||||
function editorIsDirty() {
|
||||
return editorElements().content.value !== editorState.originalContent;
|
||||
return currentEditorContent() !== editorState.originalContent;
|
||||
}
|
||||
|
||||
function resetEditorState() {
|
||||
@@ -1874,9 +2001,10 @@ function attemptCloseEditor() {
|
||||
|
||||
function closeEditor() {
|
||||
const editor = editorElements();
|
||||
disposeMonacoEditor();
|
||||
editor.overlay.classList.add("hidden");
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
editor.host.textContent = "";
|
||||
resetEditorState();
|
||||
}
|
||||
|
||||
@@ -1962,21 +2090,20 @@ async function openEditor() {
|
||||
editor.fileName.textContent = selected.name;
|
||||
editor.filePath.textContent = selected.path;
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
editor.content.disabled = true;
|
||||
editor.host.textContent = "Loading editor...";
|
||||
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;
|
||||
await ensureMonacoEditor(data.path, data.content);
|
||||
editor.saveButton.disabled = false;
|
||||
editorState.path = data.path;
|
||||
editorState.originalContent = data.content;
|
||||
editorState.modified = data.modified;
|
||||
editor.content.focus();
|
||||
} catch (err) {
|
||||
disposeMonacoEditor();
|
||||
editor.host.textContent = "";
|
||||
editor.error.textContent = err.message;
|
||||
}
|
||||
}
|
||||
@@ -2007,10 +2134,10 @@ async function saveEditor() {
|
||||
try {
|
||||
const response = await apiRequest("POST", "/api/files/save", {
|
||||
path: editorState.path,
|
||||
content: editor.content.value,
|
||||
content: currentEditorContent(),
|
||||
expected_modified: editorState.modified,
|
||||
});
|
||||
editorState.originalContent = editor.content.value;
|
||||
editorState.originalContent = currentEditorContent();
|
||||
editorState.modified = response.modified;
|
||||
setStatus(`Saved ${response.path}`);
|
||||
closeEditor();
|
||||
|
||||
@@ -226,13 +226,13 @@
|
||||
</div>
|
||||
|
||||
<div id="editor-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="editor-title">
|
||||
<div class="popup-card viewer-card">
|
||||
<div class="popup-card viewer-card editor-card">
|
||||
<button id="editor-close-btn" class="viewer-close" type="button" aria-label="Close editor">X</button>
|
||||
<h3 id="editor-title">Edit</h3>
|
||||
<div id="editor-file-name" class="popup-meta"></div>
|
||||
<div id="editor-file-path" class="popup-meta"></div>
|
||||
<div id="editor-error" class="error"></div>
|
||||
<textarea id="editor-content" class="editor-content" spellcheck="false"></textarea>
|
||||
<div id="editor-host" class="editor-host" aria-label="Code editor"></div>
|
||||
<div class="popup-actions">
|
||||
<button id="editor-save-btn" type="button">Save</button>
|
||||
<button id="editor-cancel-btn" type="button">Cancel</button>
|
||||
|
||||
+28
-6
@@ -587,6 +587,19 @@ button:disabled {
|
||||
margin: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
#editor-file-name {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#editor-file-path {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
@@ -625,6 +638,11 @@ button:disabled {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-card {
|
||||
width: min(1180px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 24px);
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 180px);
|
||||
@@ -802,7 +820,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.viewer-content,
|
||||
.editor-content {
|
||||
.editor-host {
|
||||
margin: 6px 0 0 0;
|
||||
padding: 11px;
|
||||
overflow: auto;
|
||||
@@ -821,13 +839,17 @@ button:disabled {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
.editor-host {
|
||||
margin-bottom: 8px;
|
||||
min-height: 280px;
|
||||
max-height: calc(100vh - 220px);
|
||||
min-height: 520px;
|
||||
height: calc(100vh - 220px);
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-host .monaco-editor {
|
||||
border-radius: calc(var(--radius-sm) - 1px);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
Reference in New Issue
Block a user