From 6b3289620547e2df3e9f35b7b5638fc53588c8aa Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Feb 2026 19:21:30 +0100 Subject: [PATCH 01/15] feat: add file system using indexedDB --- package.json | 6 +- src/lib/components/Menu/MenuBar.svelte | 3 +- src/lib/components/Menu/MenuItem.svelte | 16 +- src/lib/components/Stores/tabs.ts | 5 + src/lib/components/Tabs/TabBar.svelte | 15 +- src/lib/components/Tabs/types.ts | 1 + src/lib/components/Utils/fileSystem.ts | 138 ++++++++++++++++++ .../Widget/CodeEditor/MonacoEditor.svelte | 15 +- .../ContentBrowserItemCard.svelte | 18 +-- .../ContentBrowserWidget.svelte | 18 ++- .../components/Widget/ContentBrowser/types.ts | 24 ++- src/lib/components/utils.ts | 0 src/routes/+page.svelte | 13 +- 13 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 src/lib/components/Stores/tabs.ts create mode 100644 src/lib/components/Utils/fileSystem.ts delete mode 100644 src/lib/components/utils.ts diff --git a/package.json b/package.json index b65d3bc..7a7b64c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@sveltejs/vite-plugin-svelte": "catalog:core", "@trivago/prettier-plugin-sort-imports": "catalog:lint", "@tsconfig/svelte": "catalog:build", + "@types/file-saver": "^2.0.7", "@unocss/extractor-svelte": "catalog:css", "@unocss/preset-icons": "catalog:css", "@unocss/preset-web-fonts": "catalog:css", @@ -110,6 +111,9 @@ ] }, "dependencies": { - "svelte-kit-sessions": "catalog:core" + "svelte-kit-sessions": "catalog:core", + "file-saver": "^2.0.5", + "idb": "^8.0.3", + "jszip": "^3.10.1" } } diff --git a/src/lib/components/Menu/MenuBar.svelte b/src/lib/components/Menu/MenuBar.svelte index 0800a5d..34458a3 100644 --- a/src/lib/components/Menu/MenuBar.svelte +++ b/src/lib/components/Menu/MenuBar.svelte @@ -1,12 +1,13 @@
Save - Export + exportToZip()}>Export Exit diff --git a/src/lib/components/Menu/MenuItem.svelte b/src/lib/components/Menu/MenuItem.svelte index 940db92..ee24f35 100644 --- a/src/lib/components/Menu/MenuItem.svelte +++ b/src/lib/components/Menu/MenuItem.svelte @@ -5,15 +5,19 @@ interface Props { icon?: string; children?: Snippet; + onClick?: () => void; } - let { icon, children }: Props = $props(); + let { icon, children, onClick }: Props = $props();
- - - {@render children?.()} + +
diff --git a/src/lib/components/Stores/tabs.ts b/src/lib/components/Stores/tabs.ts new file mode 100644 index 0000000..b2a2561 --- /dev/null +++ b/src/lib/components/Stores/tabs.ts @@ -0,0 +1,5 @@ +import { type Tab, tabTypes } from '$lib/components/Tabs/types'; +import { type Writable, writable } from 'svelte/store'; + +export const tabsStore: Writable = writable([{ type: tabTypes[0], title: 'Game' }]); +export const tabSelectedStore: Writable = writable(0); diff --git a/src/lib/components/Tabs/TabBar.svelte b/src/lib/components/Tabs/TabBar.svelte index f732ebb..f3778a7 100644 --- a/src/lib/components/Tabs/TabBar.svelte +++ b/src/lib/components/Tabs/TabBar.svelte @@ -1,22 +1,19 @@
{#each tabs as tab, index (tab)} (selected = index)} - onClose={() => tabs.splice(index, 1)} + onSelect={() => tabSelectedStore.set(index)} + onClose={() => tabsStore.update((tabs) => tabs.splice(index, 1))} /> {/each}
diff --git a/src/lib/components/Tabs/types.ts b/src/lib/components/Tabs/types.ts index 52d4859..f3e7c7d 100644 --- a/src/lib/components/Tabs/types.ts +++ b/src/lib/components/Tabs/types.ts @@ -12,6 +12,7 @@ export interface TabType { export interface Tab { type: TabType; title: string; + filePath?: string; } export const tabTypes: TabType[] = [ diff --git a/src/lib/components/Utils/fileSystem.ts b/src/lib/components/Utils/fileSystem.ts new file mode 100644 index 0000000..bfea074 --- /dev/null +++ b/src/lib/components/Utils/fileSystem.ts @@ -0,0 +1,138 @@ +import fileSaver from 'file-saver'; +import { openDB } from 'idb'; +import JSZip from 'jszip'; + +const dbName = 'NanoForge'; +let dbPromise: Promise | null = null; + +export interface File { + id: string; + lastModified: number; +} + +async function getDB() { + if (!dbPromise) { + dbPromise = openDB(dbName, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains('files')) { + db.createObjectStore('files', { keyPath: 'id' }); + } + }, + }); + } + return dbPromise; +} + +export async function initDB() { + await getDB(); +} + +export async function listFiles(): Promise> { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + + return allFiles.map((file: File) => ({ + id: file.id, + lastModified: file.lastModified, + })); +} + +export async function listFolderContents( + path: string = '', +): Promise> { + const allFiles = await listFiles(); + const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path; + + const items = new Map(); + + const files = allFiles.filter((file) => { + const filePath = file.id; + if (cleanPath === '') { + return !filePath.includes('/'); // Racine + } + return filePath.startsWith(cleanPath + '/') && !filePath.includes('/', cleanPath.length + 1); + }); + + for (const file of files) { + const fileName = file.id.split('/').pop(); + const name = cleanPath === '' ? file.id : (fileName ?? file.id); + items.set(name, { name, type: 'file' as const, lastModified: file.lastModified }); + } + + const folders = new Set(); + for (const file of allFiles) { + const filePath = file.id; + if (cleanPath === '') { + const firstFolder = filePath.split('/')[0]; + if (firstFolder) folders.add(firstFolder); + } else { + const relativePath = filePath.substring(cleanPath.length + 1); + const firstFolder = relativePath.split('/')[0]; + if (firstFolder) folders.add(firstFolder); + } + } + + for (const folder of folders) { + if (!items.has(folder)) { + items.set(folder, { name: folder, type: 'folder' as const }); + } + } + + return Array.from(items.values()).sort((a, b) => { + if (a.type === 'folder' && b.type === 'file') return -1; + if (a.type === 'file' && b.type === 'folder') return 1; + return a.name.localeCompare(b.name); + }); +} + +export async function saveFile(fileName: string, content: string) { + const db = await getDB(); + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + await store.put({ id: fileName, content, lastModified: Date.now() }); + await tx.done; +} + +export async function loadFile(fileName: string) { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + const file = await store.get(fileName); + return file?.content || ''; +} + +function ensureFolder(zip: JSZip, path: string): JSZip { + return zip.folder(path) ?? zip; +} + +export async function exportToZip(filename: string = 'NanoForge.zip') { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + const zip = new JSZip(); + + for (const file of allFiles) { + const parts = file.id.split('/'); + let currentFolder = zip; + + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + const nextFolder = currentFolder.folder(folderName); + if (nextFolder) { + currentFolder = nextFolder; + } else { + currentFolder = ensureFolder(currentFolder, parts[i]); + } + } + + currentFolder.file(parts[parts.length - 1], file.content); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + fileSaver.saveAs(zipBlob, filename); +} diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index 966e67f..7032cef 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,14 +1,21 @@ diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index 6dc89a8..3c7f911 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -3,8 +3,13 @@ import ContentBrowserListFolder from './ContentBrowserListFolder.svelte'; import ContentBrowserItemCard from './ContentBrowserItemCard.svelte'; import { entities } from '../../demo/entities'; + import { onMount } from 'svelte'; + import { listFolderContents } from '$lib/components/Utils/fileSystem'; let items = entities; + let contents: + | Array<{ name: string; type: 'file' | 'folder'; lastModified?: number }> + | undefined = $state(); let selected: string[] = $state(['id1']); function findRecursive(items: ContentBrowserItem[], id: string): ContentBrowserItem | undefined { @@ -18,7 +23,9 @@ } } - let selectedContent = $derived(findRecursive(items, selected.at(-1) || '')); + onMount(async () => { + contents = await listFolderContents('/'); + });
@@ -41,12 +48,9 @@ {/each}
- {#if selectedContent && selectedContent.children} - {#each selectedContent.children as item (item)} - selected.push(item.id) : () => {}} - /> + {#if contents} + {#each contents as content (content)} + {/each} {/if}
diff --git a/src/lib/components/Widget/ContentBrowser/types.ts b/src/lib/components/Widget/ContentBrowser/types.ts index e399485..88bde08 100644 --- a/src/lib/components/Widget/ContentBrowser/types.ts +++ b/src/lib/components/Widget/ContentBrowser/types.ts @@ -1,3 +1,6 @@ +import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; +import { tabTypes } from '$lib/components/Tabs/types'; + export interface ContentBrowserItem { id: string; name: string; @@ -7,12 +10,25 @@ export interface ContentBrowserItem { export interface ContentBrowserItemType { type: string; + suffix: string; icon: string; + onClickEvent?: (filePath: string) => void; } export const contentBrowserItemType: ContentBrowserItemType[] = [ - { type: 'folder', icon: 'i-material-icon-theme-folder-interceptor' }, - { type: 'ts', icon: 'i-material-icon-theme-typescript' }, - { type: 'fbx', icon: 'i-material-icon-theme-3d' }, - { type: 'song', icon: 'i-material-icon-theme-lyric' }, + { type: 'folder', suffix: '/', icon: 'i-material-icon-theme-folder-interceptor' }, + { + type: 'ts', + suffix: '.ts', + icon: 'i-material-icon-theme-typescript', + onClickEvent: (filePath: string) => { + tabsStore.update((tabs) => [ + ...tabs, + { type: tabTypes[1], title: filePath.split('/').pop() || filePath, filePath }, + ]); + tabSelectedStore.update((tabSelected) => tabSelected + 1); + }, + }, + { type: 'fbx', suffix: '.fbx', icon: 'i-material-icon-theme-3d' }, + { type: 'song', suffix: '.mp3', icon: 'i-material-icon-theme-lyric' }, ]; diff --git a/src/lib/components/utils.ts b/src/lib/components/utils.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ef24d60..0cf4470 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,13 +3,10 @@ import MenuBar from '$lib/components/Menu/MenuBar.svelte'; import Logo from '$lib/assets/logo.png'; import TabBar from '$lib/components/Tabs/TabBar.svelte'; - import { type Tab } from '$lib/components/Tabs/types'; - import { tabsExample } from '$lib/components/demo/tabs'; + import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; - let tabs: Tab[] = $state(tabsExample); - let selected = $state(0); - - const Component = $derived(tabs[selected].type.component); + const Component = $derived($tabsStore[$tabSelectedStore].type.component); + const filePath = $derived($tabsStore[$tabSelectedStore].filePath);
@@ -20,7 +17,7 @@
- +
@@ -39,6 +36,6 @@
- +
From ab6778b2714e69d30b6119a3223ce0f1b2ab816d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 01:37:49 +0100 Subject: [PATCH 02/15] fix: list local folder in ContentBrowser --- src/lib/components/Menu/MenuBar.svelte | 30 +++- src/lib/components/Panel/Layout.svelte | 7 +- src/lib/components/Panel/Widget.svelte | 6 +- src/lib/components/Storage/db.ts | 21 +++ src/lib/components/Storage/fileSystem.ts | 91 ++++++++++++ src/lib/components/Storage/tabs.ts | 9 ++ src/lib/components/Stores/workingFile.ts | 3 + .../Tabs/CodeEditor/CodeEditor.svelte | 8 +- .../components/Tabs/MainTab/MainTab.svelte | 6 + src/lib/components/Tabs/TabBar.svelte | 6 +- src/lib/components/Tabs/types.ts | 3 +- src/lib/components/Utils/fileSystem.ts | 138 ------------------ src/lib/components/Utils/zip.ts | 52 +++++++ .../Widget/CodeEditor/MonacoEditor.svelte | 21 ++- .../ContentBrowserItemCard.svelte | 14 +- .../ContentBrowserListFolder.svelte | 93 ++++++------ .../ContentBrowserWidget.svelte | 75 ++++++---- .../components/Widget/ContentBrowser/store.ts | 3 + .../components/Widget/ContentBrowser/types.ts | 7 +- src/routes/+page.svelte | 19 ++- 20 files changed, 368 insertions(+), 244 deletions(-) create mode 100644 src/lib/components/Storage/db.ts create mode 100644 src/lib/components/Storage/fileSystem.ts create mode 100644 src/lib/components/Storage/tabs.ts create mode 100644 src/lib/components/Stores/workingFile.ts delete mode 100644 src/lib/components/Utils/fileSystem.ts create mode 100644 src/lib/components/Utils/zip.ts create mode 100644 src/lib/components/Widget/ContentBrowser/store.ts diff --git a/src/lib/components/Menu/MenuBar.svelte b/src/lib/components/Menu/MenuBar.svelte index 34458a3..ee6299d 100644 --- a/src/lib/components/Menu/MenuBar.svelte +++ b/src/lib/components/Menu/MenuBar.svelte @@ -1,12 +1,38 @@ -
Save + + Import + + exportToZip()}>Export Exit diff --git a/src/lib/components/Panel/Layout.svelte b/src/lib/components/Panel/Layout.svelte index f0f0801..89bed5b 100644 --- a/src/lib/components/Panel/Layout.svelte +++ b/src/lib/components/Panel/Layout.svelte @@ -5,14 +5,16 @@ import type { LayoutItem } from './types'; import { isWidget, isPanel } from './utils'; import Self from './Layout.svelte'; + import type { Tab } from '$lib/components/Tabs/types'; interface Props { layout: LayoutItem; onLayoutChange?: (newLayout: LayoutItem) => void; path?: number[]; + tab: Tab; } - let { layout, onLayoutChange, path = [] }: Props = $props(); + let { layout, onLayoutChange, path = [], tab = $bindable() }: Props = $props(); let dragStartSizes = $state(new Map()); @@ -94,7 +96,7 @@ {#if isWidget(layout)} - + {:else if isPanel(layout)} {#each layout.children as child, index (index)} @@ -102,6 +104,7 @@ layout={child} onLayoutChange={handleChildLayoutChange(index)} path={[...path, index]} + bind:tab /> {#if index < layout.children.length - 1} diff --git a/src/lib/components/Panel/Widget.svelte b/src/lib/components/Panel/Widget.svelte index 46426c0..c23269a 100644 --- a/src/lib/components/Panel/Widget.svelte +++ b/src/lib/components/Panel/Widget.svelte @@ -1,12 +1,14 @@
- +
diff --git a/src/lib/components/Tabs/MainTab/MainTab.svelte b/src/lib/components/Tabs/MainTab/MainTab.svelte index d8afac5..ae9d688 100644 --- a/src/lib/components/Tabs/MainTab/MainTab.svelte +++ b/src/lib/components/Tabs/MainTab/MainTab.svelte @@ -2,6 +2,12 @@ import type { LayoutItem } from '../../Panel/types'; import Layout from '../../Panel/Layout.svelte'; import { cloneLayout } from '../../Panel/utils'; + import type { Tab } from '$lib/components/Tabs/types'; + + interface Props { + tab: Tab; + } + let { tab = $bindable() }: Props = $props(); let layout: LayoutItem = $state({ type: 'panel', diff --git a/src/lib/components/Tabs/TabBar.svelte b/src/lib/components/Tabs/TabBar.svelte index f3778a7..3275b79 100644 --- a/src/lib/components/Tabs/TabBar.svelte +++ b/src/lib/components/Tabs/TabBar.svelte @@ -2,6 +2,7 @@ import TabComponent from './Tab.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; import type { Tab } from '$lib/components/Tabs/types'; + import { workingFileStore } from '$lib/components/Stores/workingFile'; let tabs: Tab[] = $derived($tabsStore); @@ -12,7 +13,10 @@ {tab} selected={$tabSelectedStore === index} closable={tab.type.name !== 'main'} - onSelect={() => tabSelectedStore.set(index)} + onSelect={() => { + tabSelectedStore.set(index); + workingFileStore.set(tabs[index].filePath || ''); + }} onClose={() => tabsStore.update((tabs) => tabs.splice(index, 1))} /> {/each} diff --git a/src/lib/components/Tabs/types.ts b/src/lib/components/Tabs/types.ts index f3e7c7d..6d18bf5 100644 --- a/src/lib/components/Tabs/types.ts +++ b/src/lib/components/Tabs/types.ts @@ -6,13 +6,14 @@ import MainTab from './MainTab/MainTab.svelte'; export interface TabType { name: string; icon: string; - component: Component; + component: Component<{ tab: Tab }>; } export interface Tab { type: TabType; title: string; filePath?: string; + content?: string; } export const tabTypes: TabType[] = [ diff --git a/src/lib/components/Utils/fileSystem.ts b/src/lib/components/Utils/fileSystem.ts deleted file mode 100644 index bfea074..0000000 --- a/src/lib/components/Utils/fileSystem.ts +++ /dev/null @@ -1,138 +0,0 @@ -import fileSaver from 'file-saver'; -import { openDB } from 'idb'; -import JSZip from 'jszip'; - -const dbName = 'NanoForge'; -let dbPromise: Promise | null = null; - -export interface File { - id: string; - lastModified: number; -} - -async function getDB() { - if (!dbPromise) { - dbPromise = openDB(dbName, 1, { - upgrade(db) { - if (!db.objectStoreNames.contains('files')) { - db.createObjectStore('files', { keyPath: 'id' }); - } - }, - }); - } - return dbPromise; -} - -export async function initDB() { - await getDB(); -} - -export async function listFiles(): Promise> { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - - const allFiles = await store.getAll(); - - return allFiles.map((file: File) => ({ - id: file.id, - lastModified: file.lastModified, - })); -} - -export async function listFolderContents( - path: string = '', -): Promise> { - const allFiles = await listFiles(); - const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path; - - const items = new Map(); - - const files = allFiles.filter((file) => { - const filePath = file.id; - if (cleanPath === '') { - return !filePath.includes('/'); // Racine - } - return filePath.startsWith(cleanPath + '/') && !filePath.includes('/', cleanPath.length + 1); - }); - - for (const file of files) { - const fileName = file.id.split('/').pop(); - const name = cleanPath === '' ? file.id : (fileName ?? file.id); - items.set(name, { name, type: 'file' as const, lastModified: file.lastModified }); - } - - const folders = new Set(); - for (const file of allFiles) { - const filePath = file.id; - if (cleanPath === '') { - const firstFolder = filePath.split('/')[0]; - if (firstFolder) folders.add(firstFolder); - } else { - const relativePath = filePath.substring(cleanPath.length + 1); - const firstFolder = relativePath.split('/')[0]; - if (firstFolder) folders.add(firstFolder); - } - } - - for (const folder of folders) { - if (!items.has(folder)) { - items.set(folder, { name: folder, type: 'folder' as const }); - } - } - - return Array.from(items.values()).sort((a, b) => { - if (a.type === 'folder' && b.type === 'file') return -1; - if (a.type === 'file' && b.type === 'folder') return 1; - return a.name.localeCompare(b.name); - }); -} - -export async function saveFile(fileName: string, content: string) { - const db = await getDB(); - const tx = db.transaction('files', 'readwrite'); - const store = tx.objectStore('files'); - await store.put({ id: fileName, content, lastModified: Date.now() }); - await tx.done; -} - -export async function loadFile(fileName: string) { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - const file = await store.get(fileName); - return file?.content || ''; -} - -function ensureFolder(zip: JSZip, path: string): JSZip { - return zip.folder(path) ?? zip; -} - -export async function exportToZip(filename: string = 'NanoForge.zip') { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - - const allFiles = await store.getAll(); - const zip = new JSZip(); - - for (const file of allFiles) { - const parts = file.id.split('/'); - let currentFolder = zip; - - for (let i = 0; i < parts.length - 1; i++) { - const folderName = parts[i]; - const nextFolder = currentFolder.folder(folderName); - if (nextFolder) { - currentFolder = nextFolder; - } else { - currentFolder = ensureFolder(currentFolder, parts[i]); - } - } - - currentFolder.file(parts[parts.length - 1], file.content); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - fileSaver.saveAs(zipBlob, filename); -} diff --git a/src/lib/components/Utils/zip.ts b/src/lib/components/Utils/zip.ts new file mode 100644 index 0000000..403d64d --- /dev/null +++ b/src/lib/components/Utils/zip.ts @@ -0,0 +1,52 @@ +import { getDB } from '$lib/components/Storage/db'; +import { saveFile } from '$lib/components/Storage/fileSystem'; +import fileSaver from 'file-saver'; +import JSZip from 'jszip'; + +function ensureFolder(zip: JSZip, path: string): JSZip { + return zip.folder(path) ?? zip; +} + +export async function exportToZip(filename: string = 'NanoForge.zip') { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + const zip = new JSZip(); + + for (const file of allFiles) { + const parts = file.id.split('/'); + let currentFolder = zip; + + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + const nextFolder = currentFolder.folder(folderName); + if (nextFolder) { + currentFolder = nextFolder; + } else { + currentFolder = ensureFolder(currentFolder, parts[i]); + } + } + + currentFolder.file(parts[parts.length - 1], file.content); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + fileSaver.saveAs(zipBlob, filename); +} + +export async function importFromZip(zipFile: File): Promise { + const zip = new JSZip(); + + await zip.loadAsync(zipFile); + const zipFiles = Object.values(zip.files); + + for (const zipFile of zipFiles) { + if (!zipFile.dir) { + const content = await zipFile.async('string'); + const filePath = zipFile.name; + await saveFile(filePath, content); + } + } +} diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index 7032cef..b3f4d67 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,25 +1,21 @@ diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte index b5b6e75..886a1ee 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte @@ -1,66 +1,55 @@ -{#if item.type === 'folder' || !onlyFolder} - - - {#if open && item.children} - {#each item.children as child, i (i)} - { - ids.unshift(item.id); - return select(ids); - }} - bind:selected - onlyFolder - /> - {/each} + + +{#if open && childFolders.length > 0} + {#each childFolders as child, i (i)} + + {/each} {/if} diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index 3c7f911..a39ca90 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -1,56 +1,79 @@
- {#each items as item (item)} - (selected = ids)} bind:selected /> - {/each} + {#if contents} + {#each rootFolderContent.filter((folder) => folder.type === 'folder') as folder (folder)} + + {/each} + {/if}
-
+
- {#each selected as id, index (id)} + {#each ['Content', ...$ContentBrowserPath] as folder, index (folder)} ($ContentBrowserPath = $ContentBrowserPath.slice(0, index))} + >{folder} - {#if index + 1 < selected.length} + {#if index < $ContentBrowserPath.length} {/if} {/each}
-
+
{#if contents} {#each contents as content (content)} - + { + if (content.type === 'folder') { + navigateToFolder(content.name); + } else { + const itemType = getFileType(content.name); + itemType?.onClickEvent?.([...$ContentBrowserPath, content.name].join('/')); + } + }} + /> {/each} {/if}
diff --git a/src/lib/components/Widget/ContentBrowser/store.ts b/src/lib/components/Widget/ContentBrowser/store.ts new file mode 100644 index 0000000..bb413c6 --- /dev/null +++ b/src/lib/components/Widget/ContentBrowser/store.ts @@ -0,0 +1,3 @@ +import { type Writable, writable } from 'svelte/store'; + +export const ContentBrowserPath: Writable = writable([]); diff --git a/src/lib/components/Widget/ContentBrowser/types.ts b/src/lib/components/Widget/ContentBrowser/types.ts index 88bde08..549b5c4 100644 --- a/src/lib/components/Widget/ContentBrowser/types.ts +++ b/src/lib/components/Widget/ContentBrowser/types.ts @@ -1,5 +1,7 @@ import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; +import { workingFileStore } from '$lib/components/Stores/workingFile'; import { tabTypes } from '$lib/components/Tabs/types'; +import { get } from 'svelte/store'; export interface ContentBrowserItem { id: string; @@ -22,13 +24,16 @@ export const contentBrowserItemType: ContentBrowserItemType[] = [ suffix: '.ts', icon: 'i-material-icon-theme-typescript', onClickEvent: (filePath: string) => { + workingFileStore.set(filePath); tabsStore.update((tabs) => [ ...tabs, { type: tabTypes[1], title: filePath.split('/').pop() || filePath, filePath }, ]); - tabSelectedStore.update((tabSelected) => tabSelected + 1); + tabSelectedStore.set(get(tabsStore).length - 1); }, }, { type: 'fbx', suffix: '.fbx', icon: 'i-material-icon-theme-3d' }, { type: 'song', suffix: '.mp3', icon: 'i-material-icon-theme-lyric' }, + { type: 'json', suffix: '.json', icon: 'i-material-icon-theme-json' }, + { type: 'git', suffix: '.gitignore', icon: 'i-material-icon-theme-git' }, ]; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0cf4470..16070a6 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,9 +4,24 @@ import Logo from '$lib/assets/logo.png'; import TabBar from '$lib/components/Tabs/TabBar.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; + import { onMount } from 'svelte'; + import { loadFile } from '$lib/components/Storage/fileSystem'; const Component = $derived($tabsStore[$tabSelectedStore].type.component); - const filePath = $derived($tabsStore[$tabSelectedStore].filePath); + let tab = $derived($tabsStore[$tabSelectedStore]); + + let contentLoader: Promise; + + onMount(() => { + $effect(() => { + if (tab.filePath) { + contentLoader = loadFile(tab.filePath); + contentLoader.then((c) => { + tab.content = c; + }); + } + }); + });
@@ -36,6 +51,6 @@
- +
From 17056bb5dee42ad1c950fed6feb45c17451985ca Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 01:47:41 +0100 Subject: [PATCH 03/15] fix: install svelte-kit-sessions after rebase --- pnpm-lock.yaml | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b42ca1b..c841179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,15 @@ importers: .: dependencies: + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + idb: + specifier: ^8.0.3 + version: 8.0.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 svelte-kit-sessions: specifier: catalog:core version: 0.4.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)))(svelte@5.48.0) @@ -206,6 +215,9 @@ importers: '@tsconfig/svelte': specifier: catalog:build version: 5.0.8 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@unocss/extractor-svelte': specifier: catalog:css version: 66.6.6 @@ -1220,6 +1232,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1939,6 +1954,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2100,6 +2118,9 @@ packages: engines: {node: '>=18'} hasBin: true + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2108,6 +2129,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2123,6 +2147,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2180,6 +2207,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2240,6 +2270,9 @@ packages: engines: {node: '>=6'} hasBin: true + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2258,6 +2291,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -2498,6 +2534,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2638,6 +2677,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2645,6 +2687,9 @@ packages: quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -2695,6 +2740,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -2703,6 +2751,9 @@ packages: set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2765,6 +2816,9 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4033,6 +4087,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/file-saver@2.0.7': {} + '@types/json-schema@7.0.15': {} '@types/node@25.0.10': @@ -4884,6 +4940,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-saver@2.0.5: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5047,10 +5105,14 @@ snapshots: husky@9.1.7: {} + idb@8.0.3: {} + ignore@5.3.2: {} ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -5065,6 +5127,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + ini@4.1.1: {} irregular-plurals@3.5.0: {} @@ -5103,6 +5167,8 @@ snapshots: is-unicode-supported@2.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5146,6 +5212,13 @@ snapshots: json5@2.2.3: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5161,6 +5234,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.30.2: optional: true @@ -5377,6 +5454,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5485,10 +5564,22 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} + punycode@2.3.1: {} quansync@1.0.0: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -5554,10 +5645,14 @@ snapshots: dependencies: mri: 1.2.0 + safe-buffer@5.1.2: {} + semver@7.7.3: {} set-cookie-parser@3.0.1: {} + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5613,6 +5708,10 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 From 82ef35138fc846117d4c5c665843b455f9bea810 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 02:33:54 +0100 Subject: [PATCH 04/15] fix(test): add windows compatibility on project load test --- src/routes/load-project/load-project.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/load-project/load-project.spec.ts b/src/routes/load-project/load-project.spec.ts index a5456ec..9c9d1dc 100644 --- a/src/routes/load-project/load-project.spec.ts +++ b/src/routes/load-project/load-project.spec.ts @@ -35,7 +35,9 @@ describe('load', () => { location: '/', }); - expect(session.setData).toHaveBeenCalledWith({ path: '/tmp' }); + expect(session.setData).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.any(String) }), + ); expect(cookies.set).toHaveBeenCalledWith('session', 'abc123', { path: '/', httpOnly: true, From 023d9cfc8ad8a6a5dbb348d6d0a1aff6632360cb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 17 Mar 2026 02:31:43 +0100 Subject: [PATCH 05/15] feat: create project push and select project overlay --- src/lib/components/{ => Utils}/Storage/db.ts | 10 ++ .../{ => Utils}/Storage/fileSystem.ts | 2 +- .../components/Utils/Storage/projectSync.ts | 55 ++++++++++ .../components/{ => Utils}/Storage/tabs.ts | 2 +- src/lib/components/Utils/zip.ts | 6 +- .../Widget/CodeEditor/MonacoEditor.svelte | 2 +- .../ContentBrowserListFolder.svelte | 19 ++-- .../ContentBrowserWidget.svelte | 2 +- .../Widget/ScreenView/ScreenViewWidget.svelte | 16 ++- .../server/utils/file-system/project-file.ts | 5 +- src/routes/+page.svelte | 10 +- src/routes/load-project/+page.svelte | 101 ++++++++++++++++-- 12 files changed, 205 insertions(+), 25 deletions(-) rename src/lib/components/{ => Utils}/Storage/db.ts (68%) rename src/lib/components/{ => Utils}/Storage/fileSystem.ts (97%) create mode 100644 src/lib/components/Utils/Storage/projectSync.ts rename src/lib/components/{ => Utils}/Storage/tabs.ts (82%) diff --git a/src/lib/components/Storage/db.ts b/src/lib/components/Utils/Storage/db.ts similarity index 68% rename from src/lib/components/Storage/db.ts rename to src/lib/components/Utils/Storage/db.ts index a76d4de..8c78816 100644 --- a/src/lib/components/Storage/db.ts +++ b/src/lib/components/Utils/Storage/db.ts @@ -19,3 +19,13 @@ export async function getDB() { export async function initDB() { await getDB(); } + +export async function clearDB() { + const db = await getDB(); + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + + await store.clear(); + + await tx.done; +} diff --git a/src/lib/components/Storage/fileSystem.ts b/src/lib/components/Utils/Storage/fileSystem.ts similarity index 97% rename from src/lib/components/Storage/fileSystem.ts rename to src/lib/components/Utils/Storage/fileSystem.ts index a850e10..96aadf1 100644 --- a/src/lib/components/Storage/fileSystem.ts +++ b/src/lib/components/Utils/Storage/fileSystem.ts @@ -1,4 +1,4 @@ -import { getDB } from '$lib/components/Storage/db'; +import { getDB } from '$lib/components/Utils/Storage/db'; export interface File { id: string; diff --git a/src/lib/components/Utils/Storage/projectSync.ts b/src/lib/components/Utils/Storage/projectSync.ts new file mode 100644 index 0000000..4c3593f --- /dev/null +++ b/src/lib/components/Utils/Storage/projectSync.ts @@ -0,0 +1,55 @@ +import { clearDB } from '$lib/components/Utils/Storage/db'; +import { listFiles, loadFile, saveFile } from '$lib/components/Utils/Storage/fileSystem'; + +export async function loadRemoteProject() { + const response = await fetch('/fs?/=readDirRec', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dirPath: '/' }), + }); + + const result = await response.json(); + + if (!result.success) throw new Error(result.errorMsg); + + await clearDB(); + + for (const item of result.dirContent) { + if (item.type === 'file') { + const fileRes = await fetch('/fs?/=readFile', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filePath: item.name }), + }); + const fileResult = await fileRes.json(); + + if (fileResult.success) { + await saveFile(item.name, fileResult.fileContent); + } + } + } +} + +export async function pushLocalProject() { + console.log('🔄 Push projet vers serveur distant...'); + + const localFiles = await listFiles(); + + const pushPromises = localFiles.map(async (file) => { + const content = await loadFile(file.id); + + const formData = new FormData(); + formData.append('filePath', file.id); + formData.append('fileContent', content); + + await fetch('/fs?/writeFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + fileContent: formData.get('fileContent'), + }), + }); + }); + + await Promise.all(pushPromises); +} diff --git a/src/lib/components/Storage/tabs.ts b/src/lib/components/Utils/Storage/tabs.ts similarity index 82% rename from src/lib/components/Storage/tabs.ts rename to src/lib/components/Utils/Storage/tabs.ts index 4dff86b..dce3159 100644 --- a/src/lib/components/Storage/tabs.ts +++ b/src/lib/components/Utils/Storage/tabs.ts @@ -1,4 +1,4 @@ -import { getDB } from '$lib/components/Storage/db'; +import { getDB } from '$lib/components/Utils/Storage/db'; export async function saveTab(fileName: string, content: string) { const db = await getDB(); diff --git a/src/lib/components/Utils/zip.ts b/src/lib/components/Utils/zip.ts index 403d64d..929fb4c 100644 --- a/src/lib/components/Utils/zip.ts +++ b/src/lib/components/Utils/zip.ts @@ -1,5 +1,5 @@ -import { getDB } from '$lib/components/Storage/db'; -import { saveFile } from '$lib/components/Storage/fileSystem'; +import { clearDB, getDB } from '$lib/components/Utils/Storage/db'; +import { saveFile } from '$lib/components/Utils/Storage/fileSystem'; import fileSaver from 'file-saver'; import JSZip from 'jszip'; @@ -42,6 +42,8 @@ export async function importFromZip(zipFile: File): Promise { await zip.loadAsync(zipFile); const zipFiles = Object.values(zip.files); + await clearDB(); + for (const zipFile of zipFiles) { if (!zipFile.dir) { const content = await zipFile.async('string'); diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index b3f4d67..41a315e 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,6 +1,6 @@ {#if open && childFolders.length > 0} {#each childFolders as child, i (i)} - + {/each} {/if} diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index a39ca90..3bffcc3 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -1,7 +1,7 @@ + +
+
+ +
+
diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts index 1535f71..a8af76a 100644 --- a/src/lib/server/utils/file-system/project-file.ts +++ b/src/lib/server/utils/file-system/project-file.ts @@ -25,16 +25,17 @@ export class ProjectFile { } write(text: string): void { + const folderPath = path.dirname(this.path); this._checkPathIsInsideProject(); try { this._checkPathExists(); - } catch { this._checkPathIsFile(); this._checkPathIsWritable(); + } catch { + fs.mkdirSync(folderPath, { recursive: true }); fs.writeFileSync(this.path, text, { flush: true }); return; } - const folderPath = path.dirname(this.path); this._checkPathExists(folderPath); this._checkPathIsDir(folderPath); this._checkPathIsWritable(folderPath); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 16070a6..6cc425c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,7 +5,7 @@ import TabBar from '$lib/components/Tabs/TabBar.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; import { onMount } from 'svelte'; - import { loadFile } from '$lib/components/Storage/fileSystem'; + import { loadFile } from '$lib/components/Utils/Storage/fileSystem'; const Component = $derived($tabsStore[$tabSelectedStore].type.component); let tab = $derived($tabsStore[$tabSelectedStore]); @@ -35,7 +35,13 @@
-
+
+ + +
+ +
+
+ No project created +
+
+
+
+ + From c1ae8655cb063119b87e03c32bd1aefc766f018f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Feb 2026 19:21:30 +0100 Subject: [PATCH 06/15] feat: add file system using indexedDB --- package.json | 6 +- src/lib/components/Menu/MenuBar.svelte | 3 +- src/lib/components/Menu/MenuItem.svelte | 16 +- src/lib/components/Stores/tabs.ts | 5 + src/lib/components/Tabs/TabBar.svelte | 15 +- src/lib/components/Tabs/types.ts | 1 + src/lib/components/Utils/fileSystem.ts | 138 ++++++++++++++++++ .../Widget/CodeEditor/MonacoEditor.svelte | 15 +- .../ContentBrowserItemCard.svelte | 18 +-- .../ContentBrowserWidget.svelte | 18 ++- .../components/Widget/ContentBrowser/types.ts | 24 ++- src/lib/components/utils.ts | 0 src/routes/+page.svelte | 13 +- 13 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 src/lib/components/Stores/tabs.ts create mode 100644 src/lib/components/Utils/fileSystem.ts delete mode 100644 src/lib/components/utils.ts diff --git a/package.json b/package.json index b65d3bc..7a7b64c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@sveltejs/vite-plugin-svelte": "catalog:core", "@trivago/prettier-plugin-sort-imports": "catalog:lint", "@tsconfig/svelte": "catalog:build", + "@types/file-saver": "^2.0.7", "@unocss/extractor-svelte": "catalog:css", "@unocss/preset-icons": "catalog:css", "@unocss/preset-web-fonts": "catalog:css", @@ -110,6 +111,9 @@ ] }, "dependencies": { - "svelte-kit-sessions": "catalog:core" + "svelte-kit-sessions": "catalog:core", + "file-saver": "^2.0.5", + "idb": "^8.0.3", + "jszip": "^3.10.1" } } diff --git a/src/lib/components/Menu/MenuBar.svelte b/src/lib/components/Menu/MenuBar.svelte index 0800a5d..34458a3 100644 --- a/src/lib/components/Menu/MenuBar.svelte +++ b/src/lib/components/Menu/MenuBar.svelte @@ -1,12 +1,13 @@
Save - Export + exportToZip()}>Export Exit diff --git a/src/lib/components/Menu/MenuItem.svelte b/src/lib/components/Menu/MenuItem.svelte index 940db92..ee24f35 100644 --- a/src/lib/components/Menu/MenuItem.svelte +++ b/src/lib/components/Menu/MenuItem.svelte @@ -5,15 +5,19 @@ interface Props { icon?: string; children?: Snippet; + onClick?: () => void; } - let { icon, children }: Props = $props(); + let { icon, children, onClick }: Props = $props();
- - - {@render children?.()} + +
diff --git a/src/lib/components/Stores/tabs.ts b/src/lib/components/Stores/tabs.ts new file mode 100644 index 0000000..b2a2561 --- /dev/null +++ b/src/lib/components/Stores/tabs.ts @@ -0,0 +1,5 @@ +import { type Tab, tabTypes } from '$lib/components/Tabs/types'; +import { type Writable, writable } from 'svelte/store'; + +export const tabsStore: Writable = writable([{ type: tabTypes[0], title: 'Game' }]); +export const tabSelectedStore: Writable = writable(0); diff --git a/src/lib/components/Tabs/TabBar.svelte b/src/lib/components/Tabs/TabBar.svelte index f732ebb..f3778a7 100644 --- a/src/lib/components/Tabs/TabBar.svelte +++ b/src/lib/components/Tabs/TabBar.svelte @@ -1,22 +1,19 @@
{#each tabs as tab, index (tab)} (selected = index)} - onClose={() => tabs.splice(index, 1)} + onSelect={() => tabSelectedStore.set(index)} + onClose={() => tabsStore.update((tabs) => tabs.splice(index, 1))} /> {/each}
diff --git a/src/lib/components/Tabs/types.ts b/src/lib/components/Tabs/types.ts index 52d4859..f3e7c7d 100644 --- a/src/lib/components/Tabs/types.ts +++ b/src/lib/components/Tabs/types.ts @@ -12,6 +12,7 @@ export interface TabType { export interface Tab { type: TabType; title: string; + filePath?: string; } export const tabTypes: TabType[] = [ diff --git a/src/lib/components/Utils/fileSystem.ts b/src/lib/components/Utils/fileSystem.ts new file mode 100644 index 0000000..bfea074 --- /dev/null +++ b/src/lib/components/Utils/fileSystem.ts @@ -0,0 +1,138 @@ +import fileSaver from 'file-saver'; +import { openDB } from 'idb'; +import JSZip from 'jszip'; + +const dbName = 'NanoForge'; +let dbPromise: Promise | null = null; + +export interface File { + id: string; + lastModified: number; +} + +async function getDB() { + if (!dbPromise) { + dbPromise = openDB(dbName, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains('files')) { + db.createObjectStore('files', { keyPath: 'id' }); + } + }, + }); + } + return dbPromise; +} + +export async function initDB() { + await getDB(); +} + +export async function listFiles(): Promise> { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + + return allFiles.map((file: File) => ({ + id: file.id, + lastModified: file.lastModified, + })); +} + +export async function listFolderContents( + path: string = '', +): Promise> { + const allFiles = await listFiles(); + const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path; + + const items = new Map(); + + const files = allFiles.filter((file) => { + const filePath = file.id; + if (cleanPath === '') { + return !filePath.includes('/'); // Racine + } + return filePath.startsWith(cleanPath + '/') && !filePath.includes('/', cleanPath.length + 1); + }); + + for (const file of files) { + const fileName = file.id.split('/').pop(); + const name = cleanPath === '' ? file.id : (fileName ?? file.id); + items.set(name, { name, type: 'file' as const, lastModified: file.lastModified }); + } + + const folders = new Set(); + for (const file of allFiles) { + const filePath = file.id; + if (cleanPath === '') { + const firstFolder = filePath.split('/')[0]; + if (firstFolder) folders.add(firstFolder); + } else { + const relativePath = filePath.substring(cleanPath.length + 1); + const firstFolder = relativePath.split('/')[0]; + if (firstFolder) folders.add(firstFolder); + } + } + + for (const folder of folders) { + if (!items.has(folder)) { + items.set(folder, { name: folder, type: 'folder' as const }); + } + } + + return Array.from(items.values()).sort((a, b) => { + if (a.type === 'folder' && b.type === 'file') return -1; + if (a.type === 'file' && b.type === 'folder') return 1; + return a.name.localeCompare(b.name); + }); +} + +export async function saveFile(fileName: string, content: string) { + const db = await getDB(); + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + await store.put({ id: fileName, content, lastModified: Date.now() }); + await tx.done; +} + +export async function loadFile(fileName: string) { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + const file = await store.get(fileName); + return file?.content || ''; +} + +function ensureFolder(zip: JSZip, path: string): JSZip { + return zip.folder(path) ?? zip; +} + +export async function exportToZip(filename: string = 'NanoForge.zip') { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + const zip = new JSZip(); + + for (const file of allFiles) { + const parts = file.id.split('/'); + let currentFolder = zip; + + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + const nextFolder = currentFolder.folder(folderName); + if (nextFolder) { + currentFolder = nextFolder; + } else { + currentFolder = ensureFolder(currentFolder, parts[i]); + } + } + + currentFolder.file(parts[parts.length - 1], file.content); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + fileSaver.saveAs(zipBlob, filename); +} diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index 966e67f..7032cef 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,14 +1,21 @@ diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index 6dc89a8..3c7f911 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -3,8 +3,13 @@ import ContentBrowserListFolder from './ContentBrowserListFolder.svelte'; import ContentBrowserItemCard from './ContentBrowserItemCard.svelte'; import { entities } from '../../demo/entities'; + import { onMount } from 'svelte'; + import { listFolderContents } from '$lib/components/Utils/fileSystem'; let items = entities; + let contents: + | Array<{ name: string; type: 'file' | 'folder'; lastModified?: number }> + | undefined = $state(); let selected: string[] = $state(['id1']); function findRecursive(items: ContentBrowserItem[], id: string): ContentBrowserItem | undefined { @@ -18,7 +23,9 @@ } } - let selectedContent = $derived(findRecursive(items, selected.at(-1) || '')); + onMount(async () => { + contents = await listFolderContents('/'); + });
@@ -41,12 +48,9 @@ {/each}
- {#if selectedContent && selectedContent.children} - {#each selectedContent.children as item (item)} - selected.push(item.id) : () => {}} - /> + {#if contents} + {#each contents as content (content)} + {/each} {/if}
diff --git a/src/lib/components/Widget/ContentBrowser/types.ts b/src/lib/components/Widget/ContentBrowser/types.ts index e399485..88bde08 100644 --- a/src/lib/components/Widget/ContentBrowser/types.ts +++ b/src/lib/components/Widget/ContentBrowser/types.ts @@ -1,3 +1,6 @@ +import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; +import { tabTypes } from '$lib/components/Tabs/types'; + export interface ContentBrowserItem { id: string; name: string; @@ -7,12 +10,25 @@ export interface ContentBrowserItem { export interface ContentBrowserItemType { type: string; + suffix: string; icon: string; + onClickEvent?: (filePath: string) => void; } export const contentBrowserItemType: ContentBrowserItemType[] = [ - { type: 'folder', icon: 'i-material-icon-theme-folder-interceptor' }, - { type: 'ts', icon: 'i-material-icon-theme-typescript' }, - { type: 'fbx', icon: 'i-material-icon-theme-3d' }, - { type: 'song', icon: 'i-material-icon-theme-lyric' }, + { type: 'folder', suffix: '/', icon: 'i-material-icon-theme-folder-interceptor' }, + { + type: 'ts', + suffix: '.ts', + icon: 'i-material-icon-theme-typescript', + onClickEvent: (filePath: string) => { + tabsStore.update((tabs) => [ + ...tabs, + { type: tabTypes[1], title: filePath.split('/').pop() || filePath, filePath }, + ]); + tabSelectedStore.update((tabSelected) => tabSelected + 1); + }, + }, + { type: 'fbx', suffix: '.fbx', icon: 'i-material-icon-theme-3d' }, + { type: 'song', suffix: '.mp3', icon: 'i-material-icon-theme-lyric' }, ]; diff --git a/src/lib/components/utils.ts b/src/lib/components/utils.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ef24d60..0cf4470 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,13 +3,10 @@ import MenuBar from '$lib/components/Menu/MenuBar.svelte'; import Logo from '$lib/assets/logo.png'; import TabBar from '$lib/components/Tabs/TabBar.svelte'; - import { type Tab } from '$lib/components/Tabs/types'; - import { tabsExample } from '$lib/components/demo/tabs'; + import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; - let tabs: Tab[] = $state(tabsExample); - let selected = $state(0); - - const Component = $derived(tabs[selected].type.component); + const Component = $derived($tabsStore[$tabSelectedStore].type.component); + const filePath = $derived($tabsStore[$tabSelectedStore].filePath);
@@ -20,7 +17,7 @@
- +
@@ -39,6 +36,6 @@
- +
From dde5c8a67e8021efea9449a4f4c986efc017ada1 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 01:37:49 +0100 Subject: [PATCH 07/15] fix: list local folder in ContentBrowser --- src/lib/components/Menu/MenuBar.svelte | 30 +++- src/lib/components/Panel/Layout.svelte | 7 +- src/lib/components/Panel/Widget.svelte | 6 +- src/lib/components/Storage/db.ts | 21 +++ src/lib/components/Storage/fileSystem.ts | 91 ++++++++++++ src/lib/components/Storage/tabs.ts | 9 ++ src/lib/components/Stores/workingFile.ts | 3 + .../Tabs/CodeEditor/CodeEditor.svelte | 8 +- .../components/Tabs/MainTab/MainTab.svelte | 6 + src/lib/components/Tabs/TabBar.svelte | 6 +- src/lib/components/Tabs/types.ts | 3 +- src/lib/components/Utils/fileSystem.ts | 138 ------------------ src/lib/components/Utils/zip.ts | 52 +++++++ .../Widget/CodeEditor/MonacoEditor.svelte | 21 ++- .../ContentBrowserItemCard.svelte | 14 +- .../ContentBrowserListFolder.svelte | 93 ++++++------ .../ContentBrowserWidget.svelte | 75 ++++++---- .../components/Widget/ContentBrowser/store.ts | 3 + .../components/Widget/ContentBrowser/types.ts | 7 +- src/routes/+page.svelte | 19 ++- 20 files changed, 368 insertions(+), 244 deletions(-) create mode 100644 src/lib/components/Storage/db.ts create mode 100644 src/lib/components/Storage/fileSystem.ts create mode 100644 src/lib/components/Storage/tabs.ts create mode 100644 src/lib/components/Stores/workingFile.ts delete mode 100644 src/lib/components/Utils/fileSystem.ts create mode 100644 src/lib/components/Utils/zip.ts create mode 100644 src/lib/components/Widget/ContentBrowser/store.ts diff --git a/src/lib/components/Menu/MenuBar.svelte b/src/lib/components/Menu/MenuBar.svelte index 34458a3..ee6299d 100644 --- a/src/lib/components/Menu/MenuBar.svelte +++ b/src/lib/components/Menu/MenuBar.svelte @@ -1,12 +1,38 @@ -
Save + + Import + + exportToZip()}>Export Exit diff --git a/src/lib/components/Panel/Layout.svelte b/src/lib/components/Panel/Layout.svelte index f0f0801..89bed5b 100644 --- a/src/lib/components/Panel/Layout.svelte +++ b/src/lib/components/Panel/Layout.svelte @@ -5,14 +5,16 @@ import type { LayoutItem } from './types'; import { isWidget, isPanel } from './utils'; import Self from './Layout.svelte'; + import type { Tab } from '$lib/components/Tabs/types'; interface Props { layout: LayoutItem; onLayoutChange?: (newLayout: LayoutItem) => void; path?: number[]; + tab: Tab; } - let { layout, onLayoutChange, path = [] }: Props = $props(); + let { layout, onLayoutChange, path = [], tab = $bindable() }: Props = $props(); let dragStartSizes = $state(new Map()); @@ -94,7 +96,7 @@ {#if isWidget(layout)} - + {:else if isPanel(layout)} {#each layout.children as child, index (index)} @@ -102,6 +104,7 @@ layout={child} onLayoutChange={handleChildLayoutChange(index)} path={[...path, index]} + bind:tab /> {#if index < layout.children.length - 1} diff --git a/src/lib/components/Panel/Widget.svelte b/src/lib/components/Panel/Widget.svelte index 46426c0..c23269a 100644 --- a/src/lib/components/Panel/Widget.svelte +++ b/src/lib/components/Panel/Widget.svelte @@ -1,12 +1,14 @@
- +
diff --git a/src/lib/components/Tabs/MainTab/MainTab.svelte b/src/lib/components/Tabs/MainTab/MainTab.svelte index d8afac5..ae9d688 100644 --- a/src/lib/components/Tabs/MainTab/MainTab.svelte +++ b/src/lib/components/Tabs/MainTab/MainTab.svelte @@ -2,6 +2,12 @@ import type { LayoutItem } from '../../Panel/types'; import Layout from '../../Panel/Layout.svelte'; import { cloneLayout } from '../../Panel/utils'; + import type { Tab } from '$lib/components/Tabs/types'; + + interface Props { + tab: Tab; + } + let { tab = $bindable() }: Props = $props(); let layout: LayoutItem = $state({ type: 'panel', diff --git a/src/lib/components/Tabs/TabBar.svelte b/src/lib/components/Tabs/TabBar.svelte index f3778a7..3275b79 100644 --- a/src/lib/components/Tabs/TabBar.svelte +++ b/src/lib/components/Tabs/TabBar.svelte @@ -2,6 +2,7 @@ import TabComponent from './Tab.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; import type { Tab } from '$lib/components/Tabs/types'; + import { workingFileStore } from '$lib/components/Stores/workingFile'; let tabs: Tab[] = $derived($tabsStore); @@ -12,7 +13,10 @@ {tab} selected={$tabSelectedStore === index} closable={tab.type.name !== 'main'} - onSelect={() => tabSelectedStore.set(index)} + onSelect={() => { + tabSelectedStore.set(index); + workingFileStore.set(tabs[index].filePath || ''); + }} onClose={() => tabsStore.update((tabs) => tabs.splice(index, 1))} /> {/each} diff --git a/src/lib/components/Tabs/types.ts b/src/lib/components/Tabs/types.ts index f3e7c7d..6d18bf5 100644 --- a/src/lib/components/Tabs/types.ts +++ b/src/lib/components/Tabs/types.ts @@ -6,13 +6,14 @@ import MainTab from './MainTab/MainTab.svelte'; export interface TabType { name: string; icon: string; - component: Component; + component: Component<{ tab: Tab }>; } export interface Tab { type: TabType; title: string; filePath?: string; + content?: string; } export const tabTypes: TabType[] = [ diff --git a/src/lib/components/Utils/fileSystem.ts b/src/lib/components/Utils/fileSystem.ts deleted file mode 100644 index bfea074..0000000 --- a/src/lib/components/Utils/fileSystem.ts +++ /dev/null @@ -1,138 +0,0 @@ -import fileSaver from 'file-saver'; -import { openDB } from 'idb'; -import JSZip from 'jszip'; - -const dbName = 'NanoForge'; -let dbPromise: Promise | null = null; - -export interface File { - id: string; - lastModified: number; -} - -async function getDB() { - if (!dbPromise) { - dbPromise = openDB(dbName, 1, { - upgrade(db) { - if (!db.objectStoreNames.contains('files')) { - db.createObjectStore('files', { keyPath: 'id' }); - } - }, - }); - } - return dbPromise; -} - -export async function initDB() { - await getDB(); -} - -export async function listFiles(): Promise> { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - - const allFiles = await store.getAll(); - - return allFiles.map((file: File) => ({ - id: file.id, - lastModified: file.lastModified, - })); -} - -export async function listFolderContents( - path: string = '', -): Promise> { - const allFiles = await listFiles(); - const cleanPath = path.endsWith('/') ? path.slice(0, -1) : path; - - const items = new Map(); - - const files = allFiles.filter((file) => { - const filePath = file.id; - if (cleanPath === '') { - return !filePath.includes('/'); // Racine - } - return filePath.startsWith(cleanPath + '/') && !filePath.includes('/', cleanPath.length + 1); - }); - - for (const file of files) { - const fileName = file.id.split('/').pop(); - const name = cleanPath === '' ? file.id : (fileName ?? file.id); - items.set(name, { name, type: 'file' as const, lastModified: file.lastModified }); - } - - const folders = new Set(); - for (const file of allFiles) { - const filePath = file.id; - if (cleanPath === '') { - const firstFolder = filePath.split('/')[0]; - if (firstFolder) folders.add(firstFolder); - } else { - const relativePath = filePath.substring(cleanPath.length + 1); - const firstFolder = relativePath.split('/')[0]; - if (firstFolder) folders.add(firstFolder); - } - } - - for (const folder of folders) { - if (!items.has(folder)) { - items.set(folder, { name: folder, type: 'folder' as const }); - } - } - - return Array.from(items.values()).sort((a, b) => { - if (a.type === 'folder' && b.type === 'file') return -1; - if (a.type === 'file' && b.type === 'folder') return 1; - return a.name.localeCompare(b.name); - }); -} - -export async function saveFile(fileName: string, content: string) { - const db = await getDB(); - const tx = db.transaction('files', 'readwrite'); - const store = tx.objectStore('files'); - await store.put({ id: fileName, content, lastModified: Date.now() }); - await tx.done; -} - -export async function loadFile(fileName: string) { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - const file = await store.get(fileName); - return file?.content || ''; -} - -function ensureFolder(zip: JSZip, path: string): JSZip { - return zip.folder(path) ?? zip; -} - -export async function exportToZip(filename: string = 'NanoForge.zip') { - const db = await getDB(); - const tx = db.transaction('files', 'readonly'); - const store = tx.objectStore('files'); - - const allFiles = await store.getAll(); - const zip = new JSZip(); - - for (const file of allFiles) { - const parts = file.id.split('/'); - let currentFolder = zip; - - for (let i = 0; i < parts.length - 1; i++) { - const folderName = parts[i]; - const nextFolder = currentFolder.folder(folderName); - if (nextFolder) { - currentFolder = nextFolder; - } else { - currentFolder = ensureFolder(currentFolder, parts[i]); - } - } - - currentFolder.file(parts[parts.length - 1], file.content); - } - - const zipBlob = await zip.generateAsync({ type: 'blob' }); - fileSaver.saveAs(zipBlob, filename); -} diff --git a/src/lib/components/Utils/zip.ts b/src/lib/components/Utils/zip.ts new file mode 100644 index 0000000..403d64d --- /dev/null +++ b/src/lib/components/Utils/zip.ts @@ -0,0 +1,52 @@ +import { getDB } from '$lib/components/Storage/db'; +import { saveFile } from '$lib/components/Storage/fileSystem'; +import fileSaver from 'file-saver'; +import JSZip from 'jszip'; + +function ensureFolder(zip: JSZip, path: string): JSZip { + return zip.folder(path) ?? zip; +} + +export async function exportToZip(filename: string = 'NanoForge.zip') { + const db = await getDB(); + const tx = db.transaction('files', 'readonly'); + const store = tx.objectStore('files'); + + const allFiles = await store.getAll(); + const zip = new JSZip(); + + for (const file of allFiles) { + const parts = file.id.split('/'); + let currentFolder = zip; + + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + const nextFolder = currentFolder.folder(folderName); + if (nextFolder) { + currentFolder = nextFolder; + } else { + currentFolder = ensureFolder(currentFolder, parts[i]); + } + } + + currentFolder.file(parts[parts.length - 1], file.content); + } + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + fileSaver.saveAs(zipBlob, filename); +} + +export async function importFromZip(zipFile: File): Promise { + const zip = new JSZip(); + + await zip.loadAsync(zipFile); + const zipFiles = Object.values(zip.files); + + for (const zipFile of zipFiles) { + if (!zipFile.dir) { + const content = await zipFile.async('string'); + const filePath = zipFile.name; + await saveFile(filePath, content); + } + } +} diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index 7032cef..b3f4d67 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,25 +1,21 @@ diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte index b5b6e75..886a1ee 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserListFolder.svelte @@ -1,66 +1,55 @@ -{#if item.type === 'folder' || !onlyFolder} - - - {#if open && item.children} - {#each item.children as child, i (i)} - { - ids.unshift(item.id); - return select(ids); - }} - bind:selected - onlyFolder - /> - {/each} + + +{#if open && childFolders.length > 0} + {#each childFolders as child, i (i)} + + {/each} {/if} diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index 3c7f911..a39ca90 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -1,56 +1,79 @@
- {#each items as item (item)} - (selected = ids)} bind:selected /> - {/each} + {#if contents} + {#each rootFolderContent.filter((folder) => folder.type === 'folder') as folder (folder)} + + {/each} + {/if}
-
+
- {#each selected as id, index (id)} + {#each ['Content', ...$ContentBrowserPath] as folder, index (folder)} ($ContentBrowserPath = $ContentBrowserPath.slice(0, index))} + >{folder} - {#if index + 1 < selected.length} + {#if index < $ContentBrowserPath.length} {/if} {/each}
-
+
{#if contents} {#each contents as content (content)} - + { + if (content.type === 'folder') { + navigateToFolder(content.name); + } else { + const itemType = getFileType(content.name); + itemType?.onClickEvent?.([...$ContentBrowserPath, content.name].join('/')); + } + }} + /> {/each} {/if}
diff --git a/src/lib/components/Widget/ContentBrowser/store.ts b/src/lib/components/Widget/ContentBrowser/store.ts new file mode 100644 index 0000000..bb413c6 --- /dev/null +++ b/src/lib/components/Widget/ContentBrowser/store.ts @@ -0,0 +1,3 @@ +import { type Writable, writable } from 'svelte/store'; + +export const ContentBrowserPath: Writable = writable([]); diff --git a/src/lib/components/Widget/ContentBrowser/types.ts b/src/lib/components/Widget/ContentBrowser/types.ts index 88bde08..549b5c4 100644 --- a/src/lib/components/Widget/ContentBrowser/types.ts +++ b/src/lib/components/Widget/ContentBrowser/types.ts @@ -1,5 +1,7 @@ import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; +import { workingFileStore } from '$lib/components/Stores/workingFile'; import { tabTypes } from '$lib/components/Tabs/types'; +import { get } from 'svelte/store'; export interface ContentBrowserItem { id: string; @@ -22,13 +24,16 @@ export const contentBrowserItemType: ContentBrowserItemType[] = [ suffix: '.ts', icon: 'i-material-icon-theme-typescript', onClickEvent: (filePath: string) => { + workingFileStore.set(filePath); tabsStore.update((tabs) => [ ...tabs, { type: tabTypes[1], title: filePath.split('/').pop() || filePath, filePath }, ]); - tabSelectedStore.update((tabSelected) => tabSelected + 1); + tabSelectedStore.set(get(tabsStore).length - 1); }, }, { type: 'fbx', suffix: '.fbx', icon: 'i-material-icon-theme-3d' }, { type: 'song', suffix: '.mp3', icon: 'i-material-icon-theme-lyric' }, + { type: 'json', suffix: '.json', icon: 'i-material-icon-theme-json' }, + { type: 'git', suffix: '.gitignore', icon: 'i-material-icon-theme-git' }, ]; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0cf4470..16070a6 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,9 +4,24 @@ import Logo from '$lib/assets/logo.png'; import TabBar from '$lib/components/Tabs/TabBar.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; + import { onMount } from 'svelte'; + import { loadFile } from '$lib/components/Storage/fileSystem'; const Component = $derived($tabsStore[$tabSelectedStore].type.component); - const filePath = $derived($tabsStore[$tabSelectedStore].filePath); + let tab = $derived($tabsStore[$tabSelectedStore]); + + let contentLoader: Promise; + + onMount(() => { + $effect(() => { + if (tab.filePath) { + contentLoader = loadFile(tab.filePath); + contentLoader.then((c) => { + tab.content = c; + }); + } + }); + });
@@ -36,6 +51,6 @@
- +
From 6a51d51189c117a4a654dc48698a9b2d8eea66ac Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 02:33:54 +0100 Subject: [PATCH 08/15] fix(test): add windows compatibility on project load test --- src/routes/load-project/load-project.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/load-project/load-project.spec.ts b/src/routes/load-project/load-project.spec.ts index a5456ec..9c9d1dc 100644 --- a/src/routes/load-project/load-project.spec.ts +++ b/src/routes/load-project/load-project.spec.ts @@ -35,7 +35,9 @@ describe('load', () => { location: '/', }); - expect(session.setData).toHaveBeenCalledWith({ path: '/tmp' }); + expect(session.setData).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.any(String) }), + ); expect(cookies.set).toHaveBeenCalledWith('session', 'abc123', { path: '/', httpOnly: true, From 5111bb3d8e3330284b165d3458d0027d8875157b Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 17 Mar 2026 02:31:43 +0100 Subject: [PATCH 09/15] feat: create project push and select project overlay --- src/lib/components/{ => Utils}/Storage/db.ts | 10 + .../{ => Utils}/Storage/fileSystem.ts | 2 +- .../components/Utils/Storage/projectSync.ts | 55 ++++++ .../components/{ => Utils}/Storage/tabs.ts | 2 +- src/lib/components/Utils/zip.ts | 6 +- .../Widget/CodeEditor/MonacoEditor.svelte | 2 +- .../ContentBrowserListFolder.svelte | 19 +- .../ContentBrowserWidget.svelte | 2 +- .../Widget/ScreenView/ScreenViewWidget.svelte | 16 +- .../server/utils/file-system/project-file.ts | 5 +- src/routes/+page.svelte | 10 +- src/routes/load-project/+page.svelte | 183 +++++++++--------- 12 files changed, 205 insertions(+), 107 deletions(-) rename src/lib/components/{ => Utils}/Storage/db.ts (68%) rename src/lib/components/{ => Utils}/Storage/fileSystem.ts (97%) create mode 100644 src/lib/components/Utils/Storage/projectSync.ts rename src/lib/components/{ => Utils}/Storage/tabs.ts (82%) diff --git a/src/lib/components/Storage/db.ts b/src/lib/components/Utils/Storage/db.ts similarity index 68% rename from src/lib/components/Storage/db.ts rename to src/lib/components/Utils/Storage/db.ts index a76d4de..8c78816 100644 --- a/src/lib/components/Storage/db.ts +++ b/src/lib/components/Utils/Storage/db.ts @@ -19,3 +19,13 @@ export async function getDB() { export async function initDB() { await getDB(); } + +export async function clearDB() { + const db = await getDB(); + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + + await store.clear(); + + await tx.done; +} diff --git a/src/lib/components/Storage/fileSystem.ts b/src/lib/components/Utils/Storage/fileSystem.ts similarity index 97% rename from src/lib/components/Storage/fileSystem.ts rename to src/lib/components/Utils/Storage/fileSystem.ts index a850e10..96aadf1 100644 --- a/src/lib/components/Storage/fileSystem.ts +++ b/src/lib/components/Utils/Storage/fileSystem.ts @@ -1,4 +1,4 @@ -import { getDB } from '$lib/components/Storage/db'; +import { getDB } from '$lib/components/Utils/Storage/db'; export interface File { id: string; diff --git a/src/lib/components/Utils/Storage/projectSync.ts b/src/lib/components/Utils/Storage/projectSync.ts new file mode 100644 index 0000000..4c3593f --- /dev/null +++ b/src/lib/components/Utils/Storage/projectSync.ts @@ -0,0 +1,55 @@ +import { clearDB } from '$lib/components/Utils/Storage/db'; +import { listFiles, loadFile, saveFile } from '$lib/components/Utils/Storage/fileSystem'; + +export async function loadRemoteProject() { + const response = await fetch('/fs?/=readDirRec', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dirPath: '/' }), + }); + + const result = await response.json(); + + if (!result.success) throw new Error(result.errorMsg); + + await clearDB(); + + for (const item of result.dirContent) { + if (item.type === 'file') { + const fileRes = await fetch('/fs?/=readFile', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filePath: item.name }), + }); + const fileResult = await fileRes.json(); + + if (fileResult.success) { + await saveFile(item.name, fileResult.fileContent); + } + } + } +} + +export async function pushLocalProject() { + console.log('🔄 Push projet vers serveur distant...'); + + const localFiles = await listFiles(); + + const pushPromises = localFiles.map(async (file) => { + const content = await loadFile(file.id); + + const formData = new FormData(); + formData.append('filePath', file.id); + formData.append('fileContent', content); + + await fetch('/fs?/writeFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + fileContent: formData.get('fileContent'), + }), + }); + }); + + await Promise.all(pushPromises); +} diff --git a/src/lib/components/Storage/tabs.ts b/src/lib/components/Utils/Storage/tabs.ts similarity index 82% rename from src/lib/components/Storage/tabs.ts rename to src/lib/components/Utils/Storage/tabs.ts index 4dff86b..dce3159 100644 --- a/src/lib/components/Storage/tabs.ts +++ b/src/lib/components/Utils/Storage/tabs.ts @@ -1,4 +1,4 @@ -import { getDB } from '$lib/components/Storage/db'; +import { getDB } from '$lib/components/Utils/Storage/db'; export async function saveTab(fileName: string, content: string) { const db = await getDB(); diff --git a/src/lib/components/Utils/zip.ts b/src/lib/components/Utils/zip.ts index 403d64d..929fb4c 100644 --- a/src/lib/components/Utils/zip.ts +++ b/src/lib/components/Utils/zip.ts @@ -1,5 +1,5 @@ -import { getDB } from '$lib/components/Storage/db'; -import { saveFile } from '$lib/components/Storage/fileSystem'; +import { clearDB, getDB } from '$lib/components/Utils/Storage/db'; +import { saveFile } from '$lib/components/Utils/Storage/fileSystem'; import fileSaver from 'file-saver'; import JSZip from 'jszip'; @@ -42,6 +42,8 @@ export async function importFromZip(zipFile: File): Promise { await zip.loadAsync(zipFile); const zipFiles = Object.values(zip.files); + await clearDB(); + for (const zipFile of zipFiles) { if (!zipFile.dir) { const content = await zipFile.async('string'); diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index b3f4d67..41a315e 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,6 +1,6 @@ {#if open && childFolders.length > 0} {#each childFolders as child, i (i)} - + {/each} {/if} diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte index a39ca90..3bffcc3 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserWidget.svelte @@ -1,7 +1,7 @@ + +
+
+ +
+
diff --git a/src/lib/server/utils/file-system/project-file.ts b/src/lib/server/utils/file-system/project-file.ts index 1535f71..a8af76a 100644 --- a/src/lib/server/utils/file-system/project-file.ts +++ b/src/lib/server/utils/file-system/project-file.ts @@ -25,16 +25,17 @@ export class ProjectFile { } write(text: string): void { + const folderPath = path.dirname(this.path); this._checkPathIsInsideProject(); try { this._checkPathExists(); - } catch { this._checkPathIsFile(); this._checkPathIsWritable(); + } catch { + fs.mkdirSync(folderPath, { recursive: true }); fs.writeFileSync(this.path, text, { flush: true }); return; } - const folderPath = path.dirname(this.path); this._checkPathExists(folderPath); this._checkPathIsDir(folderPath); this._checkPathIsWritable(folderPath); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 16070a6..6cc425c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,7 +5,7 @@ import TabBar from '$lib/components/Tabs/TabBar.svelte'; import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; import { onMount } from 'svelte'; - import { loadFile } from '$lib/components/Storage/fileSystem'; + import { loadFile } from '$lib/components/Utils/Storage/fileSystem'; const Component = $derived($tabsStore[$tabSelectedStore].type.component); let tab = $derived($tabsStore[$tabSelectedStore]); @@ -35,7 +35,13 @@
-
+
+ - - {/if} -
+
+
+
+ + +
+ +
+
+ No project created +
+
+
+
+ + From 3d886f46939d4de3ddd4fffd55f31b56b4a94396 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 17 Mar 2026 23:41:45 +0100 Subject: [PATCH 10/15] feat: create new project dialog --- pnpm-lock.yaml | 220 +++++++++++++----- src/lib/assets/Tools/play.png | Bin 0 -> 961 bytes .../components/Utils/Storage/projectSync.ts | 25 +- src/routes/load-project/+page.svelte | 171 ++++++++++---- 4 files changed, 312 insertions(+), 104 deletions(-) create mode 100644 src/lib/assets/Tools/play.png diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08a7574..763b9a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,7 +56,7 @@ catalogs: version: 6.2.4 svelte: specifier: ^5.48.0 - version: 5.53.12 + version: 5.54.0 svelte-check: specifier: ^4.3.5 version: 4.4.5 @@ -151,9 +151,18 @@ importers: .: dependencies: + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + idb: + specifier: ^8.0.3 + version: 8.0.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 svelte-kit-sessions: specifier: catalog:core - version: 0.4.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12) + version: 0.4.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0) devDependencies: '@alexanderniebuhr/prettier-plugin-unocss': specifier: catalog:css @@ -193,19 +202,22 @@ importers: version: 1.58.2 '@sveltejs/adapter-auto': specifier: catalog:core - version: 7.0.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))) + version: 7.0.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))) '@sveltejs/kit': specifier: catalog:core - version: 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: catalog:core - version: 6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + version: 6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@trivago/prettier-plugin-sort-imports': specifier: catalog:lint - version: 6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.12))(prettier@3.8.1)(svelte@5.53.12) + version: 6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0))(prettier@3.8.1)(svelte@5.54.0) '@tsconfig/svelte': specifier: catalog:build version: 5.0.8 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@unocss/extractor-svelte': specifier: catalog:css version: 66.6.6 @@ -229,13 +241,13 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-plugin-svelte: specifier: catalog:lint - version: 3.15.2(eslint@9.39.4(jiti@2.6.1))(svelte@5.53.12) + version: 3.15.2(eslint@9.39.4(jiti@2.6.1))(svelte@5.54.0) flowbite: specifier: catalog:components version: 4.0.1(rollup@4.59.0) flowbite-svelte: specifier: catalog:components - version: 1.31.0(rollup@4.59.0)(svelte@5.53.12)(tailwindcss@4.2.1) + version: 1.31.0(rollup@4.59.0)(svelte@5.54.0)(tailwindcss@4.2.1) globals: specifier: catalog:lint version: 17.4.0 @@ -256,16 +268,16 @@ importers: version: 3.8.1 prettier-plugin-svelte: specifier: catalog:lint - version: 3.5.1(prettier@3.8.1)(svelte@5.53.12) + version: 3.5.1(prettier@3.8.1)(svelte@5.54.0) svelte: specifier: catalog:core - version: 5.53.12 + version: 5.54.0 svelte-check: specifier: catalog:core - version: 4.4.5(picomatch@4.0.3)(svelte@5.53.12)(typescript@5.9.3) + version: 4.4.5(picomatch@4.0.3)(svelte@5.54.0)(typescript@5.9.3) svelte-sonner: specifier: catalog:components - version: 1.1.0(svelte@5.53.12) + version: 1.1.0(svelte@5.54.0) typescript: specifier: catalog:build version: 5.9.3 @@ -283,7 +295,7 @@ importers: version: 4.1.0(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) vitest-browser-svelte: specifier: catalog:test - version: 2.1.0(svelte@5.53.12)(vitest@4.1.0) + version: 2.1.0(svelte@5.54.0)(vitest@4.1.0) packages: @@ -1449,6 +1461,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1865,6 +1880,9 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig-typescript-loader@6.2.0: resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} engines: {node: '>=v18'} @@ -2144,6 +2162,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2152,8 +2173,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.1: - resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} flowbite-datepicker@1.3.2: resolution: {integrity: sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==} @@ -2296,6 +2317,9 @@ packages: engines: {node: '>=18'} hasBin: true + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2304,6 +2328,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2319,6 +2346,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2372,6 +2402,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2435,6 +2468,9 @@ packages: engines: {node: '>=6'} hasBin: true + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2453,6 +2489,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.31.1: resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} @@ -2699,6 +2738,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2826,6 +2868,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2833,6 +2878,9 @@ packages: quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -2887,6 +2935,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2895,6 +2946,9 @@ packages: set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2957,6 +3011,9 @@ packages: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3017,8 +3074,8 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte@5.53.12: - resolution: {integrity: sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==} + svelte@5.54.0: + resolution: {integrity: sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==} engines: {node: '>=18'} synckit@0.11.12: @@ -4203,15 +4260,15 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))': + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': + '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -4222,25 +4279,25 @@ snapshots: mrmime: 2.0.1 set-cookie-parser: 3.0.1 sirv: 3.0.2 - svelte: 5.53.12 + svelte: 5.54.0 vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) optionalDependencies: typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) obug: 2.1.1 - svelte: 5.53.12 + svelte: 5.54.0 vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.53.12 + svelte: 5.54.0 vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) vitefu: 1.1.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) @@ -4313,11 +4370,11 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.1 - '@testing-library/svelte-core@1.0.0(svelte@5.53.12)': + '@testing-library/svelte-core@1.0.0(svelte@5.54.0)': dependencies: - svelte: 5.53.12 + svelte: 5.54.0 - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.12))(prettier@3.8.1)(svelte@5.53.12)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0))(prettier@3.8.1)(svelte@5.54.0)': dependencies: '@babel/generator': 7.29.1 '@babel/parser': 7.29.2 @@ -4329,8 +4386,8 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.8.1 optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.53.12) - svelte: 5.53.12 + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.54.0) + svelte: 5.54.0 transitivePeerDependencies: - supports-color @@ -4357,6 +4414,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/file-saver@2.0.7': {} + '@types/json-schema@7.0.15': {} '@types/node@25.5.0': @@ -4871,6 +4930,8 @@ snapshots: cookie@0.6.0: {} + core-util-is@1.0.3: {} + cosmiconfig-typescript-loader@6.2.0(@types/node@25.5.0)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 25.5.0 @@ -5027,7 +5088,7 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-svelte@3.15.2(eslint@9.39.4(jiti@2.6.1))(svelte@5.53.12): + eslint-plugin-svelte@3.15.2(eslint@9.39.4(jiti@2.6.1))(svelte@5.54.0): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -5039,9 +5100,9 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.8) postcss-safe-parser: 7.0.1(postcss@8.5.8) semver: 7.7.4 - svelte-eslint-parser: 1.6.0(svelte@5.53.12) + svelte-eslint-parser: 1.6.0(svelte@5.54.0) optionalDependencies: - svelte: 5.53.12 + svelte: 5.54.0 transitivePeerDependencies: - ts-node @@ -5177,6 +5238,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-saver@2.0.5: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5184,10 +5247,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.1 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.4.1: {} + flatted@3.4.2: {} flowbite-datepicker@1.3.2(rollup@4.59.0): dependencies: @@ -5203,7 +5266,7 @@ snapshots: transitivePeerDependencies: - rollup - flowbite-svelte@1.31.0(rollup@4.59.0)(svelte@5.53.12)(tailwindcss@4.2.1): + flowbite-svelte@1.31.0(rollup@4.59.0)(svelte@5.54.0)(tailwindcss@4.2.1): dependencies: '@floating-ui/dom': 1.7.6 '@floating-ui/utils': 0.2.11 @@ -5212,7 +5275,7 @@ snapshots: date-fns: 4.1.0 esm-env: 1.2.2 flowbite: 3.1.2(rollup@4.59.0) - svelte: 5.53.12 + svelte: 5.54.0 tailwind-merge: 3.5.0 tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.1) tailwindcss: 4.2.1 @@ -5336,10 +5399,14 @@ snapshots: husky@9.1.7: {} + idb@8.0.3: {} + ignore@5.3.2: {} ignore@7.0.5: {} + immediate@3.0.6: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -5354,6 +5421,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + ini@4.1.1: {} irregular-plurals@3.5.0: {} @@ -5390,6 +5459,8 @@ snapshots: is-unicode-supported@2.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5435,6 +5506,13 @@ snapshots: json5@2.2.3: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5450,6 +5528,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.31.1: optional: true @@ -5720,6 +5802,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5807,10 +5891,10 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.12): + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0): dependencies: prettier: 3.8.1 - svelte: 5.53.12 + svelte: 5.54.0 prettier@2.5.1: {} @@ -5820,10 +5904,22 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-nextick-args@2.0.1: {} + punycode@2.3.1: {} quansync@1.0.0: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -5882,19 +5978,23 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - runed@0.28.0(svelte@5.53.12): + runed@0.28.0(svelte@5.54.0): dependencies: esm-env: 1.2.2 - svelte: 5.53.12 + svelte: 5.54.0 sade@1.8.1: dependencies: mri: 1.2.0 + safe-buffer@5.1.2: {} + semver@7.7.4: {} set-cookie-parser@3.0.1: {} + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5953,6 +6053,10 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5978,19 +6082,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.4.5(picomatch@4.0.3)(svelte@5.53.12)(typescript@5.9.3): + svelte-check@4.4.5(picomatch@4.0.3)(svelte@5.54.0)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.53.12 + svelte: 5.54.0 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.6.0(svelte@5.53.12): + svelte-eslint-parser@1.6.0(svelte@5.54.0): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -6000,20 +6104,20 @@ snapshots: postcss-selector-parser: 7.1.1 semver: 7.7.4 optionalDependencies: - svelte: 5.53.12 + svelte: 5.54.0 - svelte-kit-sessions@0.4.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12): + svelte-kit-sessions@0.4.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0): dependencies: '@isaacs/ttlcache': 1.4.1 - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.12)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.53.12)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) - svelte: 5.53.12 + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + svelte: 5.54.0 - svelte-sonner@1.1.0(svelte@5.53.12): + svelte-sonner@1.1.0(svelte@5.54.0): dependencies: - runed: 0.28.0(svelte@5.53.12) - svelte: 5.53.12 + runed: 0.28.0(svelte@5.54.0) + svelte: 5.54.0 - svelte@5.53.12: + svelte@5.54.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -6179,11 +6283,11 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) - vitest-browser-svelte@2.1.0(svelte@5.53.12)(vitest@4.1.0): + vitest-browser-svelte@2.1.0(svelte@5.54.0)(vitest@4.1.0): dependencies: '@playwright/test': 1.58.2 - '@testing-library/svelte-core': 1.0.0(svelte@5.53.12) - svelte: 5.53.12 + '@testing-library/svelte-core': 1.0.0(svelte@5.54.0) + svelte: 5.54.0 vitest: 4.1.0(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) vitest@4.1.0(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): diff --git a/src/lib/assets/Tools/play.png b/src/lib/assets/Tools/play.png new file mode 100644 index 0000000000000000000000000000000000000000..6df693530797eee1559d2658b553360f311f969a GIT binary patch literal 961 zcmeAS@N?(olHy`uVBq!ia0vp^DImNS%Lh_0G|-ozDK)sPt`N*PiESm;&P$a_FRX-=?1Th6AVu`CSIRse5T3hbfeMf zCgamhCTD=C*%X9Y%+9o!p9P{;i?gkkXWQ5gq!ry>X??Ce|Mqg$18L@GTH>$G^|;vI z|7e%w(IU4CeL&lqH$^A`DdmzNzu^A_0YjQ*^E9Ax&H|6fVg?4jBOuH;Rhv&5C@5Lt z8c`CQpH@imtTM>b0?2~uGBhv%$u@2Hv+2^WOnzhaLP<~&kw61UUB}oRqRBcV)-Nu(@Jj>xfnK)~ygVD3J&nKmx z#C+pQ6JgX#*&m=Lpl94^Fq4TX=G47&B7$PZ%?#d_hbBh09Mlka;MHg3*6Jdu$`o^Y zV%v&|9LWxUdN>*Lo>YYPbo3i4%N=XHAt}yu=akh^3&8|cZP7<=Iz9{+jTK!^vk1xx zyP5DXGQLX*734BHK0)9?T-dwTwkZ-yMrQ*A6LeqaJG&i|v@$xTqTsB?$|#qm@Y_e~ zQo4}f04#2hT9`KqrybcoKg+46h4Z5jo64r>^)5;+LY?y#XiU&p=PSC>#JP{Wa}=ihYl g*~;zx@~8a?{ujH>s;K_@wFnf!p00i_>zopr00CfDSO5S3 literal 0 HcmV?d00001 diff --git a/src/lib/components/Utils/Storage/projectSync.ts b/src/lib/components/Utils/Storage/projectSync.ts index 4c3593f..a7d33ba 100644 --- a/src/lib/components/Utils/Storage/projectSync.ts +++ b/src/lib/components/Utils/Storage/projectSync.ts @@ -1,7 +1,22 @@ -import { clearDB } from '$lib/components/Utils/Storage/db'; -import { listFiles, loadFile, saveFile } from '$lib/components/Utils/Storage/fileSystem'; +import { listFiles, loadFile } from '$lib/components/Utils/Storage/fileSystem'; -export async function loadRemoteProject() { +export async function createProject(formData: FormData) { + await fetch('/cli?/createProject', { + method: 'POST', + body: JSON.stringify({ + projectPath: formData.get('projectPath'), + projectName: formData.get('projectName'), + packageManager: formData.get('packageManager'), + language: formData.get('language'), + strictTypeChecking: formData.get('strictTypeChecking') ?? false, + multiplayerServer: formData.get('multiplayerServer') ?? false, + skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false, + dockerContainerization: formData.get('dockerContainerization') ?? false, + }), + }); +} + +/*export async function loadRemoteProject() { const response = await fetch('/fs?/=readDirRec', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -28,11 +43,9 @@ export async function loadRemoteProject() { } } } -} +}*/ export async function pushLocalProject() { - console.log('🔄 Push projet vers serveur distant...'); - const localFiles = await listFiles(); const pushPromises = localFiles.map(async (file) => { diff --git a/src/routes/load-project/+page.svelte b/src/routes/load-project/+page.svelte index e0188b8..11bf397 100644 --- a/src/routes/load-project/+page.svelte +++ b/src/routes/load-project/+page.svelte @@ -3,14 +3,6 @@ import Logo from '$lib/assets/logo.png'; import { clearDB } from '$lib/components/Utils/Storage/db'; - async function createProject() { - await clearDB(); - if (projectName.trim()) { - window.location.href = `/load-project?projectPath=${encodeURIComponent(projectName)}`; - showPopup = false; - } - } - export function importProject() { clearDB(); } @@ -29,7 +21,7 @@ } let showPopup: boolean = $state(false); - let projectName: string = $state(''); + let showAdvancedSettings: boolean = $state(false);
@@ -41,8 +33,10 @@
-
-
+
+
-
-
+
+
No project created
@@ -64,46 +58,143 @@
- +
+ {#if showAdvancedSettings} + + + + + + + + + + + + + + + + + + + + + + + + + {/if} - - - + + + + + From b3bd26c8622d27059567b45f82bd85c622577169 Mon Sep 17 00:00:00 2001 From: bill Date: Thu, 19 Mar 2026 04:26:56 +0100 Subject: [PATCH 11/15] feat: create API interface with local API. Use create and load project files --- src/lib/components/Stores/project.ts | 3 + .../components/Utils/Storage/projectSync.ts | 68 ---------- src/lib/components/Utils/api/api.ts | 5 + src/lib/components/Utils/api/local-api.ts | 97 ++++++++++++++ src/lib/components/Utils/api/project-api.ts | 12 ++ .../ContentBrowserItemCard.svelte | 10 +- .../Widget/ScreenView/ScreenViewWidget.svelte | 4 +- src/routes/load-project/+page.server.ts | 118 ++++++++++-------- src/routes/load-project/+page.svelte | 51 +++++--- src/routes/load-project/load-project.spec.ts | 40 ++---- uno.config.ts | 1 + 11 files changed, 236 insertions(+), 173 deletions(-) create mode 100644 src/lib/components/Stores/project.ts delete mode 100644 src/lib/components/Utils/Storage/projectSync.ts create mode 100644 src/lib/components/Utils/api/api.ts create mode 100644 src/lib/components/Utils/api/local-api.ts create mode 100644 src/lib/components/Utils/api/project-api.ts diff --git a/src/lib/components/Stores/project.ts b/src/lib/components/Stores/project.ts new file mode 100644 index 0000000..74a9c72 --- /dev/null +++ b/src/lib/components/Stores/project.ts @@ -0,0 +1,3 @@ +import { type Writable, writable } from 'svelte/store'; + +export const projectPathStore: Writable = writable(''); diff --git a/src/lib/components/Utils/Storage/projectSync.ts b/src/lib/components/Utils/Storage/projectSync.ts deleted file mode 100644 index a7d33ba..0000000 --- a/src/lib/components/Utils/Storage/projectSync.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { listFiles, loadFile } from '$lib/components/Utils/Storage/fileSystem'; - -export async function createProject(formData: FormData) { - await fetch('/cli?/createProject', { - method: 'POST', - body: JSON.stringify({ - projectPath: formData.get('projectPath'), - projectName: formData.get('projectName'), - packageManager: formData.get('packageManager'), - language: formData.get('language'), - strictTypeChecking: formData.get('strictTypeChecking') ?? false, - multiplayerServer: formData.get('multiplayerServer') ?? false, - skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false, - dockerContainerization: formData.get('dockerContainerization') ?? false, - }), - }); -} - -/*export async function loadRemoteProject() { - const response = await fetch('/fs?/=readDirRec', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dirPath: '/' }), - }); - - const result = await response.json(); - - if (!result.success) throw new Error(result.errorMsg); - - await clearDB(); - - for (const item of result.dirContent) { - if (item.type === 'file') { - const fileRes = await fetch('/fs?/=readFile', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filePath: item.name }), - }); - const fileResult = await fileRes.json(); - - if (fileResult.success) { - await saveFile(item.name, fileResult.fileContent); - } - } - } -}*/ - -export async function pushLocalProject() { - const localFiles = await listFiles(); - - const pushPromises = localFiles.map(async (file) => { - const content = await loadFile(file.id); - - const formData = new FormData(); - formData.append('filePath', file.id); - formData.append('fileContent', content); - - await fetch('/fs?/writeFile', { - method: 'POST', - body: JSON.stringify({ - filePath: formData.get('filePath'), - fileContent: formData.get('fileContent'), - }), - }); - }); - - await Promise.all(pushPromises); -} diff --git a/src/lib/components/Utils/api/api.ts b/src/lib/components/Utils/api/api.ts new file mode 100644 index 0000000..c3462da --- /dev/null +++ b/src/lib/components/Utils/api/api.ts @@ -0,0 +1,5 @@ +import { LocalAPI } from '$lib/components/Utils/api/local-api'; + +const api = new LocalAPI(); + +export default api; diff --git a/src/lib/components/Utils/api/local-api.ts b/src/lib/components/Utils/api/local-api.ts new file mode 100644 index 0000000..13b25fb --- /dev/null +++ b/src/lib/components/Utils/api/local-api.ts @@ -0,0 +1,97 @@ +import { deserialize } from '$app/forms'; +import { clearDB } from '$lib/components/Utils/Storage/db'; +import { listFiles, loadFile, saveFile } from '$lib/components/Utils/Storage/fileSystem'; +import { type DirectoryRec, ProjectApi } from '$lib/components/Utils/api/project-api'; + +export class LocalAPI extends ProjectApi { + async createProject(formData: FormData): Promise { + const resp = await fetch('/cli?/createProject', { + method: 'POST', + body: JSON.stringify({ + projectPath: formData.get('projectPath'), + projectName: formData.get('projectName'), + packageManager: formData.get('packageManager'), + language: formData.get('language'), + strictTypeChecking: formData.get('strictTypeChecking') ?? false, + multiplayerServer: formData.get('multiplayerServer') ?? false, + skipDependencyInstallation: formData.get('skipDependencyInstallation') ?? false, + dockerContainerization: formData.get('dockerContainerization') ?? false, + }), + }); + const result = deserialize(await resp.text()); + if (result.type !== 'success') { + if (result.type === 'failure' && result.data) throw new Error(result.data.errorMsg as string); + throw new Error('Failed to load remote project'); + } + } + + async loadProject(formData: FormData): Promise { + const loadResp = await fetch('/load-project?/loadProject', { + method: 'POST', + body: JSON.stringify({ projectPath: formData.get('projectPath') }), + }); + const loadResult = deserialize(await loadResp.text()); + if (loadResult.type !== 'success') { + if (loadResult.type === 'failure' && loadResult.data) + throw new Error(loadResult.data.errorMsg as string); + throw new Error('Failed to load remote project'); + } + } + + async uploadFiles(): Promise { + const localFiles = await listFiles(); + + localFiles.map(async (file) => { + const content = await loadFile(file.id); + + const formData = new FormData(); + formData.append('filePath', file.id); + formData.append('fileContent', content); + + await fetch('/fs?/writeFile', { + method: 'POST', + body: JSON.stringify({ + filePath: formData.get('filePath'), + fileContent: formData.get('fileContent'), + }), + }); + }); + } + + async downloadFiles(): Promise { + const readDirResp = await fetch('/fs?/readDirRec', { + method: 'POST', + body: JSON.stringify({ dirPath: '/' }), + }); + + const readDirResult = deserialize(await readDirResp.text()); + + if (readDirResult.type !== 'success' || !readDirResult.data) { + if (readDirResult.type === 'failure' && readDirResult.data) + throw new Error(readDirResult.data.errorMsg as string); + throw new Error('Failed to read remote directory'); + } + + await clearDB(); + await this._downloadDirectoryRec(readDirResult.data.dirContent as DirectoryRec); + } + + private async _downloadDirectoryRec(dir: DirectoryRec, currentPath: string = ''): Promise { + for (const file of dir.files) { + const fileRes = await fetch('/fs?/readFile', { + method: 'POST', + body: JSON.stringify({ filePath: '/' + currentPath + file }), + }); + const fileResult = deserialize(await fileRes.text()); + + if (fileResult.type === 'success' && fileResult.data) { + await saveFile(currentPath + file, fileResult.data.fileContent as string); + } + } + for (const [dirName, children] of Object.entries(dir.directories)) { + if (dirName !== 'node_modules') { + await this._downloadDirectoryRec(children, currentPath + dirName + '/'); + } + } + } +} diff --git a/src/lib/components/Utils/api/project-api.ts b/src/lib/components/Utils/api/project-api.ts new file mode 100644 index 0000000..087a4e0 --- /dev/null +++ b/src/lib/components/Utils/api/project-api.ts @@ -0,0 +1,12 @@ +export interface DirectoryRec { + files: string[]; + directories: Record; +} + +export abstract class ProjectApi { + abstract createProject(formData: FormData): Promise; + abstract loadProject(formData: FormData): Promise; + + abstract uploadFiles(): Promise; + abstract downloadFiles(): Promise; +} diff --git a/src/lib/components/Widget/ContentBrowser/ContentBrowserItemCard.svelte b/src/lib/components/Widget/ContentBrowser/ContentBrowserItemCard.svelte index 866f7f1..1504403 100644 --- a/src/lib/components/Widget/ContentBrowser/ContentBrowserItemCard.svelte +++ b/src/lib/components/Widget/ContentBrowser/ContentBrowserItemCard.svelte @@ -17,6 +17,12 @@ class="h-32 w-24 flex flex-col cursor-pointer items-center rounded-md p-1 hover:bg-neutral-700" onclick={onClickEvent} > - - {item.name} + + + + {item.name} diff --git a/src/lib/components/Widget/ScreenView/ScreenViewWidget.svelte b/src/lib/components/Widget/ScreenView/ScreenViewWidget.svelte index 2482f15..eeb112e 100644 --- a/src/lib/components/Widget/ScreenView/ScreenViewWidget.svelte +++ b/src/lib/components/Widget/ScreenView/ScreenViewWidget.svelte @@ -1,11 +1,11 @@
- {#if showAdvancedSettings} +
- {/if} +
+ + {error}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + {error} + +
+ + +
+ +
diff --git a/src/lib/components/ProjectLoader/LoadProject.svelte b/src/lib/components/ProjectLoader/LoadProject.svelte new file mode 100644 index 0000000..9f3b897 --- /dev/null +++ b/src/lib/components/ProjectLoader/LoadProject.svelte @@ -0,0 +1,81 @@ + + + diff --git a/src/lib/components/Utils/Storage/db.ts b/src/lib/components/Utils/IndexedDB/db.ts similarity index 100% rename from src/lib/components/Utils/Storage/db.ts rename to src/lib/components/Utils/IndexedDB/db.ts diff --git a/src/lib/components/Utils/Storage/fileSystem.ts b/src/lib/components/Utils/IndexedDB/fileSystem.ts similarity index 97% rename from src/lib/components/Utils/Storage/fileSystem.ts rename to src/lib/components/Utils/IndexedDB/fileSystem.ts index 96aadf1..7d78f49 100644 --- a/src/lib/components/Utils/Storage/fileSystem.ts +++ b/src/lib/components/Utils/IndexedDB/fileSystem.ts @@ -1,4 +1,4 @@ -import { getDB } from '$lib/components/Utils/Storage/db'; +import { getDB } from '$lib/components/Utils/IndexedDB/db'; export interface File { id: string; diff --git a/src/lib/components/Utils/LocalStorage/ProjectCache.ts b/src/lib/components/Utils/LocalStorage/ProjectCache.ts new file mode 100644 index 0000000..cf33d0b --- /dev/null +++ b/src/lib/components/Utils/LocalStorage/ProjectCache.ts @@ -0,0 +1,45 @@ +export interface ProjectDataCache { + name: string; + path: string; + imageUrl: string; +} + +class ProjectCache { + private static storageKey = 'projects'; + + static getProjects(): Array { + const storedProjects = localStorage.getItem(this.storageKey); + if (storedProjects) { + return JSON.parse(storedProjects); + } + return []; + } + + static addProject(project: ProjectDataCache) { + const projects = this.getProjects(); + projects.push(project); + localStorage.setItem(this.storageKey, JSON.stringify(projects)); + } + + static removeProject(projectName: string) { + let projects = this.getProjects(); + projects = projects.filter((project) => project.name !== projectName); + localStorage.setItem(this.storageKey, JSON.stringify(projects)); + } + + static updateProject(updatedProject: ProjectDataCache) { + const projects = this.getProjects(); + const projectIndex = projects.findIndex((project) => project.name === updatedProject.name); + + if (projectIndex !== -1) { + projects[projectIndex] = updatedProject; + localStorage.setItem(this.storageKey, JSON.stringify(projects)); + } + } + + static clearProjects() { + localStorage.removeItem(this.storageKey); + } +} + +export default ProjectCache; diff --git a/src/lib/components/Utils/Storage/tabs.ts b/src/lib/components/Utils/Storage/tabs.ts deleted file mode 100644 index dce3159..0000000 --- a/src/lib/components/Utils/Storage/tabs.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { getDB } from '$lib/components/Utils/Storage/db'; - -export async function saveTab(fileName: string, content: string) { - const db = await getDB(); - const tx = db.transaction('tabs', 'readwrite'); - const store = tx.objectStore('tabs'); - await store.put({ id: fileName, content, lastModified: Date.now() }); - await tx.done; -} diff --git a/src/lib/components/Utils/api/local-api.ts b/src/lib/components/Utils/api/local-api.ts index 13b25fb..ca330d7 100644 --- a/src/lib/components/Utils/api/local-api.ts +++ b/src/lib/components/Utils/api/local-api.ts @@ -1,6 +1,6 @@ import { deserialize } from '$app/forms'; -import { clearDB } from '$lib/components/Utils/Storage/db'; -import { listFiles, loadFile, saveFile } from '$lib/components/Utils/Storage/fileSystem'; +import { clearDB } from '$lib/components/Utils/IndexedDB/db'; +import { listFiles, loadFile, saveFile } from '$lib/components/Utils/IndexedDB/fileSystem'; import { type DirectoryRec, ProjectApi } from '$lib/components/Utils/api/project-api'; export class LocalAPI extends ProjectApi { diff --git a/src/lib/components/Utils/zip.ts b/src/lib/components/Utils/zip.ts index 929fb4c..ea9e167 100644 --- a/src/lib/components/Utils/zip.ts +++ b/src/lib/components/Utils/zip.ts @@ -1,5 +1,5 @@ -import { clearDB, getDB } from '$lib/components/Utils/Storage/db'; -import { saveFile } from '$lib/components/Utils/Storage/fileSystem'; +import { clearDB, getDB } from '$lib/components/Utils/IndexedDB/db'; +import { saveFile } from '$lib/components/Utils/IndexedDB/fileSystem'; import fileSaver from 'file-saver'; import JSZip from 'jszip'; diff --git a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte index 41a315e..a082e1f 100644 --- a/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte +++ b/src/lib/components/Widget/CodeEditor/MonacoEditor.svelte @@ -1,6 +1,6 @@
@@ -58,158 +62,59 @@
-
-
- No project created -
+
+ {#if projectListCache.length > 0} +
+ {#each projectListCache as project (project)} + + {/each} +
+ + {:else} +
+ No project created +
+ {/if}
- - + + diff --git a/uno.config.ts b/uno.config.ts index 19f9738..80773b1 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -47,5 +47,6 @@ export default defineConfig({ 'i-ic-baseline-add', 'i-ic-baseline-folder', 'i-material-icon-theme-json', + 'i-ic-baseline-upload', ], }); From b397a9c4ad4705a7acd2a0d3fdef91b8c860d1bb Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 20 Mar 2026 04:49:48 +0100 Subject: [PATCH 13/15] fix: add load-project url params --- .../ProjectLoader/CreateProject.svelte | 9 ++++---- src/routes/load-project/+page.svelte | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/lib/components/ProjectLoader/CreateProject.svelte b/src/lib/components/ProjectLoader/CreateProject.svelte index 1e0b37f..69fa814 100644 --- a/src/lib/components/ProjectLoader/CreateProject.svelte +++ b/src/lib/components/ProjectLoader/CreateProject.svelte @@ -112,9 +112,7 @@
- + @@ -164,8 +162,9 @@
diff --git a/src/routes/load-project/+page.svelte b/src/routes/load-project/+page.svelte index 8e2a0e4..1373d7b 100644 --- a/src/routes/load-project/+page.svelte +++ b/src/routes/load-project/+page.svelte @@ -47,8 +47,24 @@ } } - onMount(() => { + onMount(async () => { projectListCache = ProjectCache.getProjects(); + + const params = new URLSearchParams(window.location.search); + + const projectPath = params.get('projectPath'); + const projectId = params.get('projectId'); + + if (!projectPath && !projectId) return; + + const formData = new FormData(); + + if (projectPath) formData.append('projectPath', projectPath); + if (projectId) formData.append('projectId', projectId); + + await api.loadProject(formData); + await api.downloadFiles(); + await goto(resolve('/')); }); @@ -116,5 +132,5 @@
- - + + From 3e9da6bc1fa5bec4664d5cbfd8aa8cadb5340795 Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 20 Mar 2026 05:10:24 +0100 Subject: [PATCH 14/15] fix: use workspace dep package manager for fs components --- package.json | 8 ++++---- pnpm-lock.yaml | 20 ++++++++++++++++---- pnpm-workspace.yaml | 4 ++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7a7b64c..7cdc581 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@sveltejs/vite-plugin-svelte": "catalog:core", "@trivago/prettier-plugin-sort-imports": "catalog:lint", "@tsconfig/svelte": "catalog:build", - "@types/file-saver": "^2.0.7", + "@types/file-saver": "catalog:components", "@unocss/extractor-svelte": "catalog:css", "@unocss/preset-icons": "catalog:css", "@unocss/preset-web-fonts": "catalog:css", @@ -112,8 +112,8 @@ }, "dependencies": { "svelte-kit-sessions": "catalog:core", - "file-saver": "^2.0.5", - "idb": "^8.0.3", - "jszip": "^3.10.1" + "file-saver": "catalog:components", + "idb": "catalog:components", + "jszip": "catalog:components" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c841179..7874792 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,12 +32,24 @@ catalogs: specifier: ^16.2.7 version: 16.2.7 components: + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 flowbite: specifier: ^4.0.1 version: 4.0.1 flowbite-svelte: specifier: ^1.31.0 version: 1.31.0 + idb: + specifier: ^8.0.3 + version: 8.0.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 monaco-editor: specifier: ^0.55.1 version: 0.55.1 @@ -152,13 +164,13 @@ importers: .: dependencies: file-saver: - specifier: ^2.0.5 + specifier: catalog:components version: 2.0.5 idb: - specifier: ^8.0.3 + specifier: catalog:components version: 8.0.3 jszip: - specifier: ^3.10.1 + specifier: catalog:components version: 3.10.1 svelte-kit-sessions: specifier: catalog:core @@ -216,7 +228,7 @@ importers: specifier: catalog:build version: 5.0.8 '@types/file-saver': - specifier: ^2.0.7 + specifier: catalog:components version: 2.0.7 '@unocss/extractor-svelte': specifier: catalog:css diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index beede95..22d8f2b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,10 @@ catalogs: flowbite-svelte: ^1.31.0 monaco-editor: ^0.55.1 svelte-sonner: ^1.0.7 + file-saver: ^2.0.5 + idb: ^8.0.3 + jszip: ^3.10.1 + "@types/file-saver": ^2.0.7 core: '@sveltejs/adapter-auto': ^7.0.1 '@sveltejs/kit': ^2.53.4 From f5e4b7af010215794357481bc0bb6e6059888e74 Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 20 Mar 2026 08:13:55 +0100 Subject: [PATCH 15/15] fix: delete skip dependencies option --- src/lib/components/ProjectLoader/CreateProject.svelte | 11 ----------- src/lib/components/Tabs/TabBar.svelte | 4 ++-- src/lib/components/Widget/ContentBrowser/types.ts | 5 +++-- src/routes/+page.svelte | 2 +- src/{lib/components/Stores => stores}/project.ts | 0 src/{lib/components/Stores => stores}/tabs.ts | 0 src/{lib/components/Stores => stores}/workingFile.ts | 0 7 files changed, 6 insertions(+), 16 deletions(-) rename src/{lib/components/Stores => stores}/project.ts (100%) rename src/{lib/components/Stores => stores}/tabs.ts (100%) rename src/{lib/components/Stores => stores}/workingFile.ts (100%) diff --git a/src/lib/components/ProjectLoader/CreateProject.svelte b/src/lib/components/ProjectLoader/CreateProject.svelte index 69fa814..d2c3428 100644 --- a/src/lib/components/ProjectLoader/CreateProject.svelte +++ b/src/lib/components/ProjectLoader/CreateProject.svelte @@ -134,17 +134,6 @@ >
- - - - import TabComponent from './Tab.svelte'; - import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; + import { tabSelectedStore, tabsStore } from '../../../stores/tabs'; import type { Tab } from '$lib/components/Tabs/types'; - import { workingFileStore } from '$lib/components/Stores/workingFile'; + import { workingFileStore } from '../../../stores/workingFile'; let tabs: Tab[] = $derived($tabsStore); diff --git a/src/lib/components/Widget/ContentBrowser/types.ts b/src/lib/components/Widget/ContentBrowser/types.ts index 549b5c4..fe99a5c 100644 --- a/src/lib/components/Widget/ContentBrowser/types.ts +++ b/src/lib/components/Widget/ContentBrowser/types.ts @@ -1,8 +1,9 @@ -import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; -import { workingFileStore } from '$lib/components/Stores/workingFile'; import { tabTypes } from '$lib/components/Tabs/types'; import { get } from 'svelte/store'; +import { tabSelectedStore, tabsStore } from '../../../../stores/tabs'; +import { workingFileStore } from '../../../../stores/workingFile'; + export interface ContentBrowserItem { id: string; name: string; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 60c142b..4ca76e4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,7 +3,7 @@ import MenuBar from '$lib/components/Menu/MenuBar.svelte'; import Logo from '$lib/assets/logo.png'; import TabBar from '$lib/components/Tabs/TabBar.svelte'; - import { tabSelectedStore, tabsStore } from '$lib/components/Stores/tabs'; + import { tabSelectedStore, tabsStore } from '../stores/tabs'; import { onMount } from 'svelte'; import { loadFile } from '$lib/components/Utils/IndexedDB/fileSystem'; diff --git a/src/lib/components/Stores/project.ts b/src/stores/project.ts similarity index 100% rename from src/lib/components/Stores/project.ts rename to src/stores/project.ts diff --git a/src/lib/components/Stores/tabs.ts b/src/stores/tabs.ts similarity index 100% rename from src/lib/components/Stores/tabs.ts rename to src/stores/tabs.ts diff --git a/src/lib/components/Stores/workingFile.ts b/src/stores/workingFile.ts similarity index 100% rename from src/lib/components/Stores/workingFile.ts rename to src/stores/workingFile.ts