mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-03 18:32:12 +00:00
progress
This commit is contained in:
@@ -6,13 +6,22 @@
|
||||
import { setContext } from 'svelte';
|
||||
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
||||
import { copied, pasteSelection, selectAll, selection } from './Selection';
|
||||
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
|
||||
import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { createFile } from '$lib/stores';
|
||||
|
||||
export let orientation: 'vertical' | 'horizontal';
|
||||
export let recursive = false;
|
||||
let {
|
||||
orientation,
|
||||
recursive = false,
|
||||
class: className = '',
|
||||
style = '',
|
||||
}: {
|
||||
orientation: 'vertical' | 'horizontal';
|
||||
recursive?: boolean;
|
||||
class?: string;
|
||||
style?: string;
|
||||
} = $props();
|
||||
|
||||
setContext('orientation', orientation);
|
||||
setContext('recursive', recursive);
|
||||
@@ -52,23 +61,23 @@
|
||||
<div
|
||||
class="flex {orientation === 'vertical'
|
||||
? 'flex-col py-1 pl-1 min-h-screen'
|
||||
: 'flex-row'} {$$props.class ?? ''}"
|
||||
{...$$restProps}
|
||||
: 'flex-row'} {className ?? ''}"
|
||||
{style}
|
||||
>
|
||||
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
||||
{#if orientation === 'vertical'}
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger class="grow" />
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item on:click={createFile}>
|
||||
<ContextMenu.Item onclick={createFile}>
|
||||
<Plus size="16" class="mr-1" />
|
||||
{$_('menu.new_file')}
|
||||
{i18n._('menu.new_file')}
|
||||
<Shortcut key="+" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
|
||||
<ContextMenu.Item onclick={selectAll} disabled={$fileObservers.size === 0}>
|
||||
<FileStack size="16" class="mr-1" />
|
||||
{$_('menu.select_all')}
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
@@ -76,10 +85,10 @@
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 0 ||
|
||||
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
|
||||
on:click={pasteSelection}
|
||||
onclick={pasteSelection}
|
||||
>
|
||||
<ClipboardPaste size="16" class="mr-1" />
|
||||
{$_('menu.paste')}
|
||||
{i18n._('menu.paste')}
|
||||
<Shortcut key="V" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
type ListItem,
|
||||
type ListTrackItem,
|
||||
} from './FileList';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { selection } from './Selection';
|
||||
|
||||
export let node:
|
||||
@@ -39,14 +39,14 @@
|
||||
node instanceof GPXFile && item instanceof ListFileItem
|
||||
? node.metadata.name
|
||||
: node instanceof Track
|
||||
? (node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
|
||||
? (node.name ?? `${i18n._('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
|
||||
: node instanceof TrackSegment
|
||||
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||
? `${i18n._('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||
: node instanceof Waypoint
|
||||
? (node.name ??
|
||||
`${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
|
||||
`${i18n._('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
|
||||
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||
? $_('gpx.waypoints')
|
||||
? i18n._('gpx.waypoints')
|
||||
: '';
|
||||
|
||||
const { treeFileView } = settings;
|
||||
@@ -72,12 +72,16 @@
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
{:else if recursive}
|
||||
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
|
||||
<FileListNodeLabel {node} {item} {label} slot="trigger" />
|
||||
<div slot="content" class="ml-2">
|
||||
{#key node}
|
||||
<FileListNodeContent {node} {item} />
|
||||
{/key}
|
||||
</div>
|
||||
{#snippet trigger()}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="ml-2">
|
||||
{#key node}
|
||||
<FileListNodeContent {node} {item} />
|
||||
{/key}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CollapsibleTreeNode>
|
||||
{:else}
|
||||
<FileListNodeLabel {node} {item} {label} />
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
} from './FileList';
|
||||
import { selection } from './Selection';
|
||||
import { isMac } from '$lib/utils';
|
||||
import { _ } from '$lib/i18n';
|
||||
|
||||
export let node:
|
||||
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||
@@ -345,6 +344,8 @@
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
@reference "../../../app.css";
|
||||
|
||||
.sortable > div {
|
||||
@apply rounded-md;
|
||||
@apply h-fit;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Scissors,
|
||||
FileStack,
|
||||
FileX,
|
||||
} from 'lucide-svelte';
|
||||
} from '@lucide/svelte';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListLevel,
|
||||
@@ -40,80 +40,92 @@
|
||||
} from './Selection';
|
||||
import { getContext } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
allHidden,
|
||||
editMetadata,
|
||||
editStyle,
|
||||
embedding,
|
||||
centerMapOnSelection,
|
||||
gpxLayers,
|
||||
map,
|
||||
} from '$lib/stores';
|
||||
import { allHidden, embedding, gpxLayers } from '$lib/stores';
|
||||
import { map, centerMapOnSelection } from '$lib/components/map/map.svelte';
|
||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||
import { _ } from '$lib/i18n';
|
||||
import MetadataDialog from './MetadataDialog.svelte';
|
||||
import StyleDialog from './StyleDialog.svelte';
|
||||
import { waypointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
|
||||
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 './style/StyleDialog.svelte';
|
||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||
import { waypointPopup } from '$lib/components/map/gpx-layer/GPXLayerPopup';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
|
||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
export let item: ListItem;
|
||||
export let label: string | undefined;
|
||||
let {
|
||||
node,
|
||||
item,
|
||||
label,
|
||||
}: {
|
||||
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
item: ListItem;
|
||||
label: string | undefined;
|
||||
} = $props();
|
||||
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
|
||||
$: singleSelection = $selection.size === 1;
|
||||
let singleSelection = $derived($selection.size === 1);
|
||||
|
||||
let nodeColors: string[] = [];
|
||||
let nodeColors: string[] = $derived.by(() => {
|
||||
let colors: string[] = [];
|
||||
if (node && map.current) {
|
||||
if (node instanceof GPXFile) {
|
||||
let defaultColor = undefined;
|
||||
|
||||
$: if (node && $map) {
|
||||
nodeColors = [];
|
||||
|
||||
if (node instanceof GPXFile) {
|
||||
let defaultColor = undefined;
|
||||
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (layer) {
|
||||
defaultColor = layer.layerColor;
|
||||
}
|
||||
|
||||
let style = node.getStyle(defaultColor);
|
||||
style.color.forEach((c) => {
|
||||
if (!nodeColors.includes(c)) {
|
||||
nodeColors.push(c);
|
||||
}
|
||||
});
|
||||
} else if (node instanceof Track) {
|
||||
let style = node.getStyle();
|
||||
if (style) {
|
||||
if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
|
||||
nodeColors.push(style['gpx_style:color']);
|
||||
}
|
||||
}
|
||||
if (nodeColors.length === 0) {
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (layer) {
|
||||
nodeColors.push(layer.layerColor);
|
||||
defaultColor = layer.layerColor;
|
||||
}
|
||||
|
||||
let style = node.getStyle(defaultColor);
|
||||
style.color.forEach((c) => {
|
||||
if (!colors.includes(c)) {
|
||||
colors.push(c);
|
||||
}
|
||||
});
|
||||
} else if (node instanceof Track) {
|
||||
let style = node.getStyle();
|
||||
if (style) {
|
||||
if (
|
||||
style['gpx_style:color'] &&
|
||||
!nodeColors.includes(style['gpx_style:color'])
|
||||
) {
|
||||
nodeColors.push(style['gpx_style:color']);
|
||||
}
|
||||
}
|
||||
if (colors.length === 0) {
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (layer) {
|
||||
colors.push(layer.layerColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return colors;
|
||||
});
|
||||
|
||||
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
|
||||
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
||||
|
||||
let openEditMetadata: boolean = false;
|
||||
let openEditStyle: boolean = false;
|
||||
let openEditMetadata: boolean = $state(false);
|
||||
let openEditStyle: boolean = $state(false);
|
||||
|
||||
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
|
||||
$: openEditStyle =
|
||||
$editStyle &&
|
||||
$selection.has(item) &&
|
||||
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
|
||||
$effect(() => {
|
||||
openEditMetadata = editMetadata.current && singleSelection && $selection.has(item);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
openEditStyle =
|
||||
editStyle.current &&
|
||||
$selection.has(item) &&
|
||||
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||
});
|
||||
|
||||
let hidden = $derived(
|
||||
item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<ContextMenu.Root
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
@@ -156,7 +168,7 @@
|
||||
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
|
||||
? 'text-muted-foreground'
|
||||
: ''}"
|
||||
on:contextmenu={(e) => {
|
||||
oncontextmenu={(e) => {
|
||||
if ($embedding) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -170,7 +182,7 @@
|
||||
$selection = $selection;
|
||||
}
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
onmouseenter={() => {
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
let file = getFile(item.getFileId());
|
||||
@@ -185,7 +197,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
onmouseleave={() => {
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (layer) {
|
||||
@@ -198,11 +210,8 @@
|
||||
<Waypoints size="16" class="mr-1 shrink-0" />
|
||||
{:else if item.level === ListLevel.WAYPOINT}
|
||||
{#if symbolKey && symbols[symbolKey].icon}
|
||||
<svelte:component
|
||||
this={symbols[symbolKey].icon}
|
||||
size="16"
|
||||
class="mr-1 shrink-0"
|
||||
/>
|
||||
{@const SymbolIcon = symbols[symbolKey].icon}
|
||||
<SymbolIcon size="16" class="mr-1 shrink-0" />
|
||||
{:else}
|
||||
<MapPin size="16" class="mr-1 shrink-0" />
|
||||
{/if}
|
||||
@@ -230,18 +239,21 @@
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
onclick={() => (editMetadata.current = true)}
|
||||
>
|
||||
<Info size="16" class="mr-1" />
|
||||
{$_('menu.metadata.button')}
|
||||
{i18n._('menu.metadata.button')}
|
||||
<Shortcut key="I" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
||||
<ContextMenu.Item onclick={() => (editStyle.current = true)}>
|
||||
<PaintBucket size="16" class="mr-1" />
|
||||
{$_('menu.style.button')}
|
||||
{i18n._('menu.style.button')}
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Item
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
if ($allHidden) {
|
||||
dbUtils.setHiddenToSelection(false);
|
||||
} else {
|
||||
@@ -251,10 +263,10 @@
|
||||
>
|
||||
{#if $allHidden}
|
||||
<Eye size="16" class="mr-1" />
|
||||
{$_('menu.unhide')}
|
||||
{i18n._('menu.unhide')}
|
||||
{:else}
|
||||
<EyeOff size="16" class="mr-1" />
|
||||
{$_('menu.hide')}
|
||||
{i18n._('menu.hide')}
|
||||
{/if}
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
@@ -263,71 +275,71 @@
|
||||
{#if item instanceof ListFileItem}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
on:click={() => dbUtils.addNewTrack(item.getFileId())}
|
||||
onclick={() => dbUtils.addNewTrack(item.getFileId())}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
{$_('menu.new_track')}
|
||||
{i18n._('menu.new_track')}
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{:else if item instanceof ListTrackItem}
|
||||
<ContextMenu.Item
|
||||
disabled={!singleSelection}
|
||||
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||
onclick={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
{$_('menu.new_segment')}
|
||||
{i18n._('menu.new_segment')}
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if item.level !== ListLevel.WAYPOINTS}
|
||||
<ContextMenu.Item on:click={selectAll}>
|
||||
<ContextMenu.Item onclick={selectAll}>
|
||||
<FileStack size="16" class="mr-1" />
|
||||
{$_('menu.select_all')}
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Item on:click={centerMapOnSelection}>
|
||||
<ContextMenu.Item onclick={centerMapOnSelection}>
|
||||
<Maximize size="16" class="mr-1" />
|
||||
{$_('menu.center')}
|
||||
{i18n._('menu.center')}
|
||||
<Shortcut key="⏎" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
||||
<ContextMenu.Item onclick={dbUtils.duplicateSelection}>
|
||||
<Copy size="16" class="mr-1" />
|
||||
{$_('menu.duplicate')}
|
||||
{i18n._('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||
>
|
||||
{#if orientation === 'vertical'}
|
||||
<ContextMenu.Item on:click={copySelection}>
|
||||
<ContextMenu.Item onclick={copySelection}>
|
||||
<ClipboardCopy size="16" class="mr-1" />
|
||||
{$_('menu.copy')}
|
||||
{i18n._('menu.copy')}
|
||||
<Shortcut key="C" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item on:click={cutSelection}>
|
||||
<ContextMenu.Item onclick={cutSelection}>
|
||||
<Scissors size="16" class="mr-1" />
|
||||
{$_('menu.cut')}
|
||||
{i18n._('menu.cut')}
|
||||
<Shortcut key="X" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
disabled={$copied === undefined ||
|
||||
$copied.length === 0 ||
|
||||
!allowedPastes[$copied[0].level].includes(item.level)}
|
||||
on:click={pasteSelection}
|
||||
onclick={pasteSelection}
|
||||
>
|
||||
<ClipboardPaste size="16" class="mr-1" />
|
||||
{$_('menu.paste')}
|
||||
{i18n._('menu.paste')}
|
||||
<Shortcut key="V" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
{/if}
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
||||
<ContextMenu.Item onclick={dbUtils.deleteSelection}>
|
||||
{#if item instanceof ListFileItem}
|
||||
<FileX size="16" class="mr-1" />
|
||||
{$_('menu.close')}
|
||||
{i18n._('menu.close')}
|
||||
{:else}
|
||||
<Trash2 size="16" class="mr-1" />
|
||||
{$_('menu.delete')}
|
||||
{i18n._('menu.delete')}
|
||||
{/if}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
</ContextMenu.Item>
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { ListFileItem } from './FileList';
|
||||
|
||||
export let file: Readable<GPXFileWithStatistics | undefined>;
|
||||
let {
|
||||
file,
|
||||
}: {
|
||||
file: Readable<GPXFileWithStatistics | undefined>;
|
||||
} = $props();
|
||||
|
||||
let recursive = getContext<boolean>('recursive');
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
ListWaypointsItem,
|
||||
moveItems,
|
||||
} from './FileList';
|
||||
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
|
||||
import { fileObservers, getFile, getFileIds } from '$lib/db';
|
||||
// import { settings } from '$lib/logic/settings.svelte';
|
||||
|
||||
export class SelectionTreeType {
|
||||
item: ListItem;
|
||||
@@ -232,29 +233,28 @@ export function applyToOrderedItemsFromFile(
|
||||
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
|
||||
reverse: boolean = true
|
||||
) {
|
||||
get(settings.fileOrder).forEach((fileId) => {
|
||||
let level: ListLevel | undefined = undefined;
|
||||
let items: ListItem[] = [];
|
||||
selectedItems.forEach((item) => {
|
||||
if (item.getFileId() === fileId) {
|
||||
level = item.level;
|
||||
if (
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem ||
|
||||
item instanceof ListTrackSegmentItem ||
|
||||
item instanceof ListWaypointsItem ||
|
||||
item instanceof ListWaypointItem
|
||||
) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length > 0) {
|
||||
sortItems(items, reverse);
|
||||
callback(fileId, level, items);
|
||||
}
|
||||
});
|
||||
// settings.fileOrder.value.forEach((fileId) => {
|
||||
// let level: ListLevel | undefined = undefined;
|
||||
// let items: ListItem[] = [];
|
||||
// selectedItems.forEach((item) => {
|
||||
// if (item.getFileId() === fileId) {
|
||||
// level = item.level;
|
||||
// if (
|
||||
// item instanceof ListFileItem ||
|
||||
// item instanceof ListTrackItem ||
|
||||
// item instanceof ListTrackSegmentItem ||
|
||||
// item instanceof ListWaypointsItem ||
|
||||
// item instanceof ListWaypointItem
|
||||
// ) {
|
||||
// items.push(item);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// if (items.length > 0) {
|
||||
// sortItems(items, reverse);
|
||||
// callback(fileId, level, items);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
export function applyToOrderedSelectedItemsFromFile(
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { dbUtils, getFile, settings } from '$lib/db';
|
||||
import { Save } from 'lucide-svelte';
|
||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||
import { selection } from './Selection';
|
||||
import { editStyle, gpxLayers } from '$lib/stores';
|
||||
import { _ } from '$lib/i18n';
|
||||
|
||||
export let item: ListItem;
|
||||
export let open = false;
|
||||
|
||||
const { defaultOpacity, defaultWidth } = settings;
|
||||
|
||||
let colors: string[] = [];
|
||||
let color: string | undefined = undefined;
|
||||
let opacity: number[] = [];
|
||||
let width: number[] = [];
|
||||
let colorChanged = false;
|
||||
let opacityChanged = false;
|
||||
let widthChanged = false;
|
||||
|
||||
function setStyleInputs() {
|
||||
colors = [];
|
||||
opacity = [];
|
||||
width = [];
|
||||
|
||||
$selection.forEach((item) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
let file = getFile(item.getFileId());
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (file && layer) {
|
||||
let style = file.getStyle();
|
||||
style.color.push(layer.layerColor);
|
||||
|
||||
style.color.forEach((c) => {
|
||||
if (!colors.includes(c)) {
|
||||
colors.push(c);
|
||||
}
|
||||
});
|
||||
style.opacity.forEach((o) => {
|
||||
if (!opacity.includes(o)) {
|
||||
opacity.push(o);
|
||||
}
|
||||
});
|
||||
style.width.forEach((w) => {
|
||||
if (!width.includes(w)) {
|
||||
width.push(w);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
let file = getFile(item.getFileId());
|
||||
let layer = gpxLayers.get(item.getFileId());
|
||||
if (file && layer) {
|
||||
let track = file.trk[item.getTrackIndex()];
|
||||
let style = track.getStyle();
|
||||
if (style) {
|
||||
if (
|
||||
style['gpx_style:color'] &&
|
||||
!colors.includes(style['gpx_style:color'])
|
||||
) {
|
||||
colors.push(style['gpx_style:color']);
|
||||
}
|
||||
if (
|
||||
style['gpx_style:opacity'] &&
|
||||
!opacity.includes(style['gpx_style:opacity'])
|
||||
) {
|
||||
opacity.push(style['gpx_style:opacity']);
|
||||
}
|
||||
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
|
||||
width.push(style['gpx_style:width']);
|
||||
}
|
||||
}
|
||||
if (!colors.includes(layer.layerColor)) {
|
||||
colors.push(layer.layerColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
color = colors[0];
|
||||
opacity = [opacity[0] ?? $defaultOpacity];
|
||||
width = [width[0] ?? $defaultWidth];
|
||||
|
||||
colorChanged = false;
|
||||
opacityChanged = false;
|
||||
widthChanged = false;
|
||||
}
|
||||
|
||||
$: if ($selection && open) {
|
||||
setStyleInputs();
|
||||
}
|
||||
|
||||
$: if (!open) {
|
||||
$editStyle = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger />
|
||||
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{$_('menu.style.color')}
|
||||
<Input
|
||||
bind:value={color}
|
||||
type="color"
|
||||
class="p-0 h-6 w-40"
|
||||
on:change={() => (colorChanged = true)}
|
||||
/>
|
||||
</Label>
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{$_('menu.style.opacity')}
|
||||
<div class="w-40 p-2">
|
||||
<Slider
|
||||
bind:value={opacity}
|
||||
min={0.3}
|
||||
max={1}
|
||||
step={0.1}
|
||||
onValueChange={() => (opacityChanged = true)}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{$_('menu.style.width')}
|
||||
<div class="w-40 p-2">
|
||||
<Slider
|
||||
bind:value={width}
|
||||
id="width"
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
onValueChange={() => (widthChanged = true)}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!colorChanged && !opacityChanged && !widthChanged}
|
||||
on:click={() => {
|
||||
let style = {};
|
||||
if (colorChanged) {
|
||||
style['gpx_style:color'] = color;
|
||||
}
|
||||
if (opacityChanged) {
|
||||
style['gpx_style:opacity'] = opacity[0];
|
||||
}
|
||||
if (widthChanged) {
|
||||
style['gpx_style:width'] = width[0];
|
||||
}
|
||||
dbUtils.setStyleToSelection(style);
|
||||
|
||||
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
|
||||
if (style['gpx_style:opacity']) {
|
||||
$defaultOpacity = style['gpx_style:opacity'];
|
||||
}
|
||||
if (style['gpx_style:width']) {
|
||||
$defaultWidth = style['gpx_style:width'];
|
||||
}
|
||||
}
|
||||
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<Save size="16" class="mr-1" />
|
||||
{$_('menu.metadata.save')}
|
||||
</Button>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -5,44 +5,54 @@
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { Save } from 'lucide-svelte';
|
||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||
import { Save } from '@lucide/svelte';
|
||||
import { ListFileItem, ListTrackItem, type ListItem } from '../FileList';
|
||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { editMetadata } from '$lib/stores';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
|
||||
|
||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
export let item: ListItem;
|
||||
export let open = false;
|
||||
let {
|
||||
node,
|
||||
item,
|
||||
open = $bindable(),
|
||||
}: {
|
||||
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||
item: ListItem;
|
||||
open: boolean;
|
||||
} = $props();
|
||||
|
||||
let name: string =
|
||||
let name: string = $derived(
|
||||
node instanceof GPXFile
|
||||
? (node.metadata.name ?? '')
|
||||
: node instanceof Track
|
||||
? (node.name ?? '')
|
||||
: '';
|
||||
let description: string =
|
||||
: ''
|
||||
);
|
||||
let description: string = $derived(
|
||||
node instanceof GPXFile
|
||||
? (node.metadata.desc ?? '')
|
||||
: node instanceof Track
|
||||
? (node.desc ?? '')
|
||||
: '';
|
||||
: ''
|
||||
);
|
||||
|
||||
$: if (!open) {
|
||||
$editMetadata = false;
|
||||
}
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
editMetadata.current = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger />
|
||||
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||
<Label for="name">{i18n._('menu.metadata.name')}</Label>
|
||||
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||
<Label for="description">{i18n._('menu.metadata.description')}</Label>
|
||||
<Textarea bind:value={description} id="description" />
|
||||
<Button
|
||||
variant="outline"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
dbUtils.applyToFile(item.getFileId(), (file) => {
|
||||
if (item instanceof ListFileItem && node instanceof GPXFile) {
|
||||
file.metadata.name = name;
|
||||
@@ -59,7 +69,7 @@
|
||||
}}
|
||||
>
|
||||
<Save size="16" class="mr-1" />
|
||||
{$_('menu.metadata.save')}
|
||||
{i18n._('menu.metadata.save')}
|
||||
</Button>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -0,0 +1,3 @@
|
||||
export const editMetadata = $state({
|
||||
current: false,
|
||||
});
|
||||
164
website/src/lib/components/file-list/style/StyleDialog.svelte
Normal file
164
website/src/lib/components/file-list/style/StyleDialog.svelte
Normal file
@@ -0,0 +1,164 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { dbUtils, getFile, settings } from '$lib/db';
|
||||
import { Save } from '@lucide/svelte';
|
||||
import { ListFileItem, ListTrackItem, type ListItem } from '$lib/components/file-list/FileList';
|
||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||
import { selection } from '../Selection';
|
||||
import { gpxLayers } from '$lib/stores';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { LineStyleExtension } from 'gpx';
|
||||
|
||||
let {
|
||||
item,
|
||||
open = $bindable(),
|
||||
}: {
|
||||
item: ListItem;
|
||||
open: boolean;
|
||||
} = $props();
|
||||
|
||||
const { defaultOpacity, defaultWidth } = settings;
|
||||
|
||||
let color: string = $state('');
|
||||
let opacity: number = $state(0);
|
||||
let width: number = $state(0);
|
||||
let colorChanged = $state(false);
|
||||
let opacityChanged = $state(false);
|
||||
let widthChanged = $state(false);
|
||||
|
||||
function setStyleInputs() {
|
||||
opacity = $defaultOpacity;
|
||||
width = $defaultWidth;
|
||||
|
||||
$selection.forEach((item) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
let file = getFile(item.getFileId());
|
||||
let layer = gpxLayers.get(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 = getFile(item.getFileId());
|
||||
let layer = gpxLayers.get(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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
colorChanged = false;
|
||||
opacityChanged = false;
|
||||
widthChanged = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($selection && open) {
|
||||
setStyleInputs();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
editStyle.current = false;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
dbUtils.setStyleToSelection(style);
|
||||
|
||||
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
|
||||
if (style['gpx_style:opacity']) {
|
||||
$defaultOpacity = style['gpx_style:opacity'];
|
||||
}
|
||||
if (style['gpx_style:width']) {
|
||||
$defaultWidth = style['gpx_style:width'];
|
||||
}
|
||||
}
|
||||
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger />
|
||||
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{i18n._('menu.style.color')}
|
||||
<Input
|
||||
bind:value={color}
|
||||
type="color"
|
||||
class="p-0 h-6 w-40"
|
||||
onchange={() => (colorChanged = true)}
|
||||
/>
|
||||
</Label>
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{i18n._('menu.style.opacity')}
|
||||
<div class="w-40 p-2">
|
||||
<Slider
|
||||
bind:value={opacity}
|
||||
min={0.3}
|
||||
max={1}
|
||||
step={0.1}
|
||||
onValueChange={() => (opacityChanged = true)}
|
||||
type="single"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||
{i18n._('menu.style.width')}
|
||||
<div class="w-40 p-2">
|
||||
<Slider
|
||||
bind:value={width}
|
||||
id="width"
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
onValueChange={() => (widthChanged = true)}
|
||||
type="single"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!colorChanged && !opacityChanged && !widthChanged}
|
||||
onclick={applyStyle}
|
||||
>
|
||||
<Save size="16" class="mr-1" />
|
||||
{i18n._('menu.metadata.save')}
|
||||
</Button>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -0,0 +1,3 @@
|
||||
export const editStyle = $state({
|
||||
current: false,
|
||||
});
|
||||
Reference in New Issue
Block a user