mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-30 23:30:04 +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