fix: xử lý phân quyền tải file từ lệnh AI và hỗ trợ nén MP3/OGG qua API máy chủ
This commit is contained in:
+141
-10
@@ -5089,13 +5089,15 @@ const App = () => {
|
|||||||
}, [tracks, selectedTrackId, selLeft, selRight]);
|
}, [tracks, selectedTrackId, selLeft, selRight]);
|
||||||
|
|
||||||
// ── Toast helper ──
|
// ── Toast helper ──
|
||||||
const showToast = (text, type = 'info') => {
|
const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
|
||||||
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
||||||
setToastMessage({
|
setToastMessage({
|
||||||
text,
|
text,
|
||||||
type
|
type,
|
||||||
|
actionText,
|
||||||
|
onActionClick
|
||||||
});
|
});
|
||||||
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500);
|
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Server-side upload ──
|
// ── Server-side upload ──
|
||||||
@@ -6605,12 +6607,63 @@ const App = () => {
|
|||||||
type: 'audio/wav'
|
type: 'audio/wav'
|
||||||
});
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
|
||||||
|
setIsExporting(true);
|
||||||
|
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
|
||||||
|
try {
|
||||||
|
const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!uploadResp.ok) throw new Error("Upload to transcode server failed");
|
||||||
|
const uploadData = await uploadResp.json();
|
||||||
|
const fileId = uploadData.file_id;
|
||||||
|
|
||||||
|
const exportResp = await fetch(`${API_AUDIO}/export`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_id: fileId,
|
||||||
|
format: exportSettings.format,
|
||||||
|
sample_rate: targetRate,
|
||||||
|
bit_depth: bitDepth
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!exportResp.ok) throw new Error("Transcode request failed");
|
||||||
|
const exportData = await exportResp.json();
|
||||||
|
const result = await pollTaskResult(exportData.task_id, 20);
|
||||||
|
if (result.success) {
|
||||||
|
const outId = result.output_file_id;
|
||||||
|
const downloadUrl = `${API_AUDIO}/download/${outId}`;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
|
||||||
|
a.click();
|
||||||
|
showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success");
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Server encoding failed');
|
||||||
|
}
|
||||||
|
} catch (transcodeErr) {
|
||||||
|
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải dạng WAV thay thế.`, "warning");
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (exportSettings.format !== 'wav') {
|
||||||
|
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
|
||||||
|
}
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -7239,6 +7292,10 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
if (hasCalls) {
|
if (hasCalls) {
|
||||||
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]);
|
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]);
|
||||||
|
if (window.DAWCommandDispatcher) {
|
||||||
|
window.DAWCommandDispatcher.isExecutingAI = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
for (const fc of result.functionCalls) {
|
for (const fc of result.functionCalls) {
|
||||||
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
|
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
|
||||||
const cmdName = fc.name.toUpperCase();
|
const cmdName = fc.name.toUpperCase();
|
||||||
@@ -7254,6 +7311,11 @@ const App = () => {
|
|||||||
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: DAWCommandDispatcher not available`, time: Date.now() }]);
|
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: DAWCommandDispatcher not available`, time: Date.now() }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (window.DAWCommandDispatcher) {
|
||||||
|
window.DAWCommandDispatcher.isExecutingAI = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!hasText && !hasCalls) {
|
if (!hasText && !hasCalls) {
|
||||||
const rawKeys = result.raw ? Object.keys(result.raw).join(', ') : 'null';
|
const rawKeys = result.raw ? Object.keys(result.raw).join(', ') : 'null';
|
||||||
@@ -7782,13 +7844,76 @@ const App = () => {
|
|||||||
ofs += bps;
|
ofs += bps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const blob = new Blob([fileBuf], { type: 'audio/' + fmt });
|
const blob = new Blob([fileBuf], { type: 'audio/wav' });
|
||||||
const url = URL.createObjectURL(blob);
|
const localUrl = URL.createObjectURL(blob);
|
||||||
|
const targetFilename = `export_${Date.now()}.${fmt}`;
|
||||||
|
|
||||||
|
const triggerDownload = (downloadUrl, finalFilename) => {
|
||||||
|
if (window.DAWCommandDispatcher?.isExecutingAI) {
|
||||||
|
showToast(`Xuất nhạc thành công (${fmt.toUpperCase()})!`, "success", "Tải về", () => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = downloadUrl;
|
||||||
a.download = `export_${Date.now()}.${fmt}`;
|
a.download = finalFilename;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
if (downloadUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = finalFilename;
|
||||||
|
a.click();
|
||||||
|
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||||
|
if (downloadUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((fmt === 'mp3' || fmt === 'ogg') && serverStatus === 'connected') {
|
||||||
|
try {
|
||||||
|
const file = new File([blob], `export_ai.wav`, { type: 'audio/wav' });
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!uploadResp.ok) throw new Error("Upload failed");
|
||||||
|
const uploadData = await uploadResp.json();
|
||||||
|
const uploadId = uploadData.file_id;
|
||||||
|
|
||||||
|
const exportResp = await fetch(`${API_AUDIO}/export`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_id: uploadId,
|
||||||
|
format: fmt,
|
||||||
|
sample_rate: sr,
|
||||||
|
bit_depth: bd
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!exportResp.ok) throw new Error("Export failed");
|
||||||
|
const exportData = await exportResp.json();
|
||||||
|
const result = await pollTaskResult(exportData.task_id, 20);
|
||||||
|
if (result.success) {
|
||||||
|
const downloadUrl = `${API_AUDIO}/download/${result.output_file_id}`;
|
||||||
|
triggerDownload(downloadUrl, targetFilename);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Server encoding failed');
|
||||||
|
}
|
||||||
|
} catch (transcodeErr) {
|
||||||
|
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải về dạng WAV thay thế.`, "warning");
|
||||||
|
triggerDownload(localUrl, `export_${Date.now()}.wav`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const finalFilename = fmt === 'wav' ? `export_${Date.now()}.wav` : `export_${Date.now()}.wav`;
|
||||||
|
if (fmt !== 'wav') {
|
||||||
|
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
|
||||||
|
}
|
||||||
|
triggerDownload(localUrl, finalFilename);
|
||||||
|
}
|
||||||
return { success: true, trackId: tid, range: parseFloat((rawEnd - rawStart).toFixed(3)) + 's', format: fmt, channels: numCh === 1 ? 'mono' : 'stereo' };
|
return { success: true, trackId: tid, range: parseFloat((rawEnd - rawStart).toFixed(3)) + 's', format: fmt, channels: numCh === 1 ? 'mono' : 'stereo' };
|
||||||
},
|
},
|
||||||
selectItem: (args) => {
|
selectItem: (args) => {
|
||||||
@@ -10461,7 +10586,13 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "bell",
|
"data-lucide": "bell",
|
||||||
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
|
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
|
||||||
})), toastMessage.text), /*#__PURE__*/React.createElement(AuthModal, {
|
})), toastMessage.text, toastMessage.onActionClick && /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => {
|
||||||
|
toastMessage.onActionClick();
|
||||||
|
setToastMessage(null);
|
||||||
|
},
|
||||||
|
className: "ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"
|
||||||
|
}, toastMessage.actionText || 'Tải về')), /*#__PURE__*/React.createElement(AuthModal, {
|
||||||
isOpen: authModalOpen,
|
isOpen: authModalOpen,
|
||||||
mode: authMode,
|
mode: authMode,
|
||||||
forceMandatory: isMandatoryLogin,
|
forceMandatory: isMandatoryLogin,
|
||||||
|
|||||||
@@ -5144,13 +5144,15 @@ const App = () => {
|
|||||||
}, [tracks, selectedTrackId, selLeft, selRight]);
|
}, [tracks, selectedTrackId, selLeft, selRight]);
|
||||||
|
|
||||||
// ── Toast helper ──
|
// ── Toast helper ──
|
||||||
const showToast = (text, type = 'info') => {
|
const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
|
||||||
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
||||||
setToastMessage({
|
setToastMessage({
|
||||||
text,
|
text,
|
||||||
type
|
type,
|
||||||
|
actionText,
|
||||||
|
onActionClick
|
||||||
});
|
});
|
||||||
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500);
|
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Server-side upload ──
|
// ── Server-side upload ──
|
||||||
@@ -6738,12 +6740,66 @@ const App = () => {
|
|||||||
type: 'audio/wav'
|
type: 'audio/wav'
|
||||||
});
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
|
||||||
|
setIsExporting(true);
|
||||||
|
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
|
||||||
|
try {
|
||||||
|
const file = new File([blob], `session_export.wav`, {
|
||||||
|
type: 'audio/wav'
|
||||||
|
});
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!uploadResp.ok) throw new Error("Upload to transcode server failed");
|
||||||
|
const uploadData = await uploadResp.json();
|
||||||
|
const fileId = uploadData.file_id;
|
||||||
|
const exportResp = await fetch(`${API_AUDIO}/export`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_id: fileId,
|
||||||
|
format: exportSettings.format,
|
||||||
|
sample_rate: targetRate,
|
||||||
|
bit_depth: bitDepth
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!exportResp.ok) throw new Error("Transcode request failed");
|
||||||
|
const exportData = await exportResp.json();
|
||||||
|
const result = await pollTaskResult(exportData.task_id, 20);
|
||||||
|
if (result.success) {
|
||||||
|
const outId = result.output_file_id;
|
||||||
|
const downloadUrl = `${API_AUDIO}/download/${outId}`;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
|
||||||
|
a.click();
|
||||||
|
showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success");
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Server encoding failed');
|
||||||
|
}
|
||||||
|
} catch (transcodeErr) {
|
||||||
|
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải dạng WAV thay thế.`, "warning");
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (exportSettings.format !== 'wav') {
|
||||||
|
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
|
||||||
|
}
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
showToast("Lỗi xuất âm thanh: " + err.message, "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -7396,6 +7452,10 @@ const App = () => {
|
|||||||
text: ` Gọi ${result.functionCalls.length} lệnh...`,
|
text: ` Gọi ${result.functionCalls.length} lệnh...`,
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
}]);
|
}]);
|
||||||
|
if (window.DAWCommandDispatcher) {
|
||||||
|
window.DAWCommandDispatcher.isExecutingAI = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
for (const fc of result.functionCalls) {
|
for (const fc of result.functionCalls) {
|
||||||
setAiActionLog(prev => [...prev, {
|
setAiActionLog(prev => [...prev, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -7427,6 +7487,11 @@ const App = () => {
|
|||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (window.DAWCommandDispatcher) {
|
||||||
|
window.DAWCommandDispatcher.isExecutingAI = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!hasText && !hasCalls) {
|
if (!hasText && !hasCalls) {
|
||||||
const rawKeys = result.raw ? Object.keys(result.raw).join(', ') : 'null';
|
const rawKeys = result.raw ? Object.keys(result.raw).join(', ') : 'null';
|
||||||
@@ -8110,14 +8175,78 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const blob = new Blob([fileBuf], {
|
const blob = new Blob([fileBuf], {
|
||||||
type: 'audio/' + fmt
|
type: 'audio/wav'
|
||||||
});
|
});
|
||||||
const url = URL.createObjectURL(blob);
|
const localUrl = URL.createObjectURL(blob);
|
||||||
|
const targetFilename = `export_${Date.now()}.${fmt}`;
|
||||||
|
const triggerDownload = (downloadUrl, finalFilename) => {
|
||||||
|
if (window.DAWCommandDispatcher?.isExecutingAI) {
|
||||||
|
showToast(`Xuất nhạc thành công (${fmt.toUpperCase()})!`, "success", "Tải về", () => {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = downloadUrl;
|
||||||
a.download = `export_${Date.now()}.${fmt}`;
|
a.download = finalFilename;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
if (downloadUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = downloadUrl;
|
||||||
|
a.download = finalFilename;
|
||||||
|
a.click();
|
||||||
|
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||||
|
if (downloadUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ((fmt === 'mp3' || fmt === 'ogg') && serverStatus === 'connected') {
|
||||||
|
try {
|
||||||
|
const file = new File([blob], `export_ai.wav`, {
|
||||||
|
type: 'audio/wav'
|
||||||
|
});
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!uploadResp.ok) throw new Error("Upload failed");
|
||||||
|
const uploadData = await uploadResp.json();
|
||||||
|
const uploadId = uploadData.file_id;
|
||||||
|
const exportResp = await fetch(`${API_AUDIO}/export`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_id: uploadId,
|
||||||
|
format: fmt,
|
||||||
|
sample_rate: sr,
|
||||||
|
bit_depth: bd
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!exportResp.ok) throw new Error("Export failed");
|
||||||
|
const exportData = await exportResp.json();
|
||||||
|
const result = await pollTaskResult(exportData.task_id, 20);
|
||||||
|
if (result.success) {
|
||||||
|
const downloadUrl = `${API_AUDIO}/download/${result.output_file_id}`;
|
||||||
|
triggerDownload(downloadUrl, targetFilename);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Server encoding failed');
|
||||||
|
}
|
||||||
|
} catch (transcodeErr) {
|
||||||
|
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải về dạng WAV thay thế.`, "warning");
|
||||||
|
triggerDownload(localUrl, `export_${Date.now()}.wav`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const finalFilename = fmt === 'wav' ? `export_${Date.now()}.wav` : `export_${Date.now()}.wav`;
|
||||||
|
if (fmt !== 'wav') {
|
||||||
|
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
|
||||||
|
}
|
||||||
|
triggerDownload(localUrl, finalFilename);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
trackId: tid,
|
trackId: tid,
|
||||||
@@ -10972,7 +11101,13 @@ const App = () => {
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "bell",
|
"data-lucide": "bell",
|
||||||
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
|
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
|
||||||
})), toastMessage.text), /*#__PURE__*/React.createElement(AuthModal, {
|
})), toastMessage.text, toastMessage.onActionClick && /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => {
|
||||||
|
toastMessage.onActionClick();
|
||||||
|
setToastMessage(null);
|
||||||
|
},
|
||||||
|
className: "ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"
|
||||||
|
}, toastMessage.actionText || 'Tải về')), /*#__PURE__*/React.createElement(AuthModal, {
|
||||||
isOpen: authModalOpen,
|
isOpen: authModalOpen,
|
||||||
mode: authMode,
|
mode: authMode,
|
||||||
forceMandatory: isMandatoryLogin,
|
forceMandatory: isMandatoryLogin,
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user