From 874eea271e25b289f1fc6513b81314f8363a9843 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 2 Jul 2024 10:21:04 +0200 Subject: [PATCH 1/6] improve metadata and style ux --- website/src/lib/components/Menu.svelte | 11 +++++++++++ .../lib/components/file-list/FileListNodeLabel.svelte | 1 + website/src/locales/en.json | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index b2c70028..66267cfb 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -217,6 +217,7 @@ > {$_('menu.metadata.button')} + item instanceof ListFileItem || item instanceof ListTrackItem) + ) { + $editMetadata = true; + } + e.preventDefault(); } else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) { $elevationProfile = !$elevationProfile; e.preventDefault(); diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 4a92380b..815ebe92 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -200,6 +200,7 @@ ($editMetadata = true)}> {$_('menu.metadata.button')} + ($editStyle = true)}> diff --git a/website/src/locales/en.json b/website/src/locales/en.json index d0c400f6..ff100cf0 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -58,13 +58,13 @@ "click": "Click", "drag": "Drag", "metadata": { - "button": "Edit info", + "button": "Info...", "name": "Name", "description": "Description", "save": "Save" }, "style": { - "button": "Change style", + "button": "Appearance...", "color": "Color", "opacity": "Opacity", "weight": "Weight" From c5dea0aeda43865703b6556815aada7d852ae9f9 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Tue, 2 Jul 2024 20:04:17 +0200 Subject: [PATCH 2/6] hide any file item and undo-redo it --- gpx/src/gpx.ts | 106 ++++++++++++++- website/src/lib/components/Menu.svelte | 21 +-- .../lib/components/file-list/FileList.svelte | 10 +- .../components/file-list/FileListNode.svelte | 6 +- .../file-list/FileListNodeContent.svelte | 33 +++-- .../file-list/FileListNodeLabel.svelte | 61 +++++---- .../src/lib/components/gpx-layer/GPXLayer.ts | 27 ++-- website/src/lib/db.ts | 124 +++++++++++++++--- website/src/lib/stores.ts | 71 ++++------ 9 files changed, 316 insertions(+), 143 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 7adf1f60..084d90a1 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -123,7 +123,7 @@ export class GPXFile extends GPXTreeNode{ if (gpx) { this.attributes = gpx.attributes this.metadata = gpx.metadata; - this.wpt = gpx.wpt ? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index)) : []; + this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; if (gpx.hasOwnProperty('_data')) { this._data = gpx._data; @@ -134,6 +134,17 @@ export class GPXFile extends GPXTreeNode{ this.wpt = []; this.trk = [new Track()]; } + + this.trk.forEach((track, trackIndex) => { + track._data['trackIndex'] = trackIndex; + track.trkseg.forEach((segment, segmentIndex) => { + segment._data['trackIndex'] = trackIndex; + segment._data['segmentIndex'] = segmentIndex; + }); + }); + this.wpt.forEach((waypoint, waypointIndex) => { + waypoint._data['index'] = waypointIndex; + }); } get children(): ReadonlyArray { @@ -367,6 +378,63 @@ export class GPXFile extends GPXTreeNode{ } }); } + + setHidden(hidden: boolean, trackIndices?: number[], segmentIndices?: number[]) { + return produce(this, (draft) => { + let og = getOriginal(draft); // Read as much as possible from the original object because it is faster + let allHidden = hidden; + let trk = og.trk.map((track, index) => { + if (trackIndices === undefined || trackIndices.includes(index)) { + return track.setHidden(hidden, segmentIndices); + } else { + allHidden = allHidden && (track._data.hidden === true); + return track; + } + }); + draft.trk = freeze(trk); // Pre-freeze the array, faster as well + + let wpt = og.wpt.map((waypoint) => { + if (trackIndices === undefined && segmentIndices === undefined) { + return waypoint.setHidden(hidden); + } else { + allHidden = allHidden && (waypoint._data.hidden === true); + return waypoint; + } + }); + draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well + + if (trackIndices === undefined && segmentIndices === undefined) { + draft._data.hiddenWpt = hidden; + } + + draft._data.hidden = allHidden; + }); + } + + setHiddenWaypoints(hidden: boolean, waypointIndices?: number[]) { + return produce(this, (draft) => { + let og = getOriginal(draft); // Read as much as possible from the original object because it is faster + + let allHiddenWpt = hidden; + let wpt = og.wpt.map((waypoint, index) => { + if (waypointIndices === undefined || waypointIndices.includes(index)) { + return waypoint.setHidden(hidden); + } else { + allHiddenWpt = allHiddenWpt && (waypoint._data.hidden === true); + return waypoint; + } + }); + draft.wpt = freeze(wpt); // Pre-freeze the array, faster as well + + let allHiddenTrk = true; + og.trk.forEach((track) => { + allHiddenTrk = allHiddenTrk && (track._data.hidden === true); + }); + + draft._data.hiddenWpt = allHiddenWpt; + draft._data.hidden = allHiddenTrk && allHiddenWpt; + }); + } }; // A class that represents a Track in a GPX file @@ -573,7 +641,24 @@ export class Track extends GPXTreeNode { } }); } -}; + + setHidden(hidden: boolean, segmentIndices?: number[]) { + return produce(this, (draft) => { + let og = getOriginal(draft); // Read as much as possible from the original object because it is faster + let allHidden = hidden; + let trkseg = og.trkseg.map((segment, index) => { + if (segmentIndices === undefined || segmentIndices.includes(index)) { + return segment.setHidden(hidden); + } else { + allHidden = allHidden && (segment._data.hidden === true); + return segment; + } + }); + draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well + draft._data.hidden = allHidden; + }); + } +} // A class that represents a TrackSegment in a GPX file export class TrackSegment extends GPXTreeLeaf { @@ -881,6 +966,11 @@ export class TrackSegment extends GPXTreeLeaf { } }); } + setHidden(hidden: boolean) { + return produce(this, (draft) => { + draft._data.hidden = hidden; + }); + } }; export class TrackPoint { @@ -985,7 +1075,7 @@ export class Waypoint { type?: string; _data: { [key: string]: any } = {}; - constructor(waypoint: WaypointType & { _data?: any } | Waypoint, index?: number) { + constructor(waypoint: WaypointType & { _data?: any } | Waypoint) { this.attributes = waypoint.attributes; this.ele = waypoint.ele; this.time = waypoint.time; @@ -998,9 +1088,6 @@ export class Waypoint { if (waypoint.hasOwnProperty('_data')) { this._data = waypoint._data; } - if (index !== undefined) { - this._data['index'] = index; - } } getCoordinates(): Coordinates { @@ -1032,6 +1119,13 @@ export class Waypoint { type: this.type, }); } + + // Producers + setHidden(hidden: boolean) { + return produce(this, (draft) => { + draft._data.hidden = hidden; + }); + } } export class GPXStatistics { diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 66267cfb..ae00b296 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -48,11 +48,8 @@ triggerFileInput, createFile, loadFiles, - toggleSelectionVisibility, updateSelectionFromKey, - showSelection, - hideSelection, - anyHidden, + allHidden, editMetadata, editStyle, exportState, @@ -231,15 +228,15 @@ { - if ($anyHidden) { - showSelection(); + if ($allHidden) { + dbUtils.setHiddenToSelection(false); } else { - hideSelection(); + dbUtils.setHiddenToSelection(true); } }} disabled={$selection.size == 0} > - {#if $anyHidden} + {#if $allHidden} {$_('menu.unhide')} {:else} @@ -249,7 +246,7 @@ - + {$_('menu.select_all')} @@ -543,7 +540,11 @@ $verticalFileView = !$verticalFileView; e.preventDefault(); } else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) { - toggleSelectionVisibility(); + if ($allHidden) { + dbUtils.setHiddenToSelection(false); + } else { + dbUtils.setHiddenToSelection(true); + } e.preventDefault(); } else if (e.key === 'F1') { switchBasemaps(); diff --git a/website/src/lib/components/file-list/FileList.svelte b/website/src/lib/components/file-list/FileList.svelte index 46549f95..5f5e33eb 100644 --- a/website/src/lib/components/file-list/FileList.svelte +++ b/website/src/lib/components/file-list/FileList.svelte @@ -5,8 +5,8 @@ import { fileObservers, settings } from '$lib/db'; import { setContext } from 'svelte'; import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList'; - import { copied, pasteSelection, selection } from './Selection'; - import { ClipboardPaste, Plus } from 'lucide-svelte'; + import { copied, pasteSelection, selectAll, selection } from './Selection'; + import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte'; import Shortcut from '$lib/components/Shortcut.svelte'; import { _ } from 'svelte-i18n'; import { createFile } from '$lib/stores'; @@ -66,6 +66,12 @@ + + + {$_('menu.select_all')} + + + 0 && node[0] instanceof Waypoint + : node instanceof GPXFile && item instanceof ListWaypointsItem ? $_('gpx.waypoints') : ''; diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte index 2d110594..0fa1c6cb 100644 --- a/website/src/lib/components/file-list/FileListNodeContent.svelte +++ b/website/src/lib/components/file-list/FileListNodeContent.svelte @@ -12,14 +12,21 @@ import { get, writable, type Readable, type Writable } from 'svelte/store'; import FileListNodeStore from './FileListNodeStore.svelte'; import FileListNode from './FileListNode.svelte'; - import { ListLevel, ListRootItem, allowedMoves, moveItems, type ListItem } from './FileList'; + import { + ListFileItem, + ListLevel, + ListRootItem, + ListWaypointsItem, + allowedMoves, + moveItems, + type ListItem + } from './FileList'; import { selection } from './Selection'; import { _ } from 'svelte-i18n'; export let node: | Map> | GPXTreeElement - | ReadonlyArray> | Readonly; export let item: ListItem; export let waypointRoot: boolean = false; @@ -32,7 +39,9 @@ : node instanceof GPXFile ? waypointRoot ? ListLevel.WAYPOINTS - : ListLevel.TRACK + : item instanceof ListWaypointsItem + ? ListLevel.WAYPOINT + : ListLevel.TRACK : node instanceof Track ? ListLevel.SEGMENT : ListLevel.WAYPOINT; @@ -268,10 +277,16 @@ {/each} {:else if node instanceof GPXFile} - {#if waypointRoot} + {#if item instanceof ListWaypointsItem} + {#each node.wpt as wpt, i (wpt)} +
+ +
+ {/each} + {:else if waypointRoot} {#if node.wpt.length > 0}
- +
{/if} {:else} @@ -287,16 +302,10 @@ {/each} - {:else if Array.isArray(node) && node.length > 0 && node[0] instanceof Waypoint} - {#each node as wpt, i (wpt)} -
- -
- {/each} {/if} -{#if node instanceof GPXFile} +{#if node instanceof GPXFile && item instanceof ListFileItem} {#if !waypointRoot} {/if} diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index 815ebe92..9becd751 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -38,15 +38,7 @@ } from './Selection'; import { getContext } from 'svelte'; import { get } from 'svelte/store'; - import { - anyHidden, - editMetadata, - editStyle, - gpxLayers, - hideSelection, - map, - showSelection - } from '$lib/stores'; + import { allHidden, editMetadata, editStyle, gpxLayers, map } from '$lib/stores'; import { GPXTreeElement, Track, @@ -189,9 +181,18 @@ {:else if item.level === ListLevel.WAYPOINT} {/if} - + {label} + {#if (item.level !== ListLevel.WAYPOINTS && node._data.hidden) || (item.level === ListLevel.WAYPOINTS && node._data.hiddenWpt)} + + {/if} @@ -206,28 +207,26 @@ {$_('menu.style.button')}
- {#if item instanceof ListFileItem} - { - if ($anyHidden) { - showSelection(); - } else { - hideSelection(); - } - }} - > - {#if $anyHidden} - - {$_('menu.unhide')} - {:else} - - {$_('menu.hide')} - {/if} - - - {/if} - {/if} + { + if ($allHidden) { + dbUtils.setHiddenToSelection(false); + } else { + dbUtils.setHiddenToSelection(true); + } + }} + > + {#if $allHidden} + + {$_('menu.unhide')} + {:else} + + {$_('menu.hide')} + {/if} + + + {#if $verticalFileView} {#if item instanceof ListFileItem} ; layerColor: string; - hidden: boolean = false; markers: mapboxgl.Marker[] = []; selected: boolean = false; draggable: boolean; @@ -165,6 +164,15 @@ export class GPXLayer { this.map.removeLayer(this.fileId + '-direction'); } } + + let visibleItems: [number, number][] = []; + file.forEachSegment((segment, trackIndex, segmentIndex) => { + if (!segment._data.hidden) { + visibleItems.push([trackIndex, segmentIndex]); + } + }); + + this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false }); } catch (e) { // No reliable way to check if the map is ready to add sources and layers return; } @@ -244,7 +252,11 @@ export class GPXLayer { } this.markers.forEach((marker) => { - marker.addTo(this.map); + if (!marker._waypoint._data.hidden) { + marker.addTo(this.map); + } else { + marker.remove(); + } }); } @@ -366,17 +378,6 @@ export class GPXLayer { } } - toggleVisibility() { - this.hidden = !this.hidden; - if (this.hidden) { - this.map.setLayoutProperty(this.fileId, 'visibility', 'none'); - this.markers.forEach(marker => marker.remove()); - } else { - this.map.setLayoutProperty(this.fileId, 'visibility', 'visible'); - this.markers.forEach(marker => marker.addTo(this.map)); - } - } - getGeoJSON(): GeoJSON.FeatureCollection { let file = get(this.file)?.file; if (!file) { diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 18b5832e..df303b90 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -2,7 +2,7 @@ import Dexie, { liveQuery } from 'dexie'; import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance, type LineStyleExtension } from 'gpx'; import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, freeze, produceWithPatches, original, produce } from 'immer'; import { writable, get, derived, type Readable, type Writable } from 'svelte/store'; -import { gpxStatistics, initTargetMapBounds, splitAs, updateTargetMapBounds } from './stores'; +import { gpxStatistics, initTargetMapBounds, splitAs, updateAllHidden, updateTargetMapBounds } from './stores'; import { mode } from 'mode-watcher'; import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays, type CustomLayer, defaultOpacities } from './assets/layers'; import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection'; @@ -192,6 +192,10 @@ function dexieGPXFileStore(id: string): Readable & { dest file: gpx, statistics }); + + if (get(selection).hasAnyChildren(new ListFileItem(id))) { + updateAllHidden(); + } } }); return { @@ -203,10 +207,61 @@ function dexieGPXFileStore(id: string): Readable & { dest }; } +function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) { + let removedItems: ListItem[] = []; + + applyToOrderedItemsFromFile(get(selection).getSelected(), (fileId, level, items) => { + let file = updatedFiles.find((file) => file._data.id === fileId); + if (file) { + items.forEach((item) => { + if (item instanceof ListTrackItem) { + let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex()); + if (newTrackIndex === -1) { + removedItems.push(item); + } + } else if (item instanceof ListTrackSegmentItem) { + let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex()); + if (newTrackIndex === -1) { + removedItems.push(item); + } else { + let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex((segment) => segment._data.segmentIndex === item.getSegmentIndex()); + if (newSegmentIndex === -1) { + removedItems.push(item); + } + } + } else if (item instanceof ListWaypointItem) { + let newWaypointIndex = file.wpt.findIndex((wpt) => wpt._data.index === item.getWaypointIndex()); + if (newWaypointIndex === -1) { + removedItems.push(item); + } + } + }); + } else if (deletedFileIds.includes(fileId)) { + items.forEach((item) => { + removedItems.push(item); + }); + } + }); + + if (removedItems.length > 0) { + selection.update(($selection) => { + removedItems.forEach((item) => { + if (item instanceof ListFileItem) { + $selection.deleteChild(item.getFileId()); + } else { + $selection.set(item, false); + } + }); + return $selection; + }); + } +} + // Commit the changes to the file state to the database function commitFileStateChange(newFileState: ReadonlyMap, patch: Patch[]) { let changedFileIds = getChangedFileIds(patch); let updatedFileIds: string[] = [], deletedFileIds: string[] = []; + changedFileIds.forEach(id => { if (newFileState.has(id)) { updatedFileIds.push(id); @@ -218,6 +273,8 @@ function commitFileStateChange(newFileState: ReadonlyMap, patch let updatedFiles = updatedFileIds.map(id => newFileState.get(id)).filter(file => file !== undefined) as GPXFile[]; updatedFileIds = updatedFiles.map(file => file._data.id); + updateSelection(updatedFiles, deletedFileIds); + return db.transaction('rw', db.fileids, db.files, async () => { if (updatedFileIds.length > 0) { await db.fileids.bulkPut(updatedFileIds, updatedFileIds); @@ -255,14 +312,6 @@ liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => { }); return $files; }); - if (deletedFiles.length > 0) { - selection.update(($selection) => { - deletedFiles.forEach((fileId) => { - $selection.deleteChild(fileId); - }); - return $selection; - }); - } settings.fileOrder.update((order) => { newFiles.forEach((fileId) => { if (!order.includes(fileId)) { @@ -427,7 +476,7 @@ export const dbUtils = { let ids = getFileIds(get(settings.fileOrder).length); let index = 0; applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; if (level === ListLevel.FILE) { @@ -467,7 +516,7 @@ export const dbUtils = { } applyGlobal((draft) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; if (level === ListLevel.FILE) { @@ -504,7 +553,7 @@ export const dbUtils = { wpt: [] }; applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; if (level === ListLevel.FILE) { @@ -616,7 +665,7 @@ export const dbUtils = { } applyGlobal((draft) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { if (level === ListLevel.FILE) { let length = file.getNumberOfTrackPoints(); @@ -645,7 +694,7 @@ export const dbUtils = { extractSelection: () => { return applyGlobal((draft) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { if (level === ListLevel.FILE) { if (file.trk.length > 1) { @@ -678,7 +727,9 @@ export const dbUtils = { let tracks = track.trkseg.map((segment, segmentIndex) => { let t = track.replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0]; if (track.name) { - t.name = `${track.name} (${segmentIndex + 1})`; + t = produce(t, (t) => { + t.name = `${track.name} (${segmentIndex + 1})`; + }); } return t; }); @@ -734,7 +785,9 @@ export const dbUtils = { let tracks = track.trkseg.map((segment, segmentIndex) => { let t = track.clone().replaceTrackSegments(0, track.trkseg.length - 1, [segment])[0]; if (track.name) { - t.name = `${track.name} (${segmentIndex + 1})`; + t = produce(t, (t) => { + t.name = `${track.name} (${segmentIndex + 1})`; + }); } return t; }); @@ -749,7 +802,7 @@ export const dbUtils = { split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates) { let splitType = get(splitAs); return applyGlobal((draft) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let segment = file.trk[trackIndex].trkseg[segmentIndex]; @@ -794,7 +847,7 @@ export const dbUtils = { } applyGlobal((draft) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; if (level === ListLevel.FILE) { @@ -824,7 +877,7 @@ export const dbUtils = { applyGlobal((draft) => { let allItems = Array.from(itemsAndPoints.keys()); applyToOrderedItemsFromFile(allItems, (fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; for (let item of items) { @@ -848,7 +901,7 @@ export const dbUtils = { } applyGlobal((draft) => { applyToOrderedSelectedItemsFromFile((fileId, level, items) => { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file && (level === ListLevel.FILE || level === ListLevel.TRACK)) { let newFile = file; if (level === ListLevel.FILE) { @@ -867,6 +920,35 @@ export const dbUtils = { }); }); }, + setHiddenToSelection: (hidden: boolean) => { + if (get(selection).size === 0) { + return; + } + applyGlobal((draft) => { + applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + let file = getFile(fileId); + if (file) { + let newFile = file; + if (level === ListLevel.FILE) { + newFile = file.setHidden(hidden); + } else if (level === ListLevel.TRACK) { + let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex()); + newFile = newFile.setHidden(hidden, trackIndices); + } else if (level === ListLevel.SEGMENT) { + let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()]; + let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex()); + newFile = newFile.setHidden(hidden, trackIndices, segmentIndices); + } else if (level === ListLevel.WAYPOINTS) { + newFile = newFile.setHiddenWaypoints(hidden); + } else if (level === ListLevel.WAYPOINT) { + let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex()); + newFile = newFile.setHiddenWaypoints(hidden, waypointIndices); + } + draft.set(newFile._data.id, freeze(newFile)); + } + }); + }); + }, deleteSelection: () => { if (get(selection).size === 0) { return; @@ -876,7 +958,7 @@ export const dbUtils = { if (level === ListLevel.FILE) { draft.delete(fileId); } else { - let file = original(draft)?.get(fileId); + let file = getFile(fileId); if (file) { let newFile = file; if (level === ListLevel.TRACK) { diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index e69e32d2..e8d3da8a 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -344,56 +344,35 @@ export function exportFile(file: GPXFile) { 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 const allHidden = writable(false); -export function toggleSelectionVisibility() { - let files = new Set(); - get(selection).forEach((item) => { - files.add(item.getFileId()); - }); - files.forEach((fileId) => { - let layer = gpxLayers.get(fileId); - if (layer) { - layer.toggleVisibility(); +export function updateAllHidden() { + let hidden = true; + applyToOrderedSelectedItemsFromFile((fileId, level, items) => { + let file = getFile(fileId); + if (file) { + for (let item of items) { + if (!hidden) { + return; + } + + if (item instanceof ListFileItem) { + hidden = hidden && (file._data.hidden === true); + } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { + hidden = hidden && (file.trk[item.getTrackIndex()]._data.hidden === true); + } else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) { + hidden = hidden && (file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data.hidden === true); + } else if (item instanceof ListWaypointsItem) { + hidden = hidden && (file._data.hiddenWpt === true); + } else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) { + hidden = hidden && (file.wpt[item.getWaypointIndex()]._data.hidden === true); + } + } } }); - updateAnyHidden(); -} - -export function hideSelection() { - let files = new Set(); - get(selection).forEach((item) => { - files.add(item.getFileId()); - }); - files.forEach((fileId) => { - let layer = gpxLayers.get(fileId); - if (layer && !layer.hidden) { - layer.toggleVisibility(); - } - }); - anyHidden.set(true); -} - -export function showSelection() { - let files = new Set(); - get(selection).forEach((item) => { - files.add(item.getFileId()); - }); - files.forEach((fileId) => { - let layer = gpxLayers.get(fileId); - if (layer && layer.hidden) { - layer.toggleVisibility(); - } - }); - anyHidden.set(false); + allHidden.set(hidden); } +selection.subscribe(updateAllHidden); export const editMetadata = writable(false); export const editStyle = writable(false); From f75e2a52784a30d019a7b8f3b1fbe88340d8d65f Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 3 Jul 2024 11:30:28 +0200 Subject: [PATCH 3/6] handle series of (de)select events together --- .../file-list/FileListNodeContent.svelte | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte index 0fa1c6cb..507562e5 100644 --- a/website/src/lib/components/file-list/FileListNodeContent.svelte +++ b/website/src/lib/components/file-list/FileListNodeContent.svelte @@ -48,35 +48,44 @@ let sortable: Sortable; let orientation = getContext<'vertical' | 'horizontal'>('orientation'); + let lastUpdateStart = 0; function updateToSelection(e) { - if (updating) return; - updating = true; - // Sortable updates selection - let changed = getChangedIds(); - if (changed.length > 0) { - selection.update(($selection) => { - $selection.clear(); - Object.entries(elements).forEach(([id, element]) => { - $selection.set( - item.extend(getRealId(id)), - element.classList.contains('sortable-selected') - ); - }); - - if ( - e.originalEvent && - $selection.size > 1 && - !(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) - ) { - // Fix bug that sometimes causes a single select to be treated as a multi-select - $selection.clear(); - $selection.set(item.extend(getRealId(changed[0])), true); + lastUpdateStart = Date.now(); + setTimeout(() => { + if (Date.now() - lastUpdateStart >= 40) { + if (updating) { + return; } - return $selection; - }); - } - updating = false; + updating = true; + // Sortable updates selection + let changed = getChangedIds(); + if (changed.length > 0) { + selection.update(($selection) => { + $selection.clear(); + Object.entries(elements).forEach(([id, element]) => { + $selection.set( + item.extend(getRealId(id)), + element.classList.contains('sortable-selected') + ); + }); + + if ( + e.originalEvent && + $selection.size > 1 && + !(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) + ) { + // Fix bug that sometimes causes a single select to be treated as a multi-select + $selection.clear(); + $selection.set(item.extend(getRealId(changed[0])), true); + } + + return $selection; + }); + } + updating = false; + } + }, 50); } function updateFromSelection() { From affc3ed946ddde8a9a342330f5d09f036000e079 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 3 Jul 2024 22:42:08 +0200 Subject: [PATCH 4/6] fix selection reset after updates --- .../file-list/FileListNodeContent.svelte | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/website/src/lib/components/file-list/FileListNodeContent.svelte b/website/src/lib/components/file-list/FileListNodeContent.svelte index 507562e5..62d97804 100644 --- a/website/src/lib/components/file-list/FileListNodeContent.svelte +++ b/website/src/lib/components/file-list/FileListNodeContent.svelte @@ -6,7 +6,7 @@ @@ -142,7 +143,9 @@ /> {/if} { if (e.ctrlKey) { // Add to selection instead of opening context menu @@ -181,7 +184,7 @@ {label} - {#if (item.level !== ListLevel.WAYPOINTS && node._data.hidden) || (item.level === ListLevel.WAYPOINTS && node._data.hiddenWpt)} + {#if hidden}