From cc92ccc1938095070f382bb0ef7deba4438691b8 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Thu, 20 Jun 2024 15:18:21 +0200 Subject: [PATCH] copy paste file items --- website/src/lib/components/App.svelte | 2 +- website/src/lib/components/Menu.svelte | 17 ++- .../lib/components/file-list/FileList.svelte | 41 ++++++- .../src/lib/components/file-list/FileList.ts | 105 ++++++++++++++++-- .../file-list/FileListNodeContent.svelte | 15 +-- .../file-list/FileListNodeLabel.svelte | 53 +++++++-- .../src/lib/components/file-list/Selection.ts | 89 ++++++++++++++- website/src/lib/db.ts | 2 +- website/src/lib/stores.ts | 4 +- website/src/locales/en.json | 5 +- 10 files changed, 289 insertions(+), 44 deletions(-) diff --git a/website/src/lib/components/App.svelte b/website/src/lib/components/App.svelte index 0ea50361..b57f12cf 100644 --- a/website/src/lib/components/App.svelte +++ b/website/src/lib/components/App.svelte @@ -25,7 +25,7 @@ {#if !$verticalFileView}
- +
{/if} diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 70291d88..a86438f4 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -45,7 +45,13 @@ toggleSelectionVisibility, updateSelectionFromKey } 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 { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db'; import { anySelectedLayer } from '$lib/components/layer-control/utils'; @@ -368,6 +374,15 @@ } else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) { dbUtils.duplicateSelection(); 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)) { if (e.shiftKey) { exportAllFiles(); diff --git a/website/src/lib/components/file-list/FileList.svelte b/website/src/lib/components/file-list/FileList.svelte index 92433407..30f9751e 100644 --- a/website/src/lib/components/file-list/FileList.svelte +++ b/website/src/lib/components/file-list/FileList.svelte @@ -1,11 +1,15 @@ -
+
+ {#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",