From 98262b2efc20e9ecfd6fd49c340936aa624adfa1 Mon Sep 17 00:00:00 2001 From: Rune Harlyk Date: Sat, 24 May 2025 19:23:46 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Improves=20UI=20filesys?= =?UTF-8?q?tem=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/lib/components/icons/index.ts | 1 + app/src/routes/system/filesystem/File.svelte | 29 +++- .../system/filesystem/FileSystem.svelte | 157 +++++++++++++++--- .../routes/system/filesystem/Folder.svelte | 51 +++--- .../system/filesystem/NewFileDialog.svelte | 44 +++++ .../system/filesystem/NewFolderDialog.svelte | 44 +++++ esp32/lib/ESP32-sveltekit/filesystem.cpp | 25 ++- esp32/lib/ESP32-sveltekit/filesystem.h | 2 + esp32/src/spot.cpp | 1 + 9 files changed, 291 insertions(+), 63 deletions(-) create mode 100644 app/src/routes/system/filesystem/NewFileDialog.svelte create mode 100644 app/src/routes/system/filesystem/NewFolderDialog.svelte diff --git a/app/src/lib/components/icons/index.ts b/app/src/lib/components/icons/index.ts index 3a26707..91604d4 100644 --- a/app/src/lib/components/icons/index.ts +++ b/app/src/lib/components/icons/index.ts @@ -35,6 +35,7 @@ export { default as Hamburger } from '~icons/mdi/hamburger-menu' export { default as FileIcon } from '~icons/mdi/file' export { default as FolderIcon } from '~icons/mdi/folder-outline' export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline' +export { default as TrashIcon } from '~icons/mdi/trash' export { default as Down } from '~icons/tabler/chevron-down' export { default as Cancel } from '~icons/tabler/x' diff --git a/app/src/routes/system/filesystem/File.svelte b/app/src/routes/system/filesystem/File.svelte index 52b1777..d079510 100644 --- a/app/src/routes/system/filesystem/File.svelte +++ b/app/src/routes/system/filesystem/File.svelte @@ -1,11 +1,24 @@ - - - - selected(name)}> - {name} - +
+ + + +
diff --git a/app/src/routes/system/filesystem/FileSystem.svelte b/app/src/routes/system/filesystem/FileSystem.svelte index 9b53fcd..4fbddc7 100644 --- a/app/src/routes/system/filesystem/FileSystem.svelte +++ b/app/src/routes/system/filesystem/FileSystem.svelte @@ -4,9 +4,14 @@ import Folder from './Folder.svelte' import { api } from '$lib/api' import type { Directory } from '$lib/types/models' - import { FolderIcon } from '$lib/components/icons' + import { FolderIcon, Add, FileIcon } from '$lib/components/icons' + import { modals } from 'svelte-modals' + import NewFolderDialog from './NewFolderDialog.svelte' + import NewFileDialog from './NewFileDialog.svelte' let filename = $state('') + let content = $state('') + let isEditing = $state(false) const getFiles = async () => { const result = await api.get('/api/files') @@ -20,44 +25,148 @@ if (!name) return '' const result = await api.get(`/api/config/${name}`) if (result.isOk()) { - return JSON.stringify(result.inner, null, 4) + content = JSON.stringify(result.inner, null, 4) + return content } return '' } - const deleteFile = async (name: string) => { - const result = await api.post(`/api/files/delete`, { file: '/config/' + name }) + const saveContent = async () => { + if (!filename) return + const result = await api.post('/api/files/edit', { + file: '/config/' + filename, + content + }) if (result.isOk()) { - return result.inner + isEditing = false + } + } + + const deleteFile = async (name: string) => { + if (!confirm(`Are you sure you want to delete ${name}?`)) return + const result = await api.post('/api/files/delete', { file: '/config/' + name }) + if (result.isOk()) { + filename = '' + content = '' + } + } + + const createFolder = async (folderName: string) => { + if (!folderName) return + const result = await api.post('/api/files/mkdir', { + path: '/config/' + folderName + }) + if (result.isOk()) { + // Refresh the file list + await getFiles() } - return '' } const updateSelected = async (name: string) => { filename = name + isEditing = false + await getContent(name) + } + + const openNewFolderDialog = () => { + modals.open(NewFolderDialog, { + onConfirm: createFolder + }) + } + + const createFile = async (fileName: string) => { + if (!fileName) return + const result = await api.post('/api/files/edit', { + file: '/config/' + fileName, + content: '{}' // Default empty JSON object + }) + if (result.isOk()) { + // Refresh the file list and select the new file + await getFiles() + await updateSelected(fileName) + } + } + + const openNewFileDialog = () => { + modals.open(NewFileDialog, { + onConfirm: createFile + }) } - - {#snippet icon()} - - {/snippet} - {#snippet title()} - File System - {/snippet} -
+ + + + +
+ File System +
+ + +
+
+ + +
+ +
{#await getFiles()} {:then files} - - {/await} - - {#await getContent(filename)} -
- -
- {:then content} -
{content}
+ {/await}
- + + +
+ {#if filename} +
+

{filename}

+
+ {#if isEditing} + + + {:else} + + + {/if} +
+
+ + {#await getContent(filename)} + + {:then _} + {#if isEditing} + + {:else} +
{content}
+ {/if} + {/await} + {:else} +
Select a file to view its contents
+ {/if} +
+
+ diff --git a/app/src/routes/system/filesystem/Folder.svelte b/app/src/routes/system/filesystem/Folder.svelte index 42fc4cf..084f296 100644 --- a/app/src/routes/system/filesystem/Folder.svelte +++ b/app/src/routes/system/filesystem/Folder.svelte @@ -5,37 +5,40 @@ interface Props { expanded?: boolean - name: any + name: string files: any - selected: any + selected: (name: string) => void + onDelete: (name: string) => void } - let { expanded = $bindable(false), name, files, selected }: Props = $props() + let { expanded = $bindable(false), name, files, selected, onDelete }: Props = $props() function toggle() { expanded = !expanded } - +
+ -{#if expanded} -
    - {#each Object.entries(files) as [name, content]} -
  • - {#if typeof content == 'object'} - - {:else} - - {/if} -
  • - {/each} -
-{/if} + {#if expanded} +
    + {#each Object.entries(files) as [itemName, content]} +
  • + {#if typeof content === 'object'} + + {:else} + + {/if} +
  • + {/each} +
+ {/if} +
diff --git a/app/src/routes/system/filesystem/NewFileDialog.svelte b/app/src/routes/system/filesystem/NewFileDialog.svelte new file mode 100644 index 0000000..4695a2b --- /dev/null +++ b/app/src/routes/system/filesystem/NewFileDialog.svelte @@ -0,0 +1,44 @@ + + +{#if isOpen} + +{/if} diff --git a/app/src/routes/system/filesystem/NewFolderDialog.svelte b/app/src/routes/system/filesystem/NewFolderDialog.svelte new file mode 100644 index 0000000..92d0a75 --- /dev/null +++ b/app/src/routes/system/filesystem/NewFolderDialog.svelte @@ -0,0 +1,44 @@ + + +{#if isOpen} + +{/if} diff --git a/esp32/lib/ESP32-sveltekit/filesystem.cpp b/esp32/lib/ESP32-sveltekit/filesystem.cpp index 9e426ef..cf52878 100644 --- a/esp32/lib/ESP32-sveltekit/filesystem.cpp +++ b/esp32/lib/ESP32-sveltekit/filesystem.cpp @@ -44,23 +44,28 @@ bool deleteFile(const char *filename) { return ESPFS.remove(filename); } String listFiles(const String &directory, bool isRoot) { File root = ESPFS.open(directory.startsWith("/") ? directory : "/" + directory); - if (!root.isDirectory()) return ""; + if (!root.isDirectory()) return "{}"; File file = root.openNextFile(); + if (!file) { + return isRoot ? "{ \"root\": {} }" : "{}"; + } + String output = isRoot ? "{ \"root\": {" : "{"; while (file) { + String name = String(file.name()); if (file.isDirectory()) { - output += "\"" + String(file.name()) + "\": " + listFiles(file.name(), false) + ", "; + output += "\"" + name + "\": " + listFiles(name, false); } else { - output += "\"" + String(file.name()) + "\": " + String(file.size()) + ", "; + output += "\"" + name + "\": " + String(file.size()); } - file = root.openNextFile(); + + File next = root.openNextFile(); + if (next) output += ", "; + file = next; } - if (output.endsWith(", ")) { - output.remove(output.length() - 2); - } output += "}"; if (isRoot) output += "}"; @@ -98,4 +103,10 @@ bool editFile(const char *filename, const char *content) { return true; } +esp_err_t mkdir(PsychicRequest *request, JsonVariant &json) { + const char *path = json["path"].as(); + ESP_LOGI(TAG, "Creating directory: %s", path); + return ESPFS.mkdir(path) ? request->reply(200) : request->reply(500); +} + } // namespace FileSystem \ No newline at end of file diff --git a/esp32/lib/ESP32-sveltekit/filesystem.h b/esp32/lib/ESP32-sveltekit/filesystem.h index 3fdbb4f..f13cedf 100644 --- a/esp32/lib/ESP32-sveltekit/filesystem.h +++ b/esp32/lib/ESP32-sveltekit/filesystem.h @@ -26,4 +26,6 @@ esp_err_t uploadFile(PsychicRequest *request, const String &filename, uint64_t i esp_err_t getFiles(PsychicRequest *request); esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json); esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json); + +esp_err_t mkdir(PsychicRequest *request, JsonVariant &json); } // namespace FileSystem \ No newline at end of file diff --git a/esp32/src/spot.cpp b/esp32/src/spot.cpp index ce13f06..bb21bd3 100644 --- a/esp32/src/spot.cpp +++ b/esp32/src/spot.cpp @@ -79,6 +79,7 @@ void Spot::setupServer() { _server.on("/api/files/delete", HTTP_POST, FileSystem::handleDelete); _server.on("/api/files/upload/*", HTTP_POST, FileSystem::uploadHandler); _server.on("/api/files/edit", HTTP_POST, FileSystem::handleEdit); + _server.on("/api/files/mkdir", HTTP_POST, FileSystem::mkdir); // SERVO _server.on("/api/servo/config", HTTP_GET,