mirror of
				https://github.com/gpxstudio/gpx.studio.git
				synced 2025-11-04 05:21:09 +00:00 
			
		
		
		
	basic file operations
This commit is contained in:
		@@ -1,16 +1,30 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { files } from '$lib/stores';
 | 
			
		||||
	import { files, selectedFiles, addSelectFile, selectFile } from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
	import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import { Label } from '$lib/components/ui/label';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<ScrollArea class="w-full h-full">
 | 
			
		||||
	<div class="flex flex-col">
 | 
			
		||||
		{#each $files as file}
 | 
			
		||||
			<Button variant="outline" class="w-full">
 | 
			
		||||
				{file.metadata.name}
 | 
			
		||||
			</Button>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
</ScrollArea>
 | 
			
		||||
<div class="flex flex-col h-full w-full">
 | 
			
		||||
	<Label class="w-full">Files</Label>
 | 
			
		||||
	<ScrollArea class="w-full h-full">
 | 
			
		||||
		<div class="flex flex-col">
 | 
			
		||||
			{#each $files as file}
 | 
			
		||||
				<Button
 | 
			
		||||
					variant={$selectedFiles.has(file) ? 'outline' : 'secondary'}
 | 
			
		||||
					class="w-full {$selectedFiles.has(file) ? 'hover:bg-background' : 'hover:bg-secondary'}"
 | 
			
		||||
					on:click={(e) => {
 | 
			
		||||
						if (e.shiftKey) {
 | 
			
		||||
							addSelectFile(file);
 | 
			
		||||
						} else {
 | 
			
		||||
							selectFile(file);
 | 
			
		||||
						}
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					{file.metadata.name}
 | 
			
		||||
				</Button>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</div>
 | 
			
		||||
	</ScrollArea>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -32,17 +32,42 @@
 | 
			
		||||
		colorCount[color]++;
 | 
			
		||||
		return color;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function decrementColor(color: string) {
 | 
			
		||||
		colorCount[color]--;
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { GPXFile } from 'gpx';
 | 
			
		||||
	import { map } from '$lib/stores';
 | 
			
		||||
	import { map, selectedFiles, addSelectFile, selectFile } from '$lib/stores';
 | 
			
		||||
	import { onDestroy } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	export let file: GPXFile;
 | 
			
		||||
 | 
			
		||||
	let layerId = getLayerId();
 | 
			
		||||
	let layerColor = getColor();
 | 
			
		||||
 | 
			
		||||
	function selectOnClick(e: any) {
 | 
			
		||||
		if (e.originalEvent.shiftKey) {
 | 
			
		||||
			addSelectFile(file);
 | 
			
		||||
		} else {
 | 
			
		||||
			selectFile(file);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function toPointerCursor() {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			$map.getCanvas().style.cursor = 'pointer';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function toDefaultCursor() {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			$map.getCanvas().style.cursor = '';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function addGPXLayer() {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			if (!$map.getSource(layerId)) {
 | 
			
		||||
@@ -80,6 +105,10 @@
 | 
			
		||||
						'line-opacity': ['get', 'opacity']
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				$map.on('click', layerId, selectOnClick);
 | 
			
		||||
				$map.on('mouseenter', layerId, toPointerCursor);
 | 
			
		||||
				$map.on('mouseleave', layerId, toDefaultCursor);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -91,4 +120,22 @@
 | 
			
		||||
			addGPXLayer();
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	$: if ($selectedFiles.has(file)) {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			$map.moveLayer(layerId);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if ($map) {
 | 
			
		||||
			$map.off('click', layerId, selectOnClick);
 | 
			
		||||
			$map.off('mouseenter', layerId, toPointerCursor);
 | 
			
		||||
			$map.off('mouseleave', layerId, toDefaultCursor);
 | 
			
		||||
 | 
			
		||||
			$map.removeLayer(layerId);
 | 
			
		||||
			$map.removeSource(layerId);
 | 
			
		||||
		}
 | 
			
		||||
		decrementColor(layerColor);
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,16 @@
 | 
			
		||||
	import Fa from 'svelte-fa';
 | 
			
		||||
	import { faGoogleDrive } from '@fortawesome/free-brands-svg-icons';
 | 
			
		||||
 | 
			
		||||
	import { triggerFileInput } from '$lib/stores';
 | 
			
		||||
	import {
 | 
			
		||||
		files,
 | 
			
		||||
		selectedFiles,
 | 
			
		||||
		duplicateSelectedFiles,
 | 
			
		||||
		exportAllFiles,
 | 
			
		||||
		exportSelectedFiles,
 | 
			
		||||
		removeAllFiles,
 | 
			
		||||
		removeSelectedFiles,
 | 
			
		||||
		triggerFileInput
 | 
			
		||||
	} from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
	let distanceUnits = 'metric';
 | 
			
		||||
	let velocityUnits = 'speed';
 | 
			
		||||
@@ -45,14 +54,14 @@
 | 
			
		||||
						Load from Google Drive...</Menubar.Item
 | 
			
		||||
					>
 | 
			
		||||
					<Menubar.Separator />
 | 
			
		||||
					<Menubar.Item>
 | 
			
		||||
					<Menubar.Item on:click={duplicateSelectedFiles} disabled={$selectedFiles.size == 0}>
 | 
			
		||||
						<Copy size="16" class="mr-1" /> Duplicate <Menubar.Shortcut>⌘D</Menubar.Shortcut>
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Separator />
 | 
			
		||||
					<Menubar.Item>
 | 
			
		||||
					<Menubar.Item on:click={exportSelectedFiles} disabled={$selectedFiles.size == 0}>
 | 
			
		||||
						<Download size="16" class="mr-1" /> Export... <Menubar.Shortcut>⌘S</Menubar.Shortcut>
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Item>
 | 
			
		||||
					<Menubar.Item on:click={exportAllFiles} disabled={$files.length == 0}>
 | 
			
		||||
						<Download size="16" class="mr-1" /> Export all... <Menubar.Shortcut
 | 
			
		||||
							>⇧⌘S</Menubar.Shortcut
 | 
			
		||||
						>
 | 
			
		||||
@@ -69,12 +78,16 @@
 | 
			
		||||
						<Redo2 size="16" class="mr-1" /> Redo <Menubar.Shortcut>⇧⌘Z</Menubar.Shortcut>
 | 
			
		||||
					</Menubar.Item>
 | 
			
		||||
					<Menubar.Separator />
 | 
			
		||||
					<Menubar.Item
 | 
			
		||||
						><Trash2 size="16" class="mr-1" /> Delete <Menubar.Shortcut>⌘⌫</Menubar.Shortcut
 | 
			
		||||
					<Menubar.Item on:click={removeSelectedFiles} disabled={$selectedFiles.size == 0}>
 | 
			
		||||
						<Trash2 size="16" class="mr-1" /> Delete <Menubar.Shortcut>⌘⌫</Menubar.Shortcut
 | 
			
		||||
						></Menubar.Item
 | 
			
		||||
					>
 | 
			
		||||
					<Menubar.Item class="text-destructive data-[highlighted]:text-destructive"
 | 
			
		||||
						><Trash2 size="16" class="mr-1" /> Delete all<Menubar.Shortcut>⇧⌘⌫</Menubar.Shortcut
 | 
			
		||||
					<Menubar.Item
 | 
			
		||||
						class="text-destructive data-[highlighted]:text-destructive"
 | 
			
		||||
						on:click={removeAllFiles}
 | 
			
		||||
						disabled={$files.length == 0}
 | 
			
		||||
					>
 | 
			
		||||
						<Trash2 size="16" class="mr-1" /> Delete all<Menubar.Shortcut>⇧⌘⌫</Menubar.Shortcut
 | 
			
		||||
						></Menubar.Item
 | 
			
		||||
					>
 | 
			
		||||
				</Menubar.Content>
 | 
			
		||||
@@ -134,6 +147,34 @@
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
	on:keydown={(e) => {
 | 
			
		||||
		e.stopImmediatePropagation();
 | 
			
		||||
		if (e.key === 'o' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			triggerFileInput();
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			duplicateSelectedFiles();
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			if (e.shiftKey) {
 | 
			
		||||
				exportAllFiles();
 | 
			
		||||
			} else {
 | 
			
		||||
				exportSelectedFiles();
 | 
			
		||||
			}
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			if (e.shiftKey) {
 | 
			
		||||
				console.log('removeAllFiles');
 | 
			
		||||
				removeAllFiles();
 | 
			
		||||
			} else {
 | 
			
		||||
				removeSelectedFiles();
 | 
			
		||||
			}
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
	}}
 | 
			
		||||
/>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
	div :global(button) {
 | 
			
		||||
		@apply hover:bg-accent;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,18 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { Button } from '$lib/components/ui/button';
 | 
			
		||||
	import * as Tooltip from '$lib/components/ui/tooltip/index.js';
 | 
			
		||||
 | 
			
		||||
	import { selectedFiles } from '$lib/stores';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Tooltip.Root openDelay="300">
 | 
			
		||||
<Tooltip.Root openDelay={300}>
 | 
			
		||||
	<Tooltip.Trigger asChild let:builder>
 | 
			
		||||
		<Button builders={[builder]} variant="ghost" class="h-fit px-1 py-1.5">
 | 
			
		||||
		<Button
 | 
			
		||||
			builders={[builder]}
 | 
			
		||||
			variant="ghost"
 | 
			
		||||
			class="h-fit px-1 py-1.5"
 | 
			
		||||
			disabled={$selectedFiles.size == 0}
 | 
			
		||||
		>
 | 
			
		||||
			<slot name="icon" />
 | 
			
		||||
		</Button>
 | 
			
		||||
	</Tooltip.Trigger>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,13 @@
 | 
			
		||||
 | 
			
		||||
	$: if ($map && container) {
 | 
			
		||||
		$map.on('load', () => {
 | 
			
		||||
			if (position.includes('right')) container.classList.add('float-right');
 | 
			
		||||
			else container.classList.add('float-left');
 | 
			
		||||
			container.classList.remove('hidden');
 | 
			
		||||
			let control = new CustomControl(container);
 | 
			
		||||
			$map.addControl(control, position);
 | 
			
		||||
			if ($map && container) {
 | 
			
		||||
				if (position.includes('right')) container.classList.add('float-right');
 | 
			
		||||
				else container.classList.add('float-left');
 | 
			
		||||
				container.classList.remove('hidden');
 | 
			
		||||
				let control = new CustomControl(container);
 | 
			
		||||
				$map.addControl(control, position);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import { writable } from 'svelte/store';
 | 
			
		||||
import { writable, get } from 'svelte/store';
 | 
			
		||||
 | 
			
		||||
import mapboxgl from 'mapbox-gl';
 | 
			
		||||
import { GPXFile, parseGPX } from 'gpx';
 | 
			
		||||
import { GPXFile, buildGPX, parseGPX } from 'gpx';
 | 
			
		||||
 | 
			
		||||
export const map = writable<mapboxgl.Map | null>(null);
 | 
			
		||||
export const files = writable<GPXFile[]>([]);
 | 
			
		||||
export const selectedFiles = writable<Set<GPXFile>>(new Set());
 | 
			
		||||
 | 
			
		||||
export function triggerFileInput() {
 | 
			
		||||
    const input = document.createElement('input');
 | 
			
		||||
@@ -36,7 +37,85 @@ export function loadFile(file: File) {
 | 
			
		||||
                gpx.metadata['name'] = file.name.split('.').slice(0, -1).join('.');
 | 
			
		||||
            }
 | 
			
		||||
            files.update($files => [...$files, gpx]);
 | 
			
		||||
            selectFile(gpx);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    reader.readAsText(file);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function duplicateSelectedFiles() {
 | 
			
		||||
    let selected: GPXFile[] = [];
 | 
			
		||||
    get(selectedFiles).forEach(file => selected.push(file));
 | 
			
		||||
    selected.forEach(file => duplicateFile(file));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function duplicateFile(file: GPXFile) {
 | 
			
		||||
    let clone = file.clone();
 | 
			
		||||
    files.update($files => [...$files, clone]);
 | 
			
		||||
    selectFile(clone);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeSelectedFiles() {
 | 
			
		||||
    let index = 0;
 | 
			
		||||
    while (index < get(files).length) {
 | 
			
		||||
        if (get(selectedFiles).has(get(files)[index])) {
 | 
			
		||||
            files.update($files => {
 | 
			
		||||
                $files.splice(index, 1);
 | 
			
		||||
                return $files;
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            index++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    selectedFiles.update($selectedFiles => {
 | 
			
		||||
        $selectedFiles.clear();
 | 
			
		||||
        return $selectedFiles;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeAllFiles() {
 | 
			
		||||
    files.update($files => {
 | 
			
		||||
        $files.splice(0, $files.length);
 | 
			
		||||
        return $files;
 | 
			
		||||
    });
 | 
			
		||||
    selectedFiles.update($selectedFiles => {
 | 
			
		||||
        $selectedFiles.clear();
 | 
			
		||||
        return $selectedFiles;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportSelectedFiles() {
 | 
			
		||||
    get(selectedFiles).forEach(file => exportFile(file));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function exportAllFiles() {
 | 
			
		||||
    for (let i = 0; i < get(files).length; i++) {
 | 
			
		||||
        exportFile(get(files)[i]);
 | 
			
		||||
        await new Promise(resolve => setTimeout(resolve, 200));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportFile(file: GPXFile) {
 | 
			
		||||
    let blob = new Blob([buildGPX(file)], { type: 'application/gpx+xml' });
 | 
			
		||||
    let url = URL.createObjectURL(blob);
 | 
			
		||||
    let a = document.createElement('a');
 | 
			
		||||
    a.href = url;
 | 
			
		||||
    a.download = file.metadata.name + '.gpx';
 | 
			
		||||
    a.click();
 | 
			
		||||
    URL.revokeObjectURL(url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function selectFile(file: GPXFile) {
 | 
			
		||||
    selectedFiles.update($selectedFiles => {
 | 
			
		||||
        $selectedFiles.clear();
 | 
			
		||||
        $selectedFiles.add(file);
 | 
			
		||||
        return $selectedFiles;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addSelectFile(file: GPXFile) {
 | 
			
		||||
    selectedFiles.update($selectedFiles => {
 | 
			
		||||
        $selectedFiles.add(file);
 | 
			
		||||
        return $selectedFiles;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -5,8 +5,6 @@
 | 
			
		||||
	import Menu from '$lib/components/Menu.svelte';
 | 
			
		||||
	import Toolbar from '$lib/components/Toolbar.svelte';
 | 
			
		||||
	import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
 | 
			
		||||
 | 
			
		||||
	import { triggerFileInput } from '$lib/stores';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col w-screen h-screen">
 | 
			
		||||
@@ -21,12 +19,3 @@
 | 
			
		||||
		<FileList />
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<svelte:window
 | 
			
		||||
	on:keydown={(e) => {
 | 
			
		||||
		if (e.key === 'o' && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
			triggerFileInput();
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
	}}
 | 
			
		||||
/>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user