feature: poll files to auto-update on external file changes
This commit is contained in:
parent
c9bc7d061b
commit
8b852b4ca4
10 changed files with 209 additions and 100 deletions
5
bun.lock
5
bun.lock
|
@ -5,6 +5,7 @@
|
|||
"name": "calque",
|
||||
"dependencies": {
|
||||
"@solid-primitives/clipboard": "^1.5.10",
|
||||
"@solid-primitives/destructure": "^0.2.0",
|
||||
"@solid-primitives/i18n": "^2.1.1",
|
||||
"@solid-primitives/scheduled": "^1.4.4",
|
||||
"@solid-primitives/storage": "^4.2.1",
|
||||
|
@ -415,6 +416,8 @@
|
|||
|
||||
"@solid-primitives/clipboard": ["@solid-primitives/clipboard@1.5.10", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ohwlrBP4j+Qjksg01CFWaP/USpzX78dBNVA1DPRZkf/vJgytX0T6KMc2YxF6o8fs6ePIYSI8Nt3sKxF0sh1q+Q=="],
|
||||
|
||||
"@solid-primitives/destructure": ["@solid-primitives/destructure@0.2.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nfE6nSkyLle+hIQzvxGwzyt3TvFwgBjhFiQ7y2Cq+amBiwpvVVm+1qncE8tKaKk6JNn/CilgXZgJ/KMb/p3csA=="],
|
||||
|
||||
"@solid-primitives/i18n": ["@solid-primitives/i18n@2.1.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-1p9B8hveu+gzFRWfXcRtdzzEdr7gw3c8uLXm+2bU33JHgiI8kYJsWvG128sE6vU1ZtYGPrGq980Jd6hxYupyZQ=="],
|
||||
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BTGdFP7t+s7RSak+s1u0eTix4lHP23MrbGkgQTFlt1E+4fmnD/bEx3ZfNW7Grylz3GXgKyXrgDKA7jQ/wuWKgA=="],
|
||||
|
@ -1601,6 +1604,8 @@
|
|||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@solid-primitives/destructure/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||
|
||||
"@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
|
||||
|
||||
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"name": "calque",
|
||||
"dependencies": {
|
||||
"@solid-primitives/clipboard": "^1.5.10",
|
||||
"@solid-primitives/destructure": "^0.2.0",
|
||||
"@solid-primitives/i18n": "^2.1.1",
|
||||
"@solid-primitives/scheduled": "^1.4.4",
|
||||
"@solid-primitives/storage": "^4.2.1",
|
||||
|
|
|
@ -232,7 +232,6 @@ function Row<K extends number | string, T extends Record<string, any>>(props: {
|
|||
({ id }) => {
|
||||
const content = table.cellRenderers()[id]?.({ row: props.key as number, column: id, value: props.value[id] }) ?? props.value[id];
|
||||
|
||||
// return <>{content}</>;
|
||||
return <td class={css.cell}>{content}</td>;
|
||||
}
|
||||
}</For>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import Dexie, { EntityTable } from "dexie";
|
||||
import { Accessor, createContext, createMemo, onMount, ParentComponent, useContext } from "solid-js";
|
||||
import { Accessor, createContext, createMemo, createResource, InitializedResource, onCleanup, onMount, ParentComponent, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { isServer } from "solid-js/web";
|
||||
import { json } from "./parser";
|
||||
|
||||
const ROOT = '__root__';
|
||||
|
||||
|
@ -163,12 +162,4 @@ export const FilesProvider: ParentComponent = (props) => {
|
|||
return <FilesContext.Provider value={context}>{props.children}</FilesContext.Provider>;
|
||||
}
|
||||
|
||||
export const useFiles = () => useContext(FilesContext)!;
|
||||
|
||||
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
||||
switch (file.type) {
|
||||
case 'application/json': return json.load(file.stream())
|
||||
|
||||
default: return Promise.resolve(undefined);
|
||||
}
|
||||
};
|
||||
export const useFiles = () => useContext(FilesContext)!;
|
|
@ -13,6 +13,7 @@ export interface GridApi {
|
|||
readonly selection: Accessor<SelectionItem<number, Entry>[]>;
|
||||
remove(indices: number[]): void;
|
||||
addKey(key: string): void;
|
||||
addLocale(locale: string): void;
|
||||
selectAll(): void;
|
||||
clearSelection(): void;
|
||||
};
|
||||
|
@ -33,8 +34,9 @@ const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
|
|||
export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any, children?: (key: string) => JSX.Element }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const [addedLocales, setAddedLocales] = createSignal<string[]>([]);
|
||||
const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } }));
|
||||
const locales = createMemo(() => props.locales);
|
||||
const locales = createMemo(() => [...props.locales, ...addedLocales()]);
|
||||
const columns = createMemo<Column<Entry>[]>(() => [
|
||||
{
|
||||
id: 'key',
|
||||
|
@ -56,9 +58,9 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
|
|||
|
||||
createEffect(() => {
|
||||
const r = rows();
|
||||
const l = locales();
|
||||
const l = addedLocales();
|
||||
|
||||
r.mutateEach(({ key, ...rest }) => ({ key, ...Object.fromEntries(l.map(locale => [locale, rest[locale] ?? ''])) }));
|
||||
r.mutateEach(({ key, ...rest }) => ({ key, ...rest, ...Object.fromEntries(l.map(locale => [locale, rest[locale] ?? ''])) }));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
|
@ -71,6 +73,9 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
|
|||
addKey(key) {
|
||||
r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) });
|
||||
},
|
||||
addLocale(locale) {
|
||||
setAddedLocales(locales => new Set([...locales, locale]).values().toArray())
|
||||
},
|
||||
selectAll() {
|
||||
api()?.selectAll();
|
||||
},
|
||||
|
|
51
src/features/file/helpers.ts
Normal file
51
src/features/file/helpers.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { Accessor, createResource, InitializedResource, onCleanup } from "solid-js";
|
||||
import { json } from "./parser";
|
||||
import { filter } from "~/utilities";
|
||||
|
||||
interface Files extends Record<string, { handle: FileSystemFileHandle, file: File }> { }
|
||||
|
||||
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
||||
switch (file.type) {
|
||||
case 'application/json': return json.load(file.stream())
|
||||
|
||||
default: return Promise.resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
export const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): InitializedResource<Files> => {
|
||||
const [value, { refetch }] = createResource<Files>(async (_, { value: prev }) => {
|
||||
prev ??= {};
|
||||
|
||||
const next: Files = Object.fromEntries(await Array.fromAsync(
|
||||
filter(directory().values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')),
|
||||
async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }]
|
||||
));
|
||||
|
||||
const keysPrev = Object.keys(prev);
|
||||
const keysNext = Object.keys(next);
|
||||
|
||||
if (keysPrev.length !== keysNext.length) {
|
||||
return next;
|
||||
}
|
||||
|
||||
if (keysPrev.some(prev => keysNext.includes(prev) === false)) {
|
||||
return next;
|
||||
}
|
||||
|
||||
if (Object.entries(prev).every(([id, { file }]) => next[id].file.lastModified === file.lastModified) === false) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, { initialValue: {} })
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refetch();
|
||||
}, 1000);
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
return value;
|
||||
};
|
|
@ -1,4 +1,7 @@
|
|||
|
||||
export { useFiles, FilesProvider, load } from './context';
|
||||
export { load, readFiles } from './helpers';
|
||||
export { useFiles, FilesProvider } from './context';
|
||||
export { Grid } from './grid';
|
||||
export { TreeProvider, Tree, useTree } from './tree';
|
||||
|
||||
export type { Entry } from './grid';
|
|
@ -1,8 +1,8 @@
|
|||
import { Accessor, Component, createContext, createSignal, For, JSX, Show, useContext } from "solid-js";
|
||||
import { Accessor, children, Component, createContext, createEffect, createMemo, createResource, createSignal, For, InitializedResource, JSX, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
|
||||
import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "solid-icons/ai";
|
||||
import { SelectionProvider, selectable } from "~/features/selectable";
|
||||
import { debounce } from "@solid-primitives/scheduled";
|
||||
import css from "./filetree.module.css";
|
||||
import css from "./tree.module.css";
|
||||
|
||||
selectable;
|
||||
|
||||
|
@ -16,7 +16,7 @@ export interface FileEntry {
|
|||
}
|
||||
|
||||
export interface FolderEntry {
|
||||
name: string;
|
||||
name: string; handle
|
||||
id: string;
|
||||
kind: 'folder';
|
||||
handle: FileSystemDirectoryHandle;
|
||||
|
@ -49,27 +49,59 @@ export async function* walk(directory: FileSystemDirectoryHandle, filters: RegEx
|
|||
}
|
||||
|
||||
interface TreeContextType {
|
||||
open(file: File): void;
|
||||
readonly tree: Accessor<FolderEntry>;
|
||||
readonly name: Accessor<string>;
|
||||
readonly open: Accessor<boolean>;
|
||||
readonly setOpen: Setter<boolean>;
|
||||
|
||||
onOpen(file: File): void;
|
||||
}
|
||||
|
||||
const TreeContext = createContext<TreeContextType>();
|
||||
|
||||
export const Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element], open?: TreeContextType['open'] }> = (props) => {
|
||||
const [, setSelection] = createSignal<object[]>([]);
|
||||
export const TreeProvider: ParentComponent<{ directory: FileSystemDirectoryHandle, onOpen?: (file: File) => void }> = (props) => {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const tree = readTree(() => props.directory);
|
||||
|
||||
const context = {
|
||||
open: props.open ?? (() => { }),
|
||||
tree,
|
||||
name: createMemo(() => props.directory.name),
|
||||
open,
|
||||
setOpen,
|
||||
|
||||
onOpen(file: File) {
|
||||
props.onOpen?.(file);
|
||||
},
|
||||
};
|
||||
|
||||
return <TreeContext.Provider value={context}>
|
||||
{props.children}
|
||||
</TreeContext.Provider>;
|
||||
}
|
||||
|
||||
export const useTree = () => {
|
||||
const context = useContext(TreeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('`useTree` is called outside of a <TreeProvider />');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export const Tree: Component<{
|
||||
children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element]
|
||||
}> = (props) => {
|
||||
const [, setSelection] = createSignal<object[]>([]);
|
||||
const context = useTree();
|
||||
|
||||
return <SelectionProvider selection={setSelection}>
|
||||
<TreeContext.Provider value={context}>
|
||||
<div class={css.root}><_Tree entries={props.entries} children={props.children} /></div>
|
||||
</TreeContext.Provider>
|
||||
<div class={css.root}><_Tree entries={context.tree().entries} children={props.children} /></div>
|
||||
</SelectionProvider>;
|
||||
}
|
||||
|
||||
const _Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element] }> = (props) => {
|
||||
const context = useContext(TreeContext);
|
||||
const context = useTree();
|
||||
|
||||
return <For each={props.entries.toSorted(sort_by('kind'))}>{
|
||||
entry => <>
|
||||
|
@ -78,7 +110,7 @@ const _Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor
|
|||
}</Show>
|
||||
|
||||
<Show when={entry.kind === 'file' ? entry : undefined}>{
|
||||
file => <span use:selectable={{ key: file().id, value: file() }} ondblclick={() => context?.open(file().meta)}><AiFillFile /> {props.children[1](file)}</span>
|
||||
file => <span use:selectable={{ key: file().id, value: file() }} ondblclick={() => context.onOpen(file().meta)}><AiFillFile /> {props.children[1](file)}</span>
|
||||
}</Show>
|
||||
</>
|
||||
}</For>
|
||||
|
@ -98,4 +130,44 @@ const sort_by = (key: string) => (objA: Record<string, any>, objB: Record<string
|
|||
const b = objB[key];
|
||||
|
||||
return Number(a < b) - Number(b < a);
|
||||
};
|
||||
};
|
||||
|
||||
const readTree = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<FolderEntry> => {
|
||||
const [entries, { refetch }] = createResource<Entry[]>(async (_, { value: prev }) => {
|
||||
const dir = directory();
|
||||
|
||||
prev ??= [];
|
||||
const next: Entry[] = await Array.fromAsync(walk(dir));
|
||||
|
||||
const prevEntries = flatten(prev).map(e => e.id).toSorted();
|
||||
const nextEntries = flatten(next).map(e => e.id).toSorted();
|
||||
|
||||
if (prevEntries.length !== nextEntries.length) {
|
||||
return next;
|
||||
}
|
||||
|
||||
if (prevEntries.some((entry, i) => entry !== nextEntries[i])) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, { initialValue: [] })
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refetch();
|
||||
}, 1000);
|
||||
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(entries.latest);
|
||||
});
|
||||
|
||||
return createMemo<FolderEntry>(() => ({ name: directory().name, id: '', kind: 'folder', handle: directory(), entries: entries.latest }));
|
||||
};
|
||||
|
||||
const flatten = (entries: Entry[]): Entry[] => {
|
||||
return entries.flatMap(entry => entry.kind === 'folder' ? [entry, ...flatten(entry.entries)] : entry)
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import { Component, createEffect, createMemo, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js";
|
||||
import { Component, createEffect, createMemo, createResource, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js";
|
||||
import { Created, filter, MutarionKind, Mutation, splitAt } from "~/utilities";
|
||||
import { Sidebar } from "~/components/sidebar";
|
||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
||||
import { Menu } from "~/features/menu";
|
||||
import { Grid, load, useFiles } from "~/features/file";
|
||||
import { Grid, load, readFiles, TreeProvider, Tree, useFiles } from "~/features/file";
|
||||
import { Command, CommandType, Context, createCommand, Modifier } from "~/features/command";
|
||||
import { Entry, GridApi } from "~/features/file/grid";
|
||||
import { Tab, Tabs } from "~/components/tabs";
|
||||
|
@ -13,6 +12,7 @@ import EditBlankImage from '~/assets/edit-blank.svg'
|
|||
import { useI18n } from "~/features/i18n";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import { writeClipboard } from "@solid-primitives/clipboard";
|
||||
import { destructure } from "@solid-primitives/destructure";
|
||||
import css from "./edit.module.css";
|
||||
|
||||
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
|
||||
|
@ -93,7 +93,6 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
}));
|
||||
const [active, setActive] = makePersisted(createSignal<string>(), { name: 'edit__aciveTab' });
|
||||
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
||||
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
||||
const [newKeyPrompt, setNewKeyPrompt] = createSignal<PromptApi>();
|
||||
const [newLanguagePrompt, setNewLanguagePrompt] = createSignal<PromptApi>();
|
||||
|
||||
|
@ -232,7 +231,6 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
|
||||
(async () => {
|
||||
setContents(new Map(await Array.fromAsync(walk(directory), ({ id, entries }) => [id, entries] as const)))
|
||||
setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
||||
})();
|
||||
});
|
||||
|
||||
|
@ -350,24 +348,26 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
<input name="locale" placeholder={t('page.edit.prompt.newLanguage.placeholder')} />
|
||||
</Prompt>
|
||||
|
||||
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
||||
<Tree entries={tree().entries}>{[
|
||||
folder => {
|
||||
return <span onDblClick={() => {
|
||||
filesContext?.set(folder().name, folder().handle);
|
||||
}}>{folder().name}</span>;
|
||||
},
|
||||
file => {
|
||||
const mutated = createMemo(() => mutatedFiles().values().find(({ id }) => id === file().id) !== undefined);
|
||||
<TreeProvider directory={props.root}>
|
||||
<Sidebar as="aside" label={props.root.name} class={css.sidebar}>
|
||||
<Tree>{[
|
||||
folder => {
|
||||
return <span onDblClick={() => {
|
||||
filesContext?.set(folder().name, folder().handle);
|
||||
}}>{folder().name}</span>;
|
||||
},
|
||||
file => {
|
||||
const mutated = createMemo(() => mutatedFiles().values().find(({ id }) => id === file().id) !== undefined);
|
||||
|
||||
return <span class={`${mutated() ? css.mutated : ''}`} onDblClick={() => {
|
||||
const folder = file().directory;
|
||||
filesContext?.set(folder.name, folder);
|
||||
setActive(folder.name);
|
||||
}}>{file().name}</span>;
|
||||
},
|
||||
] as const}</Tree>
|
||||
</Sidebar>
|
||||
return <span class={`${mutated() ? css.mutated : ''}`} onDblClick={() => {
|
||||
const folder = file().directory;
|
||||
filesContext?.set(folder.name, folder);
|
||||
setActive(folder.name);
|
||||
}}>{file().name}</span>;
|
||||
},
|
||||
] as const}</Tree>
|
||||
</Sidebar>
|
||||
</TreeProvider>
|
||||
|
||||
<Tabs class={css.content} active={active()} setActive={setActive} onClose={commands.closeTab}>
|
||||
<For each={tabs()}>{
|
||||
|
@ -379,12 +379,39 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<(GridApi & { addLocale(locale: string): void }) | undefined>, entries?: Setter<Entries> }> = (props) => {
|
||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => {
|
||||
const [locales, setLocales] = createSignal<string[]>([]);
|
||||
const [rows, setRows] = createSignal<Entry[]>([]);
|
||||
const [api, setApi] = createSignal<GridApi>();
|
||||
|
||||
const files = readFiles(() => props.directory);
|
||||
const [contents] = createResource(() => files.latest, (files) => Promise.all(Object.entries(files).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await load(file))! }))), { initialValue: [] });
|
||||
|
||||
const [entries, rows] = destructure(() => {
|
||||
const template = contents.latest.map(({ lang, handle }) => [lang, { handle, value: '' }]);
|
||||
const merged = contents.latest.reduce((aggregate, { id, handle, lang, entries }) => {
|
||||
for (const [key, value] of entries.entries()) {
|
||||
if (!aggregate.has(key)) {
|
||||
aggregate.set(key, Object.fromEntries(template));
|
||||
}
|
||||
|
||||
aggregate.get(key)![lang] = { value, handle, id };
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
||||
|
||||
const rows = merged.entries().map(([key, langs]) => ({ key, ...Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value])) } as Entry)).toArray();
|
||||
|
||||
return [
|
||||
new Map(merged.entries().map(([key, langs], i) => [i.toString(), { key, ...langs }])) as Entries,
|
||||
rows
|
||||
] as const;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setLocales(contents.latest.map(({ lang }) => lang));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
props.entries?.(entries());
|
||||
});
|
||||
|
@ -396,52 +423,7 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<(G
|
|||
return;
|
||||
}
|
||||
|
||||
props.api?.({
|
||||
...a,
|
||||
addLocale(locale) {
|
||||
setLocales(current => new Set([...current, locale]).values().toArray());
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const directory = props.directory;
|
||||
|
||||
if (!directory) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const contents = await Array.fromAsync(
|
||||
filter(directory.values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')),
|
||||
async handle => {
|
||||
const id = await handle.getUniqueId();
|
||||
const file = await handle.getFile();
|
||||
const lang = file.name.split('.').at(0)!;
|
||||
const entries = (await load(file))!;
|
||||
|
||||
return { id, handle, lang, entries };
|
||||
}
|
||||
);
|
||||
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
|
||||
|
||||
setLocales(contents.map(({ lang }) => lang));
|
||||
|
||||
const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => {
|
||||
for (const [key, value] of entries.entries()) {
|
||||
if (!aggregate.has(key)) {
|
||||
aggregate.set(key, Object.fromEntries(template));
|
||||
}
|
||||
|
||||
aggregate.get(key)![lang] = { value, handle, id };
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
||||
|
||||
setEntries(new Map(merged.entries().map(([key, langs], i) => [i.toString(), { key, ...langs }])) as Entries);
|
||||
setRows(merged.entries().map(([key, langs]) => ({ key, ...Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value])) } as Entry)).toArray());
|
||||
})();
|
||||
props.api?.(a);
|
||||
});
|
||||
|
||||
const copyKey = createCommand('page.edit.command.copyKey', (key: string) => writeClipboard(key));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue