initial attempt with pwa
This commit is contained in:
parent
ddf4519f41
commit
b27abe928d
16 changed files with 382 additions and 220 deletions
7
public/images/favicon.dark.svg
Normal file
7
public/images/favicon.dark.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512px" height="512px" viewBox="0 0 512 512" version="1.1">
|
||||||
|
<g id="surface1">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(93.333334%,93.333334%,93.333334%);fill-opacity:1;" d="M 145.441406 214.847656 L 131.519531 256 L 96 256 L 155.585938 96 L 196.671875 96 L 256 256 L 218.65625 256 L 204.734375 214.847656 Z M 197.726562 191.296875 L 176 126.59375 L 174.433594 126.59375 L 152.703125 191.296875 L 197.761719 191.296875 Z M 197.726562 191.296875 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(93.333334%,93.333334%,93.333334%);fill-opacity:1;" d="M 0 64 C 0 28.652344 28.652344 0 64 0 L 288 0 C 323.347656 0 352 28.652344 352 64 L 352 160 L 448 160 C 483.347656 160 512 188.652344 512 224 L 512 448 C 512 483.347656 483.347656 512 448 512 L 224 512 C 188.652344 512 160 483.347656 160 448 L 160 352 L 64 352 C 28.652344 352 0 323.347656 0 288 Z M 64 32 C 46.328125 32 32 46.328125 32 64 L 32 288 C 32 305.671875 46.328125 320 64 320 L 288 320 C 305.671875 320 320 305.671875 320 288 L 320 64 C 320 46.328125 305.671875 32 288 32 Z M 292.414062 351.839844 C 298.59375 361.472656 305.28125 370.496094 312.574219 378.910156 C 288.640625 397.3125 259.039062 410.945312 224 420.257812 C 229.695312 427.199219 238.433594 440.574219 241.761719 448 C 277.761719 436.511719 308.320312 420.992188 334.113281 400.191406 C 358.976562 421.472656 389.761719 437.472656 427.871094 447.296875 C 432.128906 439.167969 441.121094 425.761719 448 418.816406 C 412 410.71875 382.175781 396.609375 357.761719 377.726562 C 379.550781 353.824219 396.863281 324.894531 409.632812 289.503906 L 448 289.503906 L 448 256 L 352 256 L 352 289.503906 L 376.480469 289.503906 C 366.304688 316.511719 352.800781 338.976562 335.777344 357.664062 C 331.074219 352.65625 326.640625 347.398438 322.496094 341.921875 C 313.464844 347.710938 303.117188 351.121094 292.414062 351.839844 Z M 292.414062 351.839844 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
7
public/images/favicon.light.svg
Normal file
7
public/images/favicon.light.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512px" height="512px" viewBox="0 0 512 512" version="1.1">
|
||||||
|
<g id="surface1">
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(13.333334%,13.333334%,13.333334%);fill-opacity:1;" d="M 145.441406 214.847656 L 131.519531 256 L 96 256 L 155.585938 96 L 196.671875 96 L 256 256 L 218.65625 256 L 204.734375 214.847656 Z M 197.726562 191.296875 L 176 126.59375 L 174.433594 126.59375 L 152.703125 191.296875 L 197.761719 191.296875 Z M 197.726562 191.296875 "/>
|
||||||
|
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(13.333334%,13.333334%,13.333334%);fill-opacity:1;" d="M 0 64 C 0 28.652344 28.652344 0 64 0 L 288 0 C 323.347656 0 352 28.652344 352 64 L 352 160 L 448 160 C 483.347656 160 512 188.652344 512 224 L 512 448 C 512 483.347656 483.347656 512 448 512 L 224 512 C 188.652344 512 160 483.347656 160 448 L 160 352 L 64 352 C 28.652344 352 0 323.347656 0 288 Z M 64 32 C 46.328125 32 32 46.328125 32 64 L 32 288 C 32 305.671875 46.328125 320 64 320 L 288 320 C 305.671875 320 320 305.671875 320 288 L 320 64 C 320 46.328125 305.671875 32 288 32 Z M 292.414062 351.839844 C 298.59375 361.472656 305.28125 370.496094 312.574219 378.910156 C 288.640625 397.3125 259.039062 410.945312 224 420.257812 C 229.695312 427.199219 238.433594 440.574219 241.761719 448 C 277.761719 436.511719 308.320312 420.992188 334.113281 400.191406 C 358.976562 421.472656 389.761719 437.472656 427.871094 447.296875 C 432.128906 439.167969 441.121094 425.761719 448 418.816406 C 412 410.71875 382.175781 396.609375 357.761719 377.726562 C 379.550781 353.824219 396.863281 324.894531 409.632812 289.503906 L 448 289.503906 L 448 256 L 352 256 L 352 289.503906 L 376.480469 289.503906 C 366.304688 316.511719 352.800781 338.976562 335.777344 357.664062 C 331.074219 352.65625 326.640625 347.398438 322.496094 341.921875 C 313.464844 347.710938 303.117188 351.121094 292.414062 351.839844 Z M 292.414062 351.839844 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
BIN
public/images/screenshots/narrow.png
Normal file
BIN
public/images/screenshots/narrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
public/images/screenshots/wide.png
Normal file
BIN
public/images/screenshots/wide.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
33
public/manifest.json
Normal file
33
public/manifest.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"short_name": "T-Tool",
|
||||||
|
"name": "Translation Tool",
|
||||||
|
"description": "Simple tool to help with maitaining i18n files",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/images/favicon.dark.svg",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"sizes": "any"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"display_override": [
|
||||||
|
"window-controls-overlay"
|
||||||
|
],
|
||||||
|
"theme_color": "#222",
|
||||||
|
"background_color": "#222",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/images/screenshots/narrow.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "538x1133",
|
||||||
|
"form_factor": "narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/images/screenshots/wide.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "2092x1295",
|
||||||
|
"form_factor": "wide"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,6 +3,10 @@
|
||||||
grid: auto 1fr / 100%;
|
grid: auto 1fr / 100%;
|
||||||
|
|
||||||
& > button {
|
& > button {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
gap: var(--padding-s);
|
||||||
|
align-items: center;
|
||||||
inline-size: max-content;
|
inline-size: max-content;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--surface-1);
|
background-color: var(--surface-1);
|
||||||
|
|
|
@ -14,13 +14,32 @@
|
||||||
|
|
||||||
border-block-end: 1px solid var(--surface-5);
|
border-block-end: 1px solid var(--surface-5);
|
||||||
|
|
||||||
& > button {
|
& > .handle {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
column-gap: var(--padding-m);
|
||||||
|
|
||||||
background-color: var(--surface-1);
|
background-color: var(--surface-1);
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
padding: var(--padding-m) var(--padding-l);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
background-color: inherit;
|
||||||
|
color: inherit;
|
||||||
|
padding: var(--padding-m) 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-inline-start: var(--padding-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-inline-end: var(--padding-l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--surface-3);
|
background-color: var(--surface-3);
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, JSX, onMount, ParentComponent, Setter, Show, useContext } from "solid-js";
|
import { Accessor, children, createContext, createEffect, createMemo, createSignal, For, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
|
||||||
|
import { IoCloseCircleOutline } from "solid-icons/io";
|
||||||
import css from "./tabs.module.css";
|
import css from "./tabs.module.css";
|
||||||
|
import { Command, CommandType, commandArguments, noop, useCommands } from "~/features/command";
|
||||||
|
|
||||||
|
commandArguments;
|
||||||
|
|
||||||
interface TabsContextType {
|
interface TabsContextType {
|
||||||
register(id: string, label: string): Accessor<boolean>;
|
register(id: string, label: string, options?: Partial<TabOptions>): Accessor<boolean>;
|
||||||
|
readonly onClose: Accessor<CommandType<[string]> | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabOptions {
|
||||||
|
closable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabsContext = createContext<TabsContextType>();
|
const TabsContext = createContext<TabsContextType>();
|
||||||
|
@ -17,9 +26,10 @@ const useTabs = () => {
|
||||||
return context!;
|
return context!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs: ParentComponent<{ active?: Setter<string | undefined> }> = (props) => {
|
export const Tabs: ParentComponent<{ active?: Setter<string | undefined>, onClose?: CommandType<[string]> }> = (props) => {
|
||||||
|
const commandsContext = useCommands();
|
||||||
const [active, setActive] = createSignal<string | undefined>(undefined);
|
const [active, setActive] = createSignal<string | undefined>(undefined);
|
||||||
const [tabs, setTabs] = createSignal<Map<string, string>>(new Map());
|
const [tabs, setTabs] = createSignal<Map<string, { label: string, options: Partial<TabOptions> }>>(new Map());
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
props.active?.(active());
|
props.active?.(active());
|
||||||
|
@ -30,35 +40,53 @@ export const Tabs: ParentComponent<{ active?: Setter<string | undefined> }> = (p
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
register(id: string, label: string) {
|
register(id: string, label: string, options: Partial<TabOptions>) {
|
||||||
setTabs(tabs => {
|
setTabs(tabs => {
|
||||||
tabs.set(id, label);
|
tabs.set(id, { label, options });
|
||||||
|
|
||||||
return new Map(tabs);
|
return new Map(tabs);
|
||||||
});
|
});
|
||||||
|
|
||||||
return createMemo(() => active() === id);
|
return createMemo(() => active() === id);
|
||||||
},
|
},
|
||||||
|
onClose: createMemo(() => props.onClose),
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = (e: Event) => {
|
||||||
|
if (!commandsContext || !props.onClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandsContext.execute(props.onClose, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <TabsContext.Provider value={ctx}>
|
return <TabsContext.Provider value={ctx}>
|
||||||
<div class={css.tabs}>
|
<div class={css.tabs}>
|
||||||
<header>
|
<header>
|
||||||
<For each={tabs().entries().toArray()}>{
|
<For each={tabs().entries().toArray()}>{
|
||||||
([id, label]) => <button onpointerdown={() => setActive(id)} classList={{ [css.active]: active() === id }}>{label}</button>
|
([id, { label, options: { closable = false } }]) => <Command.Context for={props.onClose} with={[id]}>
|
||||||
|
<span class={css.handle} classList={{ [css.active]: active() === id }}>
|
||||||
|
<button onpointerdown={() => setActive(id)}>{label}</button>
|
||||||
|
<Show when={closable}>
|
||||||
|
<button onPointerDown={onClose}> <IoCloseCircleOutline /></button>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</Command.Context>
|
||||||
}</For>
|
}</For>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</TabsContext.Provider>;
|
</TabsContext.Provider >;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab: ParentComponent<{ id: string, label: string }> = (props) => {
|
export const Tab: ParentComponent<{ id: string, label: string, closable?: boolean }> = (props) => {
|
||||||
const context = useTabs();
|
const context = useTabs();
|
||||||
|
|
||||||
const isActive = context.register(props.id, props.label);
|
const isActive = context.register(props.id, props.label, {
|
||||||
|
closable: props.closable ?? false
|
||||||
|
});
|
||||||
const resolved = children(() => props.children);
|
const resolved = children(() => props.children);
|
||||||
|
|
||||||
return <Show when={isActive()}>{resolved()}</Show>;
|
return <Show when={isActive()}><Command.Context for={context.onClose()} with={[props.id]}>{resolved()}</Command.Context></Show>;
|
||||||
}
|
}
|
|
@ -11,7 +11,6 @@ export default createHandler(() => (
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
{assets}
|
{assets}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface ContextMenuType {
|
||||||
|
|
||||||
const ContextMenu = createContext<ContextMenuType>()
|
const ContextMenu = createContext<ContextMenuType>()
|
||||||
|
|
||||||
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
const Root: ParentComponent<{ commands: CommandType<any[]>[] }> = (props) => {
|
||||||
const [target, setTarget] = createSignal<HTMLElement>();
|
const [target, setTarget] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
|
|
|
@ -1,48 +1,115 @@
|
||||||
import { Component, Show } from 'solid-js';
|
import { Accessor, children, Component, createContext, createEffect, createMemo, JSX, ParentComponent, ParentProps, Show, useContext } from 'solid-js';
|
||||||
|
|
||||||
export enum Modifier {
|
interface CommandContextType {
|
||||||
None = 0,
|
set(commands: CommandType<any[]>[]): void;
|
||||||
Shift = 1 << 0,
|
addContextualArguments<T extends any[] = any[]>(command: CommandType<T>, target: EventTarget, args: Accessor<T>): void;
|
||||||
Control = 1 << 1,
|
execute<TArgs extends any[] = []>(command: CommandType<TArgs>, event: Event): void;
|
||||||
Meta = 1 << 2,
|
|
||||||
Alt = 1 << 3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandType {
|
const CommandContext = createContext<CommandContextType>();
|
||||||
(): any;
|
|
||||||
label: string;
|
|
||||||
shortcut?: {
|
|
||||||
key: string;
|
|
||||||
modifier: Modifier;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createCommand = (label: string, command: () => any, shortcut?: CommandType['shortcut']): CommandType => {
|
export const useCommands = () => useContext(CommandContext);
|
||||||
return Object.defineProperties(command as CommandType, {
|
|
||||||
label: {
|
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||||
value: label,
|
// const commands = () => props.commands ?? [];
|
||||||
configurable: false,
|
const contextualArguments = new Map<CommandType, WeakMap<EventTarget, Accessor<any[]>>>();
|
||||||
writable: false,
|
const commands = new Set<CommandType<any[]>>();
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
set(c: CommandType<any[]>[]): void {
|
||||||
|
for (const command of c) {
|
||||||
|
commands.add(command);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shortcut: {
|
|
||||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
addContextualArguments<T extends any[] = any[]>(command: CommandType<T>, target: EventTarget, args: Accessor<T>): void {
|
||||||
configurable: false,
|
if (contextualArguments.has(command) === false) {
|
||||||
writable: false,
|
contextualArguments.set(command, new WeakMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextualArguments.get(command)?.set(target, args);
|
||||||
|
},
|
||||||
|
|
||||||
|
execute<T extends any[] = any[]>(command: CommandType<T>, event: Event): boolean | undefined {
|
||||||
|
const contexts = contextualArguments.get(command);
|
||||||
|
|
||||||
|
if (contexts === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = event.composedPath().find(el => contexts.has(el));
|
||||||
|
|
||||||
|
if (element === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const args = contexts.get(element)! as Accessor<T>;
|
||||||
|
|
||||||
|
command(...args());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
context.set(props.commands ?? []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const listener = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
const modifiers =
|
||||||
|
(e.shiftKey ? 1 : 0) << 0 |
|
||||||
|
(e.ctrlKey ? 1 : 0) << 1 |
|
||||||
|
(e.metaKey ? 1 : 0) << 2 |
|
||||||
|
(e.altKey ? 1 : 0) << 3;
|
||||||
|
|
||||||
|
const command = commands.values().find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers));
|
||||||
|
|
||||||
|
if (command === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.execute(command, e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <CommandContext.Provider value={context}>
|
||||||
|
<div tabIndex={0} style="display: contents;" onKeyDown={listener}>{props.children}</div>
|
||||||
|
</CommandContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
const Add: Component<{ command: CommandType<any[]> } | { commands: CommandType<any[]>[] }> = (props) => {
|
||||||
withLabel: {
|
const context = useCommands();
|
||||||
value(label: string) {
|
const commands = createMemo<CommandType<any[]>[]>(() => props.commands ?? [props.command]);
|
||||||
return createCommand(label, () => { });
|
|
||||||
},
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
},
|
|
||||||
}) as CommandType & { withLabel(label: string): CommandType };
|
|
||||||
|
|
||||||
export const Command: Component<{ command: CommandType }> = (props) => {
|
createEffect(() => {
|
||||||
|
context?.set(commands());
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Context = <T extends any[] = any[]>(props: ParentProps<{ for: CommandType<T>, with: T }>): JSX.Element => {
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
const context = useCommands();
|
||||||
|
const args = createMemo(() => props.with);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const children = resolved();
|
||||||
|
|
||||||
|
if (Array.isArray(children) || !(children instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.addContextualArguments(props.for, children, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{resolved()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Handle: Component<{ command: CommandType }> = (props) => {
|
||||||
return <>
|
return <>
|
||||||
{props.command.label}
|
{props.command.label}
|
||||||
<Show when={props.command.shortcut}>{
|
<Show when={props.command.shortcut}>{
|
||||||
|
@ -58,4 +125,67 @@ export const Command: Component<{ command: CommandType }> = (props) => {
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Command = { Root, Handle, Add, Context };
|
||||||
|
|
||||||
|
export enum Modifier {
|
||||||
|
None = 0,
|
||||||
|
Shift = 1 << 0,
|
||||||
|
Control = 1 << 1,
|
||||||
|
Meta = 1 << 2,
|
||||||
|
Alt = 1 << 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandType<TArgs extends any[] = []> {
|
||||||
|
(...args: TArgs): any;
|
||||||
|
label: string;
|
||||||
|
shortcut?: {
|
||||||
|
key: string;
|
||||||
|
modifier: Modifier;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCommand = <TArgs extends any[] = []>(label: string, command: (...args: TArgs) => any, shortcut?: CommandType['shortcut']): CommandType<TArgs> => {
|
||||||
|
return Object.defineProperties(command as CommandType<TArgs>, {
|
||||||
|
label: {
|
||||||
|
value: label,
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandArguments = <T extends any[] = any[]>(element: Element, commandAndArgs: Accessor<[CommandType<T>, T]>) => {
|
||||||
|
const ctx = useContext(CommandContext);
|
||||||
|
const args = createMemo(() => commandAndArgs()[1]);
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addContextualArguments(commandAndArgs()[0], element, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
||||||
|
withLabel: {
|
||||||
|
value(label: string) {
|
||||||
|
return createCommand(label, () => { });
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
}) as CommandType & { withLabel(label: string): CommandType };
|
||||||
|
|
||||||
|
declare module "solid-js" {
|
||||||
|
namespace JSX {
|
||||||
|
interface Directives {
|
||||||
|
commandArguments<T extends any[] = any[]>(): [CommandType<T>, T];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export { Context } from './contextMenu';
|
export { Context } from './contextMenu';
|
|
@ -3,7 +3,6 @@ import { Portal } from "solid-js/web";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { CommandType, Command } from "../command";
|
import { CommandType, Command } from "../command";
|
||||||
import css from "./index.module.css";
|
import css from "./index.module.css";
|
||||||
import { join } from "vinxi/dist/types/lib/path";
|
|
||||||
|
|
||||||
export interface MenuContextType {
|
export interface MenuContextType {
|
||||||
ref: Accessor<Node | undefined>;
|
ref: Accessor<Node | undefined>;
|
||||||
|
@ -61,7 +60,9 @@ export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (prop
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <MenuContext.Provider value={ctx}>{props.children}</MenuContext.Provider>;
|
return <Command.Root commands={ctx.commands()}>
|
||||||
|
<MenuContext.Provider value={ctx}>{props.children}</MenuContext.Provider>
|
||||||
|
</Command.Root>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMenu = () => {
|
const useMenu = () => {
|
||||||
|
@ -127,7 +128,7 @@ const Root: ParentComponent<{}> = (props) => {
|
||||||
|
|
||||||
const Child: Component<{ command: CommandType }> = (props) => {
|
const Child: Component<{ command: CommandType }> = (props) => {
|
||||||
return <button class={css.item} type="button" onpointerdown={onExecute(props.command)}>
|
return <button class={css.item} type="button" onpointerdown={onExecute(props.command)}>
|
||||||
<Command command={props.command} />
|
<Command.Handle command={props.command} />
|
||||||
</button>
|
</button>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -177,43 +178,10 @@ const Root: ParentComponent<{}> = (props) => {
|
||||||
</Portal>
|
</Portal>
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module "solid-js" {
|
|
||||||
namespace JSX {
|
|
||||||
interface HTMLAttributes<T> {
|
|
||||||
anchor?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Directives {
|
|
||||||
asMenuRoot: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Mount: Component = (props) => {
|
const Mount: Component = (props) => {
|
||||||
const menu = useMenu();
|
const menu = useMenu();
|
||||||
|
|
||||||
const listener = (e: KeyboardEvent) => {
|
return <div class={css.root} ref={menu.setRef} />;
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
const modifiers =
|
|
||||||
(e.shiftKey ? 1 : 0) << 0 |
|
|
||||||
(e.ctrlKey ? 1 : 0) << 1 |
|
|
||||||
(e.metaKey ? 1 : 0) << 2 |
|
|
||||||
(e.altKey ? 1 : 0) << 3;
|
|
||||||
|
|
||||||
const commands = menu.commands();
|
|
||||||
const command = commands.find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers));
|
|
||||||
|
|
||||||
if (command === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
command();
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div class={css.root} ref={menu.setRef} onKeyDown={listener}></div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Menu = { Mount, Root, Item, Separator } as const;
|
export const Menu = { Mount, Root, Item, Separator } as const;
|
||||||
|
@ -374,3 +342,11 @@ function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
|
||||||
</output>
|
</output>
|
||||||
</form>;
|
</form>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare module "solid-js" {
|
||||||
|
namespace JSX {
|
||||||
|
interface HTMLAttributes<T> {
|
||||||
|
anchor?: string | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -177,22 +177,7 @@ const Root: ParentComponent = (props) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
return <div ref={setRoot} tabIndex={0} onKeyDown={onKeyboardEvent} onKeyUp={onKeyboardEvent} style={{ 'display': 'contents' }}>{c()}</div>;
|
||||||
document.addEventListener('keydown', onKeyboardEvent);
|
|
||||||
document.addEventListener('keyup', onKeyboardEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (isServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.removeEventListener('keydown', onKeyboardEvent);
|
|
||||||
document.removeEventListener('keyup', onKeyboardEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div ref={setRoot} style={{ 'display': 'contents' }}>{c()}</div>;
|
|
||||||
// return <div ref={setRoot}>{c()}</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => {
|
export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Meta, Title } from "@solidjs/meta";
|
import { Link, Meta, Title } from "@solidjs/meta";
|
||||||
import { createSignal, For, ParentProps, Show } from "solid-js";
|
import { createSignal, For, ParentProps, Show } from "solid-js";
|
||||||
import { BsTranslate } from "solid-icons/bs";
|
import { BsTranslate } from "solid-icons/bs";
|
||||||
import { FilesProvider } from "~/features/file";
|
import { FilesProvider } from "~/features/file";
|
||||||
|
@ -23,6 +23,9 @@ export default function Editor(props: ParentProps) {
|
||||||
return <MenuProvider commands={commands}>
|
return <MenuProvider commands={commands}>
|
||||||
<Title>Translation-Tool</Title>
|
<Title>Translation-Tool</Title>
|
||||||
<Meta name="color-scheme" content={colorScheme()} />
|
<Meta name="color-scheme" content={colorScheme()} />
|
||||||
|
<Link rel="icon" href="/images/favicon.dark.svg" media="screen and (prefers-color-scheme: dark)" />
|
||||||
|
<Link rel="icon" href="/images/favicon.light.svg" media="screen and (prefers-color-scheme: light)" />
|
||||||
|
<Link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<main class={css.layout} inert={commandPalette()?.open()}>
|
<main class={css.layout} inert={commandPalette()?.open()}>
|
||||||
<nav class={css.menu}>
|
<nav class={css.menu}>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
& .sidebar {
|
& .sidebar {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: var(--padding-l);
|
padding: var(--padding-l);
|
||||||
|
padding-block-start: calc(2 * var(--padding-l));
|
||||||
background-color: var(--surface-2);
|
background-color: var(--surface-2);
|
||||||
|
|
||||||
& > ul {
|
& > ul {
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { Accessor, children, Component, createEffect, createMemo, createResource, createSignal, createUniqueId, For, onMount, ParentProps, Setter, Show } from "solid-js";
|
import { Component, createEffect, createMemo, createSignal, For, ParentProps, Setter, Show } from "solid-js";
|
||||||
import { filter, MutarionKind, Mutation, splitAt } from "~/utilities";
|
import { filter, MutarionKind, Mutation, splitAt } from "~/utilities";
|
||||||
import { Sidebar } from "~/components/sidebar";
|
import { Sidebar } from "~/components/sidebar";
|
||||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree, FileEntry, Entry } from "~/components/filetree";
|
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
||||||
import { Menu } from "~/features/menu";
|
import { Menu } from "~/features/menu";
|
||||||
import { Grid, load, useFiles } from "~/features/file";
|
import { Grid, load, useFiles } from "~/features/file";
|
||||||
import { Command, Context, createCommand, Modifier, noop } from "~/features/command";
|
import { Command, Context, createCommand, Modifier, noop, useCommands } from "~/features/command";
|
||||||
import { GridApi } from "~/features/file/grid";
|
import { GridApi } from "~/features/file/grid";
|
||||||
import css from "./edit.module.css";
|
|
||||||
import { Tab, Tabs } from "~/components/tabs";
|
import { Tab, Tabs } from "~/components/tabs";
|
||||||
|
import css from "./edit.module.css";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
|
||||||
async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], lang: string, entries: Map<string, string> }, void, never> {
|
async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], lang: string, entries: Map<string, string> }, void, never> {
|
||||||
for await (const handle of directory.values()) {
|
for await (const handle of directory.values()) {
|
||||||
|
@ -32,39 +35,30 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []):
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function* breadthFirstTraverse(subject: FolderEntry): Generator<{ path: string[] } & Entry, void, unknown> {
|
const open = createCommand('open folder', async () => {
|
||||||
const queue: ({ path: string[] } & Entry)[] = subject.entries.map(e => ({ path: [], ...e }));
|
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||||
|
|
||||||
while (queue.length > 0) {
|
useFiles().set('__root__', directory);
|
||||||
const entry = queue.shift()!;
|
}, { key: 'o', modifier: Modifier.Control });
|
||||||
|
|
||||||
yield entry;
|
|
||||||
|
|
||||||
if (entry.kind === 'folder') {
|
|
||||||
queue.push(...entry.entries.map(e => ({ path: [...entry.path, entry.name], ...e })));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const findFile = (folder: FolderEntry, id: string) => {
|
|
||||||
return breadthFirstTraverse(folder).find((entry): entry is { path: string[] } & FileEntry => entry.kind === 'file' && entry.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { }
|
interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { }
|
||||||
|
|
||||||
interface ContentTabType {
|
|
||||||
handle: FileSystemDirectoryHandle;
|
|
||||||
readonly api: Accessor<GridApi | undefined>;
|
|
||||||
readonly setApi: Setter<GridApi | undefined>;
|
|
||||||
readonly entries: Accessor<Entries>;
|
|
||||||
readonly setEntries: Setter<Entries>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Edit(props: ParentProps) {
|
export default function Edit(props: ParentProps) {
|
||||||
const filesContext = useFiles();
|
const filesContext = useFiles();
|
||||||
|
|
||||||
const root = filesContext.get('__root__');
|
const root = filesContext.get('__root__');
|
||||||
const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => {
|
|
||||||
|
return <Context.Root commands={[open]}>
|
||||||
|
<Show when={root()} fallback={<button onpointerdown={() => open()}>open a folder</button>}>{
|
||||||
|
root => <Editor root={root()} />
|
||||||
|
}</Show>
|
||||||
|
</Context.Root>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
|
const filesContext = useFiles();
|
||||||
|
|
||||||
|
const tabs = createMemo(() => filesContext.files().map(({ handle }) => {
|
||||||
const [api, setApi] = createSignal<GridApi>();
|
const [api, setApi] = createSignal<GridApi>();
|
||||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||||
|
|
||||||
|
@ -73,14 +67,12 @@ export default function Edit(props: ParentProps) {
|
||||||
const [active, setActive] = createSignal<string>();
|
const [active, setActive] = createSignal<string>();
|
||||||
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
||||||
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
||||||
const [entries, setEntries] = createSignal<Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>>(new Map);
|
|
||||||
|
|
||||||
const tab = createMemo(() => {
|
const tab = createMemo(() => {
|
||||||
const name = active();
|
const name = active();
|
||||||
return tabs().find(t => t.handle.name === name);
|
return tabs().find(t => t.handle.name === name);
|
||||||
});
|
});
|
||||||
const api = createMemo(() => tab()?.api());
|
const api = createMemo(() => tab()?.api());
|
||||||
|
|
||||||
const mutations = createMemo<(Mutation & { file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => {
|
const mutations = createMemo<(Mutation & { file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => {
|
||||||
const entries = tab.entries();
|
const entries = tab.entries();
|
||||||
const mutations = tab.api()?.mutations() ?? [];
|
const mutations = tab.api()?.mutations() ?? [];
|
||||||
|
@ -91,11 +83,9 @@ export default function Edit(props: ParentProps) {
|
||||||
return { ...m, key, file: entries.get(key)?.[lang] };
|
return { ...m, key, file: entries.get(key)?.[lang] };
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mutatedFiles = createMemo(() =>
|
const mutatedFiles = createMemo(() =>
|
||||||
new Set((mutations()).map(({ file }) => file).filter(file => file !== undefined))
|
new Set((mutations()).map(({ file }) => file).filter(file => file !== undefined))
|
||||||
);
|
);
|
||||||
|
|
||||||
const mutatedData = createMemo(() => {
|
const mutatedData = createMemo(() => {
|
||||||
const muts = mutations();
|
const muts = mutations();
|
||||||
const files = contents();
|
const files = contents();
|
||||||
|
@ -149,47 +139,21 @@ export default function Edit(props: ParentProps) {
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const directory = root();
|
const directory = props.root;
|
||||||
|
|
||||||
if (directory === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const contents = await Array.fromAsync(walk(directory));
|
setContents(new Map(await Array.fromAsync(walk(directory), ({ 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 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return aggregate;
|
|
||||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
|
||||||
|
|
||||||
setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
||||||
setEntries(merged);
|
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
open: createCommand('open folder', async () => {
|
|
||||||
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
|
||||||
|
|
||||||
filesContext.set('__root__', directory);
|
|
||||||
}, { key: 'o', modifier: Modifier.Control }),
|
|
||||||
close: createCommand('close folder', async () => {
|
close: createCommand('close folder', async () => {
|
||||||
filesContext.remove('__root__');
|
filesContext.remove('__root__');
|
||||||
}),
|
}),
|
||||||
|
closeTab: createCommand('close tab', async (id: string) => {
|
||||||
|
filesContext.remove(id);
|
||||||
|
}, { key: 'w', modifier: Modifier.Control | (isInstalledPWA ? Modifier.None : Modifier.Alt) }),
|
||||||
save: createCommand('save', async () => {
|
save: createCommand('save', async () => {
|
||||||
await Promise.allSettled(mutatedData().map(async ([handle, data]) => {
|
await Promise.allSettled(mutatedData().map(async ([handle, data]) => {
|
||||||
const stream = await handle.createWritable({ keepExistingData: false });
|
const stream = await handle.createWritable({ keepExistingData: false });
|
||||||
|
@ -203,7 +167,7 @@ export default function Edit(props: ParentProps) {
|
||||||
console.log('save as ...', handle);
|
console.log('save as ...', handle);
|
||||||
|
|
||||||
window.showSaveFilePicker({
|
window.showSaveFilePicker({
|
||||||
startIn: root(),
|
startIn: props.root,
|
||||||
excludeAcceptAllOption: true,
|
excludeAcceptAllOption: true,
|
||||||
types: [
|
types: [
|
||||||
{ accept: { 'application/json': ['.json'] }, description: 'JSON' },
|
{ accept: { 'application/json': ['.json'] }, description: 'JSON' },
|
||||||
|
@ -224,70 +188,76 @@ export default function Edit(props: ParentProps) {
|
||||||
}, { key: 'delete', modifier: Modifier.None }),
|
}, { key: 'delete', modifier: Modifier.None }),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const commandCtx = useCommands();
|
||||||
|
|
||||||
return <div class={css.root}>
|
return <div class={css.root}>
|
||||||
<Context.Root commands={[commands.saveAs]}>
|
<Command.Add commands={[commands.saveAs, commands.closeTab]} />
|
||||||
<Context.Menu>{
|
|
||||||
command => <Command command={command} />
|
|
||||||
}</Context.Menu>
|
|
||||||
|
|
||||||
<Menu.Root>
|
<Context.Menu>{
|
||||||
<Menu.Item label="file">
|
command => <Command.Handle command={command} />
|
||||||
<Menu.Item command={commands.open} />
|
}</Context.Menu>
|
||||||
|
|
||||||
<Menu.Item command={commands.close} />
|
<Menu.Root>
|
||||||
|
<Menu.Item label="file">
|
||||||
|
<Menu.Item command={commands.open} />
|
||||||
|
|
||||||
<Menu.Separator />
|
<Menu.Item command={commands.close} />
|
||||||
|
|
||||||
<Menu.Item command={commands.save} />
|
<Menu.Separator />
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Item label="edit">
|
<Menu.Item command={commands.save} />
|
||||||
<Menu.Item command={noop.withLabel('insert new key')} />
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item command={noop.withLabel('insert new language')} />
|
<Menu.Item label="edit">
|
||||||
|
<Menu.Item command={noop.withLabel('insert new key')} />
|
||||||
|
|
||||||
<Menu.Separator />
|
<Menu.Item command={noop.withLabel('insert new language')} />
|
||||||
|
|
||||||
<Menu.Item command={commands.delete} />
|
<Menu.Separator />
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Item label="selection">
|
<Menu.Item command={commands.delete} />
|
||||||
<Menu.Item command={commands.selectAll} />
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item command={commands.clearSelection} />
|
<Menu.Item label="selection">
|
||||||
</Menu.Item>
|
<Menu.Item command={commands.selectAll} />
|
||||||
|
|
||||||
<Menu.Item command={noop.withLabel('view')} />
|
<Menu.Item command={commands.clearSelection} />
|
||||||
</Menu.Root>
|
</Menu.Item>
|
||||||
|
|
||||||
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
<Menu.Item command={noop.withLabel('view')} />
|
||||||
<Show when={root()} fallback={<button onpointerdown={() => commands.open()}>open a folder</button>}>
|
</Menu.Root>
|
||||||
<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);
|
|
||||||
|
|
||||||
return <Context.Handle classList={{ [css.mutated]: mutated() }} onDblClick={() => {
|
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
||||||
const folder = file().directory;
|
<Tree entries={tree().entries}>{[
|
||||||
filesContext?.set(folder.name, folder);
|
folder => {
|
||||||
}}>{file().name}</Context.Handle>;
|
return <span onDblClick={() => {
|
||||||
},
|
filesContext?.set(folder().name, folder().handle);
|
||||||
] as const}</Tree>
|
}}>{folder().name}</span>;
|
||||||
</Show>
|
},
|
||||||
</Sidebar>
|
file => {
|
||||||
|
const mutated = createMemo(() => mutatedFiles().values().find(({ id }) => id === file().id) !== undefined);
|
||||||
|
|
||||||
<Tabs active={setActive}>
|
return <Context.Handle classList={{ [css.mutated]: mutated() }} onDblClick={() => {
|
||||||
<For each={tabs()}>{
|
const folder = file().directory;
|
||||||
({ handle, setApi, setEntries }) => <Tab id={handle.name} label={handle.name} ><Content directory={handle} api={setApi} entries={setEntries} /></Tab>
|
filesContext?.set(folder.name, folder);
|
||||||
}</For>
|
}}>{file().name}</Context.Handle>;
|
||||||
</Tabs>
|
},
|
||||||
</Context.Root>
|
] as const}</Tree>
|
||||||
</div>
|
</Sidebar>
|
||||||
}
|
|
||||||
|
<Tabs active={setActive} onClose={commands.closeTab}>
|
||||||
|
<For each={tabs()}>{
|
||||||
|
({ handle, setApi, setEntries }) => <Tab
|
||||||
|
id={handle.name}
|
||||||
|
label={handle.name}
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<Content directory={handle} api={setApi} entries={setEntries} />
|
||||||
|
</Tab>
|
||||||
|
}</For>
|
||||||
|
</Tabs>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => {
|
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => {
|
||||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue