mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
basic file operations
This commit is contained in:
@@ -1,16 +1,30 @@
|
|||||||
<script lang="ts">
|
<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 { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea class="w-full h-full">
|
<div class="flex flex-col h-full w-full">
|
||||||
<div class="flex flex-col">
|
<Label class="w-full">Files</Label>
|
||||||
{#each $files as file}
|
<ScrollArea class="w-full h-full">
|
||||||
<Button variant="outline" class="w-full">
|
<div class="flex flex-col">
|
||||||
{file.metadata.name}
|
{#each $files as file}
|
||||||
</Button>
|
<Button
|
||||||
{/each}
|
variant={$selectedFiles.has(file) ? 'outline' : 'secondary'}
|
||||||
</div>
|
class="w-full {$selectedFiles.has(file) ? 'hover:bg-background' : 'hover:bg-secondary'}"
|
||||||
</ScrollArea>
|
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]++;
|
colorCount[color]++;
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decrementColor(color: string) {
|
||||||
|
colorCount[color]--;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { GPXFile } from 'gpx';
|
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;
|
export let file: GPXFile;
|
||||||
|
|
||||||
let layerId = getLayerId();
|
let layerId = getLayerId();
|
||||||
let layerColor = getColor();
|
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() {
|
function addGPXLayer() {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
if (!$map.getSource(layerId)) {
|
if (!$map.getSource(layerId)) {
|
||||||
@@ -80,6 +105,10 @@
|
|||||||
'line-opacity': ['get', 'opacity']
|
'line-opacity': ['get', 'opacity']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$map.on('click', layerId, selectOnClick);
|
||||||
|
$map.on('mouseenter', layerId, toPointerCursor);
|
||||||
|
$map.on('mouseleave', layerId, toDefaultCursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,4 +120,22 @@
|
|||||||
addGPXLayer();
|
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>
|
</script>
|
||||||
|
@@ -15,7 +15,16 @@
|
|||||||
import Fa from 'svelte-fa';
|
import Fa from 'svelte-fa';
|
||||||
import { faGoogleDrive } from '@fortawesome/free-brands-svg-icons';
|
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 distanceUnits = 'metric';
|
||||||
let velocityUnits = 'speed';
|
let velocityUnits = 'speed';
|
||||||
@@ -45,14 +54,14 @@
|
|||||||
Load from Google Drive...</Menubar.Item
|
Load from Google Drive...</Menubar.Item
|
||||||
>
|
>
|
||||||
<Menubar.Separator />
|
<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>
|
<Copy size="16" class="mr-1" /> Duplicate <Menubar.Shortcut>⌘D</Menubar.Shortcut>
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<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>
|
<Download size="16" class="mr-1" /> Export... <Menubar.Shortcut>⌘S</Menubar.Shortcut>
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Item>
|
<Menubar.Item on:click={exportAllFiles} disabled={$files.length == 0}>
|
||||||
<Download size="16" class="mr-1" /> Export all... <Menubar.Shortcut
|
<Download size="16" class="mr-1" /> Export all... <Menubar.Shortcut
|
||||||
>⇧⌘S</Menubar.Shortcut
|
>⇧⌘S</Menubar.Shortcut
|
||||||
>
|
>
|
||||||
@@ -69,12 +78,16 @@
|
|||||||
<Redo2 size="16" class="mr-1" /> Redo <Menubar.Shortcut>⇧⌘Z</Menubar.Shortcut>
|
<Redo2 size="16" class="mr-1" /> Redo <Menubar.Shortcut>⇧⌘Z</Menubar.Shortcut>
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item
|
<Menubar.Item on:click={removeSelectedFiles} disabled={$selectedFiles.size == 0}>
|
||||||
><Trash2 size="16" class="mr-1" /> Delete <Menubar.Shortcut>⌘⌫</Menubar.Shortcut
|
<Trash2 size="16" class="mr-1" /> Delete <Menubar.Shortcut>⌘⌫</Menubar.Shortcut
|
||||||
></Menubar.Item
|
></Menubar.Item
|
||||||
>
|
>
|
||||||
<Menubar.Item class="text-destructive data-[highlighted]:text-destructive"
|
<Menubar.Item
|
||||||
><Trash2 size="16" class="mr-1" /> Delete all<Menubar.Shortcut>⇧⌘⌫</Menubar.Shortcut
|
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.Item
|
||||||
>
|
>
|
||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
@@ -134,6 +147,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<style lang="postcss">
|
||||||
div :global(button) {
|
div :global(button) {
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-accent;
|
||||||
|
@@ -1,11 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
|
||||||
|
import { selectedFiles } from '$lib/stores';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root openDelay="300">
|
<Tooltip.Root openDelay={300}>
|
||||||
<Tooltip.Trigger asChild let:builder>
|
<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" />
|
<slot name="icon" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
|
@@ -9,11 +9,13 @@
|
|||||||
|
|
||||||
$: if ($map && container) {
|
$: if ($map && container) {
|
||||||
$map.on('load', () => {
|
$map.on('load', () => {
|
||||||
if (position.includes('right')) container.classList.add('float-right');
|
if ($map && container) {
|
||||||
else container.classList.add('float-left');
|
if (position.includes('right')) container.classList.add('float-right');
|
||||||
container.classList.remove('hidden');
|
else container.classList.add('float-left');
|
||||||
let control = new CustomControl(container);
|
container.classList.remove('hidden');
|
||||||
$map.addControl(control, position);
|
let control = new CustomControl(container);
|
||||||
|
$map.addControl(control, position);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
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 map = writable<mapboxgl.Map | null>(null);
|
||||||
export const files = writable<GPXFile[]>([]);
|
export const files = writable<GPXFile[]>([]);
|
||||||
|
export const selectedFiles = writable<Set<GPXFile>>(new Set());
|
||||||
|
|
||||||
export function triggerFileInput() {
|
export function triggerFileInput() {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
@@ -36,7 +37,85 @@ export function loadFile(file: File) {
|
|||||||
gpx.metadata['name'] = file.name.split('.').slice(0, -1).join('.');
|
gpx.metadata['name'] = file.name.split('.').slice(0, -1).join('.');
|
||||||
}
|
}
|
||||||
files.update($files => [...$files, gpx]);
|
files.update($files => [...$files, gpx]);
|
||||||
|
selectFile(gpx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
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 Menu from '$lib/components/Menu.svelte';
|
||||||
import Toolbar from '$lib/components/Toolbar.svelte';
|
import Toolbar from '$lib/components/Toolbar.svelte';
|
||||||
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
||||||
|
|
||||||
import { triggerFileInput } from '$lib/stores';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-screen h-screen">
|
<div class="flex flex-col w-screen h-screen">
|
||||||
@@ -21,12 +19,3 @@
|
|||||||
<FileList />
|
<FileList />
|
||||||
</div>
|
</div>
|
||||||
</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