Migration to new Rust-based engine (#401)

* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
vcoppeandvcoppe authored and GitHub committed 2026-10-31 18:08:22 +01:00
1 parent 6a0d434371
commit 8fe7d8d64d
484 files changed
+53949 -41152

No files matched your search

@@ -0,0 +1,190 @@
<script lang="ts">
import { setContext, untrack } from 'svelte';
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { CollapsibleTreeState } from '$lib/components/collapsible-tree/utils.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import ExplorerTree from './ExplorerTree.svelte';
import ExplorerRow from './ExplorerRow.svelte';
import {
clickedInExplorer,
explorerFoldersKey,
explorerSelection,
renaming,
treeRows,
type ExplorerFolders,
} from './state.svelte';
import { openPaths } from '$lib/actions/file-actions';
import { i18n } from '$lib/i18n/i18n.svelte';
import { engine } from '$lib/engine';
import { selectedFileIds } from '$lib/engine/selection-helpers';
import { folderName, parentOf, subfolder } from '$lib/engine/workspace';
const { workspace, selection } = engine;
let folder = $derived($workspace?.folder);
// which folders are open: new ones when another folder is opened, the folder itself is open at
// first and its folders are closed
let trees = $derived(
folder
? {
root: new CollapsibleTreeState('open'),
folders: new CollapsibleTreeState('closed'),
}
: undefined
);
let container: HTMLElement | undefined = $state();
// the files that are selected, if they are in the folder
let selectedPaths = $derived(
selectedFileIds($selection)
.map((id) => $workspace?.files[id]?.path)
.filter((path): path is string => subfolder(folder, path) !== undefined)
);
let selectedPath = $derived(selectedPaths[0]);
$effect(() => {
// not what the click that was just made depends on, only the selection and the folder
const clicked = untrack(() => clickedInExplorer.path);
if (clicked !== undefined && selectedPath === clicked) {
return;
}
clickedInExplorer.path = undefined;
// what is selected elsewhere (the file list, the map) is selected here
explorerSelection.paths = untrack(() => selectedPaths);
explorerSelection.anchor = selectedPath;
if (folder && selectedPath !== undefined) {
return reveal(folder, selectedPath);
}
});
/**
* Opens the folders that hold a file and scrolls to the file, if it is not in view already. Its
* row may not be there yet, because the content of a folder is read when it is opened.
*/
function reveal(folder: string, path: string) {
openFolder(parentOf(path));
let frame = 0;
let attempts = 0;
const scroll = () => {
const row = container?.querySelector(`[data-path="${CSS.escape(path)}"]`);
if (row) {
row.scrollIntoView({ block: 'nearest' });
} else if (attempts++ < 120) {
frame = requestAnimationFrame(scroll);
}
};
frame = requestAnimationFrame(scroll);
return () => cancelAnimationFrame(frame);
}
/**
* The id of the node of a folder of the tree in `trees.folders`: the names of the folders it is
* in, and its own (see `CollapsibleTreeNode`). `undefined` for the open folder itself, which is
* in `trees.root`, and for what is not in it.
*/
function nodeId(path: string): string | undefined {
const names = subfolder(folder, `${path}/_`)?.split('/').filter(Boolean);
return names && names.length > 0 ? ['root', ...names].join('.') : undefined;
}
/** Opens a folder of the tree, and the folders it is in. */
function openFolder(path: string) {
if (!trees) return;
trees.root.get('root.folder').current = true;
const id = nodeId(path);
if (id === undefined) return;
const names = id.split('.');
names.forEach((_, i) => {
if (i > 0) trees!.folders.get(names.slice(0, i + 1).join('.')).current = true;
});
}
setContext<ExplorerFolders>(explorerFoldersKey, {
open: openFolder,
moved(from, to) {
const [fromId, toId] = [nodeId(from), nodeId(to)];
if (trees && fromId !== undefined && toId !== undefined) {
trees.folders.move(fromId, toId);
}
},
});
/** Enter opens the selected files for good. */
function onkeydown(event: KeyboardEvent) {
if (renaming.path !== undefined || event.key !== 'Enter') return;
const files = treeRows(container)
.filter((row) => row.kind === 'file' && explorerSelection.paths.includes(row.path))
.map((row) => row.path);
if (files.length > 0) {
openPaths(files);
event.preventDefault();
}
}
</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 && trees}
<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
bind:this={container}
data-explorer
role="tree"
tabindex="0"
class="flex flex-col pl-1 pr-3 pb-1 outline-none"
{onkeydown}
>
<!-- starts over when another folder is opened -->
{#key folder}
<!-- the folder itself -->
<CollapsibleTree side="left" treeState={trees.root} sticky>
<CollapsibleTreeNode id="folder">
{#snippet trigger()}
<ExplorerRow
path={folder}
name={folderName(folder)}
kind="root"
class="h-7"
/>
{/snippet}
{#snippet content(opened)}
<!-- what is in it is only read when it is open -->
{#if opened}
<div class="ml-4">
<CollapsibleTree side="left" treeState={trees.folders}>
<ExplorerTree />
</CollapsibleTree>
</div>
{/if}
{/snippet}
</CollapsibleTreeNode>
</CollapsibleTree>
{/key}
</div>
</ScrollArea>
</div>
{/if}
<style lang="postcss">
@reference "../../../app.css";
/* the color of the file list: a file and a folder (the whole row of its node, with its
chevron) under the pointer, or selected */
[data-explorer] :global([data-kind='file']:hover),
[data-explorer] :global([data-slot='button']:hover),
[data-explorer] :global([data-kind='file'][aria-selected='true']),
[data-explorer] :global([data-slot='button']:has([aria-selected='true'])) {
@apply bg-[var(--selection)];
}
/* the folder where what is dragged would go */
[data-explorer] :global([data-slot='button']:has([data-drop-target])) {
@apply ring-1 ring-primary bg-[var(--selection)];
}
</style>