mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 02:27:18 +00:00
route full support
This commit is contained in:
1 parent
c842da8dc1
commit
b6c0f5190a
91 files changed
+2523
-847
No files matched your search
@@ -30,6 +30,12 @@
|
||||
switch (node.type) {
|
||||
case 'file':
|
||||
return structure.name;
|
||||
case 'route': {
|
||||
const routeIndex = structure.routes.findIndex((r) => r.id === node.routeId);
|
||||
return (
|
||||
structure.routes[routeIndex]?.name ?? `${i18n._('gpx.route')} ${routeIndex + 1}`
|
||||
);
|
||||
}
|
||||
case 'track': {
|
||||
const trackIndex = structure.tracks.findIndex((t) => t.id === node.trackId);
|
||||
return (
|
||||
@@ -75,7 +81,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if node.type === 'segment' || node.type === 'waypoint'}
|
||||
{#if node.type === 'route' || node.type === 'segment' || node.type === 'waypoint'}
|
||||
<FileListNodeLabel {fileState} {node} {label} />
|
||||
{:else if recursive}
|
||||
<CollapsibleTreeNode {id} bind:this={collapsible}>
|
||||
@@ -84,7 +90,11 @@
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="ml-4">
|
||||
<FileListNodeContent {fileState} {node} />
|
||||
{#if node.type === 'file' && fileState.structure.routes.length > 0}
|
||||
<FileListNodeContent {fileState} {node} part="routes" />
|
||||
{:else}
|
||||
<FileListNodeContent {fileState} {node} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CollapsibleTreeNode>
|
||||
|
||||
@@ -10,16 +10,21 @@
|
||||
let {
|
||||
fileState,
|
||||
node,
|
||||
waypointRoot = false,
|
||||
part = 'tracks',
|
||||
}: {
|
||||
/** State of the file of the node (not needed for the root). */
|
||||
fileState?: FileState;
|
||||
/** The node whose children are listed, `null` for the list of the files. */
|
||||
node: FileTreeNode | null;
|
||||
/** List the node standing for the waypoints of the file, instead of its tracks. */
|
||||
waypointRoot?: boolean;
|
||||
/**
|
||||
* For a file, which of its lists this is: its routes, its tracks, or the node standing for
|
||||
* its waypoints. Each one is followed by the next ones.
|
||||
*/
|
||||
part?: 'routes' | 'tracks' | 'waypoints';
|
||||
} = $props();
|
||||
|
||||
let waypointRoot = part === 'waypoints';
|
||||
|
||||
const { files, order } = engine;
|
||||
|
||||
let container: HTMLElement;
|
||||
@@ -27,9 +32,11 @@
|
||||
node === null
|
||||
? 'file'
|
||||
: node.type === 'file'
|
||||
? waypointRoot
|
||||
? part === 'waypoints'
|
||||
? 'waypoints'
|
||||
: 'track'
|
||||
: part === 'routes'
|
||||
? 'route'
|
||||
: 'track'
|
||||
: node.type === 'waypoints'
|
||||
? 'waypoint'
|
||||
: 'segment';
|
||||
@@ -74,7 +81,7 @@
|
||||
{/each}
|
||||
{:else if fileState}
|
||||
{#if node.type === 'file'}
|
||||
{#if waypointRoot}
|
||||
{#if part === 'waypoints'}
|
||||
{#if fileState.structure.waypoints.length > 0}
|
||||
<div data-id="waypoints">
|
||||
<FileListNode
|
||||
@@ -83,6 +90,15 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if part === 'routes'}
|
||||
{#each fileState.structure.routes as route (route.id)}
|
||||
<div data-id={route.id}>
|
||||
<FileListNode
|
||||
{fileState}
|
||||
node={{ type: 'route', fileId: node.fileId, routeId: route.id }}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each fileState.structure.tracks as track (track.id)}
|
||||
<div data-id={track.id}>
|
||||
@@ -120,8 +136,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fileState && node?.type === 'file' && !waypointRoot}
|
||||
<FileListNodeContent {fileState} {node} waypointRoot={true} />
|
||||
{#if fileState && node?.type === 'file' && part === 'routes'}
|
||||
<FileListNodeContent {fileState} {node} part="tracks" />
|
||||
{:else if fileState && node?.type === 'file' && part === 'tracks'}
|
||||
<FileListNodeContent {fileState} {node} part="waypoints" />
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
Scissors,
|
||||
FileStack,
|
||||
FileX,
|
||||
Route,
|
||||
} from '@lucide/svelte';
|
||||
import { getContext, untrack } from 'svelte';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
@@ -34,6 +35,7 @@
|
||||
elementId,
|
||||
isInClipboard,
|
||||
isSelected,
|
||||
pathInfo,
|
||||
selectedElementIds,
|
||||
selectedFileIds,
|
||||
selectionSize,
|
||||
@@ -62,20 +64,23 @@
|
||||
|
||||
let singleSelection = $derived(selectionSize($selection) === 1);
|
||||
|
||||
// a route or a track: it has a name, a description and a style, like a file
|
||||
let isPath = $derived(node.type === 'route' || node.type === 'track');
|
||||
|
||||
let nodeColors: string[] = $derived.by(() => {
|
||||
const { tracks } = fileState.structure;
|
||||
const { routes, tracks } = fileState.structure;
|
||||
if (node.type === 'file') {
|
||||
// the colors defined by the tracks, or else the one of the file
|
||||
// the colors defined by the routes and tracks, or else the one of the file
|
||||
const colors = [
|
||||
...new Set(
|
||||
tracks.flatMap((track) =>
|
||||
track.color !== undefined ? [normalizeColor(track.color)] : []
|
||||
[...routes, ...tracks].flatMap((path) =>
|
||||
path.color !== undefined ? [normalizeColor(path.color)] : []
|
||||
)
|
||||
),
|
||||
];
|
||||
return colors.length > 0 ? colors : [fileState.color];
|
||||
} else if (node.type === 'track') {
|
||||
const color = tracks.find((track) => track.id === node.trackId)?.color;
|
||||
} else if (isPath) {
|
||||
const color = pathInfo(fileState.structure, node)?.color;
|
||||
return [color !== undefined ? normalizeColor(color) : fileState.color];
|
||||
}
|
||||
return [];
|
||||
@@ -141,11 +146,11 @@
|
||||
? 'w-full h-7'
|
||||
: 'w-max h-9 px-1.5'} pointer-events-auto"
|
||||
>
|
||||
{#if node.type === 'file' || node.type === 'track'}
|
||||
{#if node.type === 'file' || isPath}
|
||||
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
|
||||
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
|
||||
{/if}
|
||||
{#if level === 'file' || level === 'track'}
|
||||
{#if level === 'file' || isPath}
|
||||
<div
|
||||
class="absolute {orientation === 'vertical'
|
||||
? 'top-0 bottom-0 right-0 w-1'
|
||||
@@ -197,7 +202,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if level === 'segment'}
|
||||
{#if level === 'route'}
|
||||
<Route size="16" class="mx-1 shrink-0" />
|
||||
{:else if level === 'segment'}
|
||||
<Waypoints size="16" class="mx-1 shrink-0" />
|
||||
{:else if level === 'waypoint'}
|
||||
{#if symbolKey && symbols[symbolKey].icon}
|
||||
@@ -238,7 +245,7 @@
|
||||
</Button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
{#if node.type === 'file' || node.type === 'track'}
|
||||
{#if node.type === 'file' || isPath}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
onclick={() => (editMetadata.current = true)}
|
||||
@@ -269,6 +276,10 @@
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_track')}
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item disabled={!singleSelection} onclick={() => engine.newRoute()}>
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_route')}
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{:else if node.type === 'track'}
|
||||
<ContextMenu.Item
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
import { pathInfo, type FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
import { showEngineError } from '$lib/actions/file-actions';
|
||||
|
||||
let {
|
||||
@@ -21,16 +21,13 @@
|
||||
open: boolean;
|
||||
} = $props();
|
||||
|
||||
let track = $derived(
|
||||
node.type === 'track'
|
||||
? fileState.structure.tracks.find((track) => track.id === node.trackId)
|
||||
: undefined
|
||||
);
|
||||
// the route or the track
|
||||
let path = $derived(pathInfo(fileState.structure, node));
|
||||
let name: string = $derived(
|
||||
node.type === 'file' ? fileState.structure.name : (track?.name ?? '')
|
||||
node.type === 'file' ? fileState.structure.name : (path?.name ?? '')
|
||||
);
|
||||
let description: string = $derived(
|
||||
node.type === 'file' ? (fileState.structure.desc ?? '') : (track?.desc ?? '')
|
||||
node.type === 'file' ? (fileState.structure.desc ?? '') : (path?.desc ?? '')
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -5,13 +5,22 @@ import { tick } from 'svelte';
|
||||
import { engine, type MoveTarget, type Selection } from '$lib/engine';
|
||||
import { isSelected, type FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
|
||||
/** What a list holds: the files, the tracks of a file, the segments of a track, or the waypoints. */
|
||||
export type ListLevel = 'file' | 'track' | 'segment' | 'waypoints' | 'waypoint';
|
||||
/**
|
||||
* What a list holds: the files, the routes or the tracks of a file, the segments of a track, or the
|
||||
* waypoints.
|
||||
*/
|
||||
export type ListLevel = 'file' | 'route' | 'track' | 'segment' | 'waypoints' | 'waypoint';
|
||||
|
||||
/**
|
||||
* Where the elements of a list can be moved to (a segment on its own can become a route). Routes
|
||||
* and tracks can be dropped among each other, but keep their kind: the engine puts them with the
|
||||
* others of their kind, the routes coming first.
|
||||
*/
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
file: ['file'],
|
||||
track: ['file', 'track'],
|
||||
segment: ['file', 'track', 'segment'],
|
||||
route: ['file', 'route', 'track'],
|
||||
track: ['file', 'route', 'track'],
|
||||
segment: ['file', 'route', 'track', 'segment'],
|
||||
waypoints: ['waypoints'],
|
||||
waypoint: ['waypoints', 'waypoint'],
|
||||
};
|
||||
@@ -103,6 +112,8 @@ export class SortableFileList {
|
||||
switch (this._sortableLevel) {
|
||||
case 'file':
|
||||
return { type: 'file', fileId: id };
|
||||
case 'route':
|
||||
return parent ? { type: 'route', fileId: parent.fileId, routeId: id } : undefined;
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
|
||||
case 'segment':
|
||||
@@ -129,6 +140,8 @@ export class SortableFileList {
|
||||
switch (this._sortableLevel) {
|
||||
case 'file':
|
||||
return { type: 'file', fileIds: ids };
|
||||
case 'route':
|
||||
return parent ? { type: 'route', fileId: parent.fileId, routeIds: ids } : undefined;
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
|
||||
case 'segment':
|
||||
@@ -155,6 +168,8 @@ export class SortableFileList {
|
||||
switch (this._sortableLevel) {
|
||||
case 'file':
|
||||
return { type: 'files', index };
|
||||
case 'route':
|
||||
return parent ? { type: 'routes', fileId: parent.fileId, index } : undefined;
|
||||
case 'track':
|
||||
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
|
||||
case 'segment':
|
||||
@@ -275,6 +290,9 @@ export class SortableFileList {
|
||||
case 'file':
|
||||
request = engine.select(selected);
|
||||
break;
|
||||
case 'route':
|
||||
request = engine.selectRoutes(parent!.fileId, selected);
|
||||
break;
|
||||
case 'track':
|
||||
request = engine.selectTracks(parent!.fileId, selected);
|
||||
break;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { settings } from '$lib/state/settings';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import { normalizeColor } from '$lib/engine/file-colors';
|
||||
import { selectedFileIds, type FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
import { pathInfo, selectedFileIds, type FileTreeNode } from '$lib/engine/selection-helpers';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let {
|
||||
@@ -38,17 +38,15 @@
|
||||
width = $defaultWidth;
|
||||
color = fileState.color;
|
||||
|
||||
const { tracks } = fileState.structure;
|
||||
const paths = [...fileState.structure.routes, ...fileState.structure.tracks];
|
||||
const path = pathInfo(fileState.structure, node);
|
||||
if (node.type === 'file') {
|
||||
opacity = tracks.find((track) => track.opacity !== undefined)?.opacity ?? opacity;
|
||||
width = tracks.find((track) => track.width !== undefined)?.width ?? width;
|
||||
} else if (node.type === 'track') {
|
||||
const track = tracks.find((track) => track.id === node.trackId);
|
||||
if (track) {
|
||||
color = track.color !== undefined ? normalizeColor(track.color) : color;
|
||||
opacity = track.opacity ?? opacity;
|
||||
width = track.width ?? width;
|
||||
}
|
||||
opacity = paths.find((path) => path.opacity !== undefined)?.opacity ?? opacity;
|
||||
width = paths.find((path) => path.width !== undefined)?.width ?? width;
|
||||
} else if (path) {
|
||||
color = path.color !== undefined ? normalizeColor(path.color) : color;
|
||||
opacity = path.opacity ?? opacity;
|
||||
width = path.width ?? width;
|
||||
}
|
||||
|
||||
colorChanged = false;
|
||||
|
||||
Reference in new issue
Block a user