diff --git a/server/src/wled_controller/static/js/features/assets.ts b/server/src/wled_controller/static/js/features/assets.ts index d7bd290..f1e3742 100644 --- a/server/src/wled_controller/static/js/features/assets.ts +++ b/server/src/wled_controller/static/js/features/assets.ts @@ -102,7 +102,10 @@ function initUploadDropzone(): void { dropzone.classList.remove('dragover'); const files = e.dataTransfer?.files; if (files && files.length > 0) { - fileInput.files = files; + // Use DataTransfer to safely assign files cross-browser + const dt = new DataTransfer(); + dt.items.add(files[0]); + fileInput.files = dt.files; showFile(files[0]); } }); @@ -183,9 +186,11 @@ async function _playAssetSound(assetId: string) { const blobUrl = URL.createObjectURL(blob); const audio = new Audio(blobUrl); audio.addEventListener('ended', () => URL.revokeObjectURL(blobUrl)); - audio.play().catch(() => {}); + audio.play().catch(() => showToast(t('asset.error.play_failed'), 'error')); _currentAudio = audio; - } catch { /* ignore playback errors */ } + } catch { + showToast(t('asset.error.play_failed'), 'error'); + } } // ── Modal ── @@ -281,13 +286,14 @@ export async function uploadAsset(): Promise { if (params.toString()) url += `?${params.toString()}`; try { + // Use raw fetch for multipart — fetchWithAuth forces Content-Type: application/json + // which breaks the browser's automatic multipart boundary generation const headers = getHeaders(); - delete headers['Content-Type']; // Let browser set multipart boundary - const res = await fetch(url, { - method: 'POST', - headers, - body: formData, - }); + delete headers['Content-Type']; + const res = await fetch(url, { method: 'POST', headers, body: formData }); + if (res.status === 401) { + throw new Error('Session expired'); + } if (!res.ok) { const err = await res.json(); throw new Error(err.detail || 'Upload failed'); @@ -365,8 +371,8 @@ export async function saveAssetMetadata(): Promise { method: 'PUT', body: JSON.stringify({ name, description: description || null, tags }), }); - if (!res!.ok) { - const err = await res!.json(); + if (!res.ok) { + const err = await res.json(); throw new Error(err.detail); } @@ -407,11 +413,11 @@ export async function deleteAsset(assetId: string): Promise { export async function restorePrebuiltAssets(): Promise { try { const res = await fetchWithAuth('/assets/restore-prebuilt', { method: 'POST' }); - if (!res!.ok) { - const err = await res!.json(); + if (!res.ok) { + const err = await res.json(); throw new Error(err.detail); } - const data = await res!.json(); + const data = await res.json(); if (data.restored_count > 0) { showToast(t('asset.prebuilt_restored').replace('{count}', String(data.restored_count)), 'success'); } else { @@ -439,7 +445,9 @@ async function _downloadAsset(assetId: string) { a.download = filename; a.click(); URL.revokeObjectURL(blobUrl); - } catch { /* ignore */ } + } catch { + showToast(t('asset.error.download_failed'), 'error'); + } } // ── Event delegation ── diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index f73bf0a..69714b4 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -434,6 +434,7 @@ "confirm.turn_off_device": "Turn off this device?", "common.loading": "Loading...", "common.delete": "Delete", + "common.remove": "Remove", "common.edit": "Edit", "common.clone": "Clone", "common.none": "None", @@ -1995,6 +1996,8 @@ "asset.error.name_required": "Name is required", "asset.error.no_file": "Please select a file to upload", "asset.error.delete_failed": "Failed to delete asset", + "asset.error.play_failed": "Failed to play sound", + "asset.error.download_failed": "Failed to download asset", "asset.play": "Play", "asset.download": "Download", "asset.prebuilt": "Prebuilt", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 21a4d3b..52be816 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -434,6 +434,7 @@ "confirm.turn_off_device": "Выключить это устройство?", "common.loading": "Загрузка...", "common.delete": "Удалить", + "common.remove": "Убрать", "common.edit": "Редактировать", "common.clone": "Клонировать", "common.none": "Нет", @@ -1924,6 +1925,8 @@ "asset.error.name_required": "Название обязательно", "asset.error.no_file": "Выберите файл для загрузки", "asset.error.delete_failed": "Не удалось удалить ресурс", + "asset.error.play_failed": "Не удалось воспроизвести звук", + "asset.error.download_failed": "Не удалось скачать ресурс", "asset.play": "Воспроизвести", "asset.download": "Скачать", "asset.prebuilt": "Встроенный", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 83f83fc..15555b7 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -434,6 +434,7 @@ "confirm.turn_off_device": "关闭此设备?", "common.loading": "加载中...", "common.delete": "删除", + "common.remove": "移除", "common.edit": "编辑", "common.clone": "克隆", "common.none": "无", @@ -1922,6 +1923,8 @@ "asset.error.name_required": "名称为必填项", "asset.error.no_file": "请选择要上传的文件", "asset.error.delete_failed": "删除资源失败", + "asset.error.play_failed": "播放声音失败", + "asset.error.download_failed": "下载资源失败", "asset.play": "播放", "asset.download": "下载", "asset.prebuilt": "内置", diff --git a/server/src/wled_controller/templates/modals/asset-editor.html b/server/src/wled_controller/templates/modals/asset-editor.html index 8e1c135..1c9e4a9 100644 --- a/server/src/wled_controller/templates/modals/asset-editor.html +++ b/server/src/wled_controller/templates/modals/asset-editor.html @@ -2,7 +2,7 @@