Adds import from URL

This commit is contained in:
Christian Beutel
2025-02-01 20:36:39 +01:00
parent 0cf17cbd86
commit 3ab591177a
17 changed files with 208 additions and 41 deletions

View File

@@ -1,9 +1,7 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { MouseEventHandler } from "svelte/elements";
import { createBubbler } from "svelte/legacy";
const bubble = createBubbler();
interface Props {
type?: "button" | "submit" | "reset" | null | undefined;
icon?: string;
@@ -14,6 +12,7 @@
loading?: boolean;
disabled?: boolean;
tooltip?: string | undefined;
form?: string;
id?: string | undefined;
children?: Snippet;
onclick?: MouseEventHandler<HTMLButtonElement>;
@@ -30,6 +29,7 @@
disabled = false,
tooltip = undefined,
id = undefined,
form,
children,
onclick = undefined,
}: Props = $props();
@@ -44,6 +44,7 @@
disabled={disabled || loading}
class:tooltip={(tooltip?.length ?? 0) > 0}
data-title={tooltip}
{form}
{onclick}
{type}
{id}

View File

@@ -14,6 +14,7 @@
import { browser } from "$app/environment";
import { page } from "$app/state";
import { Tween } from "svelte/motion";
import UrlImportModal from "./settings/url_import_modal.svelte";
let navBarItems = [
{ text: "Home", value: "/" },
@@ -28,6 +29,11 @@
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
];
const importDropdownItems = [
{ text: $_("from-file"), value: "file", icon: "file-import" },
{ text: $_("from-url"), value: "url", icon: "server" },
];
const indicatorPosition = new Tween(0, {
duration: 300,
easing: cubicOut,
@@ -45,6 +51,8 @@
let drawerOpen: boolean = $state(false);
let urlImportModal: UrlImportModal;
afterNavigate((e) => {
const routeId = e.to?.route.id;
const navBarLinks = document.getElementById("nav-bar-links");
@@ -92,6 +100,14 @@
}
}
function handleImportDropdownClick(item: { text: string; value: any }) {
if (item.value == "file") {
goto(`/settings/export`);
} else if (item.value == "url") {
urlImportModal.openModal();
}
}
let user = $derived(browser ? $currentUser : pb.authStore.model);
</script>
@@ -132,8 +148,12 @@
/>
</a>
<a href="/profile/{user.id}" style="width: calc(100% - 104px)">
<p class="text-sm overflow-hidden text-ellipsis">{user.username}</p>
<p class="text-sm text-gray-500 overflow-hidden text-ellipsis">
<p class="text-sm overflow-hidden text-ellipsis">
{user.username}
</p>
<p
class="text-sm text-gray-500 overflow-hidden text-ellipsis"
>
{user.email}
</p>
</a>
@@ -182,9 +202,26 @@
: 'moon'}"
onclick={() => toggleTheme()}
></button>
<a class="btn-primary btn-large" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
<div class="flex">
<a
class="btn-primary btn-large !rounded-r-none focus:ring-0"
href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
<Dropdown
items={importDropdownItems}
onchange={(item) => handleImportDropdownClick(item)}
>
{#snippet children({ toggleMenu: openDropdown })}
<button
onclick={openDropdown}
class="bg-primary rounded-r-lg text-white min-h-12 hover:bg-primary-hover px-3"
aria-label="Open trail create dropdown"
><i class="fa fa-caret-down"></i></button
>
{/snippet}
</Dropdown>
</div>
{#if page.data.notifications}
<NotificationDropdown></NotificationDropdown>
{/if}
@@ -227,3 +264,5 @@
onclick={() => (drawerOpen = !drawerOpen)}
></button>
</nav>
<UrlImportModal bind:this={urlImportModal}></UrlImportModal>

View File

@@ -96,7 +96,7 @@
</p>
</div>
<div class="space-x-2">
<button aria-label="Dismiss all" onclick={dismissAllCompleted}
<button title={$_('clear-all')} aria-label="Dismiss all" onclick={dismissAllCompleted}
><i class="fa fa-ban"></i></button
>
<button
@@ -134,7 +134,7 @@
></i>
{/if}
</div>
<p class="text-xs basis-full min-w-0 break-all">
<p class="text-xs basis-full min-w-0 break-all mr-2">
{u.file.name}
</p>
{#if u.status == "error" || u.status == "cancelled"}

View File

@@ -0,0 +1,97 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import { trails_upload } from "$lib/stores/trail_store";
import {
processUploadQueue,
uploadStore,
type Upload,
} from "$lib/stores/upload_store.svelte";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
import { z } from "zod";
import TextField from "../base/text_field.svelte";
import Button from "../base/button.svelte";
let modal: Modal;
let loading: boolean = false;
export function openModal() {
setFields("url", "")
setErrors("url", []);
loading = false;
modal.openModal();
}
const { form, errors, setFields, setErrors } = createForm<{
url: string;
}>({
initialValues: { url: "" },
extend: validator({
schema: z.object({
url: z.string().min(1, "required").url("not-a-valid-url"),
}),
}),
onSubmit: async (form) => {
try {
loading = true;
const r = await fetch("/api/v1/trail/download", {
method: "POST",
body: JSON.stringify({ url: form.url }),
});
if (!r.ok) {
throw new Error(`Failed to fetch file: ${r.statusText}`);
}
const blob = await r.blob();
const fileName =
form.url.split("/").pop() ||
"url-import-file-" + new Date().getTime();
const file = new File([blob], fileName, { type: blob.type });
const u: Upload = {
file: file,
progress: 0,
status: "enqueued",
function: trails_upload,
};
uploadStore.enqueuedUploads.push(u);
await processUploadQueue();
loading = false;
modal.closeModal();
} catch (error) {
console.error("Error:", error);
}
},
});
</script>
<Modal
id="url-import-modal"
size="max-w-xl"
title={$_("from-url")}
bind:this={modal}
>
{#snippet content()}
<form id="url-import-form" use:form>
<TextField
name="url"
label="URL"
placeholder="https://..."
error={$errors.url}
></TextField>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button class="btn-secondary" onclick={() => modal.closeModal()}
>{$_("cancel")}</button
>
<Button primary type="submit" form="url-import-form" {loading}>{$_("import")}</Button>
</div>
{/snippet}</Modal
>

View File

@@ -157,7 +157,7 @@
</script>
<div
class="trail-info-panel mx-auto {mode == 'list'
class="trail-info-panel overflow-x-hidden mx-auto {mode == 'list'
? ''
: 'border border-input-border rounded-3xl'} h-full"
class:overflow-y-scroll={mode !== "overview"}

View File

@@ -38,6 +38,7 @@
"change-password": "Passwort ändern",
"changelog": "Änderungshistorie",
"chinese": "Chinesisch (vereinfacht)",
"clear-all": "",
"close": "Schließen",
"comment": "{n, plural, =1 {Kommentar} other {Kommentare}}",
"completed": "Abgeschlossen",
@@ -121,6 +122,8 @@
"following": "Folgt",
"forgot-your-password": "Passwort vergessen?",
"french": "Französisch",
"from-file": "",
"from-url": "",
"german": "Deutsch",
"get-position-from-exif": "Koordinaten aus EXIF Daten",
"grid": "Gitter",
@@ -190,6 +193,7 @@
"no-routes-added": "Keine Routen hinzugefügt",
"no-waypoints-yet": "Noch keine Wegpunkte",
"not-a-valid-email-address": "Keine gültige Email-Adresse",
"not-a-valid-url": "",
"not-completed": "Nicht abgeschlossen",
"notification-list-create": "{user} hat eine neue Liste erstellt",
"notification-list-share": "{user} hat eine Liste mit Dir geteilt",

View File

@@ -38,6 +38,7 @@
"change-password": "Change password",
"changelog": "Changelog",
"chinese": "Chinese (simplified)",
"clear-all": "Clear all",
"close": "Close",
"comment": "{n, plural, =1 {Comment} other {Comments}}",
"completed": "Completed",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "Forgot your password?",
"french": "French",
"from-file": "From file",
"from-url": "From URL",
"german": "German",
"get-position-from-exif": "Get coordinates from EXIF data",
"grid": "Grid",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Not a valid email address",
"not-a-valid-url": "Not a valid URL",
"not-completed": "Not completed",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -38,6 +38,7 @@
"change-password": "Cambiar password",
"changelog": "Registro de cambios",
"chinese": "Chinés (simplificado)",
"clear-all": "",
"close": "Cerrar",
"comment": "{n, plural, one {}=1 {Comentario} other {Comentarios}}",
"completed": "Completado",
@@ -121,6 +122,8 @@
"following": "Siguiendo",
"forgot-your-password": "¿Contraseña olvidada?",
"french": "Francés",
"from-file": "",
"from-url": "",
"german": "Alemán",
"get-position-from-exif": "Obtener las coordenadas de los datos EXIF",
"grid": "Cuadricula",
@@ -190,6 +193,7 @@
"no-routes-added": "Ninguna ruta añadida",
"no-waypoints-yet": "No Puntos de Interés todavía",
"not-a-valid-email-address": "Correo electrónico no válido",
"not-a-valid-url": "",
"not-completed": "No completado",
"notification-list-create": "{user} ha creado una nueva lista",
"notification-list-share": "{user} ha compartido una nueva lista contigo",

View File

@@ -38,6 +38,7 @@
"change-password": "Changer de mot de passe",
"changelog": "Journal des modifications",
"chinese": "Chinois (simplifié)",
"clear-all": "",
"close": "Fermer",
"comment": "{n, plural, =1 {Commentaire} other {Commentaires}}",
"completed": "Terminé",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "Mot de passe oublié ?",
"french": "Français",
"from-file": "",
"from-url": "",
"german": "Allemand",
"get-position-from-exif": "Obtenir les coordonnées à partir des données EXIF",
"grid": "Grille",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Adresse email invalide",
"not-a-valid-url": "",
"not-completed": "Non terminés",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -38,6 +38,7 @@
"change-password": "Change password",
"changelog": "Változás napló",
"chinese": "Kínai (egyszerűsítve)",
"clear-all": "",
"close": "Close",
"comment": "{n, plural, =1 {Megjegyzés} other {Megjegyzés}}",
"completed": "Teljesítve",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "Forgot your password?",
"french": "Francia",
"from-file": "",
"from-url": "",
"german": "Német",
"get-position-from-exif": "Get coordinates from EXIF data",
"grid": "Grid",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Érvénytelen e-mail cím",
"not-a-valid-url": "",
"not-completed": "Nem teljesített",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -38,6 +38,7 @@
"change-password": "Cambia password",
"changelog": "Changelog",
"chinese": "Cinese (semplificato)",
"clear-all": "",
"close": "Chiudi",
"comment": "{n, plural, =1 {Commento} other {Commenti}}",
"completed": "Completato",
@@ -121,6 +122,8 @@
"following": "Seguendo",
"forgot-your-password": "Password dimenticata?",
"french": "Francese",
"from-file": "",
"from-url": "",
"german": "Tedesco",
"get-position-from-exif": "Ottieni posizione da dati EXIF",
"grid": "Griglia",
@@ -190,6 +193,7 @@
"no-routes-added": "Nessun percorso aggiunto",
"no-waypoints-yet": "Ancora nessun punto d'interesse",
"not-a-valid-email-address": "Indirizzo email non valido",
"not-a-valid-url": "",
"not-completed": "Non completato",
"notification-list-create": "{user} ha creato una nuova lista",
"notification-list-share": "{user} ha condiviso una lista con te",

View File

@@ -38,6 +38,7 @@
"change-password": "Wachtwoord wijzigen",
"changelog": "Wijzigingslog",
"chinese": "Versimpeld Chinees",
"clear-all": "",
"close": "Sluiten",
"comment": "{n, plural, =1 {Opmerking} other {Opmerkingen}}",
"completed": "Voltooid",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "Forgot your password?",
"french": "Frans",
"from-file": "",
"from-url": "",
"german": "Duits",
"get-position-from-exif": "Get coordinates from EXIF data",
"grid": "Rooster",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Het e-mailadres is ongeldig",
"not-a-valid-url": "",
"not-completed": "Niet voltooid",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -38,6 +38,7 @@
"change-password": "Zmień hasło",
"changelog": "Dziennik zmian",
"chinese": "Chiński (uproszczony)",
"clear-all": "",
"close": "Zamknij",
"comment": "{n, plural, =1 {Komentarz} other {Komentarze}}",
"completed": "Zakończono",
@@ -121,6 +122,8 @@
"following": "Obserwowane",
"forgot-your-password": "Zapomniałeś hasła?",
"french": "Francuski",
"from-file": "",
"from-url": "",
"german": "Niemiecki",
"get-position-from-exif": "Odczytaj współrzędne z danych EXIF",
"grid": "Siatka",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Nieprawidłowy adres email",
"not-a-valid-url": "",
"not-completed": "Nie dokończono",
"notification-list-create": "{user} utworzył(a) nową listę",
"notification-list-share": "{user} udostępnił(a) tobie listę",

View File

@@ -38,6 +38,7 @@
"change-password": "Mudar senha",
"changelog": "Registo de alterações",
"chinese": "Chinês (simplificado)",
"clear-all": "",
"close": "Fechar",
"comment": "{n, plural, =1 {Comentário} other {Comentários}}",
"completed": "Completada",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "Forgot your password?",
"french": "Francês",
"from-file": "",
"from-url": "",
"german": "Alemão",
"get-position-from-exif": "Obter coordenadas dos dados EXIF",
"grid": "Grelha",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "Não um endereço de e-mail válido",
"not-a-valid-url": "",
"not-completed": "Não preenchido",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -38,6 +38,7 @@
"change-password": "更改密码",
"changelog": "变更日志",
"chinese": "中文(简体)",
"clear-all": "",
"close": "关闭",
"comment": "{n, plural, =1 {评论} other {评论}}",
"completed": "已完成",
@@ -121,6 +122,8 @@
"following": "Following",
"forgot-your-password": "忘记密码?",
"french": "法语",
"from-file": "",
"from-url": "",
"german": "德语",
"get-position-from-exif": "从EXIF数据获取坐标",
"grid": "网格",
@@ -190,6 +193,7 @@
"no-routes-added": "No routes added",
"no-waypoints-yet": "No waypoints yet",
"not-a-valid-email-address": "无效电子邮箱地址",
"not-a-valid-url": "",
"not-completed": "未完成",
"notification-list-create": "{user} created a new list",
"notification-list-share": "{user} shared a list with you",

View File

@@ -0,0 +1,21 @@
import { handleError } from "$lib/util/api_util";
import { type RequestEvent } from "@sveltejs/kit";
export async function POST(event: RequestEvent) {
const data = await event.request.json();
try {
const response = await event.fetch(data.url);
const blob = await response.blob();
const contentType = 'determine the content type here';
return new Response(blob, {
headers: {
'Content-Type': contentType,
},
});
} catch (e) {
throw handleError(e)
}
}

View File

@@ -38,37 +38,6 @@
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)