Files
gpx.studio/website/src/routes/[[language]]/app/+page.svelte
T

261 lines
10 KiB
Svelte
Raw Normal View History

2024-07-08 15:46:00 +02:00
<script lang="ts">
2025-10-17 23:54:45 +02:00
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
2025-10-20 20:17:47 +02:00
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
2025-10-26 12:12:23 +01:00
import FileList from '$lib/components/file-list/FileList.svelte';
2026-10-17 14:42:49 +02:00
import Explorer from '$lib/components/explorer/Explorer.svelte';
2025-10-18 00:31:14 +02:00
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
2025-06-21 21:07:36 +02:00
import Map from '$lib/components/map/Map.svelte';
2025-10-05 19:34:05 +02:00
import Menu from '$lib/components/Menu.svelte';
2025-10-18 16:10:08 +02:00
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
2025-06-21 21:07:36 +02:00
import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
2025-10-23 19:07:32 +02:00
import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
2025-02-02 11:17:22 +01:00
import Resizer from '$lib/components/Resizer.svelte';
import { Toaster } from '$lib/components/ui/sonner';
2026-10-11 10:48:43 +02:00
import * as AlertDialog from '$lib/components/ui/alert-dialog';
2026-10-11 11:18:36 +02:00
import { i18n } from '$lib/i18n/i18n.svelte';
import { settings } from '$lib/state/settings';
import { loadFiles } from '$lib/actions/file-actions';
2026-10-08 18:34:19 +02:00
import { onMount } from 'svelte';
2025-10-05 19:34:05 +02:00
import { page } from '$app/state';
2026-10-17 19:36:57 +02:00
import { goto } from '$app/navigation';
import { get } from 'svelte/store';
import { getURLForLanguage } from '$lib/utils';
import { inDesktopApp } from '$lib/engine/backend';
2026-10-17 19:57:50 +02:00
import { openLinksInBrowser } from '$lib/desktop/links';
2026-10-11 11:18:36 +02:00
import { hoveredPoint, slicedStatistics } from '$lib/state/selection-statistics';
2026-10-04 11:52:46 +02:00
import { engine } from '$lib/engine';
2026-10-14 19:15:02 +02:00
import { boundsManager } from '$lib/state/bounds';
2025-11-09 18:03:27 +01:00
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
2024-07-11 18:42:49 +02:00
2026-10-11 10:48:43 +02:00
const outdated = engine.outdated;
2026-10-17 14:42:49 +02:00
const workspace = engine.workspace;
2026-10-11 10:48:43 +02:00
2025-02-02 11:17:22 +01:00
const {
treeFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
2026-10-17 14:42:49 +02:00
explorerPanelSize,
2025-02-02 11:17:22 +01:00
additionalDatasets,
elevationFill,
} = settings;
2024-07-11 18:42:49 +02:00
2026-02-17 22:24:14 +01:00
let bottomPanelWidth: number | undefined = $state();
2026-10-17 14:42:49 +02:00
// the file list and the explorer share the right panel, half of it each until it is resized
let rightPanelHeight = $state(0);
let explorerHeight = $derived(
$treeFileView
? Math.min(
Math.max($explorerPanelSize > 0 ? $explorerPanelSize : rightPanelHeight / 2, 60),
Math.max(rightPanelHeight - 80, 60)
)
: rightPanelHeight
);
2026-02-17 22:24:14 +01:00
let bottomPanelOrientation = $derived(
bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical'
);
2026-10-17 19:36:57 +02:00
// the language of the page, which is in its address (none for English)
let language = $derived(page.params.language ?? 'en');
// whether the language that was chosen last was gone back to (the desktop app)
let languageRestored = $state(false);
$effect(() => {
if (languageRestored) {
settings.language.set(language);
}
});
onMount(async () => {
2026-10-14 19:15:02 +02:00
// files that the system opens with the app while it runs
engine.onFilesOpened((ids) => boundsManager.fitBoundsOnLoad(ids));
2026-10-08 18:34:19 +02:00
settings.connect(await engine.openStorage());
2026-10-17 19:36:57 +02:00
if (inDesktopApp()) {
2026-10-17 19:57:50 +02:00
openLinksInBrowser();
2026-10-17 19:36:57 +02:00
// the window of the desktop app always opens the same page, in English: it goes to the
// language that was chosen last
const saved = get(settings.language);
if (saved !== undefined && saved !== language) {
await goto(
`${getURLForLanguage(saved, page.url.pathname)}${page.url.search}${page.url.hash}`,
{ replaceState: true }
);
}
languageRestored = true;
}
2026-10-08 18:34:19 +02:00
let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
2026-10-08 18:34:19 +02:00
if (urls.length > 0) {
let downloads: Promise<File | null>[] = [];
urls.forEach((url) => {
downloads.push(
fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? ''))
);
});
2025-11-09 18:03:27 +01:00
2026-10-08 18:34:19 +02:00
Promise.all(downloads).then((files) => {
loadFiles(files.filter((file) => file !== null));
});
}
2025-10-05 19:34:05 +02:00
});
2024-07-08 15:46:00 +02:00
</script>
2025-10-19 16:14:05 +02:00
<div class="fixed mt-[100%] -z-10 text-transparent">
2025-06-21 21:07:36 +02:00
<h1>{i18n._('metadata.home_title')} — {i18n._('metadata.app_title')}</h1>
<p>{i18n._('metadata.description')}</p>
<h2>{i18n._('toolbar.routing.tooltip')}</h2>
<p>{i18n._('toolbar.routing.help_no_file')}</p>
<p>{i18n._('toolbar.routing.help')}</p>
<h3>{i18n._('toolbar.routing.reverse.button')}</h3>
<p>{i18n._('toolbar.routing.reverse.tooltip')}</p>
<h3>{i18n._('toolbar.routing.route_back_to_start.button')}</h3>
<p>{i18n._('toolbar.routing.route_back_to_start.tooltip')}</p>
<h3>{i18n._('toolbar.routing.round_trip.button')}</h3>
<p>{i18n._('toolbar.routing.round_trip.tooltip')}</p>
<h3>{i18n._('toolbar.routing.start_loop_here')}</h3>
<h2>{i18n._('toolbar.scissors.tooltip')}</h2>
<p>{i18n._('toolbar.scissors.help')}</p>
<h2>{i18n._('toolbar.time.tooltip')}</h2>
<p>{i18n._('toolbar.time.help')}</p>
<h2>{i18n._('toolbar.merge.tooltip')}</h2>
<h3>{i18n._('toolbar.merge.merge_traces')}</h3>
<p>{i18n._('toolbar.merge.help_merge_traces')}</p>
<h3>{i18n._('toolbar.merge.merge_contents')}</h3>
<p>{i18n._('toolbar.merge.help_merge_contents')}</p>
<h2>{i18n._('toolbar.elevation.button')}</h2>
<p>{i18n._('toolbar.elevation.help')}</p>
<h2>{i18n._('toolbar.waypoint.tooltip')}</h2>
<p>{i18n._('toolbar.waypoint.help')}</p>
<h2>{i18n._('toolbar.reduce.tooltip')}</h2>
<p>{i18n._('toolbar.reduce.help')}</p>
<h2>{i18n._('toolbar.clean.tooltip')}</h2>
<p>{i18n._('toolbar.clean.help')}</p>
<h2>
{i18n._('gpx.files')}, {i18n._('gpx.tracks')}, {i18n._('gpx.segments')}, {i18n._(
'gpx.waypoints'
)}
</h2>
2024-10-10 11:39:14 +02:00
</div>
2025-11-16 13:22:26 +01:00
<div class="fixed flex flex-row w-dvw h-dvh">
2025-02-02 11:17:22 +01:00
<div class="flex flex-col grow h-full min-w-0">
<div class="grow relative">
2025-10-05 19:34:05 +02:00
<Menu />
2025-02-02 11:17:22 +01:00
<div
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
>
2025-10-18 16:10:08 +02:00
<Toolbar />
2025-02-02 11:17:22 +01:00
</div>
2025-10-17 23:54:45 +02:00
<Map class="h-full {$treeFileView ? '' : 'horizontal'}" />
2025-02-02 11:17:22 +01:00
<StreetViewControl />
<LayerControl />
2025-10-17 23:54:45 +02:00
<GPXLayers />
2025-10-23 19:07:32 +02:00
<CoordinatesPopup />
2025-02-02 11:17:22 +01:00
<Toaster richColors />
2025-10-17 23:54:45 +02:00
{#if !$treeFileView}
2026-10-17 18:33:50 +02:00
<div class="h-10 w-full pointer-events-none absolute bottom-0 left-0 z-30">
2025-10-26 12:12:23 +01:00
<FileList orientation="horizontal" />
2025-02-02 11:17:22 +01:00
</div>
2025-10-17 23:54:45 +02:00
{/if}
2025-02-02 11:17:22 +01:00
</div>
2025-10-17 23:54:45 +02:00
{#if $elevationProfile}
2025-02-02 11:17:22 +01:00
<Resizer
orientation="row"
2025-10-17 23:54:45 +02:00
bind:after={$bottomPanelSize}
2025-02-02 11:17:22 +01:00
minAfter={100}
maxAfter={300}
/>
{/if}
<div
2026-02-17 22:24:14 +01:00
bind:offsetWidth={bottomPanelWidth}
class="flex {bottomPanelOrientation == 'vertical'
? 'flex-col'
2026-03-27 21:32:33 +01:00
: 'flex-row py-2'} gap-1 px-4"
2025-10-17 23:54:45 +02:00
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
2025-02-02 11:17:22 +01:00
>
2025-10-18 00:31:14 +02:00
<GPXStatistics
2026-10-04 11:52:46 +02:00
statistics={engine.statistics}
{slicedStatistics}
2026-02-17 22:24:14 +01:00
orientation={bottomPanelOrientation == 'horizontal' ? 'vertical' : 'horizontal'}
2025-10-18 00:31:14 +02:00
/>
2025-10-18 20:12:19 +02:00
{#if $elevationProfile}
2025-02-02 11:17:22 +01:00
<ElevationProfile
2026-10-04 11:52:46 +02:00
statistics={engine.statistics}
{slicedStatistics}
2026-02-01 17:18:17 +01:00
{hoveredPoint}
2025-10-22 19:05:20 +02:00
{additionalDatasets}
{elevationFill}
2025-02-02 11:17:22 +01:00
/>
2025-10-18 20:12:19 +02:00
{/if}
2025-02-02 11:17:22 +01:00
</div>
</div>
2026-10-17 14:42:49 +02:00
{#if $treeFileView || $workspace?.folder}
2025-10-17 23:54:45 +02:00
<Resizer orientation="col" bind:after={$rightPanelSize} minAfter={100} maxAfter={400} />
2026-10-17 14:42:49 +02:00
<div
bind:clientHeight={rightPanelHeight}
class="flex flex-col h-full min-h-0 shrink-0"
style="width: {$rightPanelSize}px"
>
{#if $treeFileView}
{#if $workspace}
<div class="px-2 py-1 text-sm font-semibold shrink-0">
{i18n._('menu.workspace')}
</div>
{/if}
<!-- a row, as the file list was: its scroll area is as high as this, whatever its content -->
<div class="grow min-h-0 flex flex-row">
<FileList
orientation="vertical"
recursive={true}
style="width: {$rightPanelSize}px"
/>
</div>
{/if}
<!-- the open folder (the desktop app) under the files that are open -->
{#if $workspace?.folder}
{#if $treeFileView}
<Resizer
orientation="row"
bind:after={() => explorerHeight, (height) => ($explorerPanelSize = height)}
minAfter={60}
maxAfter={Math.max(rightPanelHeight - 80, 60)}
/>
{/if}
<div class="flex flex-col min-h-0 shrink-0" style="height: {explorerHeight}px">
<Explorer />
</div>
{/if}
</div>
2025-02-02 11:17:22 +01:00
{/if}
2024-07-11 18:42:49 +02:00
</div>
2026-10-11 10:48:43 +02:00
<AlertDialog.Root open={$outdated}>
<AlertDialog.Content escapeKeydownBehavior="ignore" interactOutsideBehavior="ignore">
<AlertDialog.Header>
<AlertDialog.Title>{i18n._('outdated.title')}</AlertDialog.Title>
<AlertDialog.Description>{i18n._('outdated.description')}</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Action onclick={() => location.reload()}>
{i18n._('outdated.reload')}
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
2024-07-11 18:42:49 +02:00
<style lang="postcss">
2025-06-21 21:07:36 +02:00
@reference "tailwindcss";
2025-02-02 11:17:22 +01:00
div :global(.toaster.group) {
@apply absolute;
@apply right-2;
--offset: 50px !important;
}
2024-07-11 18:42:49 +02:00
</style>