178 lines
5.0 KiB
Svelte
178 lines
5.0 KiB
Svelte
<script lang="ts">
|
|
import { show_toast } from "$lib/stores/toast_store.svelte";
|
|
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
|
|
import { _ } from "svelte-i18n";
|
|
import PhotoCard from "../photo_card.svelte";
|
|
|
|
interface Props {
|
|
id: string;
|
|
photos: string[];
|
|
photoFiles: File[] | undefined;
|
|
parent: { [key: string]: any };
|
|
thumbnail?: number;
|
|
showThumbnailControls?: boolean;
|
|
showExifControls?: boolean;
|
|
maxSizeBytes?: number;
|
|
onexif?: (src: string) => void;
|
|
}
|
|
|
|
let {
|
|
id,
|
|
photos = $bindable(),
|
|
photoFiles = $bindable(),
|
|
parent,
|
|
thumbnail = $bindable(0),
|
|
showThumbnailControls = true,
|
|
showExifControls = false,
|
|
maxSizeBytes = 20971520,
|
|
onexif,
|
|
}: Props = $props();
|
|
|
|
let photoPreviews: string[] = $state([]);
|
|
|
|
$effect(() => fetchPhotos(photoFiles ?? []));
|
|
|
|
function fetchPhotos(photos: File[]) {
|
|
Promise.all(
|
|
photos.map(async (f) => {
|
|
return await readAsDataURLAsync(f);
|
|
}),
|
|
).then((v) => {
|
|
photoPreviews = v;
|
|
});
|
|
}
|
|
|
|
let offerUpload: boolean = $state(false);
|
|
|
|
function handlePhotoDragOver(e: DragEvent) {
|
|
e.preventDefault();
|
|
offerUpload = true;
|
|
}
|
|
|
|
function handlePhotoDragLeave() {
|
|
offerUpload = false;
|
|
}
|
|
|
|
function handlePhotoDrop(e: DragEvent) {
|
|
e.preventDefault();
|
|
|
|
offerUpload = false;
|
|
handlePhotoSelection(e.dataTransfer?.files);
|
|
}
|
|
|
|
function openPhotoBrowser() {
|
|
document.getElementById(`${id}-photo-input`)!.click();
|
|
}
|
|
|
|
async function handlePhotoSelection(files?: FileList | null) {
|
|
if (!files) {
|
|
files = (
|
|
document.getElementById(`${id}-photo-input`) as HTMLInputElement
|
|
).files;
|
|
}
|
|
|
|
if (!files) {
|
|
return;
|
|
}
|
|
|
|
for (const file of files) {
|
|
if (file.size > maxSizeBytes) {
|
|
show_toast({
|
|
type: "error",
|
|
text: $_("file-too-big", {
|
|
values: { file: file.name, size: "20 MB" },
|
|
}),
|
|
icon: "close",
|
|
});
|
|
continue;
|
|
}
|
|
let photoFile = file;
|
|
if (
|
|
!file.type.startsWith("image") &&
|
|
!["video/mp4", "video/ogg", "video/webm"].includes(file.type)
|
|
) {
|
|
continue;
|
|
} else if (file.type === "image/heic") {
|
|
const heic2any = (await import("heic2any")).default;
|
|
photoFile = new File(
|
|
[
|
|
(await heic2any({
|
|
blob: file,
|
|
toType: "image/jpeg",
|
|
})) as Blob,
|
|
],
|
|
file.name,
|
|
);
|
|
}
|
|
if (!photoFiles) {
|
|
photoFiles = [];
|
|
}
|
|
photoFiles = [...photoFiles, photoFile];
|
|
}
|
|
}
|
|
|
|
function makePhotoThumbnail(index: number) {
|
|
thumbnail = index;
|
|
}
|
|
|
|
function handlePhotoDelete(index: number) {
|
|
if (thumbnail == index) {
|
|
thumbnail = 0;
|
|
}
|
|
|
|
if (index >= photos.length) {
|
|
if (!photoFiles) {
|
|
photoFiles = [];
|
|
}
|
|
const adjustedIndex = index - photos.length;
|
|
photoFiles.splice(adjustedIndex, 1);
|
|
photoPreviews.splice(adjustedIndex, 1);
|
|
|
|
photoPreviews = [...photoPreviews];
|
|
} else {
|
|
photos.splice(index, 1);
|
|
photos = [...photos];
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="flex gap-x-4 max-w-full shrink-0 rounded-xl {offerUpload
|
|
? 'outline-dashed outline-input-border'
|
|
: ''}"
|
|
role="dialog"
|
|
ondragover={handlePhotoDragOver}
|
|
ondragleave={handlePhotoDragLeave}
|
|
ondrop={handlePhotoDrop}
|
|
>
|
|
<button
|
|
aria-label="Open photo browser"
|
|
class="btn-secondary h-32 w-32 shrink-0 grow-0 basis-auto"
|
|
type="button"
|
|
onclick={openPhotoBrowser}><i class="fa fa-plus"></i></button
|
|
>
|
|
<input
|
|
type="file"
|
|
id="{id}-photo-input"
|
|
accept="image/*,video/mp4"
|
|
multiple={true}
|
|
style="display: none;"
|
|
onchange={() => handlePhotoSelection()}
|
|
/>
|
|
<div class="flex overflow-x-auto gap-x-3 w-full">
|
|
{#each (photos ?? []).concat(photoPreviews) as photo, i}
|
|
<div class="shrink-0 grow-0 basis-auto">
|
|
<PhotoCard
|
|
src={i >= photos.length ? photo : getFileURL(parent, photo)}
|
|
ondelete={() => handlePhotoDelete(i)}
|
|
isThumbnail={thumbnail === i}
|
|
onthumbnail={() => makePhotoThumbnail(i)}
|
|
{onexif}
|
|
{showThumbnailControls}
|
|
{showExifControls}
|
|
></PhotoCard>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|