From 9da75d3969b2743fa490d83fe3261c7b2e0f21ca Mon Sep 17 00:00:00 2001 From: Sparky <1609870+sparkyfen@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:23:49 -0700 Subject: [PATCH 01/33] feat(admin): accept drag and drop on the VR and sticker upload zones (SONA-216) --- messages/en.json | 1 + messages/ja.json | 1 + src/lib/components/StickerPackForm.svelte | 31 +++++- src/lib/components/VrAvatarForm.svelte | 59 ++++++++-- src/lib/drop-files.test.ts | 128 ++++++++++++++++++++++ src/lib/drop-files.ts | 71 ++++++++++++ tests/e2e/vr-admin-form.spec.ts | 52 +++++++++ 7 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 src/lib/drop-files.test.ts create mode 100644 src/lib/drop-files.ts diff --git a/messages/en.json b/messages/en.json index e2046b2b..0507dd03 100644 --- a/messages/en.json +++ b/messages/en.json @@ -855,6 +855,7 @@ "admin_pack_manual_intro": "Upload sticker images (PNG or WebP) and fill in the pack details. Each sticker can have its own artist and emoji. Files are uploaded to storage before saving.", "admin_pack_edit_heading": "Edit pack: {name}", "admin_pack_upload_partial": "{ok} of {total} uploaded, {failed} failed", + "admin_pack_upload_bad_type": "Only PNG and WebP files can be added to a pack.", "admin_pack_details": "Pack details", "admin_pack_name": "Pack name", "admin_pack_name_placeholder": "My sticker pack", diff --git a/messages/ja.json b/messages/ja.json index f019d1b5..57f19873 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -645,6 +645,7 @@ "admin_pack_manual_intro": "ステッカー画像(PNGまたはWebP)をアップロードし、セットの詳細を入力してください。各ステッカーには個別のアーティストと絵文字を設定できます。ファイルは保存前にストレージへアップロードされます。", "admin_pack_edit_heading": "セットを編集: {name}", "admin_pack_upload_partial": "{total}件中{ok}件をアップロードしました({failed}件は失敗)", + "admin_pack_upload_bad_type": "パックに追加できるのはPNGとWebPのファイルのみです。", "admin_pack_details": "セットの詳細", "admin_pack_name": "セット名", "admin_pack_name_placeholder": "マイステッカーセット", diff --git a/src/lib/components/StickerPackForm.svelte b/src/lib/components/StickerPackForm.svelte index 909a0d24..98c22b8f 100644 --- a/src/lib/components/StickerPackForm.svelte +++ b/src/lib/components/StickerPackForm.svelte @@ -4,6 +4,7 @@ import { X, Plus, ArrowLeft, Check, Loader2, GripVertical, UserPlus } from 'lucide-svelte'; import { toast } from '$lib/toast.svelte'; import { DragReorder } from '$lib/drag-reorder.svelte'; + import { dropFiles } from '$lib/drop-files'; import * as m from '$lib/paraglide/messages'; import StickerMedia from '$lib/components/StickerMedia.svelte'; import NewArtistDialog from '$lib/components/NewArtistDialog.svelte'; @@ -103,6 +104,10 @@ })) ); + // Shared by the file input and the drop attachment, so a drop accepts exactly + // what the picker offers. + const STICKER_ACCEPT = 'image/png,image/webp'; + let uploading = $state(false); let published = $state(pack?.published ?? false); let saving = $state(false); @@ -121,10 +126,19 @@ return url; } - async function uploadStickers(e: Event) { + function uploadStickers(e: Event) { const input = e.currentTarget as HTMLInputElement; const files = [...(input.files ?? [])]; if (!files.length) return; + addStickerFiles(files); + } + + // Shared by the file input and the drop attachment. `rejected` only ever + // arrives from a drop — the input's accept attribute filters the picker, a + // drop skips it — and those files are reported without being uploaded. + async function addStickerFiles(files: File[], rejected: File[] = []) { + if (rejected.length) toast.error(m.admin_pack_upload_bad_type()); + if (!files.length) return; uploading = true; let ok = 0; let failed = 0; @@ -314,10 +328,17 @@
{m.admin_pack_edit_hint()}
{/if} -