mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-16 08:19:59 +00:00
progress
This commit is contained in:
1 parent
caba9388af
commit
f458a1104f
15 files changed
+797
-455
No files matched your search
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||
import FileListNode from './FileListNode.svelte';
|
||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { ListFileItem, ListLevel, ListRootItem } from './file-list';
|
||||
import { ListLevel } from './file-list';
|
||||
import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { engine } from '$lib/engine';
|
||||
import { selectedFileIds } from '$lib/selection-helpers';
|
||||
import { createFile, pasteSelection } from '$lib/logic/file-actions';
|
||||
import { selection, copied } from '$lib/logic/selection';
|
||||
// TODO the clipboard still works on the previous implementation
|
||||
import { copied } from '$lib/logic/selection';
|
||||
import { allowedPastes } from './sortable-file-list';
|
||||
|
||||
let {
|
||||
@@ -27,17 +29,12 @@
|
||||
setContext('orientation', orientation);
|
||||
setContext('recursive', recursive);
|
||||
|
||||
const { files, selection } = engine;
|
||||
|
||||
onMount(() => {
|
||||
if (orientation === 'horizontal') {
|
||||
selection.update(($selection) => {
|
||||
$selection.forEach((item) => {
|
||||
if (!(item instanceof ListFileItem)) {
|
||||
$selection.toggle(item);
|
||||
$selection.set(new ListFileItem(item.getFileId()), true);
|
||||
}
|
||||
});
|
||||
return $selection;
|
||||
});
|
||||
if (orientation === 'horizontal' && $selection.type !== 'file') {
|
||||
// only files can be selected here
|
||||
engine.select(selectedFileIds($selection));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -54,7 +51,7 @@
|
||||
: 'flex-row'} {className ?? ''}"
|
||||
{style}
|
||||
>
|
||||
<FileListNode node={$fileStateCollection} item={new ListRootItem()} />
|
||||
<FileListNodeContent node={null} />
|
||||
{#if orientation === 'vertical'}
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger class="grow" />
|
||||
@@ -66,8 +63,8 @@
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item
|
||||
onclick={() => selection.selectAll()}
|
||||
disabled={$fileStateCollection.size === 0}
|
||||
onclick={() => engine.selectAll()}
|
||||
disabled={$files.size === 0}
|
||||
>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
|
||||
@@ -1,89 +1,93 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
GPXFile,
|
||||
Track,
|
||||
TrackSegment,
|
||||
Waypoint,
|
||||
type AnyGPXTreeElement,
|
||||
type GPXTreeElement,
|
||||
} from 'gpx';
|
||||
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||
import { type Readable } from 'svelte/store';
|
||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
type ListItem,
|
||||
type ListTrackItem,
|
||||
} from './file-list';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import {
|
||||
elementId,
|
||||
hasSelectionWithin,
|
||||
isSelected,
|
||||
type FileTreeNode,
|
||||
} from '$lib/selection-helpers';
|
||||
|
||||
let {
|
||||
fileState,
|
||||
node,
|
||||
item,
|
||||
}: {
|
||||
node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| Waypoint[]
|
||||
| Waypoint;
|
||||
item: ListItem;
|
||||
fileState: FileState;
|
||||
node: FileTreeNode;
|
||||
} = $props();
|
||||
|
||||
let recursive = getContext<boolean>('recursive');
|
||||
|
||||
let collapsible: CollapsibleTreeNode | undefined = $state();
|
||||
|
||||
let label = $derived(
|
||||
node instanceof GPXFile && item instanceof ListFileItem
|
||||
? node.metadata.name
|
||||
: node instanceof Track
|
||||
? (node.name ?? `${i18n._('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
|
||||
: node instanceof TrackSegment
|
||||
? `${i18n._('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||
: node instanceof Waypoint
|
||||
? (node.name ??
|
||||
`${i18n._('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
|
||||
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||
? i18n._('gpx.waypoints')
|
||||
: ''
|
||||
);
|
||||
let label = $derived.by(() => {
|
||||
const { structure } = fileState;
|
||||
switch (node.type) {
|
||||
case 'file':
|
||||
return structure.name;
|
||||
case 'track': {
|
||||
const trackIndex = structure.tracks.findIndex((t) => t.id === node.trackId);
|
||||
return (
|
||||
structure.tracks[trackIndex]?.name ?? `${i18n._('gpx.track')} ${trackIndex + 1}`
|
||||
);
|
||||
}
|
||||
case 'segment': {
|
||||
const track = structure.tracks.find((t) => t.id === node.trackId);
|
||||
const segmentIndex =
|
||||
track?.segments.findIndex((s) => s.id === node.segmentId) ?? -1;
|
||||
return `${i18n._('gpx.segment')} ${segmentIndex + 1}`;
|
||||
}
|
||||
case 'waypoints':
|
||||
return i18n._('gpx.waypoints');
|
||||
case 'waypoint': {
|
||||
const waypointIndex = structure.waypoints.findIndex(
|
||||
(w) => w.id === node.waypointId
|
||||
);
|
||||
return (
|
||||
structure.waypoints[waypointIndex]?.name ??
|
||||
`${i18n._('gpx.waypoint')} ${waypointIndex + 1}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// identifies the node in the tree
|
||||
let id = $derived(elementId(node));
|
||||
|
||||
const { treeFileView } = settings;
|
||||
const { selection } = engine;
|
||||
|
||||
$effect(() => {
|
||||
if (collapsible && $treeFileView && $selection.hasAnyChildren(item, false)) {
|
||||
// open the node when something below it is selected
|
||||
if (
|
||||
collapsible &&
|
||||
$treeFileView &&
|
||||
hasSelectionWithin($selection, node) &&
|
||||
!isSelected($selection, node)
|
||||
) {
|
||||
collapsible.openNode();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if node instanceof Map}
|
||||
<FileListNodeContent {node} {item} />
|
||||
{:else if node instanceof TrackSegment}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
{:else if node instanceof Waypoint}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
{#if node.type === 'segment' || node.type === 'waypoint'}
|
||||
<FileListNodeLabel {fileState} {node} {label} />
|
||||
{:else if recursive}
|
||||
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
|
||||
<CollapsibleTreeNode {id} bind:this={collapsible}>
|
||||
{#snippet trigger()}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
<FileListNodeLabel {fileState} {node} {label} />
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="ml-4">
|
||||
{#key node}
|
||||
<FileListNodeContent {node} {item} />
|
||||
{/key}
|
||||
<FileListNodeContent {fileState} {node} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</CollapsibleTreeNode>
|
||||
{:else}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
<FileListNodeLabel {fileState} {node} {label} />
|
||||
{/if}
|
||||
@@ -1,41 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||
import { getContext, onDestroy, onMount } from 'svelte';
|
||||
import { type Readable } from 'svelte/store';
|
||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||
import FileListNode from './FileListNode.svelte';
|
||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||
import { ListFileItem, ListLevel, ListWaypointsItem, type ListItem } from './file-list';
|
||||
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||
import { ListLevel } from './file-list';
|
||||
import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
let {
|
||||
fileState,
|
||||
node,
|
||||
item,
|
||||
waypointRoot = false,
|
||||
}: {
|
||||
node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| Waypoint[]
|
||||
| Waypoint;
|
||||
item: ListItem;
|
||||
/** 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;
|
||||
} = $props();
|
||||
|
||||
const { files, order } = engine;
|
||||
|
||||
let container: HTMLElement;
|
||||
let sortableLevel: ListLevel =
|
||||
node instanceof Map
|
||||
node === null
|
||||
? ListLevel.FILE
|
||||
: node instanceof GPXFile
|
||||
: node.type === 'file'
|
||||
? waypointRoot
|
||||
? ListLevel.WAYPOINTS
|
||||
: item instanceof ListWaypointsItem
|
||||
? ListLevel.WAYPOINT
|
||||
: ListLevel.TRACK
|
||||
: node instanceof Track
|
||||
? ListLevel.SEGMENT
|
||||
: ListLevel.WAYPOINT;
|
||||
: ListLevel.TRACK
|
||||
: node.type === 'waypoints'
|
||||
? ListLevel.WAYPOINT
|
||||
: ListLevel.SEGMENT;
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
|
||||
let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
|
||||
@@ -43,18 +41,14 @@
|
||||
let sortable: SortableFileList;
|
||||
|
||||
onMount(() => {
|
||||
sortable = new SortableFileList(
|
||||
container,
|
||||
node,
|
||||
item,
|
||||
waypointRoot,
|
||||
sortableLevel,
|
||||
orientation
|
||||
);
|
||||
sortable = new SortableFileList(container, node, waypointRoot, sortableLevel, orientation);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (sortable && node) {
|
||||
// the elements of the list changed
|
||||
$order;
|
||||
fileState;
|
||||
if (sortable) {
|
||||
sortable.updateElements();
|
||||
}
|
||||
});
|
||||
@@ -70,45 +64,65 @@
|
||||
? 'flex-col'
|
||||
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
|
||||
>
|
||||
{#if node instanceof Map}
|
||||
{#each node as [fileId, file] (fileId)}
|
||||
<div data-id={fileId}>
|
||||
<FileListNodeStore {file} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if node instanceof GPXFile}
|
||||
{#if item instanceof ListWaypointsItem}
|
||||
{#each node.wpt as wpt, i (wpt)}
|
||||
<div data-id={i} class="ml-1">
|
||||
<FileListNode node={wpt} item={item.extend(i)} />
|
||||
</div>
|
||||
{/each}
|
||||
{:else if waypointRoot}
|
||||
{#if node.wpt.length > 0}
|
||||
<div data-id="waypoints">
|
||||
<FileListNode {node} item={item.extend('waypoints')} />
|
||||
{#if node === null}
|
||||
{#each $order as fileId (fileId)}
|
||||
{@const file = $files.get(fileId)}
|
||||
{#if file}
|
||||
<div data-id={fileId}>
|
||||
<FileListNodeStore {file} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each node.children as child, i (child)}
|
||||
<div data-id={i}>
|
||||
<FileListNode node={child} item={item.extend(i)} />
|
||||
{/each}
|
||||
{:else if fileState}
|
||||
{#if node.type === 'file'}
|
||||
{#if waypointRoot}
|
||||
{#if fileState.structure.waypoints.length > 0}
|
||||
<div data-id="waypoints">
|
||||
<FileListNode
|
||||
{fileState}
|
||||
node={{ type: 'waypoints', fileId: node.fileId }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each fileState.structure.tracks as track (track.id)}
|
||||
<div data-id={track.id}>
|
||||
<FileListNode
|
||||
{fileState}
|
||||
node={{ type: 'track', fileId: node.fileId, trackId: track.id }}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if node.type === 'waypoints'}
|
||||
{#each fileState.structure.waypoints as waypoint (waypoint.id)}
|
||||
<div data-id={waypoint.id} class="ml-1">
|
||||
<FileListNode
|
||||
{fileState}
|
||||
node={{ type: 'waypoint', fileId: node.fileId, waypointId: waypoint.id }}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if node.type === 'track'}
|
||||
{#each fileState.structure.tracks.find((t) => t.id === node.trackId)?.segments ?? [] as segment (segment.id)}
|
||||
<div data-id={segment.id} class="ml-1">
|
||||
<FileListNode
|
||||
{fileState}
|
||||
node={{
|
||||
type: 'segment',
|
||||
fileId: node.fileId,
|
||||
trackId: node.trackId,
|
||||
segmentId: segment.id,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if node instanceof Track}
|
||||
{#each node.children as child, i (child)}
|
||||
<div data-id={i} class="ml-1">
|
||||
<FileListNode node={child} item={item.extend(i)} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if node instanceof GPXFile && item instanceof ListFileItem}
|
||||
{#if !waypointRoot}
|
||||
<FileListNodeContent {node} {item} waypointRoot={true} />
|
||||
{/if}
|
||||
{#if fileState && node?.type === 'file' && !waypointRoot}
|
||||
<FileListNodeContent {fileState} {node} waypointRoot={true} />
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@@ -18,86 +18,95 @@
|
||||
Scissors,
|
||||
FileStack,
|
||||
} from '@lucide/svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListLevel,
|
||||
ListTrackItem,
|
||||
ListWaypointItem,
|
||||
type ListItem,
|
||||
} from './file-list';
|
||||
import { ListLevel } from './file-list';
|
||||
import { getContext } from 'svelte';
|
||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
|
||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||
import StyleDialog from '$lib/components/file-list/style/StyleDialog.svelte';
|
||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
import { selection, copied, cut } from '$lib/logic/selection';
|
||||
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
|
||||
import { allHidden } from '$lib/logic/hidden';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import { allHidden } from '$lib/all-hidden';
|
||||
import { normalizeColor } from '$lib/file-colors';
|
||||
import { isHidden } from '$lib/file-visibility';
|
||||
import {
|
||||
elementId,
|
||||
isSelected,
|
||||
selectedElementIds,
|
||||
selectionSize,
|
||||
type FileTreeNode,
|
||||
} from '$lib/selection-helpers';
|
||||
// TODO the clipboard, centering on the selection and the waypoint popup still work on the
|
||||
// previous implementation
|
||||
import { copied, selection as oldSelection } from '$lib/logic/selection';
|
||||
import { pasteSelection } from '$lib/logic/file-actions';
|
||||
import { boundsManager } from '$lib/logic/bounds';
|
||||
import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
||||
// import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||
// import { fileStateCollection } from '$lib/logic/file-state';
|
||||
// import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
||||
import { allowedPastes } from './sortable-file-list';
|
||||
|
||||
let {
|
||||
fileState,
|
||||
node,
|
||||
item,
|
||||
label,
|
||||
}: {
|
||||
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
item: ListItem;
|
||||
fileState: FileState;
|
||||
node: FileTreeNode;
|
||||
label: string | undefined;
|
||||
} = $props();
|
||||
|
||||
const { selection } = engine;
|
||||
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
let embedding = getContext<boolean>('embedding');
|
||||
|
||||
let singleSelection = $derived($selection.size === 1);
|
||||
const levels = {
|
||||
file: ListLevel.FILE,
|
||||
track: ListLevel.TRACK,
|
||||
segment: ListLevel.SEGMENT,
|
||||
waypoints: ListLevel.WAYPOINTS,
|
||||
waypoint: ListLevel.WAYPOINT,
|
||||
};
|
||||
let level = $derived(levels[node.type]);
|
||||
|
||||
let singleSelection = $derived(selectionSize($selection) === 1);
|
||||
|
||||
let nodeColors: string[] = $derived.by(() => {
|
||||
let colors: string[] = [];
|
||||
if (node) {
|
||||
if (node instanceof GPXFile) {
|
||||
let defaultColor = $gpxColors.get(item.getFileId());
|
||||
let style = node.getStyle(defaultColor);
|
||||
colors = style.color;
|
||||
} else if (node instanceof Track) {
|
||||
let style = node.getStyle();
|
||||
if (
|
||||
style &&
|
||||
style['gpx_style:color'] &&
|
||||
!colors.includes(style['gpx_style:color'])
|
||||
) {
|
||||
colors.push(style['gpx_style:color']);
|
||||
}
|
||||
if (colors.length === 0) {
|
||||
let defaultColor = $gpxColors.get(item.getFileId());
|
||||
if (defaultColor) {
|
||||
colors.push(defaultColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
const { tracks } = fileState.structure;
|
||||
if (node.type === 'file') {
|
||||
// the colors defined by the tracks, or else the one of the file
|
||||
const colors = [
|
||||
...new Set(
|
||||
tracks.flatMap((track) =>
|
||||
track.color !== undefined ? [normalizeColor(track.color)] : []
|
||||
)
|
||||
),
|
||||
];
|
||||
return colors.length > 0 ? colors : [fileState.color];
|
||||
} else if (node.type === 'track') {
|
||||
const color = tracks.find((track) => track.id === node.trackId)?.color;
|
||||
return [color !== undefined ? normalizeColor(color) : fileState.color];
|
||||
}
|
||||
return colors;
|
||||
return [];
|
||||
});
|
||||
|
||||
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
||||
|
||||
let openEditMetadata: boolean = $derived(
|
||||
editMetadata.current && singleSelection && $selection.has(item)
|
||||
let symbolKey = $derived(
|
||||
node.type === 'waypoint'
|
||||
? getSymbolKey(fileState.structure.waypoints.find((w) => w.id === node.waypointId)?.sym)
|
||||
: undefined
|
||||
);
|
||||
|
||||
let selected = $derived(isSelected($selection, node));
|
||||
let openEditMetadata: boolean = $derived(editMetadata.current && singleSelection && selected);
|
||||
let openEditStyle: boolean = $derived(
|
||||
editStyle.current &&
|
||||
$selection.has(item) &&
|
||||
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0
|
||||
selected &&
|
||||
selectedElementIds($selection)[0]?.ids[0] === elementId(node)
|
||||
);
|
||||
|
||||
let hidden = $derived(
|
||||
item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden
|
||||
);
|
||||
let hidden = $derived(isHidden(fileState, elementId(node)));
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -105,8 +114,8 @@
|
||||
<ContextMenu.Root
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
if (!$selection.has(item)) {
|
||||
selection.selectItem(item);
|
||||
if (!selected) {
|
||||
engine.selectNode(node);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -119,11 +128,11 @@
|
||||
? 'h-7'
|
||||
: 'h-9 px-1.5'} pointer-events-auto"
|
||||
>
|
||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
|
||||
<StyleDialog bind:open={openEditStyle} {item} />
|
||||
{#if node.type === 'file' || node.type === 'track'}
|
||||
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
|
||||
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
|
||||
{/if}
|
||||
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
|
||||
{#if level === ListLevel.FILE || level === ListLevel.TRACK}
|
||||
<div
|
||||
class="absolute {orientation === 'vertical'
|
||||
? 'top-0 bottom-0 right-0 w-1'
|
||||
@@ -141,9 +150,7 @@
|
||||
<span
|
||||
class="grow text-left truncate ml-1 flex flex-row items-center {hidden
|
||||
? 'text-muted-foreground'
|
||||
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
|
||||
? 'text-muted-foreground'
|
||||
: ''}"
|
||||
: ''} {/* TODO cut elements: the clipboard works on the previous implementation */ ''}"
|
||||
oncontextmenu={(e) => {
|
||||
if (embedding) {
|
||||
e.preventDefault();
|
||||
@@ -154,37 +161,35 @@
|
||||
// Add to selection instead of opening context menu
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$selection.toggle(item);
|
||||
$selection = $selection;
|
||||
engine.selectNode(node, 'toggle');
|
||||
}
|
||||
}}
|
||||
onmouseenter={() => {
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let layer = gpxLayers.getLayer(item.getFileId());
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
if (layer && file) {
|
||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||
if (waypoint && !waypoint._data.hidden) {
|
||||
waypointPopup?.setItem({
|
||||
item: waypoint,
|
||||
fileId: item.getFileId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO waypoint popup: it needs the waypoint of the previous implementation
|
||||
// if (node.type === 'waypoint') {
|
||||
// let layer = gpxLayers.getLayer(node.fileId);
|
||||
// let file = fileStateCollection.getFile(node.fileId);
|
||||
// if (layer && file) {
|
||||
// let waypoint = file.wpt[waypointIndex];
|
||||
// if (waypoint && !waypoint._data.hidden) {
|
||||
// waypointPopup?.setItem({ item: waypoint, fileId: node.fileId });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}}
|
||||
onmouseleave={() => {
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let layer = gpxLayers.getLayer(item.getFileId());
|
||||
if (layer) {
|
||||
waypointPopup?.setItem(null);
|
||||
}
|
||||
}
|
||||
// TODO waypoint popup
|
||||
// if (node.type === 'waypoint') {
|
||||
// let layer = gpxLayers.getLayer(node.fileId);
|
||||
// if (layer) {
|
||||
// waypointPopup?.setItem(null);
|
||||
// }
|
||||
// }
|
||||
}}
|
||||
>
|
||||
{#if item.level === ListLevel.SEGMENT}
|
||||
{#if level === ListLevel.SEGMENT}
|
||||
<Waypoints size="16" class="mx-1 shrink-0" />
|
||||
{:else if item.level === ListLevel.WAYPOINT}
|
||||
{:else if level === ListLevel.WAYPOINT}
|
||||
{#if symbolKey && symbols[symbolKey].icon}
|
||||
{@const SymbolIcon = symbols[symbolKey].icon}
|
||||
<SymbolIcon size="16" class="mx-1 shrink-0" />
|
||||
@@ -211,7 +216,7 @@
|
||||
</Button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||
{#if node.type === 'file' || node.type === 'track'}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
onclick={() => (editMetadata.current = true)}
|
||||
@@ -225,15 +230,7 @@
|
||||
{i18n._('menu.style.button')}
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Item
|
||||
onclick={() => {
|
||||
if ($allHidden) {
|
||||
fileActions.setHiddenToSelection(false);
|
||||
} else {
|
||||
fileActions.setHiddenToSelection(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ContextMenu.Item onclick={() => engine.setSelectionHidden(!$allHidden)}>
|
||||
{#if $allHidden}
|
||||
<Eye size="16" />
|
||||
{i18n._('menu.unhide')}
|
||||
@@ -245,20 +242,16 @@
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{#if orientation === 'vertical'}
|
||||
{#if item instanceof ListFileItem}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
onclick={() => fileActions.addNewTrack(item.getFileId())}
|
||||
>
|
||||
{#if node.type === 'file'}
|
||||
<ContextMenu.Item disabled={!singleSelection} onclick={() => engine.newTrack()}>
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_track')}
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{:else if item instanceof ListTrackItem}
|
||||
{:else if node.type === 'track'}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
onclick={() =>
|
||||
fileActions.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||
onclick={() => engine.newTrackSegment()}
|
||||
>
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_segment')}
|
||||
@@ -266,8 +259,8 @@
|
||||
<ContextMenu.Separator />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if item.level !== ListLevel.WAYPOINTS}
|
||||
<ContextMenu.Item onclick={() => selection.selectAll()}>
|
||||
{#if level !== ListLevel.WAYPOINTS}
|
||||
<ContextMenu.Item onclick={() => engine.selectAllSiblings(node)}>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
@@ -279,18 +272,18 @@
|
||||
<Shortcut key="⏎" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item onclick={fileActions.duplicateSelection}>
|
||||
<ContextMenu.Item onclick={() => engine.duplicate()}>
|
||||
<Copy size="16" />
|
||||
{i18n._('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
{#if orientation === 'vertical'}
|
||||
<ContextMenu.Item onclick={() => selection.copySelection()}>
|
||||
<ContextMenu.Item onclick={() => oldSelection.copySelection()}>
|
||||
<ClipboardCopy size="16" />
|
||||
{i18n._('menu.copy')}
|
||||
<Shortcut key="C" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item onclick={() => selection.cutSelection()}>
|
||||
<ContextMenu.Item onclick={() => oldSelection.cutSelection()}>
|
||||
<Scissors size="16" />
|
||||
{i18n._('menu.cut')}
|
||||
<Shortcut key="X" ctrl={true} />
|
||||
@@ -298,7 +291,7 @@
|
||||
<ContextMenu.Item
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 0 ||
|
||||
!allowedPastes[$copied[0].level].includes(item.level)}
|
||||
!allowedPastes[$copied[0].level].includes(level)}
|
||||
onclick={pasteSelection}
|
||||
>
|
||||
<ClipboardPaste size="16" />
|
||||
@@ -307,7 +300,7 @@
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item onclick={fileActions.deleteSelection}>
|
||||
<ContextMenu.Item onclick={() => engine.delete()}>
|
||||
<Trash2 size="16" />
|
||||
{i18n._('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
|
||||
@@ -4,24 +4,21 @@
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { ListFileItem } from './file-list';
|
||||
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||
import type { FileState } from '$lib/engine';
|
||||
|
||||
let {
|
||||
file,
|
||||
}: {
|
||||
file: Readable<GPXFileWithStatistics | undefined>;
|
||||
file: Readable<FileState>;
|
||||
} = $props();
|
||||
|
||||
let recursive = getContext<boolean>('recursive');
|
||||
</script>
|
||||
|
||||
{#if $file}
|
||||
{#if recursive}
|
||||
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
|
||||
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||
</CollapsibleTree>
|
||||
{:else}
|
||||
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||
{/if}
|
||||
{#if recursive}
|
||||
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
|
||||
<FileListNode fileState={$file} node={{ type: 'file', fileId: $file.structure.id }} />
|
||||
</CollapsibleTree>
|
||||
{:else}
|
||||
<FileListNode fileState={$file} node={{ type: 'file', fileId: $file.structure.id }} />
|
||||
{/if}
|
||||
@@ -5,35 +5,31 @@
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Save } from '@lucide/svelte';
|
||||
import { ListFileItem, ListTrackItem, type ListItem } from '../file-list';
|
||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
let {
|
||||
fileState,
|
||||
node,
|
||||
item,
|
||||
open = $bindable(),
|
||||
}: {
|
||||
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
item: ListItem;
|
||||
fileState: FileState;
|
||||
node: FileTreeNode;
|
||||
open: boolean;
|
||||
} = $props();
|
||||
|
||||
let track = $derived(
|
||||
node.type === 'track'
|
||||
? fileState.structure.tracks.find((track) => track.id === node.trackId)
|
||||
: undefined
|
||||
);
|
||||
let name: string = $derived(
|
||||
node instanceof GPXFile
|
||||
? (node.metadata.name ?? '')
|
||||
: node instanceof Track
|
||||
? (node.name ?? '')
|
||||
: ''
|
||||
node.type === 'file' ? fileState.structure.name : (track?.name ?? '')
|
||||
);
|
||||
let description: string = $derived(
|
||||
node instanceof GPXFile
|
||||
? (node.metadata.desc ?? '')
|
||||
: node instanceof Track
|
||||
? (node.desc ?? '')
|
||||
: ''
|
||||
node.type === 'file' ? (fileState.structure.desc ?? '') : (track?.desc ?? '')
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
@@ -53,18 +49,8 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
fileActionManager.applyToFile(item.getFileId(), (file) => {
|
||||
if (item instanceof ListFileItem && node instanceof GPXFile) {
|
||||
file.metadata.name = name;
|
||||
file.metadata.desc = description;
|
||||
if (file.trk.length === 1) {
|
||||
file.trk[0].name = name;
|
||||
}
|
||||
} else if (item instanceof ListTrackItem && node instanceof Track) {
|
||||
file.trk[item.getTrackIndex()].name = name;
|
||||
file.trk[item.getTrackIndex()].desc = description;
|
||||
}
|
||||
});
|
||||
// the metadata of the selected elements are changed
|
||||
engine.metadata(name, description);
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { isMac } from '$lib/utils';
|
||||
import Sortable, { type Direction } from 'sortablejs/Sortable';
|
||||
import { ListItem, ListLevel, ListRootItem } from './file-list';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { getFileIds, moveItems } from '$lib/logic/file-actions';
|
||||
import { get, writable, type Readable } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||
import type { AnyGPXTreeElement, GPXTreeElement, Waypoint } from 'gpx';
|
||||
import { ListLevel } from './file-list';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
const { fileOrder } = settings;
|
||||
import { engine } from '$lib/engine';
|
||||
import { isSelected, type FileTreeNode } from '$lib/selection-helpers';
|
||||
// TODO moving elements between parents is not available in the engine yet
|
||||
// import { ListItem, ListRootItem } from './file-list';
|
||||
// import { getFileIds, moveItems } from '$lib/logic/file-actions';
|
||||
// import { settings } from '$lib/logic/settings';
|
||||
// const { fileOrder } = settings;
|
||||
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
[ListLevel.ROOT]: [],
|
||||
@@ -31,13 +31,12 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
||||
|
||||
export const dragging = writable<ListLevel | null>(null);
|
||||
|
||||
/**
|
||||
* Makes a list of the file tree sortable and selectable. The elements of the list are the
|
||||
* children of `parent` (the files when `parent` is null), their ids are in their `data-id`.
|
||||
*/
|
||||
export class SortableFileList {
|
||||
private _node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| Waypoint[]
|
||||
| Waypoint;
|
||||
private _item: ListItem;
|
||||
private _parent: FileTreeNode | null;
|
||||
private _sortableLevel: ListLevel;
|
||||
private _container: HTMLElement;
|
||||
private _sortable: Sortable | null = null;
|
||||
@@ -47,26 +46,24 @@ export class SortableFileList {
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
| GPXTreeElement<AnyGPXTreeElement>
|
||||
| Waypoint[]
|
||||
| Waypoint,
|
||||
item: ListItem,
|
||||
parent: FileTreeNode | null,
|
||||
waypointRoot: boolean,
|
||||
sortableLevel: ListLevel,
|
||||
orientation: Direction
|
||||
) {
|
||||
this._node = node;
|
||||
this._item = item;
|
||||
this._parent = parent;
|
||||
this._sortableLevel = sortableLevel;
|
||||
this._container = container;
|
||||
// TODO only the order of the files can be changed for now: the other lists neither sort
|
||||
// nor exchange elements (allowedMoves, to be used again once the engine can move elements)
|
||||
const sortable = sortableLevel === ListLevel.FILE;
|
||||
this._sortable = Sortable.create(container, {
|
||||
group: {
|
||||
name: sortableLevel,
|
||||
pull: allowedMoves[sortableLevel],
|
||||
put: true,
|
||||
pull: sortable ? allowedMoves[sortableLevel] : false,
|
||||
put: sortable ? [ListLevel.FILE] : false,
|
||||
},
|
||||
sort: sortable,
|
||||
direction: orientation,
|
||||
forceAutoScrollFallback: true,
|
||||
multiDrag: true,
|
||||
@@ -80,76 +77,103 @@ export class SortableFileList {
|
||||
onEnd: () => dragging.set(null),
|
||||
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
|
||||
});
|
||||
Object.defineProperty(this._sortable, '_item', {
|
||||
value: item,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(this._sortable, '_waypointRoot', {
|
||||
value: waypointRoot,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
this._unsubscribes.push(
|
||||
selection.subscribe(() => tick().then(() => this.updateFromSelection()))
|
||||
engine.selection.subscribe(() => tick().then(() => this.updateFromSelection()))
|
||||
);
|
||||
this._unsubscribes.push(fileOrder.subscribe(() => this.updateFromFileOrder()));
|
||||
this._unsubscribes.push(engine.order.subscribe(() => this.updateFromFileOrder()));
|
||||
}
|
||||
|
||||
/** The node of a child of the list. */
|
||||
childNode(id: string): FileTreeNode | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
return { type: 'file', fileId: id };
|
||||
case ListLevel.TRACK:
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
fileId: parent.fileId,
|
||||
trackId: parent.trackId,
|
||||
segmentId: id,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointId: id }
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
onSort(e: Sortable.SortableEvent) {
|
||||
this.updateToFileOrder();
|
||||
|
||||
const from = Sortable.get(e.from);
|
||||
const to = Sortable.get(e.to);
|
||||
// TODO moving elements between parents: the engine has no command for it yet, the code
|
||||
// below worked on the previous implementation
|
||||
// onSort(e: Sortable.SortableEvent) {
|
||||
// this.updateToFileOrder();
|
||||
|
||||
if (!from || !to) {
|
||||
return;
|
||||
}
|
||||
// const from = Sortable.get(e.from);
|
||||
// const to = Sortable.get(e.to);
|
||||
|
||||
let fromItem = from._item;
|
||||
let toItem = to._item;
|
||||
// if (!from || !to) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
|
||||
// Event is triggered on source and destination list, only handle it once
|
||||
let fromItems = [];
|
||||
let toItems = [];
|
||||
// let fromItem = from._item;
|
||||
// let toItem = to._item;
|
||||
|
||||
if (from._waypointRoot) {
|
||||
fromItems = [fromItem.extend('waypoints')];
|
||||
} else {
|
||||
let oldIndices: number[] =
|
||||
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
||||
oldIndices = oldIndices.filter((i) => i >= 0);
|
||||
oldIndices.sort((a, b) => a - b);
|
||||
// if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
|
||||
// // Event is triggered on source and destination list, only handle it once
|
||||
// let fromItems = [];
|
||||
// let toItems = [];
|
||||
|
||||
fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||
}
|
||||
// if (from._waypointRoot) {
|
||||
// fromItems = [fromItem.extend('waypoints')];
|
||||
// } else {
|
||||
// let oldIndices: number[] =
|
||||
// e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
|
||||
// oldIndices = oldIndices.filter((i) => i >= 0);
|
||||
// oldIndices.sort((a, b) => a - b);
|
||||
|
||||
if (from._waypointRoot && to._waypointRoot) {
|
||||
toItems = [toItem.extend('waypoints')];
|
||||
} else {
|
||||
if (to._waypointRoot) {
|
||||
toItem = toItem.extend('waypoints');
|
||||
}
|
||||
// fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||
// }
|
||||
|
||||
let newIndices: number[] =
|
||||
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
||||
newIndices = newIndices.filter((i) => i >= 0);
|
||||
newIndices.sort((a, b) => a - b);
|
||||
// if (from._waypointRoot && to._waypointRoot) {
|
||||
// toItems = [toItem.extend('waypoints')];
|
||||
// } else {
|
||||
// if (to._waypointRoot) {
|
||||
// toItem = toItem.extend('waypoints');
|
||||
// }
|
||||
|
||||
if (toItem instanceof ListRootItem) {
|
||||
let newFileIds = getFileIds(newIndices.length);
|
||||
toItems = newIndices.map((i, index) => {
|
||||
get(fileOrder).splice(i, 0, newFileIds[index]);
|
||||
return this._item.extend(newFileIds[index]);
|
||||
});
|
||||
} else {
|
||||
toItems = newIndices.map((i) => toItem.extend(i));
|
||||
}
|
||||
}
|
||||
// let newIndices: number[] =
|
||||
// e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
|
||||
// newIndices = newIndices.filter((i) => i >= 0);
|
||||
// newIndices.sort((a, b) => a - b);
|
||||
|
||||
moveItems(fromItem, toItem, fromItems, toItems);
|
||||
}
|
||||
// if (toItem instanceof ListRootItem) {
|
||||
// let newFileIds = getFileIds(newIndices.length);
|
||||
// toItems = newIndices.map((i, index) => {
|
||||
// get(fileOrder).splice(i, 0, newFileIds[index]);
|
||||
// return this._item.extend(newFileIds[index]);
|
||||
// });
|
||||
// } else {
|
||||
// toItems = newIndices.map((i) => toItem.extend(i));
|
||||
// }
|
||||
// }
|
||||
|
||||
// moveItems(fromItem, toItem, fromItems, toItems);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
updateFromSelection() {
|
||||
@@ -157,11 +181,12 @@ export class SortableFileList {
|
||||
if (changed.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selection_ = get(selection);
|
||||
const selection_ = get(engine.selection);
|
||||
for (let id of changed) {
|
||||
let element = this._elements[id];
|
||||
if (element) {
|
||||
if (selection_.has(this._item.extend(id))) {
|
||||
let node = this.childNode(id);
|
||||
if (element && node) {
|
||||
if (isSelected(selection_, node)) {
|
||||
Sortable.utils.select(element);
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
@@ -183,27 +208,42 @@ export class SortableFileList {
|
||||
this._updatingSelection = false;
|
||||
return;
|
||||
}
|
||||
selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
Object.entries(this._elements).forEach(([id, element]) => {
|
||||
$selection.set(
|
||||
this._item.extend(this.getRealId(id)),
|
||||
element.classList.contains('sortable-selected')
|
||||
);
|
||||
});
|
||||
|
||||
if (
|
||||
e.originalEvent &&
|
||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
|
||||
($selection.size > 1 ||
|
||||
!$selection.has(this._item.extend(this.getRealId(changed[0]))))
|
||||
) {
|
||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||
$selection.clear();
|
||||
$selection.set(this._item.extend(this.getRealId(changed[0])), true);
|
||||
let selected = Object.entries(this._elements)
|
||||
.filter(([, element]) => element.classList.contains('sortable-selected'))
|
||||
.map(([id]) => id);
|
||||
|
||||
if (
|
||||
e.originalEvent &&
|
||||
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
|
||||
(selected.length > 1 || !selected.includes(changed[0]))
|
||||
) {
|
||||
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||
selected = [changed[0]];
|
||||
}
|
||||
|
||||
const parent = this._parent;
|
||||
if (selected.length === 0 || !this.childNode(selected[0])) {
|
||||
engine.select([]);
|
||||
} else {
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
engine.select(selected);
|
||||
break;
|
||||
case ListLevel.TRACK:
|
||||
engine.selectTracks(parent!.fileId, selected);
|
||||
break;
|
||||
case ListLevel.SEGMENT:
|
||||
engine.selectSegments(parent!.fileId, (parent as any).trackId, selected);
|
||||
break;
|
||||
case ListLevel.WAYPOINTS:
|
||||
engine.selectWaypointGroup(parent!.fileId);
|
||||
break;
|
||||
case ListLevel.WAYPOINT:
|
||||
engine.selectWaypoints(parent!.fileId, selected);
|
||||
break;
|
||||
}
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
this._updatingSelection = false;
|
||||
}
|
||||
|
||||
@@ -212,7 +252,7 @@ export class SortableFileList {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileOrder_ = get(fileOrder);
|
||||
const fileOrder_ = get(engine.order);
|
||||
const sortableOrder = this._sortable.toArray();
|
||||
|
||||
if (
|
||||
@@ -228,24 +268,25 @@ export class SortableFileList {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileOrder_ = get(fileOrder);
|
||||
const fileOrder_ = get(engine.order);
|
||||
const sortableOrder = this._sortable.toArray();
|
||||
|
||||
if (
|
||||
fileOrder_.length !== sortableOrder.length ||
|
||||
fileOrder_.some((value, index) => value !== sortableOrder[index])
|
||||
) {
|
||||
fileOrder.set(sortableOrder);
|
||||
engine.reorder(sortableOrder, 0);
|
||||
}
|
||||
}
|
||||
|
||||
updateElements() {
|
||||
this._elements = {};
|
||||
const files = get(engine.files);
|
||||
this._container.childNodes.forEach((element) => {
|
||||
if (element instanceof HTMLElement) {
|
||||
let attr = element.getAttribute('data-id');
|
||||
if (attr) {
|
||||
if (this._node instanceof Map && !this._node.has(attr)) {
|
||||
if (this._sortableLevel === ListLevel.FILE && !files.has(attr)) {
|
||||
element.remove();
|
||||
} else {
|
||||
this._elements[attr] = element;
|
||||
@@ -262,23 +303,16 @@ export class SortableFileList {
|
||||
}
|
||||
|
||||
getChangedIds() {
|
||||
let changed: (string | number)[] = [];
|
||||
const selection_ = get(selection);
|
||||
let changed: string[] = [];
|
||||
const selection_ = get(engine.selection);
|
||||
Object.entries(this._elements).forEach(([id, element]) => {
|
||||
let realId = this.getRealId(id);
|
||||
let realItem = this._item.extend(realId);
|
||||
let inSelection = selection_.has(realItem);
|
||||
let isSelected = element.classList.contains('sortable-selected');
|
||||
if (inSelection !== isSelected) {
|
||||
changed.push(realId);
|
||||
const node = this.childNode(id);
|
||||
let inSelection = node !== undefined && isSelected(selection_, node);
|
||||
let isSelectedInList = element.classList.contains('sortable-selected');
|
||||
if (inSelection !== isSelectedInList) {
|
||||
changed.push(id);
|
||||
}
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
getRealId(id: string | number) {
|
||||
return this._sortableLevel === ListLevel.FILE || this._sortableLevel === ListLevel.WAYPOINTS
|
||||
? id
|
||||
: parseInt(id as string);
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,26 @@
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { Save } from '@lucide/svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListTrackItem,
|
||||
type ListItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { LineStyleExtension } from 'gpx';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import { normalizeColor } from '$lib/file-colors';
|
||||
import { selectedFileIds, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import { untrack } from 'svelte';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
|
||||
let {
|
||||
item,
|
||||
fileState,
|
||||
node,
|
||||
open = $bindable(),
|
||||
}: {
|
||||
item: ListItem;
|
||||
fileState: FileState;
|
||||
node: FileTreeNode;
|
||||
open: boolean;
|
||||
} = $props();
|
||||
|
||||
const { defaultOpacity, defaultWidth } = settings;
|
||||
const { files, selection } = engine;
|
||||
|
||||
let color: string = $state('');
|
||||
let opacity: number = $state(0);
|
||||
@@ -37,45 +33,24 @@
|
||||
let opacityChanged = $state(false);
|
||||
let widthChanged = $state(false);
|
||||
|
||||
// TODO the inputs used to come from the last selected item, they now come from this node
|
||||
function setStyleInputs() {
|
||||
opacity = $defaultOpacity;
|
||||
width = $defaultWidth;
|
||||
color = fileState.color;
|
||||
|
||||
$selection.forEach((item) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
let layer = gpxLayers.getLayer(item.getFileId());
|
||||
if (file && layer) {
|
||||
let style = file.getStyle();
|
||||
color = layer.layerColor;
|
||||
if (style.opacity.length > 0) {
|
||||
opacity = style.opacity[0];
|
||||
}
|
||||
if (style.width.length > 0) {
|
||||
width = style.width[0];
|
||||
}
|
||||
}
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
let layer = gpxLayers.getLayer(item.getFileId());
|
||||
if (file && layer) {
|
||||
color = layer.layerColor;
|
||||
let track = file.trk[item.getTrackIndex()];
|
||||
let style = track.getStyle();
|
||||
if (style) {
|
||||
if (style['gpx_style:color']) {
|
||||
color = style['gpx_style:color'];
|
||||
}
|
||||
if (style['gpx_style:opacity']) {
|
||||
opacity = style['gpx_style:opacity'];
|
||||
}
|
||||
if (style['gpx_style:width']) {
|
||||
width = style['gpx_style:width'];
|
||||
}
|
||||
}
|
||||
}
|
||||
const { tracks } = fileState.structure;
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
colorChanged = false;
|
||||
opacityChanged = false;
|
||||
@@ -95,24 +70,18 @@
|
||||
});
|
||||
|
||||
function applyStyle() {
|
||||
let style: LineStyleExtension = {};
|
||||
if (colorChanged) {
|
||||
style['gpx_style:color'] = color;
|
||||
}
|
||||
if (opacityChanged) {
|
||||
style['gpx_style:opacity'] = opacity;
|
||||
}
|
||||
if (widthChanged) {
|
||||
style['gpx_style:width'] = width;
|
||||
}
|
||||
fileActions.setStyleToSelection(style);
|
||||
engine.style({
|
||||
color: colorChanged ? color : undefined,
|
||||
opacity: opacityChanged ? opacity : undefined,
|
||||
width: widthChanged ? width : undefined,
|
||||
});
|
||||
|
||||
if (item instanceof ListFileItem && $selection.size === fileStateCollection.size) {
|
||||
if (style['gpx_style:opacity']) {
|
||||
$defaultOpacity = style['gpx_style:opacity'];
|
||||
if (node.type === 'file' && selectedFileIds($selection).length === $files.size) {
|
||||
if (opacityChanged) {
|
||||
$defaultOpacity = opacity;
|
||||
}
|
||||
if (style['gpx_style:width']) {
|
||||
$defaultWidth = style['gpx_style:width'];
|
||||
if (widthChanged) {
|
||||
$defaultWidth = width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user