Adds import from URL
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
<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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
97
web/src/lib/components/settings/url_import_modal.svelte
Normal file
97
web/src/lib/components/settings/url_import_modal.svelte
Normal 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
|
||||
>
|
||||
@@ -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"}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ę",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
21
web/src/routes/api/v1/trail/download/+server.ts
Normal file
21
web/src/routes/api/v1/trail/download/+server.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user