file explorer

This commit is contained in:
vcoppe committed 2026-10-17 14:42:49 +02:00
1 parent b0a3c3a9ba
commit ed90ba6a1f
14 files changed
+263 -19

No files matched your search

@@ -0,0 +1,50 @@
<script lang="ts">
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import ExplorerTree from './ExplorerTree.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { engine } from '$lib/engine';
import { folderName } from '$lib/engine/workspace';
const { workspace } = engine;
let folder = $derived($workspace?.folder);
</script>
<!--
The tree of the open folder: its folders and GPX files, one level read at a time. The files are
opened from it (the file list has the ones that are open).
-->
{#if folder}
<div class="flex flex-col h-full">
<div class="px-2 py-1 text-sm font-semibold shrink-0">{i18n._('menu.explorer')}</div>
<ScrollArea class="grow min-h-0">
<div class="flex flex-col pl-1 pr-3 pb-1">
<!-- starts over when another folder is opened -->
{#key folder}
<!-- the folder itself, which is open at first -->
<CollapsibleTree side="left" defaultState="open">
<CollapsibleTreeNode id="folder">
{#snippet trigger()}
<span class="grow truncate text-left" title={folder}>
{folderName(folder)}
</span>
{/snippet}
{#snippet content(opened)}
<!-- what is in it is only read when it is open -->
{#if opened}
<div class="ml-4">
<!-- its folders are closed at first -->
<CollapsibleTree side="left" defaultState="closed">
<ExplorerTree />
</CollapsibleTree>
</div>
{/if}
{/snippet}
</CollapsibleTreeNode>
</CollapsibleTree>
{/key}
</div>
</ScrollArea>
</div>
{/if}
@@ -0,0 +1,80 @@
<script lang="ts">
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { Button } from '$lib/components/ui/button';
import { FileText } from '@lucide/svelte';
import ExplorerTree from './ExplorerTree.svelte';
import { openPath } from '$lib/actions/file-actions';
import { engine, type FolderEntry } from '$lib/engine';
import { selectedFileIds } from '$lib/engine/selection-helpers';
import { fileIdAt } from '$lib/engine/workspace';
let {
path,
}: {
/** The folder whose content is listed, the open folder if it is not given. */
path?: string;
} = $props();
const { workspace, selection, folderVersion } = engine;
let entries: FolderEntry[] = $state([]);
$effect(() => {
// read again when something changed on the file system
$folderVersion;
let current = true;
engine.listFolder(path).then((list) => {
if (current) {
entries = list;
}
});
return () => {
current = false;
};
});
/** Opens the file, or selects it if it is open already. */
function open(entry: FolderEntry, id: string | undefined) {
if (id !== undefined) {
engine.select([id]);
} else {
openPath(entry.path);
}
}
</script>
{#each entries as entry (entry.path)}
{#if entry.isDir}
<CollapsibleTreeNode id={entry.name}>
{#snippet trigger()}
<span class="grow truncate text-left">{entry.name}</span>
{/snippet}
{#snippet content(opened)}
<!-- the content of a folder is only read when it is opened -->
{#if opened}
<div class="ml-4">
<ExplorerTree path={entry.path} />
</div>
{/if}
{/snippet}
</CollapsibleTreeNode>
{:else}
{@const id = fileIdAt($workspace, entry.path)}
{@const selected = id !== undefined && selectedFileIds($selection).includes(id)}
{@const dirty = id !== undefined && $workspace?.files[id]?.dirty === true}
<Button
variant="ghost"
class="w-full h-7 px-1 justify-start gap-1.5 border-none font-normal {selected
? 'bg-accent'
: ''} {id === undefined ? 'text-muted-foreground' : ''}"
title={entry.path}
onclick={() => open(entry, id)}
>
<FileText size="16" class="shrink-0" />
<span class="grow truncate text-left">{entry.name}</span>
{#if dirty}
<span class="size-1.5 shrink-0 rounded-full bg-foreground"></span>
{/if}
</Button>
{/if}
{/each}
@@ -0,0 +1 @@
export { default as Explorer } from './Explorer.svelte';