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:
2026-08-21 17:13:28 +07:00
parent 9b14c99a7a
commit 4e3f2fd08a
6 changed files with 288 additions and 28 deletions
+91 -18
View File
@@ -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