+
+ {#if orientation === 'vertical'}
+
+
+
+
+
+ {$_('menu.new_file')}
+
+
+
+
+
+ {$_('menu.paste')}
+
+
+
+
+ {/if}
diff --git a/website/src/lib/components/file-list/FileList.ts b/website/src/lib/components/file-list/FileList.ts
index 0e8db1b8..c72f682e 100644
--- a/website/src/lib/components/file-list/FileList.ts
+++ b/website/src/lib/components/file-list/FileList.ts
@@ -1,6 +1,6 @@
-import { dbUtils } from "$lib/db";
+import { dbUtils, getFile, getFileIds } from "$lib/db";
import { castDraft, freeze } from "immer";
-import { Track, TrackSegment, Waypoint } from "gpx";
+import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
import { selection } from "./Selection";
import { newGPXFile } from "$lib/stores";
@@ -13,6 +13,24 @@ export enum ListLevel {
WAYPOINT
}
+export const allowedMoves: Record
= {
+ [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.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 {
level: ListLevel;
@@ -24,6 +42,7 @@ export abstract class ListItem {
abstract getFullId(): string;
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
abstract getFileId(): string;
+ abstract getParent(): ListItem;
abstract extend(id: string | number): ListItem;
}
@@ -48,6 +67,10 @@ export class ListRootItem extends ListItem {
return '';
}
+ getParent(): ListItem {
+ return this;
+ }
+
extend(id: string): ListFileItem {
return new ListFileItem(id);
}
@@ -82,6 +105,10 @@ export class ListFileItem extends ListItem {
return this.fileId;
}
+ getParent(): ListItem {
+ return new ListRootItem();
+ }
+
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
if (id === 'waypoints') {
return new ListWaypointsItem(this.fileId);
@@ -128,6 +155,10 @@ export class ListTrackItem extends ListItem {
return this.trackIndex;
}
+ getParent(): ListItem {
+ return new ListFileItem(this.fileId);
+ }
+
extend(id: number): ListTrackSegmentItem {
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
}
@@ -178,6 +209,10 @@ export class ListTrackSegmentItem extends ListItem {
return this.segmentIndex;
}
+ getParent(): ListItem {
+ return new ListTrackItem(this.fileId, this.trackIndex);
+ }
+
extend(): ListTrackSegmentItem {
return this;
}
@@ -214,6 +249,10 @@ export class ListWaypointsItem extends ListItem {
return this.fileId;
}
+ getParent(): ListItem {
+ return new ListFileItem(this.fileId);
+ }
+
extend(id: number): ListWaypointItem {
return new ListWaypointItem(this.fileId, id);
}
@@ -258,6 +297,10 @@ export class ListWaypointItem extends ListItem {
return this.waypointIndex;
}
+ getParent(): ListItem {
+ return new ListWaypointsItem(this.fileId);
+ }
+
extend(): ListWaypointItem {
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[]) {
- sortItems(fromItems, true);
+export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
+ if (fromItems.length === 0) {
+ return;
+ }
+
+ sortItems(fromItems, remove && !(fromParent instanceof ListRootItem));
sortItems(toItems, false);
- dbUtils.applyEachToFilesAndGlobal([fromParent.getFileId(), toParent.getFileId()], [
- (file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
+ let context: (GPXFile | 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;
fromItems.forEach((item) => {
if (item instanceof ListTrackItem) {
@@ -308,7 +376,7 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
context.reverse();
return newFile;
},
- (file, context: (Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
+ (file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
let newFile = file;
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) {
@@ -339,10 +407,27 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
});
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) => {
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();
newFile._data.id = item.getFileId();
if (context[i].name) {
@@ -360,7 +445,7 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
}
}
});
- }, []);
+ }, context);
selection.update(($selection) => {
$selection.clear();
diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte
index 59089864..2d110594 100644
--- a/website/src/lib/components/file-list/FileListNodeContent.svelte
+++ b/website/src/lib/components/file-list/FileListNodeContent.svelte
@@ -1,13 +1,4 @@
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
@@ -416,12 +427,36 @@
{/if}
{/if}
- {#if item.level !== ListLevel.WAYPOINTS}
-
-
- {$_('menu.duplicate')}
-
+ {#if $verticalFileView || item.level !== ListLevel.WAYPOINTS}
+ {#if item.level !== ListLevel.WAYPOINTS}
+
+
+ {$_('menu.duplicate')}
+
+ {/if}
+ {#if $verticalFileView}
+
+
+ {$_('menu.copy')}
+
+
+
+
+ {$_('menu.cut')}
+
+
+
+
+ {$_('menu.paste')}
+
+
+ {/if}
{/if}
diff --git a/website/src/lib/components/file-list/Selection.ts b/website/src/lib/components/file-list/Selection.ts
index 14548e7b..c583d044 100644
--- a/website/src/lib/components/file-list/Selection.ts
+++ b/website/src/lib/components/file-list/Selection.ts
@@ -1,6 +1,6 @@
import { get, writable } from "svelte/store";
-import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, type ListLevel, sortItems, ListWaypointsItem } from "./FileList";
-import { fileObservers, getFile, settings } from "$lib/db";
+import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
+import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
export class SelectionTreeType {
item: ListItem;
@@ -222,4 +222,89 @@ export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback:
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
+}
+
+export const copied = writable(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();
+ }
}
\ No newline at end of file
diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts
index 4e58fcda..034b862f 100644
--- a/website/src/lib/db.ts
+++ b/website/src/lib/db.ts
@@ -102,7 +102,7 @@ export const settings = {
distanceMarkers: dexieSettingStore('distanceMarkers', false),
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
fileOrder: dexieSettingStore('fileOrder', []),
- defaultOpacity: dexieSettingStore('defaultOpacity', 0.6),
+ defaultOpacity: dexieSettingStore('defaultOpacity', 0.7),
defaultWeight: dexieSettingStore('defaultWeight', 5),
};
diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts
index 6a463812..9a34f4ec 100644
--- a/website/src/lib/stores.ts
+++ b/website/src/lib/stores.ts
@@ -134,14 +134,14 @@ export const currentTool = writable(null);
export const splitAs = writable(SplitType.FILES);
export function newGPXFile() {
- const newFileName = get(_)("menu.new_filename");
+ const newFileName = get(_)("menu.new_file");
let file = new GPXFile();
let maxNewFileNumber = 0;
get(fileObservers).forEach((f) => {
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');
if (!isNaN(number) && number > maxNewFileNumber) {
maxNewFileNumber = number;
diff --git a/website/src/locales/en.json b/website/src/locales/en.json
index 6bccdcb2..172ab015 100644
--- a/website/src/locales/en.json
+++ b/website/src/locales/en.json
@@ -1,12 +1,15 @@
{
"menu": {
"new": "New",
- "new_filename": "New file",
+ "new_file": "New file",
"new_track": "New track",
"new_segment": "New segment",
"load_desktop": "Load...",
"load_drive": "Load from Google Drive...",
"duplicate": "Duplicate",
+ "copy": "Copy",
+ "paste": "Paste",
+ "cut": "Cut",
"export": "Export...",
"export_all": "Export all...",
"edit": "Edit",