mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
add contextmenu actions to edit menu
This commit is contained in:
@@ -33,7 +33,13 @@
|
|||||||
View,
|
View,
|
||||||
FilePen,
|
FilePen,
|
||||||
HeartHandshake,
|
HeartHandshake,
|
||||||
PersonStanding
|
PersonStanding,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ClipboardCopy,
|
||||||
|
Scissors,
|
||||||
|
ClipboardPaste,
|
||||||
|
PaintBucket
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -44,9 +50,15 @@
|
|||||||
createFile,
|
createFile,
|
||||||
loadFiles,
|
loadFiles,
|
||||||
toggleSelectionVisibility,
|
toggleSelectionVisibility,
|
||||||
updateSelectionFromKey
|
updateSelectionFromKey,
|
||||||
|
showSelection,
|
||||||
|
hideSelection,
|
||||||
|
anyHidden,
|
||||||
|
editMetadata,
|
||||||
|
editStyle
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
|
copied,
|
||||||
copySelection,
|
copySelection,
|
||||||
cutSelection,
|
cutSelection,
|
||||||
pasteSelection,
|
pasteSelection,
|
||||||
@@ -65,6 +77,7 @@
|
|||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
import { allowedPastes, ListFileItem, ListTrackItem } from './file-list/FileList';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
distanceUnits,
|
distanceUnits,
|
||||||
@@ -180,11 +193,75 @@
|
|||||||
<Shortcut key="Z" ctrl={true} shift={true} />
|
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$selection.size !== 1 ||
|
||||||
|
!$selection
|
||||||
|
.getSelected()
|
||||||
|
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||||
|
on:click={() => ($editMetadata = true)}
|
||||||
|
>
|
||||||
|
<Info size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.button')}
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$selection.size === 0 ||
|
||||||
|
!$selection
|
||||||
|
.getSelected()
|
||||||
|
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||||
|
on:click={() => ($editStyle = true)}
|
||||||
|
>
|
||||||
|
<PaintBucket size="16" class="mr-1" />
|
||||||
|
{$_('menu.style.button')}
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => {
|
||||||
|
if ($anyHidden) {
|
||||||
|
showSelection();
|
||||||
|
} else {
|
||||||
|
hideSelection();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
|
{#if $anyHidden}
|
||||||
|
<Eye size="16" class="mr-1" />
|
||||||
|
{$_('menu.unhide')}
|
||||||
|
{:else}
|
||||||
|
<EyeOff size="16" class="mr-1" />
|
||||||
|
{$_('menu.hide')}
|
||||||
|
{/if}
|
||||||
|
<Shortcut key="H" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={selectAll}>
|
<Menubar.Item on:click={selectAll}>
|
||||||
<span class="w-4 mr-1"></span>
|
<span class="w-4 mr-1"></span>
|
||||||
{$_('menu.select_all')}
|
{$_('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
|
{#if $verticalFileView}
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
||||||
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
{$_('menu.copy')}
|
||||||
|
<Shortcut key="C" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item on:click={cutSelection} disabled={$selection.size === 0}>
|
||||||
|
<Scissors size="16" class="mr-1" />
|
||||||
|
{$_('menu.cut')}
|
||||||
|
<Shortcut key="X" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$copied === undefined ||
|
||||||
|
$copied.length === 0 ||
|
||||||
|
($selection.size > 0 &&
|
||||||
|
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))}
|
||||||
|
on:click={pasteSelection}
|
||||||
|
>
|
||||||
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
|
{$_('menu.paste')}
|
||||||
|
<Shortcut key="V" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
{/if}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}>
|
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
|
@@ -35,7 +35,15 @@
|
|||||||
} from './Selection';
|
} from './Selection';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { gpxLayers, hideSelection, map, showSelection } from '$lib/stores';
|
import {
|
||||||
|
anyHidden,
|
||||||
|
editMetadata,
|
||||||
|
editStyle,
|
||||||
|
gpxLayers,
|
||||||
|
hideSelection,
|
||||||
|
map,
|
||||||
|
showSelection
|
||||||
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
GPXTreeElement,
|
GPXTreeElement,
|
||||||
Track,
|
Track,
|
||||||
@@ -54,7 +62,6 @@
|
|||||||
| Readonly<Waypoint>;
|
| Readonly<Waypoint>;
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
export let label: string | undefined;
|
export let label: string | undefined;
|
||||||
let hidden = false;
|
|
||||||
|
|
||||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||||
|
|
||||||
@@ -62,9 +69,6 @@
|
|||||||
|
|
||||||
$: singleSelection = $selection.size === 1;
|
$: singleSelection = $selection.size === 1;
|
||||||
|
|
||||||
let openEditMetadata: boolean = false;
|
|
||||||
let openEditStyle: boolean = false;
|
|
||||||
|
|
||||||
let nodeColors: string[] = [];
|
let nodeColors: string[] = [];
|
||||||
|
|
||||||
$: if (node && $map) {
|
$: if (node && $map) {
|
||||||
@@ -98,6 +102,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let openEditMetadata: boolean = false;
|
||||||
|
let openEditStyle: boolean = false;
|
||||||
|
|
||||||
|
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
|
||||||
|
$: openEditStyle =
|
||||||
|
$editStyle &&
|
||||||
|
$selection.has(item) &&
|
||||||
|
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
@@ -108,10 +121,6 @@
|
|||||||
if (!get(selection).has(item)) {
|
if (!get(selection).has(item)) {
|
||||||
selectItem(item);
|
selectItem(item);
|
||||||
}
|
}
|
||||||
let layer = gpxLayers.get(item.getFileId());
|
|
||||||
if (layer) {
|
|
||||||
hidden = layer.hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -185,25 +194,25 @@
|
|||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
<ContextMenu.Content>
|
<ContextMenu.Content>
|
||||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
<ContextMenu.Item disabled={!singleSelection} on:click={() => (openEditMetadata = true)}>
|
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
||||||
<Info size="16" class="mr-1" />
|
<Info size="16" class="mr-1" />
|
||||||
{$_('menu.metadata.button')}
|
{$_('menu.metadata.button')}
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Item on:click={() => (openEditStyle = true)}>
|
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
||||||
<PaintBucket size="16" class="mr-1" />
|
<PaintBucket size="16" class="mr-1" />
|
||||||
{$_('menu.style.button')}
|
{$_('menu.style.button')}
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
{#if item instanceof ListFileItem}
|
{#if item instanceof ListFileItem}
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (hidden) {
|
if ($anyHidden) {
|
||||||
showSelection();
|
showSelection();
|
||||||
} else {
|
} else {
|
||||||
hideSelection();
|
hideSelection();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if hidden}
|
{#if $anyHidden}
|
||||||
<Eye size="16" class="mr-1" />
|
<Eye size="16" class="mr-1" />
|
||||||
{$_('menu.unhide')}
|
{$_('menu.unhide')}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -252,14 +261,12 @@
|
|||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $verticalFileView || item.level !== ListLevel.WAYPOINTS}
|
{#if $verticalFileView}
|
||||||
{#if item.level !== ListLevel.WAYPOINTS}
|
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
||||||
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
<Copy size="16" class="mr-1" />
|
||||||
<Copy size="16" class="mr-1" />
|
{$_('menu.duplicate')}
|
||||||
{$_('menu.duplicate')}
|
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if $verticalFileView}
|
{#if $verticalFileView}
|
||||||
<ContextMenu.Item on:click={copySelection}>
|
<ContextMenu.Item on:click={copySelection}>
|
||||||
<ClipboardCopy size="16" class="mr-1" />
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { editMetadata } from '$lib/stores';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
| GPXTreeElement<AnyGPXTreeElement>
|
| GPXTreeElement<AnyGPXTreeElement>
|
||||||
@@ -29,6 +30,10 @@
|
|||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.desc ?? ''
|
? node.desc ?? ''
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
$: if (!open) {
|
||||||
|
$editMetadata = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover.Root bind:open>
|
<Popover.Root bind:open>
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
import { Save } from 'lucide-svelte';
|
import { Save } from 'lucide-svelte';
|
||||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { gpxLayers } from '$lib/stores';
|
import { editStyle, gpxLayers } from '$lib/stores';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
@@ -89,6 +89,10 @@
|
|||||||
$: if ($selection && open) {
|
$: if ($selection && open) {
|
||||||
setStyleInputs();
|
setStyleInputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (!open) {
|
||||||
|
$editStyle = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover.Root bind:open>
|
<Popover.Root bind:open>
|
||||||
|
@@ -22,7 +22,9 @@
|
|||||||
import { Crop } from 'lucide-svelte';
|
import { Crop } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
|
|
||||||
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
|
$: validSelection =
|
||||||
|
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||||
|
$gpxStatistics.local.points.length > 0;
|
||||||
|
|
||||||
let maxSliderValue = 100;
|
let maxSliderValue = 100;
|
||||||
let sliderValues = [0, 100];
|
let sliderValues = [0, 100];
|
||||||
@@ -82,8 +84,9 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!validSelection || !canCrop}
|
disabled={!validSelection || !canCrop}
|
||||||
on:click={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
on:click={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
|
||||||
><Crop size="16" class="mr-1" />{$_('toolbar.scissors.crop')}</Button
|
|
||||||
>
|
>
|
||||||
|
<Crop size="16" class="mr-1" />{$_('toolbar.scissors.crop')}
|
||||||
|
</Button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Label class="flex flex-row gap-3 items-center">
|
<Label class="flex flex-row gap-3 items-center">
|
||||||
<span class="shrink-0">
|
<span class="shrink-0">
|
||||||
|
@@ -446,6 +446,9 @@ export const dbUtils = {
|
|||||||
let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
|
let [result, _removed] = newFile.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
|
||||||
newFile = result;
|
newFile = result;
|
||||||
}
|
}
|
||||||
|
} else if (level === ListLevel.WAYPOINTS) {
|
||||||
|
let [result, _removed] = newFile.replaceWaypoints(file.wpt.length, file.wpt.length - 1, file.wpt.map((wpt) => wpt.clone()));
|
||||||
|
newFile = result;
|
||||||
} else if (level === ListLevel.WAYPOINT) {
|
} else if (level === ListLevel.WAYPOINT) {
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
|
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
|
||||||
@@ -846,7 +849,7 @@ export const dbUtils = {
|
|||||||
applyGlobal((draft) => {
|
applyGlobal((draft) => {
|
||||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let file = original(draft)?.get(fileId);
|
let file = original(draft)?.get(fileId);
|
||||||
if (file) {
|
if (file && (level === ListLevel.FILE || level === ListLevel.TRACK)) {
|
||||||
let newFile = file;
|
let newFile = file;
|
||||||
if (level === ListLevel.FILE) {
|
if (level === ListLevel.FILE) {
|
||||||
newFile = file.setStyle(style);
|
newFile = file.setStyle(style);
|
||||||
|
@@ -344,6 +344,15 @@ export function exportFile(file: GPXFile) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const anyHidden = writable(false);
|
||||||
|
function updateAnyHidden() {
|
||||||
|
anyHidden.set(get(selection).getSelected().some((item) => {
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
return layer && layer.hidden;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
selection.subscribe(updateAnyHidden);
|
||||||
|
|
||||||
export function toggleSelectionVisibility() {
|
export function toggleSelectionVisibility() {
|
||||||
let files = new Set<string>();
|
let files = new Set<string>();
|
||||||
get(selection).forEach((item) => {
|
get(selection).forEach((item) => {
|
||||||
@@ -355,6 +364,7 @@ export function toggleSelectionVisibility() {
|
|||||||
layer.toggleVisibility();
|
layer.toggleVisibility();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
updateAnyHidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideSelection() {
|
export function hideSelection() {
|
||||||
@@ -368,6 +378,7 @@ export function hideSelection() {
|
|||||||
layer.toggleVisibility();
|
layer.toggleVisibility();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
anyHidden.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showSelection() {
|
export function showSelection() {
|
||||||
@@ -381,8 +392,12 @@ export function showSelection() {
|
|||||||
layer.toggleVisibility();
|
layer.toggleVisibility();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
anyHidden.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const editMetadata = writable(false);
|
||||||
|
export const editStyle = writable(false);
|
||||||
|
|
||||||
let stravaCookies: any = null;
|
let stravaCookies: any = null;
|
||||||
function refreshStravaCookies() {
|
function refreshStravaCookies() {
|
||||||
/*
|
/*
|
||||||
|
Reference in New Issue
Block a user