From ddf4519f416d649e1a60b0b09f7c0c3acadfe4c8 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 28 Oct 2024 15:43:53 +0100 Subject: [PATCH] refactoring stuff to make sure the right responsibility is fullfilled in the right place --- src/components/tabs.tsx | 14 ++-- src/features/file/index.tsx | 112 +++++++++++++++++++++++----- src/routes/(editor)/edit.tsx | 139 +++++++++-------------------------- 3 files changed, 138 insertions(+), 127 deletions(-) diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index 5d360ff..0a77f4e 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -19,19 +19,23 @@ const useTabs = () => { export const Tabs: ParentComponent<{ active?: Setter }> = (props) => { const [active, setActive] = createSignal(undefined); - const [tabs, setTabs] = createSignal<{ id: string, label: string }[]>([]); + const [tabs, setTabs] = createSignal>(new Map()); createEffect(() => { props.active?.(active()); }); createEffect(() => { - setActive(tabs().at(-1)?.id); + setActive(tabs().keys().toArray().at(-1)); }); const ctx = { register(id: string, label: string) { - setTabs(tabs => [...tabs, { id, label }]); + setTabs(tabs => { + tabs.set(id, label); + + return new Map(tabs); + }); return createMemo(() => active() === id); }, @@ -40,8 +44,8 @@ export const Tabs: ParentComponent<{ active?: Setter }> = (p return
- { - tab => + { + ([id, label]) => }
diff --git a/src/features/file/index.tsx b/src/features/file/index.tsx index d32df57..7110a85 100644 --- a/src/features/file/index.tsx +++ b/src/features/file/index.tsx @@ -1,38 +1,68 @@ import Dexie, { EntityTable } from "dexie"; -import { createContext, ParentComponent, useContext } from "solid-js"; +import { Accessor, createContext, createMemo, onMount, ParentComponent, useContext } from "solid-js"; +import { createStore } from "solid-js/store"; import { isServer } from "solid-js/web"; import * as json from './parser/json'; interface FileEntity { - name: string; + key: string; handle: FileSystemDirectoryHandle; } type Store = Dexie & { - files: EntityTable; + files: EntityTable; }; -interface FilesContextType { - set(name: string, handle: FileSystemDirectoryHandle): Promise; - get(name: string): Promise; +interface InternalFilesContextType { + onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any): void; + set(key: string, handle: FileSystemDirectoryHandle): Promise; + get(key: string): Promise; + remove(key: string): Promise; + keys(): Promise; + entries(): Promise; list(): Promise; } +interface FilesContextType { + readonly files: Accessor, + + get(key: string): Accessor + set(key: string, handle: FileSystemDirectoryHandle): Promise; + remove(key: string): Promise; +} + const FilesContext = createContext(); -const clientContext = (): FilesContextType => { +const clientContext = (): InternalFilesContextType => { const db = new Dexie('Files') as Store; db.version(1).stores({ - files: 'name, handle' + files: 'key, handle' }); return { - async set(name: string, handle: FileSystemDirectoryHandle) { - await db.files.put({ name, handle }); + onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) { + const callHook = (key: string, handle: FileSystemDirectoryHandle) => setTimeout(() => hook(key, handle), 1); + + db.files.hook('creating', (_: string, { key, handle }: FileEntity) => { callHook(key, handle); }); + db.files.hook('deleting', (_: string, { key, handle }: FileEntity) => callHook(key, handle)); + db.files.hook('updating', (_1: Object, _2: string, { key, handle }: FileEntity) => callHook(key, handle)); }, - async get(name: string) { - return (await db.files.get(name))?.handle; + + async set(key: string, handle: FileSystemDirectoryHandle) { + await db.files.put({ key, handle }); + }, + async get(key: string) { + return (await db.files.get(key))?.handle; + }, + async remove(key: string) { + return (await db.files.delete(key)); + }, + async keys() { + return (await db.files.toArray()).map(f => f.key); + }, + async entries() { + return await db.files.toArray(); }, async list() { const files = await db.files.toArray(); @@ -42,25 +72,69 @@ const clientContext = (): FilesContextType => { } }; -const serverContext = (): FilesContextType => ({ - set() { +const serverContext = (): InternalFilesContextType => ({ + onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) { + + }, + set(key: string, handle: FileSystemDirectoryHandle) { return Promise.resolve(); }, - get(name: string) { + get(key: string) { return Promise.resolve(undefined); }, + remove(key: string) { + return Promise.resolve(undefined); + }, + keys() { + return Promise.resolve([]); + }, + entries() { + return Promise.resolve([]); + }, list() { - return Promise.resolve([]); + return Promise.resolve([]); }, }); export const FilesProvider: ParentComponent = (props) => { - const ctx = isServer ? serverContext() : clientContext(); + const internal = isServer ? serverContext() : clientContext(); - return {props.children}; + const [state, setState] = createStore<{ files: FileEntity[] }>({ files: [] }); + + const updateFilesInState = async () => { + const entities = await internal.entries(); + + setState('files', entities); + }; + + internal.onChange((key: string, handle: FileSystemDirectoryHandle) => { + updateFilesInState(); + }); + + onMount(() => { + updateFilesInState(); + }); + + const context: FilesContextType = { + files: createMemo(() => state.files), + + get(key: string): Accessor { + return createMemo(() => state.files.find(entity => entity.key === key)?.handle); + }, + + async set(key: string, handle: FileSystemDirectoryHandle) { + await internal.set(key, handle); + }, + + async remove(key: string) { + await internal.remove(key); + }, + }; + + return {props.children}; } -export const useFiles = () => useContext(FilesContext); +export const useFiles = () => useContext(FilesContext)!; export const load = (file: File): Promise | undefined> => { switch (file.type) { diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index 03128e3..e9334e0 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -62,18 +62,14 @@ interface ContentTabType { export default function Edit(props: ParentProps) { const filesContext = useFiles(); - const [root, { refetch: getRoot, mutate: updateRoot }] = createResource(() => filesContext?.get('__root__')); - const [tabs, { refetch: getTabs }] = createResource(async () => { - const handles = (await filesContext?.list()) ?? []; - return await Promise.all(handles.map(handle => { - const [api, setApi] = createSignal(); - const [entries, setEntries] = createSignal(new Map()); - const files = handle.entries() + const root = filesContext.get('__root__'); + const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => { + const [api, setApi] = createSignal(); + const [entries, setEntries] = createSignal(new Map()); - return ({ handle, api, setApi, entries, setEntries }); - })); - }, { initialValue: [], ssrLoadFrom: 'initial' }); + return ({ handle, api, setApi, entries, setEntries }); + })); const [active, setActive] = createSignal(); const [contents, setContents] = createSignal>>(new Map()); const [tree, setFiles] = createSignal(emptyFolder); @@ -152,119 +148,56 @@ export default function Edit(props: ParentProps) { }).toArray(); }); - // Since the files are stored in indexedDb we need to refetch on the client in order to populate on page load - onMount(() => { - getRoot(); - getTabs(); - }); - createEffect(() => { const directory = root(); - if (root.state === 'ready' && directory?.kind === 'directory') { + if (directory === undefined) { + return; + } - (async () => { - const contents = await Array.fromAsync(walk(directory)); + (async () => { + const contents = await Array.fromAsync(walk(directory)); - console.log(contents); + setContents(new Map(contents.map(({ id, entries }) => [id, entries] as const))) - setContents(new Map(contents.map(({ id, entries }) => [id, entries] as const))) + const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]); - const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]); + const merged = contents.reduce((aggregate, { id, handle, path, lang, entries }) => { + for (const [key, value] of entries.entries()) { + const k = [...path, key].join('.'); - const merged = contents.reduce((aggregate, { id, handle, path, lang, entries }) => { - for (const [key, value] of entries.entries()) { - const k = [...path, key].join('.'); - - if (!aggregate.has(k)) { - aggregate.set(k, Object.fromEntries(template)); - } - - aggregate.get(k)![lang] = { value, handle, id }; + if (!aggregate.has(k)) { + aggregate.set(k, Object.fromEntries(template)); } - return aggregate; - }, new Map>()); + aggregate.get(k)![lang] = { value, handle, id }; + } - setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) }); - setEntries(merged); - })(); - } + return aggregate; + }, new Map>()); + + setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) }); + setEntries(merged); + })(); }); const commands = { - open: createCommand('open', async () => { - const [fileHandle] = await window.showOpenFilePicker({ - types: [ - { - description: "JSON File(s)", - accept: { - "application/json": [".json", ".jsonp", ".jsonc"], - }, - } - ], - excludeAcceptAllOption: true, - multiple: true, - }); - const file = await fileHandle.getFile(); - const text = await file.text(); - - console.log(fileHandle, file, text); - }, { key: 'o', modifier: Modifier.Control }), - openFolder: createCommand('open folder', async () => { + open: createCommand('open folder', async () => { const directory = await window.showDirectoryPicker({ mode: 'readwrite' }); filesContext.set('__root__', directory); - updateRoot(directory); + }, { key: 'o', modifier: Modifier.Control }), + close: createCommand('close folder', async () => { + filesContext.remove('__root__'); }), save: createCommand('save', async () => { - const results = await Promise.allSettled(mutatedData().map(async ([handle, data]) => { + await Promise.allSettled(mutatedData().map(async ([handle, data]) => { const stream = await handle.createWritable({ keepExistingData: false }); stream.write(JSON.stringify(data, null, 4)); stream.write('\n'); stream.close(); })); - - console.log(results); - - // const fileMutations = await Promise.all(mutations.map(async (mutation) => { - // const [k, lang] = splitAt(mutation.key, mutation.key.lastIndexOf('.')); - // const entry = _entries.get(k); - // const localEntry = entry?.[lang]; - - // if (!localEntry) { - // throw new Error('invalid edge case???'); - // } - - // const createNewFile = async () => { - // const [, alternativeLocalEntry] = Object.entries(entry).find(([l, e]) => l !== lang && e.id !== undefined) ?? []; - // const { directory, path } = alternativeLocalEntry ? findFile(tree(), alternativeLocalEntry.id) ?? {} : {}; - - // const handle = await window.showSaveFilePicker({ - // suggestedName: `${lang}.json`, - // startIn: directory, - // excludeAcceptAllOption: true, - // types: [ - // { accept: { 'application/json': ['.json'] }, description: 'JSON' }, - // ] - // }); - - // // TODO :: patch the tree with this new entry - // // console.log(localEntry, tree()); - - // return { handle, path }; - // }; - - // const { handle, path } = findFile(tree(), localEntry.id) ?? (mutation.kind !== MutarionKind.Delete ? await createNewFile() : {}); - // const id = await handle?.getUniqueId(); - // const key = path ? k.slice(path.join('.').length + 1) : k; - // const value = rows[k][lang]; - - // return { action: mutation.kind, key, id, value, handle }; - // })); - - // console.log(rows, entries(), Object.groupBy(fileMutations, m => m.id ?? 'undefined')) }, { key: 's', modifier: Modifier.Control }), saveAs: createCommand('save as', (handle?: FileSystemFileHandle) => { console.log('save as ...', handle); @@ -301,7 +234,9 @@ export default function Edit(props: ParentProps) { - + + + @@ -326,12 +261,11 @@ export default function Edit(props: ParentProps) { - commands.openFolder()}>open a folder}> + commands.open()}>open a folder}> {[ folder => { return { filesContext?.set(folder().name, folder().handle); - getTabs(); }}>{folder().name}; }, file => { @@ -340,7 +274,6 @@ export default function Edit(props: ParentProps) { return { const folder = file().directory; filesContext?.set(folder.name, folder); - getTabs(); }}>{file().name}; }, ] as const} @@ -349,7 +282,7 @@ export default function Edit(props: ParentProps) { { - ({ handle, setApi, setEntries }) => + ({ handle, setApi, setEntries }) => }