2024-05-17 15:02:45 +02:00
|
|
|
<script lang="ts">
|
|
|
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
2024-05-21 22:37:52 +02:00
|
|
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
2024-05-17 15:02:45 +02:00
|
|
|
import Sortable from 'sortablejs/Sortable';
|
2024-05-21 22:37:52 +02:00
|
|
|
import { settings, type GPXFileWithStatistics } from '$lib/db';
|
|
|
|
import { get, type Readable, type Writable } from 'svelte/store';
|
2024-05-17 15:02:45 +02:00
|
|
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
|
|
|
import FileListNode from './FileListNode.svelte';
|
2024-05-21 13:22:14 +02:00
|
|
|
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
2024-05-17 15:02:45 +02:00
|
|
|
|
|
|
|
export let node:
|
|
|
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
|
|
|
| GPXTreeElement<AnyGPXTreeElement>
|
|
|
|
| ReadonlyArray<Readonly<Waypoint>>;
|
2024-05-21 13:22:14 +02:00
|
|
|
export let waypointRoot: boolean = false;
|
2024-05-17 15:02:45 +02:00
|
|
|
export let id: string;
|
|
|
|
|
|
|
|
let container: HTMLElement;
|
2024-05-21 13:22:14 +02:00
|
|
|
let items: { [id: string | number]: HTMLElement } = {};
|
2024-05-17 15:02:45 +02:00
|
|
|
let sortableLevel =
|
|
|
|
node instanceof Map
|
|
|
|
? 'file'
|
|
|
|
: node instanceof GPXFile
|
2024-05-21 13:22:14 +02:00
|
|
|
? waypointRoot
|
|
|
|
? 'waypoints'
|
|
|
|
: 'track'
|
2024-05-17 15:02:45 +02:00
|
|
|
: node instanceof Track
|
|
|
|
? 'segment'
|
|
|
|
: 'waypoint';
|
2024-05-20 14:32:52 +02:00
|
|
|
let pull: Record<string, string[]> = {
|
|
|
|
file: ['file', 'track'],
|
|
|
|
track: ['file', 'track'],
|
|
|
|
segment: ['file', 'track', 'segment'],
|
|
|
|
waypoint: ['waypoint']
|
|
|
|
};
|
2024-05-17 15:02:45 +02:00
|
|
|
let sortable: Sortable;
|
|
|
|
|
2024-05-21 17:47:08 +02:00
|
|
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
2024-05-21 13:22:14 +02:00
|
|
|
let selected = getContext<Writable<Set<string>>>('selected');
|
|
|
|
|
|
|
|
function onSelectChange() {
|
|
|
|
selected.update(($selected) => {
|
|
|
|
$selected.clear();
|
|
|
|
Object.entries(items).forEach(([id, item]) => {
|
|
|
|
if (item.classList.contains('sortable-selected')) {
|
|
|
|
$selected.add(id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return $selected;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-21 22:37:52 +02:00
|
|
|
function syncFileOrder() {
|
|
|
|
if (sortableLevel !== 'file') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const currentOrder = sortable.toArray();
|
|
|
|
if (currentOrder.length !== $fileOrder.length) {
|
|
|
|
sortable.sort($fileOrder);
|
|
|
|
} else {
|
|
|
|
for (let i = 0; i < currentOrder.length; i++) {
|
|
|
|
if (currentOrder[i] !== $fileOrder[i]) {
|
|
|
|
sortable.sort($fileOrder);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const { fileOrder } = settings;
|
|
|
|
|
2024-05-17 15:02:45 +02:00
|
|
|
onMount(() => {
|
|
|
|
sortable = Sortable.create(container, {
|
2024-05-20 14:32:52 +02:00
|
|
|
group: {
|
|
|
|
name: sortableLevel
|
|
|
|
},
|
2024-05-17 15:02:45 +02:00
|
|
|
forceAutoScrollFallback: true,
|
|
|
|
multiDrag: true,
|
|
|
|
multiDragKey: 'Meta',
|
2024-05-21 13:22:14 +02:00
|
|
|
avoidImplicitDeselect: true,
|
|
|
|
onSelect: onSelectChange,
|
|
|
|
onDeselect: onSelectChange,
|
2024-05-21 22:37:52 +02:00
|
|
|
sort: sortableLevel !== 'waypoint',
|
|
|
|
onSort: () => {
|
|
|
|
if (sortableLevel !== 'file') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let newFileOrder = sortable.toArray();
|
|
|
|
if (newFileOrder.length !== get(fileOrder).length) {
|
|
|
|
fileOrder.set(newFileOrder);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < newFileOrder.length; i++) {
|
|
|
|
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
|
|
|
fileOrder.set(newFileOrder);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-05-17 15:02:45 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-05-21 22:37:52 +02:00
|
|
|
$: if ($fileOrder && sortable) {
|
|
|
|
syncFileOrder();
|
|
|
|
}
|
|
|
|
|
|
|
|
afterUpdate(() => {
|
|
|
|
syncFileOrder();
|
|
|
|
});
|
|
|
|
|
2024-05-21 13:22:14 +02:00
|
|
|
const unsubscribe = selected.subscribe(($selected) => {
|
|
|
|
Object.entries(items).forEach(([id, item]) => {
|
|
|
|
if ($selected.has(id) && !item.classList.contains('sortable-selected')) {
|
|
|
|
Sortable.utils.select(item);
|
|
|
|
} else if (!$selected.has(id) && item.classList.contains('sortable-selected')) {
|
|
|
|
Sortable.utils.deselect(item);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
unsubscribe();
|
|
|
|
});
|
|
|
|
|
|
|
|
function getChildId(i: number): string {
|
|
|
|
switch (sortableLevel) {
|
|
|
|
case 'track':
|
|
|
|
return `${id}-track-${i}`;
|
|
|
|
case 'segment':
|
|
|
|
return `${id}-seg-${i}`;
|
|
|
|
case 'waypoint':
|
|
|
|
return `${id}-${i}`;
|
|
|
|
}
|
|
|
|
return '';
|
2024-05-17 15:02:45 +02:00
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
2024-05-21 17:47:08 +02:00
|
|
|
<div
|
|
|
|
bind:this={container}
|
|
|
|
class="sortable {orientation} flex {orientation === 'vertical' ? 'flex-col' : 'flex-row gap-1'}"
|
|
|
|
>
|
2024-05-17 15:02:45 +02:00
|
|
|
{#if node instanceof Map}
|
2024-05-21 13:22:14 +02:00
|
|
|
{#each node as [fileId, file]}
|
2024-05-21 22:37:52 +02:00
|
|
|
<div bind:this={items[fileId]} data-id={fileId}>
|
2024-05-21 17:47:08 +02:00
|
|
|
<FileListNodeStore {file} />
|
2024-05-17 15:02:45 +02:00
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{:else if node instanceof GPXFile}
|
2024-05-21 13:22:14 +02:00
|
|
|
{#if waypointRoot}
|
|
|
|
{#if node.wpt.length > 0}
|
|
|
|
<div bind:this={items[`${id}-wpt`]}>
|
2024-05-21 17:47:08 +02:00
|
|
|
<FileListNode node={node.wpt} id={`${id}-wpt`} />
|
2024-05-21 13:22:14 +02:00
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{:else}
|
|
|
|
{#each node.children as child, i}
|
|
|
|
<div bind:this={items[getChildId(i)]}>
|
2024-05-21 17:47:08 +02:00
|
|
|
<FileListNode node={child} id={getChildId(i)} index={i} />
|
2024-05-21 13:22:14 +02:00
|
|
|
</div>
|
|
|
|
{/each}
|
2024-05-17 15:02:45 +02:00
|
|
|
{/if}
|
|
|
|
{:else if node instanceof Track}
|
|
|
|
{#each node.children as child, i}
|
2024-05-21 17:47:08 +02:00
|
|
|
<div bind:this={items[getChildId(i)]} class="ml-1">
|
2024-05-20 14:32:52 +02:00
|
|
|
<div>
|
2024-05-21 17:47:08 +02:00
|
|
|
<FileListNodeLabel id={getChildId(i)} label={`Segment ${i + 1}`} />
|
2024-05-20 14:32:52 +02:00
|
|
|
</div>
|
2024-05-17 15:02:45 +02:00
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint}
|
|
|
|
{#each node as wpt, i}
|
2024-05-21 17:47:08 +02:00
|
|
|
<div bind:this={items[getChildId(i)]} class="ml-1">
|
2024-05-20 14:32:52 +02:00
|
|
|
<div>
|
2024-05-21 17:47:08 +02:00
|
|
|
<FileListNodeLabel id={getChildId(i)} label={wpt.name ?? `Waypoint ${i + 1}`} />
|
2024-05-20 14:32:52 +02:00
|
|
|
</div>
|
2024-05-17 15:02:45 +02:00
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
|
2024-05-21 13:22:14 +02:00
|
|
|
{#if node instanceof GPXFile}
|
|
|
|
{#if !waypointRoot}
|
|
|
|
<svelte:self {node} {id} waypointRoot={true} />
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
|
2024-05-17 15:02:45 +02:00
|
|
|
<style lang="postcss">
|
2024-05-21 13:22:14 +02:00
|
|
|
.sortable > div {
|
|
|
|
@apply rounded-md;
|
|
|
|
@apply h-fit;
|
|
|
|
@apply leading-none;
|
|
|
|
}
|
|
|
|
|
2024-05-21 17:47:08 +02:00
|
|
|
.vertical :global(.sortable-selected) {
|
2024-05-17 15:02:45 +02:00
|
|
|
@apply bg-accent;
|
|
|
|
}
|
2024-05-21 17:47:08 +02:00
|
|
|
|
|
|
|
.horizontal :global(button) {
|
|
|
|
@apply bg-accent;
|
|
|
|
@apply hover:bg-background;
|
|
|
|
}
|
|
|
|
|
|
|
|
.horizontal :global(.sortable-selected button) {
|
|
|
|
@apply bg-background;
|
|
|
|
}
|
2024-05-17 15:02:45 +02:00
|
|
|
</style>
|