adds upload dialog and duplicates
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
import Toast from "$lib/components/base/toast.svelte";
|
||||
import Footer from "$lib/components/footer.svelte";
|
||||
import NavBar from "$lib/components/nav_bar.svelte";
|
||||
import PageLoadingBar from "$lib/components/page_loading_bar.svelte";
|
||||
import UploadDialog from "$lib/components/settings/upload_dialog.svelte";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { isRouteProtected } from "$lib/util/authorization_util";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
@@ -13,7 +15,6 @@
|
||||
import "../css/app.css";
|
||||
import "../css/components.css";
|
||||
import "../css/theme.css";
|
||||
import PageLoadingBar from "$lib/components/page_loading_bar.svelte";
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
}
|
||||
@@ -71,8 +72,9 @@
|
||||
may cause errors.
|
||||
</p>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="btn-icon self-end" onclick={() => (showWarning = false)}
|
||||
aria-label="Close"
|
||||
class="btn-icon self-end"
|
||||
onclick={() => (showWarning = false)}
|
||||
><i class="fa fa-close"></i></button
|
||||
>
|
||||
</div>
|
||||
@@ -81,6 +83,7 @@
|
||||
<NavBar></NavBar>
|
||||
<PageLoadingBar class="text-content"></PageLoadingBar>
|
||||
<Toast></Toast>
|
||||
<UploadDialog></UploadDialog>
|
||||
{@render children?.()}
|
||||
|
||||
<Footer></Footer>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
import { haversineDistance } from "$lib/models/gpx/utils";
|
||||
import { SummitLog } from "$lib/models/summit_log";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { trails_create } from "$lib/stores/trail_store";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { fetchGPX, trails_create } from "$lib/stores/trail_store";
|
||||
import { handleError } from "$lib/util/api_util";
|
||||
import { fromFile, gpx2trail } from "$lib/util/gpx_util";
|
||||
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||
@@ -15,29 +18,45 @@ export async function PUT(event: RequestEvent) {
|
||||
if (!gpxData.length) {
|
||||
throw new ClientResponseError({ status: 400, response: { message: "Empty file" } })
|
||||
}
|
||||
let trail: Trail;
|
||||
let parseResult: { trail: Trail, gpx: GPX };
|
||||
try {
|
||||
trail = (await gpx2trail(gpxData, data.get("name") as string | undefined)).trail;
|
||||
|
||||
const log = new SummitLog(trail.date as string, {
|
||||
distance: trail.distance,
|
||||
elevation_gain: trail.elevation_gain,
|
||||
elevation_loss: trail.elevation_loss,
|
||||
duration: trail.duration ? trail.duration * 60 : undefined,
|
||||
})
|
||||
log.expand!.gpx_data = gpxData;
|
||||
const fileName = (data.get("name") as string | null)?.length ? data.get("name") as string : "file"
|
||||
log._gpx = new File([gpxFile], fileName);
|
||||
|
||||
trail.expand!.summit_logs.push(log);
|
||||
parseResult = (await gpx2trail(gpxData, data.get("name") as string | undefined));
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
throw new ClientResponseError({ status: 400, response: { message: "Invalid file" } })
|
||||
}
|
||||
let trail = parseResult.trail;
|
||||
|
||||
const ignoreDuplicates = data.get("ignoreDuplicates") === "true"
|
||||
if (!ignoreDuplicates) {
|
||||
let duplicate: Trail | null = null;
|
||||
try {
|
||||
duplicate = await findDuplicate(trail)
|
||||
} catch (e: any) {
|
||||
throw new ClientResponseError({ status: 500, response: { message: "Error checking for duplicates" } })
|
||||
}
|
||||
if (duplicate !== null) {
|
||||
throw new ClientResponseError({ status: 400, response: { message: `Duplicate trail`, id: duplicate.id, name: duplicate.name }, })
|
||||
}
|
||||
}
|
||||
|
||||
const log = new SummitLog(trail.date as string, {
|
||||
distance: trail.distance,
|
||||
elevation_gain: trail.elevation_gain,
|
||||
elevation_loss: trail.elevation_loss,
|
||||
duration: trail.duration ? trail.duration * 60 : undefined,
|
||||
})
|
||||
log.expand!.gpx_data = gpxData;
|
||||
const fileName = (data.get("name") as string | null)?.length ? data.get("name") as string : "file"
|
||||
log._gpx = new File([gpxFile], fileName);
|
||||
|
||||
trail.expand!.summit_logs.push(log);
|
||||
|
||||
|
||||
try {
|
||||
trail = await trails_create(trail, [], gpxFile, event.fetch);
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
throw handleError(e)
|
||||
}
|
||||
return json(trail);
|
||||
@@ -45,4 +64,25 @@ export async function PUT(event: RequestEvent) {
|
||||
} catch (e: any) {
|
||||
throw handleError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findDuplicate(t1: Trail) {
|
||||
const trails: Trail[] = await pb.collection('trails').getFullList({ requestKey: null });
|
||||
|
||||
const distanceThreshold = 100;
|
||||
const elevationThreshhold = 50;
|
||||
const lengthThreshhold = 50;
|
||||
|
||||
for (const t2 of trails) {
|
||||
const lengthDifference = Math.abs((t1.distance ?? 0) - (t2.distance ?? 0));
|
||||
const elevationGainDifference = Math.abs((t1.elevation_gain ?? 0) - (t2.elevation_gain ?? 0));
|
||||
const elevationLossDifference = Math.abs((t1.elevation_loss ?? 0) - (t2.elevation_loss ?? 0));
|
||||
const startpointDifference = haversineDistance(t1.lat ?? 0, t1.lon ?? 0, t2.lat ?? 0, t2.lon ?? 0)
|
||||
|
||||
if (lengthDifference < lengthThreshhold && elevationGainDifference < elevationThreshhold && elevationLossDifference < elevationThreshhold && startpointDifference < distanceThreshold) {
|
||||
return t2
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,45 +1,23 @@
|
||||
<script lang="ts">
|
||||
import emptyStateUploadDark from "$lib/assets/svgs/empty_states/empty_state_upload_dark.svg";
|
||||
import emptyStateUploadLight from "$lib/assets/svgs/empty_states/empty_state_upload_light.svg";
|
||||
import Button from "$lib/components/base/button.svelte";
|
||||
import { } from "$lib/components/settings/upload_dialog.svelte";
|
||||
import TrailExportModal from "$lib/components/trail/trail_export_modal.svelte";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import {
|
||||
fetchGPX,
|
||||
trails_index,
|
||||
trails_upload,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { fetchGPX, trails_index, trails_upload } from "$lib/stores/trail_store";
|
||||
import { processUploadQueue, uploadStore, type Upload } from "$lib/stores/upload_store.svelte";
|
||||
import { getFileURL, saveAs } from "$lib/util/file_util";
|
||||
import { trail2gpx } from "$lib/util/gpx_util";
|
||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||
import JSZip from "jszip";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { linear } from "svelte/easing";
|
||||
import { Tween } from "svelte/motion";
|
||||
import emptyStateUploadDark from "$lib/assets/svgs/empty_states/empty_state_upload_dark.svg";
|
||||
import emptyStateUploadLight from "$lib/assets/svgs/empty_states/empty_state_upload_light.svg";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
import { onMount } from "svelte";
|
||||
import JSConfetti from "js-confetti";
|
||||
|
||||
const uploadProgress = new Tween(0, {
|
||||
duration: 300,
|
||||
easing: linear,
|
||||
});
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let exportModal: TrailExportModal;
|
||||
|
||||
let offerUpload: boolean = $state(false);
|
||||
let uploading: boolean = $state(false);
|
||||
|
||||
let jsConfetti: JSConfetti;
|
||||
|
||||
onMount(() => {
|
||||
jsConfetti = new JSConfetti({
|
||||
canvas:
|
||||
(document.getElementById(
|
||||
"confetti-canvas",
|
||||
) as HTMLCanvasElement | null) ?? undefined,
|
||||
});
|
||||
});
|
||||
|
||||
function openFileBrowser() {
|
||||
document.getElementById("file-input")!.click();
|
||||
@@ -56,13 +34,41 @@
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
if (uploading) {
|
||||
return;
|
||||
}
|
||||
offerUpload = false;
|
||||
handleFileSelection(e.dataTransfer?.files);
|
||||
}
|
||||
|
||||
function mockUpload(f: File, onProgess?: (progress: number) => void) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log("here");
|
||||
|
||||
setTimeout(() => {
|
||||
onProgess?.(10);
|
||||
}, 400);
|
||||
setTimeout(() => {
|
||||
onProgess?.(30);
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
onProgess?.(50);
|
||||
}, 800);
|
||||
setTimeout(() => {
|
||||
onProgess?.(70);
|
||||
}, 1200);
|
||||
setTimeout(() => {
|
||||
onProgess?.(99);
|
||||
}, 1300);
|
||||
setTimeout(() => {
|
||||
onProgess?.(100);
|
||||
const random = Math.random();
|
||||
if (random < 0.5) {
|
||||
reject(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFileSelection(files?: FileList | null) {
|
||||
if (!files) {
|
||||
files = (document.getElementById("file-input") as HTMLInputElement)
|
||||
@@ -73,42 +79,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let errorsThrown = 0;
|
||||
uploading = true;
|
||||
let progress = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
try {
|
||||
uploadProgress.set((progress += 100 / (files.length * 2)));
|
||||
await trails_upload(file);
|
||||
} catch (e) {
|
||||
errorsThrown += 1;
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: `Error uploading file: ${file.name}`,
|
||||
});
|
||||
} finally {
|
||||
uploadProgress.set((progress += 100 / (files.length * 2)));
|
||||
}
|
||||
const u: Upload = {
|
||||
file: files[i],
|
||||
progress: 0,
|
||||
status: "enqueued",
|
||||
function: trails_upload
|
||||
};
|
||||
uploadStore.enqueuedUploads.push(u);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uploadProgress.set(0, { duration: 0 });
|
||||
uploading = false;
|
||||
jsConfetti.addConfetti({
|
||||
confettiRadius: 4,
|
||||
emojis: ["🍃", "🍁"],
|
||||
});
|
||||
}, 500);
|
||||
|
||||
if (errorsThrown == 0) {
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `${files.length} ${$_("trail", { values: { n: files.length } })} ${$_("uploaded")}`,
|
||||
});
|
||||
}
|
||||
await processUploadQueue();
|
||||
}
|
||||
|
||||
async function exportTrails(exportSettings: {
|
||||
@@ -182,27 +163,18 @@
|
||||
<svelte:head>
|
||||
<title>{$_("settings")} | wanderer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h3 class="text-2xl font-semibold">{$_("import")}</h3>
|
||||
<hr class="mt-4 mb-6 border-input-border" />
|
||||
<button
|
||||
class="drop-area relative h-64 w-full p-4 {uploading
|
||||
? ''
|
||||
: 'border border-content border-dashed'} rounded-xl flex items-center justify-center text-gray-500 bg-background cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
class:bg-menu-item-background-hover={uploading}
|
||||
class:border-2={offerUpload && !uploading}
|
||||
style="--progress: {uploadProgress.current}%"
|
||||
class="drop-area relative h-64 w-full p-4 border border-content border-dashed rounded-xl flex items-center justify-center text-gray-500 bg-background cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
class:border-2={offerUpload}
|
||||
onclick={openFileBrowser}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
>
|
||||
<canvas
|
||||
id="confetti-canvas"
|
||||
class="absolute"
|
||||
style="width: 100%; height: 200%"
|
||||
>
|
||||
</canvas>
|
||||
<div class="">
|
||||
<img
|
||||
class="rounded-full aspect-square mx-auto"
|
||||
@@ -230,27 +202,5 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<TrailExportModal
|
||||
bind:this={exportModal}
|
||||
onexport={exportTrails}
|
||||
<TrailExportModal bind:this={exportModal} onexport={exportTrails}
|
||||
></TrailExportModal>
|
||||
|
||||
<style>
|
||||
.drop-area::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
background-color: transparent;
|
||||
z-index: -100;
|
||||
border-radius: 0.75rem;
|
||||
background-image: conic-gradient(
|
||||
rgba(var(--content)),
|
||||
rgba(var(--content)) var(--progress),
|
||||
transparent var(--progress)
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -792,7 +792,7 @@
|
||||
|
||||
function updateTrailWithRouteData() {
|
||||
overwriteGPX = true;
|
||||
const totals = route.getTotals();
|
||||
const totals = route.features;
|
||||
$formData.distance = totals.distance;
|
||||
$formData.duration = totals.duration;
|
||||
$formData.elevation_gain = totals.elevationGain;
|
||||
|
||||
Reference in New Issue
Block a user