diff --git a/server/src/plugins/soundboard/index.ts b/server/src/plugins/soundboard/index.ts index 21e1772..d53238e 100644 --- a/server/src/plugins/soundboard/index.ts +++ b/server/src/plugins/soundboard/index.ts @@ -1223,6 +1223,22 @@ const soundboardPlugin: Plugin = { if (err) { res.status(400).json({ error: err?.message ?? 'Upload fehlgeschlagen' }); return; } const files = (req as any).files as any[] | undefined; if (!files?.length) { res.status(400).json({ error: 'Keine gültigen Dateien' }); return; } + + // If a customName was provided, rename the first file + const customName = req.body?.customName?.trim(); + if (customName && files.length === 1) { + const sanitized = customName.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_'); + const newFilename = sanitized.endsWith('.mp3') || sanitized.endsWith('.wav') + ? sanitized : `${sanitized}.mp3`; + const oldPath = files[0].path; + const newPath = path.join(path.dirname(oldPath), newFilename); + if (!fs.existsSync(newPath)) { + fs.renameSync(oldPath, newPath); + files[0].filename = newFilename; + files[0].path = newPath; + } + } + if (NORMALIZE_ENABLE) { for (const f of files) normalizeToCache(f.path).catch(() => {}); } res.json({ ok: true, files: files.map(f => ({ name: f.filename, size: f.size })) }); }); diff --git a/web/src/plugins/soundboard/SoundboardTab.tsx b/web/src/plugins/soundboard/SoundboardTab.tsx index 380b65a..064951b 100644 --- a/web/src/plugins/soundboard/SoundboardTab.tsx +++ b/web/src/plugins/soundboard/SoundboardTab.tsx @@ -251,6 +251,36 @@ function apiUploadFile( }); } +function apiUploadFileWithName( + file: File, + customName: string, + onProgress: (pct: number) => void, +): Promise { + return new Promise((resolve, reject) => { + const form = new FormData(); + form.append('files', file); + if (customName) form.append('customName', customName); + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${API_BASE}/upload`); + xhr.upload.onprogress = e => { + if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)); + }; + xhr.onload = () => { + if (xhr.status === 200) { + try { + const data = JSON.parse(xhr.responseText); + resolve(data.files?.[0]?.name ?? file.name); + } catch { resolve(file.name); } + } else { + try { reject(new Error(JSON.parse(xhr.responseText).error)); } + catch { reject(new Error(`HTTP ${xhr.status}`)); } + } + }; + xhr.onerror = () => reject(new Error('Netzwerkfehler')); + xhr.send(form); + }); +} + /* ══════════════════════════════════════════════════════════════════ CONSTANTS ══════════════════════════════════════════════════════════════════ */ @@ -365,6 +395,13 @@ export default function SoundboardTab({ data }: SoundboardTabProps) { const dragCounterRef = useRef(0); const uploadDismissRef = useRef>(undefined); + /* ── Drop Rename Modal ── */ + const [dropFiles, setDropFiles] = useState([]); + const [dropIndex, setDropIndex] = useState(0); + const [dropName, setDropName] = useState(''); + const [dropPhase, setDropPhase] = useState<'naming' | 'uploading' | 'done'>('naming'); + const [dropProgress, setDropProgress] = useState(0); + /* ── Voice Stats ── */ const [voiceStats, setVoiceStats] = useState(null); const [showConnModal, setShowConnModal] = useState(false); @@ -687,45 +724,68 @@ export default function SoundboardTab({ data }: SoundboardTabProps) { notify('Admin-Login erforderlich zum Hochladen', 'error'); return; } - if (uploadDismissRef.current) clearTimeout(uploadDismissRef.current); + if (files.length === 0) return; + // Open rename modal for first file + setDropFiles(files); + setDropIndex(0); + const baseName = files[0].name.replace(/\.(mp3|wav)$/i, ''); + setDropName(baseName); + setDropPhase('naming'); + setDropProgress(0); + } - const items: UploadItem[] = files.map(f => ({ - id: Math.random().toString(36).slice(2), - file: f, - status: 'waiting', - progress: 0, - })); - setUploads(items); - setShowUploads(true); + async function handleDropConfirm() { + if (dropFiles.length === 0) return; + const file = dropFiles[dropIndex]; + const name = dropName.trim() || file.name.replace(/\.(mp3|wav)$/i, ''); - const updated = [...items]; - for (let i = 0; i < updated.length; i++) { - updated[i] = { ...updated[i], status: 'uploading' }; - setUploads([...updated]); - try { - const savedName = await apiUploadFile( - updated[i].file, - pct => { - updated[i] = { ...updated[i], progress: pct }; - setUploads([...updated]); - }, - ); - updated[i] = { ...updated[i], status: 'done', progress: 100, savedName }; - } catch (e: any) { - updated[i] = { ...updated[i], status: 'error', error: e?.message ?? 'Fehler' }; - } - setUploads([...updated]); + setDropPhase('uploading'); + setDropProgress(0); + + try { + await apiUploadFileWithName(file, name, pct => setDropProgress(pct)); + setDropPhase('done'); + + // After short delay, move to next file or close + setTimeout(() => { + const nextIdx = dropIndex + 1; + if (nextIdx < dropFiles.length) { + setDropIndex(nextIdx); + const nextBase = dropFiles[nextIdx].name.replace(/\.(mp3|wav)$/i, ''); + setDropName(nextBase); + setDropPhase('naming'); + setDropProgress(0); + } else { + // All done - close modal and refresh + setDropFiles([]); + setDropIndex(0); + setDropName(''); + setRefreshKey(k => k + 1); + void loadAnalytics(); + notify(`${dropFiles.length} Sound${dropFiles.length > 1 ? 's' : ''} hochgeladen`, 'info'); + } + }, 800); + } catch (e: any) { + notify(e?.message || 'Upload fehlgeschlagen', 'error'); + setDropFiles([]); } + } - // Refresh sound list - setRefreshKey(k => k + 1); - void loadAnalytics(); - - // Auto-dismiss after 3.5s - uploadDismissRef.current = setTimeout(() => { - setShowUploads(false); - setUploads([]); - }, 3500); + function handleDropSkip() { + const nextIdx = dropIndex + 1; + if (nextIdx < dropFiles.length) { + setDropIndex(nextIdx); + const nextBase = dropFiles[nextIdx].name.replace(/\.(mp3|wav)$/i, ''); + setDropName(nextBase); + setDropPhase('naming'); + setDropProgress(0); + } else { + setDropFiles([]); + if (dropIndex > 0) { + setRefreshKey(k => k + 1); + void loadAnalytics(); + } + } } async function handleStop() { @@ -1582,6 +1642,89 @@ export default function SoundboardTab({ data }: SoundboardTabProps) { )} + {/* ── Drop Rename Modal ── */} + {dropFiles.length > 0 && ( +
dropPhase === 'naming' && handleDropSkip()}> +
e.stopPropagation()}> +
+ upload_file + + {dropPhase === 'naming' ? 'Sound benennen' + : dropPhase === 'uploading' ? 'Wird hochgeladen...' + : 'Gespeichert!'} + {dropFiles.length > 1 && ` (${dropIndex + 1}/${dropFiles.length})`} + + {dropPhase === 'naming' && ( + + )} +
+ +
+ {/* File info badge */} +
+ Datei + + {dropFiles[dropIndex]?.name} + + + {((dropFiles[dropIndex]?.size ?? 0) / 1024).toFixed(0)} KB + +
+ + {/* Naming phase */} + {dropPhase === 'naming' && ( +
+ +
+ setDropName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') void handleDropConfirm(); }} + autoFocus + /> + .mp3 +
+
+ )} + + {/* Uploading phase */} + {dropPhase === 'uploading' && ( +
+
+ Upload: {dropProgress}% +
+ )} + + {/* Done phase */} + {dropPhase === 'done' && ( +
+ check_circle + Erfolgreich hochgeladen! +
+ )} +
+ + {/* Actions */} + {dropPhase === 'naming' && ( +
+ + +
+ )} +
+
+ )} + {/* ── Download Modal ── */} {dlModal && (
dlModal.phase !== 'downloading' && setDlModal(null)}>