Fix 3 bug standalone: save project lỗi schema, export MIDI/WAV không hỏi nơi lưu
- Bug 1: serializeTracksList bỏ section item không có sectionId (do insertSectionAtPlayhead tạo) — tránh source_data {} làm schema oneOf fail
- Bug 2: export MIDI qua save-dialog + write file, toast kèm đường dẫn
- Bug 3: export mix WAV/MP3/OGG qua save-dialog + write file, toast kèm đường dẫn
- Marker tauri_bridge_ready: xóa khi window destroy + guard SF_PARENT_PID — tránh standalone treo 120s
This commit is contained in:
+91
-18
@@ -11029,6 +11029,12 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
}
|
||||
if (t.sections && t.sections.length > 0) {
|
||||
t.sections.forEach(s => {
|
||||
// BỎ section item thiếu sectionId (insertSectionAtPlayhead tạo section
|
||||
// chưa gắn vào tab → s.sectionId undefined). Nếu serialize thật thì
|
||||
// referenced_section_id bị JSON.stringify bỏ → source_data = {} →
|
||||
// schema oneOf fail → lưu project báo lỗi xác thực (bug user gặp).
|
||||
// Item vô nghĩa (chưa có nội dung tab) — bỏ là đúng, không mất data.
|
||||
if (!s.sectionId) return;
|
||||
items.push({
|
||||
id: s.id,
|
||||
name: s.name || "Section Item",
|
||||
@@ -11037,9 +11043,6 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
duration_bars: s.duration / secondsPerBar,
|
||||
clip_start_offset_bars: 0.0,
|
||||
source_data: {
|
||||
// KHÔNG fallback s.id: section item thiếu sectionId (insert thiếu
|
||||
// field) → fallback = chính id item → TỰ TRỎ → block lồng + mất
|
||||
// nội dung. Chỉ dùng sectionId; undefined → deserialize block rỗng.
|
||||
referenced_section_id: s.sectionId
|
||||
}
|
||||
});
|
||||
@@ -26593,6 +26596,36 @@ const App = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
// ── Save-As qua NATIVE dialog (fix bug: export không hỏi nơi lưu) ──
|
||||
// WebView2 chặn anchor-download → engine mở SaveFileDialog qua
|
||||
// /api/v1/files/save-dialog (PowerShell hoặc Tauri bridge) → ghi bytes qua
|
||||
// /api/v1/files/write. Trả path đã ghi; null nếu user hủy / không có dialog.
|
||||
async function saveExportViaDialog(defaultName, blob) {
|
||||
try {
|
||||
const dlg = await fetch(`${API_BASE_URL}/api/v1/files/save-dialog`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
default_name: defaultName,
|
||||
filter_name: defaultName.toLowerCase().endsWith('.mid') ? 'MIDI' : 'Audio',
|
||||
filter_ext: defaultName.split('.').pop() || 'bin'
|
||||
})
|
||||
});
|
||||
if (!dlg.ok) return null;
|
||||
const d = await dlg.json();
|
||||
if (!d.path) return null;
|
||||
const fd = new FormData();
|
||||
fd.append('path', d.path);
|
||||
fd.append('file', new File([blob], defaultName, { type: blob.type || 'application/octet-stream' }));
|
||||
const wr = await fetch(`${API_BASE_URL}/api/v1/files/write`, { method: 'POST', body: fd });
|
||||
if (!wr.ok) return null;
|
||||
const w = await wr.json();
|
||||
return w.path;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── MIDI Export: tạo file MIDI từ tất cả tracks ──
|
||||
// ── Server-side MIDI export (BIG_PHASE_3 Phase 1) ──
|
||||
// Blob download bị WebView2 chặn sau khi chuyển standalone → POST
|
||||
@@ -26610,7 +26643,18 @@ const App = () => {
|
||||
throw new Error(e.detail || ('HTTP ' + res.status));
|
||||
}
|
||||
return res.json();
|
||||
}).then(data => {
|
||||
}).then(async data => {
|
||||
// NATIVE Save-As dialog (WebView2 chặn anchor download) → ghi file thật.
|
||||
try {
|
||||
const blobResp = await fetch(`${API_BASE_URL}${data.download_url}`);
|
||||
if (blobResp.ok) {
|
||||
const blob = await blobResp.blob();
|
||||
const savedPath = await saveExportViaDialog(filename, blob);
|
||||
if (savedPath) {
|
||||
return { ...data, saved_path: savedPath };
|
||||
}
|
||||
}
|
||||
} catch (e) { /* fallback anchor dưới */ }
|
||||
const a = document.createElement('a');
|
||||
a.href = `${API_BASE_URL}${data.download_url}`;
|
||||
a.download = filename;
|
||||
@@ -26627,6 +26671,11 @@ const App = () => {
|
||||
// /api/v1/audio/download/{file_id} (Content-Disposition attachment).
|
||||
// Nếu upload thất bại (server offline) → fallback blob download cũ.
|
||||
async function downloadBlobViaServer(blob, filename) {
|
||||
// NATIVE Save-As dialog trước (WebView2 chặn anchor download) → ghi file thật.
|
||||
try {
|
||||
const savedPath = await saveExportViaDialog(filename, blob);
|
||||
if (savedPath) return { saved_path: savedPath };
|
||||
} catch (e) { /* fallback upload+anchor dưới */ }
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', new File([blob], filename, { type: blob.type || 'audio/wav' }));
|
||||
@@ -26677,8 +26726,12 @@ const App = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await serverDownloadMidi({ bpm: bpmNum, ppq: 480, tracks: midiTracks }, (projectName || 'Project') + '.mid');
|
||||
showToast(`Đã xuất file MIDI với ${midiTracks.length} tracks!`, "success");
|
||||
const r = await serverDownloadMidi({ bpm: bpmNum, ppq: 480, tracks: midiTracks }, (projectName || 'Project') + '.mid');
|
||||
if (r && r.saved_path) {
|
||||
showToast(`Đã xuất file MIDI (${midiTracks.length} tracks) → ${r.saved_path}`, "success");
|
||||
} else {
|
||||
showToast(`Đã xuất file MIDI với ${midiTracks.length} tracks!`, "success");
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Export MIDI thất bại: ' + (err && err.message ? err.message : String(err)), 'error');
|
||||
}
|
||||
@@ -27008,11 +27061,19 @@ const App = () => {
|
||||
const fileId = result.output_file_id || result.output_path?.split('/').pop();
|
||||
if (fileId) {
|
||||
const downloadUrl = `${API_AUDIO}/download/${fileId}`;
|
||||
showToast("Xuất bản âm thanh từ máy chủ hoàn tất!", "success", "Tải về", () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = fileId;
|
||||
a.click();
|
||||
showToast("Xuất bản âm thanh từ máy chủ hoàn tất!", "success", "Tải về", async () => {
|
||||
try {
|
||||
const br = await fetch(downloadUrl);
|
||||
if (!br.ok) throw new Error('HTTP ' + br.status);
|
||||
const b = await br.blob();
|
||||
const sp = await saveExportViaDialog((projectName || 'Project') + '.wav', b);
|
||||
if (sp) showToast(`Đã lưu → ${sp}`, "success");
|
||||
} catch (e) {
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = fileId;
|
||||
a.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -27495,11 +27556,19 @@ const App = () => {
|
||||
if (result.success) {
|
||||
const outId = result.output_file_id;
|
||||
const downloadUrl = `${API_AUDIO}/download/${outId}`;
|
||||
showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success", "Tải về", () => {
|
||||
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", "Tải về", async () => {
|
||||
try {
|
||||
const br = await fetch(downloadUrl);
|
||||
if (!br.ok) throw new Error('HTTP ' + br.status);
|
||||
const b = await br.blob();
|
||||
const sp = await saveExportViaDialog(`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`, b);
|
||||
if (sp) showToast(`Đã lưu → ${sp}`, "success");
|
||||
} catch (e) {
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
|
||||
a.click();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.error || 'Server encoding failed');
|
||||
@@ -27513,8 +27582,12 @@ const App = () => {
|
||||
if (exportSettings.format !== 'wav') {
|
||||
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
|
||||
}
|
||||
await downloadBlobViaServer(blob, wavName).catch(() => {});
|
||||
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||
const dres = await downloadBlobViaServer(blob, wavName).catch(() => null);
|
||||
if (dres && dres.saved_path) {
|
||||
showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`, "success");
|
||||
} else {
|
||||
showToast("Xuất bản âm thanh hoàn tất!", "success");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Restore the live master bus + live mastering state after offline render.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user