mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 00:32:33 +00:00
copy paste file items
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
{#if !$verticalFileView}
|
{#if !$verticalFileView}
|
||||||
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
||||||
<FileList orientation="horizontal" class="pointer-events-auto" />
|
<FileList orientation="horizontal" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -45,7 +45,13 @@
|
|||||||
toggleSelectionVisibility,
|
toggleSelectionVisibility,
|
||||||
updateSelectionFromKey
|
updateSelectionFromKey
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { selectAll, selection } from '$lib/components/file-list/Selection';
|
import {
|
||||||
|
copySelection,
|
||||||
|
cutSelection,
|
||||||
|
pasteSelection,
|
||||||
|
selectAll,
|
||||||
|
selection
|
||||||
|
} from '$lib/components/file-list/Selection';
|
||||||
import { derived } from 'svelte/store';
|
import { derived } from 'svelte/store';
|
||||||
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
||||||
import { anySelectedLayer } from '$lib/components/layer-control/utils';
|
import { anySelectedLayer } from '$lib/components/layer-control/utils';
|
||||||
@@ -368,6 +374,15 @@
|
|||||||
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
||||||
dbUtils.duplicateSelection();
|
dbUtils.duplicateSelection();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
copySelection();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
cutSelection();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
pasteSelection();
|
||||||
|
e.preventDefault();
|
||||||
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
exportAllFiles();
|
exportAllFiles();
|
||||||
|
@@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
|
|
||||||
import { fileObservers, settings } from '$lib/db';
|
import { fileObservers, settings } from '$lib/db';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import { ListFileItem, ListRootItem } from './FileList';
|
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
||||||
import { selection } from './Selection';
|
import { copied, pasteSelection, selection } from './Selection';
|
||||||
|
import { ClipboardPaste, Plus } from 'lucide-svelte';
|
||||||
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { createFile } from '$lib/stores';
|
||||||
|
|
||||||
export let orientation: 'vertical' | 'horizontal';
|
export let orientation: 'vertical' | 'horizontal';
|
||||||
export let recursive = false;
|
export let recursive = false;
|
||||||
@@ -40,12 +44,39 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
class="shrink-0 {orientation === 'vertical' ? 'p-1 pr-3' : 'h-10 px-1'}"
|
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
||||||
{orientation}
|
{orientation}
|
||||||
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
||||||
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
>
|
>
|
||||||
<div class="flex {orientation === 'vertical' ? 'flex-col' : 'flex-row'} {$$props.class ?? ''}">
|
<div
|
||||||
|
class="flex {orientation === 'vertical'
|
||||||
|
? 'flex-col py-1 pl-1 min-h-screen'
|
||||||
|
: 'flex-row'} {$$props.class ?? ''}"
|
||||||
|
>
|
||||||
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
||||||
|
{#if orientation === 'vertical'}
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger class="grow" />
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item on:click={createFile}>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_file')}
|
||||||
|
<Shortcut key="+" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={$copied === undefined ||
|
||||||
|
$copied.length === 0 ||
|
||||||
|
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
|
||||||
|
on:click={pasteSelection}
|
||||||
|
>
|
||||||
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
|
{$_('menu.paste')}
|
||||||
|
<Shortcut key="V" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { dbUtils } from "$lib/db";
|
import { dbUtils, getFile, getFileIds } from "$lib/db";
|
||||||
import { castDraft, freeze } from "immer";
|
import { castDraft, freeze } from "immer";
|
||||||
import { Track, TrackSegment, Waypoint } from "gpx";
|
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
||||||
import { selection } from "./Selection";
|
import { selection } from "./Selection";
|
||||||
import { newGPXFile } from "$lib/stores";
|
import { newGPXFile } from "$lib/stores";
|
||||||
|
|
||||||
@@ -13,6 +13,24 @@ export enum ListLevel {
|
|||||||
WAYPOINT
|
WAYPOINT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||||
|
[ListLevel.ROOT]: [],
|
||||||
|
[ListLevel.FILE]: [ListLevel.FILE],
|
||||||
|
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||||
|
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
|
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||||
|
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
||||||
|
[ListLevel.ROOT]: [],
|
||||||
|
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
|
||||||
|
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
|
||||||
|
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
|
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||||
|
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class ListItem {
|
export abstract class ListItem {
|
||||||
level: ListLevel;
|
level: ListLevel;
|
||||||
|
|
||||||
@@ -24,6 +42,7 @@ export abstract class ListItem {
|
|||||||
abstract getFullId(): string;
|
abstract getFullId(): string;
|
||||||
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
|
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
|
||||||
abstract getFileId(): string;
|
abstract getFileId(): string;
|
||||||
|
abstract getParent(): ListItem;
|
||||||
abstract extend(id: string | number): ListItem;
|
abstract extend(id: string | number): ListItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +67,10 @@ export class ListRootItem extends ListItem {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
extend(id: string): ListFileItem {
|
extend(id: string): ListFileItem {
|
||||||
return new ListFileItem(id);
|
return new ListFileItem(id);
|
||||||
}
|
}
|
||||||
@@ -82,6 +105,10 @@ export class ListFileItem extends ListItem {
|
|||||||
return this.fileId;
|
return this.fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListRootItem();
|
||||||
|
}
|
||||||
|
|
||||||
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
|
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
|
||||||
if (id === 'waypoints') {
|
if (id === 'waypoints') {
|
||||||
return new ListWaypointsItem(this.fileId);
|
return new ListWaypointsItem(this.fileId);
|
||||||
@@ -128,6 +155,10 @@ export class ListTrackItem extends ListItem {
|
|||||||
return this.trackIndex;
|
return this.trackIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListFileItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
extend(id: number): ListTrackSegmentItem {
|
extend(id: number): ListTrackSegmentItem {
|
||||||
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
|
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
|
||||||
}
|
}
|
||||||
@@ -178,6 +209,10 @@ export class ListTrackSegmentItem extends ListItem {
|
|||||||
return this.segmentIndex;
|
return this.segmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListTrackItem(this.fileId, this.trackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
extend(): ListTrackSegmentItem {
|
extend(): ListTrackSegmentItem {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -214,6 +249,10 @@ export class ListWaypointsItem extends ListItem {
|
|||||||
return this.fileId;
|
return this.fileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListFileItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
extend(id: number): ListWaypointItem {
|
extend(id: number): ListWaypointItem {
|
||||||
return new ListWaypointItem(this.fileId, id);
|
return new ListWaypointItem(this.fileId, id);
|
||||||
}
|
}
|
||||||
@@ -258,6 +297,10 @@ export class ListWaypointItem extends ListItem {
|
|||||||
return this.waypointIndex;
|
return this.waypointIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListWaypointsItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
extend(): ListWaypointItem {
|
extend(): ListWaypointItem {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -279,12 +322,37 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[]) {
|
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
|
||||||
sortItems(fromItems, true);
|
if (fromItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortItems(fromItems, remove && !(fromParent instanceof ListRootItem));
|
||||||
sortItems(toItems, false);
|
sortItems(toItems, false);
|
||||||
|
|
||||||
dbUtils.applyEachToFilesAndGlobal([fromParent.getFileId(), toParent.getFileId()], [
|
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
|
||||||
(file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
if (!remove || fromParent instanceof ListRootItem) {
|
||||||
|
fromItems.forEach((item) => {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (file) {
|
||||||
|
if (item instanceof ListFileItem) {
|
||||||
|
context.push(file.clone());
|
||||||
|
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||||
|
context.push(file.trk[item.getTrackIndex()].clone());
|
||||||
|
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
|
||||||
|
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
context.push(file.wpt.map((wpt) => wpt.clone()));
|
||||||
|
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
|
||||||
|
context.push(file.wpt[item.getWaypointIndex()].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = [fromParent.getFileId(), toParent.getFileId()];
|
||||||
|
let callbacks = [
|
||||||
|
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
let newFile = file;
|
let newFile = file;
|
||||||
fromItems.forEach((item) => {
|
fromItems.forEach((item) => {
|
||||||
if (item instanceof ListTrackItem) {
|
if (item instanceof ListTrackItem) {
|
||||||
@@ -308,7 +376,7 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
context.reverse();
|
context.reverse();
|
||||||
return newFile;
|
return newFile;
|
||||||
},
|
},
|
||||||
(file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
let newFile = file;
|
let newFile = file;
|
||||||
toItems.forEach((item, i) => {
|
toItems.forEach((item, i) => {
|
||||||
if (item instanceof ListTrackItem) {
|
if (item instanceof ListTrackItem) {
|
||||||
@@ -339,10 +407,27 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
});
|
});
|
||||||
return newFile;
|
return newFile;
|
||||||
}
|
}
|
||||||
], (files, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
];
|
||||||
|
|
||||||
|
if (fromParent instanceof ListRootItem) {
|
||||||
|
files = [];
|
||||||
|
callbacks = [];
|
||||||
|
} else if (!remove) {
|
||||||
|
files.splice(0, 1);
|
||||||
|
callbacks.splice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
toItems.forEach((item, i) => {
|
toItems.forEach((item, i) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (context[i] instanceof Track) {
|
if (context[i] instanceof GPXFile) {
|
||||||
|
let newFile = context[i];
|
||||||
|
if (remove) {
|
||||||
|
files.delete(newFile._data.id);
|
||||||
|
}
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
} else if (context[i] instanceof Track) {
|
||||||
let newFile = newGPXFile();
|
let newFile = newGPXFile();
|
||||||
newFile._data.id = item.getFileId();
|
newFile._data.id = item.getFileId();
|
||||||
if (context[i].name) {
|
if (context[i].name) {
|
||||||
@@ -360,7 +445,7 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, context);
|
||||||
|
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
$selection.clear();
|
$selection.clear();
|
||||||
|
@@ -1,13 +1,4 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
let pull: Record<ListLevel, ListLevel[]> = {
|
|
||||||
[ListLevel.ROOT]: [],
|
|
||||||
[ListLevel.FILE]: [ListLevel.FILE],
|
|
||||||
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
|
||||||
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
|
||||||
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
|
||||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
|
||||||
};
|
|
||||||
|
|
||||||
let dragging: Writable<ListLevel | null> = writable(null);
|
let dragging: Writable<ListLevel | null> = writable(null);
|
||||||
|
|
||||||
let updating = false;
|
let updating = false;
|
||||||
@@ -21,7 +12,7 @@
|
|||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
import { ListLevel, ListRootItem, moveItems, type ListItem } from './FileList';
|
import { ListLevel, ListRootItem, allowedMoves, moveItems, type ListItem } from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
@@ -132,7 +123,7 @@
|
|||||||
sortable = Sortable.create(container, {
|
sortable = Sortable.create(container, {
|
||||||
group: {
|
group: {
|
||||||
name: sortableLevel,
|
name: sortableLevel,
|
||||||
pull: pull[sortableLevel],
|
pull: allowedMoves[sortableLevel],
|
||||||
put: true
|
put: true
|
||||||
},
|
},
|
||||||
direction: orientation,
|
direction: orientation,
|
||||||
@@ -261,7 +252,7 @@
|
|||||||
: parseInt(id);
|
: parseInt(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: canDrop = $dragging !== null && pull[$dragging].includes(sortableLevel);
|
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@@ -18,16 +18,27 @@
|
|||||||
Trash2,
|
Trash2,
|
||||||
Waypoints,
|
Waypoints,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff
|
EyeOff,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardPaste,
|
||||||
|
Scissors
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListLevel,
|
ListLevel,
|
||||||
ListTrackItem,
|
ListTrackItem,
|
||||||
ListWaypointItem,
|
ListWaypointItem,
|
||||||
|
allowedPastes,
|
||||||
type ListItem
|
type ListItem
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { selectItem, selection } from './Selection';
|
import {
|
||||||
|
copied,
|
||||||
|
copySelection,
|
||||||
|
cutSelection,
|
||||||
|
pasteSelection,
|
||||||
|
selectItem,
|
||||||
|
selection
|
||||||
|
} from './Selection';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { gpxLayers, map, toggleSelectionVisibility } from '$lib/stores';
|
import { gpxLayers, map, toggleSelectionVisibility } from '$lib/stores';
|
||||||
@@ -198,7 +209,7 @@
|
|||||||
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
|
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
|
||||||
'vertical'
|
'vertical'
|
||||||
? 'h-fit'
|
? 'h-fit'
|
||||||
: 'h-9 px-1.5 shadow-md'}"
|
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
|
||||||
>
|
>
|
||||||
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
<Popover.Root bind:open={openEditMetadata}>
|
<Popover.Root bind:open={openEditMetadata}>
|
||||||
@@ -416,12 +427,36 @@
|
|||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.level !== ListLevel.WAYPOINTS}
|
{#if $verticalFileView || item.level !== ListLevel.WAYPOINTS}
|
||||||
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
{#if item.level !== ListLevel.WAYPOINTS}
|
||||||
<Copy size="16" class="mr-1" />
|
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
||||||
{$_('menu.duplicate')}
|
<Copy size="16" class="mr-1" />
|
||||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
{$_('menu.duplicate')}
|
||||||
>
|
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if $verticalFileView}
|
||||||
|
<ContextMenu.Item on:click={copySelection}>
|
||||||
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
{$_('menu.copy')}
|
||||||
|
<Shortcut key="C" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item on:click={cutSelection}>
|
||||||
|
<Scissors size="16" class="mr-1" />
|
||||||
|
{$_('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}
|
||||||
|
>
|
||||||
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
|
{$_('menu.paste')}
|
||||||
|
<Shortcut key="V" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/if}
|
||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
{/if}
|
{/if}
|
||||||
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { get, writable } from "svelte/store";
|
import { get, writable } from "svelte/store";
|
||||||
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, type ListLevel, sortItems, ListWaypointsItem } from "./FileList";
|
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
|
||||||
import { fileObservers, getFile, settings } from "$lib/db";
|
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
|
||||||
|
|
||||||
export class SelectionTreeType {
|
export class SelectionTreeType {
|
||||||
item: ListItem;
|
item: ListItem;
|
||||||
@@ -223,3 +223,88 @@ export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback:
|
|||||||
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
||||||
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
|
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const copied = writable<ListItem[] | undefined>(undefined);
|
||||||
|
const cut = writable(false);
|
||||||
|
|
||||||
|
export function copySelection(): boolean {
|
||||||
|
let selected = get(selection).getSelected();
|
||||||
|
if (selected.length > 0) {
|
||||||
|
copied.set(selected);
|
||||||
|
cut.set(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cutSelection() {
|
||||||
|
if (copySelection()) {
|
||||||
|
cut.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCopied() {
|
||||||
|
copied.set(undefined);
|
||||||
|
cut.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pasteSelection() {
|
||||||
|
let fromItems = get(copied);
|
||||||
|
if (fromItems === undefined || fromItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = get(selection).getSelected();
|
||||||
|
if (selected.length === 0) {
|
||||||
|
selected = [new ListRootItem()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromParent = fromItems[0].getParent();
|
||||||
|
let toParent = selected[selected.length - 1];
|
||||||
|
|
||||||
|
let startIndex: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (fromItems[0].level === toParent.level) {
|
||||||
|
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
|
||||||
|
startIndex = toParent.getId() + 1;
|
||||||
|
}
|
||||||
|
toParent = toParent.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
let toItems: ListItem[] = [];
|
||||||
|
if (toParent.level === ListLevel.ROOT) {
|
||||||
|
let fileIds = getFileIds(fromItems.length);
|
||||||
|
fileIds.forEach((fileId) => {
|
||||||
|
toItems.push(new ListFileItem(fileId));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let toFile = getFile(toParent.getFileId());
|
||||||
|
if (toFile) {
|
||||||
|
fromItems.forEach((item, index) => {
|
||||||
|
if (toParent instanceof ListFileItem) {
|
||||||
|
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||||
|
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
toItems.push(new ListWaypointsItem(toParent.getFileId()));
|
||||||
|
} else if (item instanceof ListWaypointItem) {
|
||||||
|
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
||||||
|
}
|
||||||
|
} else if (toParent instanceof ListTrackItem) {
|
||||||
|
if (item instanceof ListTrackSegmentItem) {
|
||||||
|
let toTrackIndex = toParent.getTrackIndex();
|
||||||
|
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
|
||||||
|
}
|
||||||
|
} else if (toParent instanceof ListWaypointsItem) {
|
||||||
|
if (item instanceof ListWaypointItem) {
|
||||||
|
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromItems.length === toItems.length) {
|
||||||
|
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
|
||||||
|
resetCopied();
|
||||||
|
}
|
||||||
|
}
|
@@ -102,7 +102,7 @@ export const settings = {
|
|||||||
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
||||||
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
|
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
|
||||||
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
|
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
|
||||||
defaultOpacity: dexieSettingStore('defaultOpacity', 0.6),
|
defaultOpacity: dexieSettingStore('defaultOpacity', 0.7),
|
||||||
defaultWeight: dexieSettingStore('defaultWeight', 5),
|
defaultWeight: dexieSettingStore('defaultWeight', 5),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -134,14 +134,14 @@ export const currentTool = writable<Tool | null>(null);
|
|||||||
export const splitAs = writable(SplitType.FILES);
|
export const splitAs = writable(SplitType.FILES);
|
||||||
|
|
||||||
export function newGPXFile() {
|
export function newGPXFile() {
|
||||||
const newFileName = get(_)("menu.new_filename");
|
const newFileName = get(_)("menu.new_file");
|
||||||
|
|
||||||
let file = new GPXFile();
|
let file = new GPXFile();
|
||||||
|
|
||||||
let maxNewFileNumber = 0;
|
let maxNewFileNumber = 0;
|
||||||
get(fileObservers).forEach((f) => {
|
get(fileObservers).forEach((f) => {
|
||||||
let file = get(f)?.file;
|
let file = get(f)?.file;
|
||||||
if (file && file.metadata.name.startsWith(newFileName)) {
|
if (file && file.metadata.name && file.metadata.name.startsWith(newFileName)) {
|
||||||
let number = parseInt(file.metadata.name.split(' ').pop() ?? '0');
|
let number = parseInt(file.metadata.name.split(' ').pop() ?? '0');
|
||||||
if (!isNaN(number) && number > maxNewFileNumber) {
|
if (!isNaN(number) && number > maxNewFileNumber) {
|
||||||
maxNewFileNumber = number;
|
maxNewFileNumber = number;
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"menu": {
|
"menu": {
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"new_filename": "New file",
|
"new_file": "New file",
|
||||||
"new_track": "New track",
|
"new_track": "New track",
|
||||||
"new_segment": "New segment",
|
"new_segment": "New segment",
|
||||||
"load_desktop": "Load...",
|
"load_desktop": "Load...",
|
||||||
"load_drive": "Load from Google Drive...",
|
"load_drive": "Load from Google Drive...",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
|
"copy": "Copy",
|
||||||
|
"paste": "Paste",
|
||||||
|
"cut": "Cut",
|
||||||
"export": "Export...",
|
"export": "Export...",
|
||||||
"export_all": "Export all...",
|
"export_all": "Export all...",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
Reference in New Issue
Block a user