fix(ui): persist transport loop + panel mixer + redesign export modal (folder picker + default dir)
This commit is contained in:
+158
-34
@@ -11784,7 +11784,10 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
time_signature_numerator: 4,
|
||||
time_signature_denominator: 4,
|
||||
sample_rate: 44100,
|
||||
zoom: window.__sfTimelineZoom || window.__currentZoom || 1.0
|
||||
zoom: window.__sfTimelineZoom || window.__currentZoom || 1.0,
|
||||
loop_selection: !!window.__sfLoopSelection,
|
||||
selection_start: (window.__sfLoopSelection && window.__sfSelectionStart != null) ? window.__sfSelectionStart : null,
|
||||
selection_end: (window.__sfLoopSelection && window.__sfSelectionEnd != null) ? window.__sfSelectionEnd : null
|
||||
},
|
||||
main_session: {
|
||||
id: "main",
|
||||
@@ -11969,7 +11972,10 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
||||
masterMuted: !!(_masterBus.muted || _masterBus.mute),
|
||||
masterAutoVol: (_masterBus.automation && Array.isArray(_masterBus.automation.volume_db)) ? _masterBus.automation.volume_db.filter(p => p && typeof p.time === 'number' && typeof p.db === 'number') : [],
|
||||
masterAutoOn: !!(_masterBus.automation && Array.isArray(_masterBus.automation.volume_db) && _masterBus.automation.volume_db.length),
|
||||
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
|
||||
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0,
|
||||
loopSelection: !!(schemaObj.metadata && schemaObj.metadata.loop_selection),
|
||||
selectionStart: (schemaObj.metadata && schemaObj.metadata.selection_start != null) ? schemaObj.metadata.selection_start : null,
|
||||
selectionEnd: (schemaObj.metadata && schemaObj.metadata.selection_end != null) ? schemaObj.metadata.selection_end : null
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12426,6 +12432,15 @@ const InteractiveEqPro = ({ track, params, onChange, getModule, applyTo, spectru
|
||||
|
||||
const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExporting, onExport, onBounce, onRenderMastering }) => {
|
||||
if (!open) return null;
|
||||
const pickDir = async () => {
|
||||
try {
|
||||
const r = await fetch(`${API_BASE_URL}/api/v1/plugins/pick-dir`, { method: 'POST' });
|
||||
if (r.ok) {
|
||||
const d = await r.json();
|
||||
if (d && d.path) setExportSettings(p => ({ ...p, exportDir: d.path }));
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
return (
|
||||
<div className="fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6" onClick={onClose}>
|
||||
<div className="w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden" onClick={e => e.stopPropagation()}>
|
||||
@@ -12436,6 +12451,38 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<button onClick={onClose} className="text-zinc-400 hover:text-white"><i data-lucide="x" className="w-4 h-4" /></button>
|
||||
</div>
|
||||
<div className="p-4 max-h-[72vh] overflow-y-auto space-y-3">
|
||||
<div className="bg-[#141414] border border-zinc-800 rounded px-2 py-1.5 space-y-1.5">
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Tên file export</label>
|
||||
<input type="text" value={exportSettings.fileName}
|
||||
onChange={e => setExportSettings(p => ({ ...p, fileName: e.target.value }))}
|
||||
placeholder="Để trống = dùng tên mặc định"
|
||||
className="w-full bg-[#0d0d0d] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none" />
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Thư mục export</label>
|
||||
<div className="flex items-center gap-1.5 bg-[#0d0d0d] border border-zinc-800 rounded px-1.5 py-0.5 min-h-[22px]">
|
||||
<i className="fa-solid fa-folder text-cyan-400 text-[10px] shrink-0"></i>
|
||||
{exportSettings.exportDir ? (
|
||||
<a className="text-cyan-400 underline truncate cursor-pointer" title={exportSettings.exportDir} onClick={pickDir}>{exportSettings.exportDir}</a>
|
||||
) : (
|
||||
<span className="text-zinc-600 truncate">Chưa chọn — xuất sẽ mở Save-As</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={pickDir}
|
||||
className="shrink-0 px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-[10px] font-bold flex items-center gap-1">
|
||||
<i className="fa-solid fa-folder-open text-[10px]"></i> Chọn thư mục
|
||||
</button>
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" checked={!!exportSettings.rememberExportDir}
|
||||
onChange={e => setExportSettings(p => ({ ...p, rememberExportDir: e.target.checked }))}
|
||||
className="accent-cyan-500 w-3.5 h-3.5" />
|
||||
<span className="text-xs text-zinc-300">Lưu thư mục export làm mặc định</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Nguồn</label>
|
||||
@@ -12459,7 +12506,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
</div>
|
||||
</div>
|
||||
{exportSettings.format === 'wav' ? (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">SR (Hz)</label>
|
||||
<select value={exportSettings.sampleRate} onChange={e => setExportSettings(p => ({ ...p, sampleRate: e.target.value }))}
|
||||
@@ -12478,6 +12525,14 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<option value="float">32 float</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Kênh</label>
|
||||
<select value={exportSettings.channels} onChange={e => setExportSettings(p => ({ ...p, channels: e.target.value }))}
|
||||
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="stereo">Stereo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -12489,20 +12544,16 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
<option value="lossless">Lossless</option>
|
||||
</select>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Kênh</label>
|
||||
<select value={exportSettings.channels} onChange={e => setExportSettings(p => ({ ...p, channels: e.target.value }))}
|
||||
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="stereo">Stereo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Kênh</label>
|
||||
<select value={exportSettings.channels} onChange={e => setExportSettings(p => ({ ...p, channels: e.target.value }))}
|
||||
className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="stereo">Stereo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" checked={!!exportSettings.dither}
|
||||
@@ -17733,6 +17784,14 @@ const App = () => {
|
||||
// timeline thu nhỏ ở góc — user phải zoom in mới về đúng).
|
||||
window.__sfTimelineZoom = zoom;
|
||||
}, [zoom]);
|
||||
// Persist transport loop selection qua save/reload — serialize đọc từ window
|
||||
// refs (module-level không truy cập state). Lưu cả vùng chọn (giây) để loop
|
||||
// vùng chọn hoạt động lại đúng sau khi mở project.
|
||||
React.useEffect(() => { window.__sfLoopSelection = isLoopingSelection; }, [isLoopingSelection]);
|
||||
React.useEffect(() => {
|
||||
window.__sfSelectionStart = selectionStart;
|
||||
window.__sfSelectionEnd = selectionEnd;
|
||||
}, [selectionStart, selectionEnd]);
|
||||
const [isLoopingSelection, setIsLoopingSelection] = useState(false);
|
||||
const [beginBar, setBeginBar] = useState(0);
|
||||
const [endBar, setEndBar] = useState(0);
|
||||
@@ -18321,24 +18380,44 @@ const App = () => {
|
||||
const aiPromptMgrRef = useRef(null);
|
||||
const aiTypeaheadRef = useRef(null);
|
||||
const [selectedProviderId, setSelectedProviderId] = useState('');
|
||||
const [exportSettings, setExportSettings] = useState({
|
||||
sampleRate: '44100',
|
||||
bitDepth: 'float',
|
||||
format: 'wav',
|
||||
source: 'project',
|
||||
quality: '44khz',
|
||||
channels: 'stereo',
|
||||
loop: false,
|
||||
loopBars: 4,
|
||||
dither: false,
|
||||
normalize: true,
|
||||
normalizeTarget: '-14lufs',
|
||||
monoCheck: true,
|
||||
// Gap 13: BWF/INFO metadata cho WAV mastering export
|
||||
metaTitle: '',
|
||||
metaArtist: '',
|
||||
metaIsrc: ''
|
||||
const [exportSettings, setExportSettings] = useState(() => {
|
||||
let expDir = '', remExpDir = false;
|
||||
try { expDir = localStorage.getItem('sonic_export_dir') || ''; } catch (e) { }
|
||||
try { remExpDir = localStorage.getItem('sonic_remember_export_dir') === '1'; } catch (e) { }
|
||||
return {
|
||||
sampleRate: '44100',
|
||||
bitDepth: 'float',
|
||||
format: 'wav',
|
||||
source: 'project',
|
||||
quality: '44khz',
|
||||
channels: 'stereo',
|
||||
loop: false,
|
||||
loopBars: 4,
|
||||
dither: false,
|
||||
normalize: true,
|
||||
normalizeTarget: '-14lufs',
|
||||
monoCheck: true,
|
||||
// Gap 13: BWF/INFO metadata cho WAV mastering export
|
||||
metaTitle: '',
|
||||
metaArtist: '',
|
||||
metaIsrc: '',
|
||||
// Export file + thư mục (ExportModal) — dir lưu localStorage nếu remember
|
||||
fileName: '',
|
||||
exportDir: expDir,
|
||||
rememberExportDir: remExpDir
|
||||
};
|
||||
});
|
||||
// Persist export dir mặc định (chỉ khi user bật remember) — ExportModal
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (exportSettings.rememberExportDir && exportSettings.exportDir) {
|
||||
localStorage.setItem('sonic_export_dir', exportSettings.exportDir);
|
||||
} else {
|
||||
localStorage.removeItem('sonic_export_dir');
|
||||
}
|
||||
localStorage.setItem('sonic_remember_export_dir', exportSettings.rememberExportDir ? '1' : '0');
|
||||
} catch (e) { }
|
||||
}, [exportSettings.exportDir, exportSettings.rememberExportDir]);
|
||||
const [serverStatus, setServerStatus] = useState('checking...');
|
||||
const _lan = window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.lan;
|
||||
const serverAddr = _lan && _lan.urls && _lan.urls[0] ? (function () { try { return new URL(_lan.urls[0]).host; } catch (e) { return null; } })() : window.location.host;
|
||||
@@ -19444,6 +19523,14 @@ const App = () => {
|
||||
// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
|
||||
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage — hết thu nhỏ.
|
||||
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
|
||||
// Restore transport loop selection (loop vùng chọn sau save/reload)
|
||||
if (result.loopSelection !== undefined && result.loopSelection) {
|
||||
setIsLoopingSelection(true);
|
||||
if (result.selectionStart != null && result.selectionEnd != null) {
|
||||
setSelectionStart(result.selectionStart);
|
||||
setSelectionEnd(result.selectionEnd);
|
||||
}
|
||||
}
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
|
||||
@@ -19705,6 +19792,7 @@ const App = () => {
|
||||
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
|
||||
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
|
||||
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
|
||||
if (p.showMixer !== undefined) setShowMixer(p.showMixer);
|
||||
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
|
||||
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
|
||||
if (p.panelPositions) setPanelPositions(p.panelPositions);
|
||||
@@ -27481,6 +27569,34 @@ const App = () => {
|
||||
// /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) {
|
||||
// Export dir đã chọn trong ExportModal → ghi thẳng file, không mở Save-As.
|
||||
// fileName từ modal nếu user nhập (thêm đuôi theo defaultName nếu thiếu),
|
||||
// ngược lại dùng tên mặc định của flow export.
|
||||
const dir = (exportSettings.exportDir || '').trim();
|
||||
if (dir) {
|
||||
try {
|
||||
const ext = defaultName.includes('.') ? defaultName.split('.').pop() : 'bin';
|
||||
let fname = (exportSettings.fileName || '').trim();
|
||||
if (fname) {
|
||||
if (!fname.includes('.')) fname = fname + '.' + ext;
|
||||
else if (!fname.toLowerCase().endsWith('.' + ext.toLowerCase())) fname = fname + '.' + ext;
|
||||
} else {
|
||||
fname = defaultName;
|
||||
}
|
||||
const path = dir.replace(/[\\/]+$/, '') + '\\' + fname;
|
||||
const fd = new FormData();
|
||||
fd.append('path', path);
|
||||
fd.append('file', new File([blob], fname, { type: blob.type || 'application/octet-stream' }));
|
||||
const wr = await fetch(`${API_BASE_URL}/api/v1/files/write`, { method: 'POST', body: fd });
|
||||
if (wr.ok) {
|
||||
const w = await wr.json();
|
||||
return w.path;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const dlg = await fetch(`${API_BASE_URL}/api/v1/files/save-dialog`, {
|
||||
method: 'POST',
|
||||
@@ -30693,7 +30809,7 @@ STRICT CONSTRAINTS:
|
||||
const prefsRef = useRef({});
|
||||
prefsRef.current = {
|
||||
showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||
showMediaExplorer, showFxRack, showMidiEvents,
|
||||
showMediaExplorer, showMixer, showFxRack, showMidiEvents,
|
||||
panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
|
||||
};
|
||||
useEffect(() => {
|
||||
@@ -30705,7 +30821,7 @@ STRICT CONSTRAINTS:
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||
showMediaExplorer, showFxRack, showMidiEvents, panelPositions,
|
||||
showMediaExplorer, showMixer, showFxRack, showMidiEvents, panelPositions,
|
||||
rightSidebarWidth, mediaExplorerHeight, selectedProviderId, currentUser]);
|
||||
|
||||
const drawMixerVuMeter = (canvas, peak) => {
|
||||
@@ -30929,6 +31045,14 @@ STRICT CONSTRAINTS:
|
||||
// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
|
||||
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage.
|
||||
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
|
||||
// Restore transport loop selection (loop vùng chọn sau save/reload)
|
||||
if (result.loopSelection !== undefined && result.loopSelection) {
|
||||
setIsLoopingSelection(true);
|
||||
if (result.selectionStart != null && result.selectionEnd != null) {
|
||||
setSelectionStart(result.selectionStart);
|
||||
setSelectionEnd(result.selectionEnd);
|
||||
}
|
||||
}
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
if (result.masterVolL !== undefined && result.masterVolL !== null) setMasterVolL(result.masterVolL);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -51,7 +51,7 @@
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608241800" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608231900" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user