diff --git a/app/templates/index.html b/app/templates/index.html
index 695f82c..95eb89f 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -474,8 +474,9 @@
if (ctrl && e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndoRef.current(); return; }
if (ctrl && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedoRef.current(); return; }
if (ctrl && !alt && e.key === 'o') { e.preventDefault(); showToast('Open Project dialog','info'); return; }
+ if (ctrl && !alt && e.key === 'n') { e.preventDefault(); setTracks([{ id:'1', name:'Track 01', buffer:null, volume:0.8, muted:false, solo:false, color:'#0f766e', markers:[], serverFileId:null }, { id:'2', name:'Track 02', buffer:null, volume:0.8, muted:false, solo:false, color:'#1d4ed8', markers:[], serverFileId:null }]); setSelectedTrackId('1'); showToast('New project created','info'); return; }
if (ctrl && !alt && e.key === 's') { e.preventDefault(); showToast('Project saved','success'); return; }
- if (ctrl && alt && e.key === 's') { e.preventDefault(); showToast('Save As dialog','info'); return; }
+ if ((ctrl && alt && e.key === 's') || (ctrl && e.shiftKey && e.key === 's')) { e.preventDefault(); showToast('Save As dialog','info'); return; }
if (ctrl && !alt && e.key === 'i') { e.preventDefault(); addNewTrack(); return; }
if (ctrl && alt && e.key === 'i') { e.preventDefault(); showToast('Import audio','info'); return; }
if (ctrl && !alt && e.key === 'e') { e.preventDefault(); openTempTab(); return; }
@@ -722,6 +723,34 @@
};
const contextMenuCut = () => {
+ const t = tracks.find(x => x.id === contextMenu.trackId);
+ if (!t || !t.buffer) { contextMenuDelete(); return; }
+ const beforeSnap = captureTrackSnapshot(contextMenu.trackId);
+ const sr = t.buffer.sampleRate;
+ const data = t.buffer.getChannelData(0);
+ if (selLeft !== null && selRight !== null && selRight > selLeft) {
+ const startSample = Math.floor(selLeft * sr);
+ const endSample = Math.min(data.length, Math.floor(selRight * sr));
+ const len = endSample - startSample;
+ if (len > 0) {
+ const ctx = getAudioContext();
+ const clipBuffer = ctx.createBuffer(1, len, sr);
+ clipBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
+ clipboardRef.current = { buffer: clipBuffer, name: t.name, volume: t.volume, color: t.color };
+ const newLen = data.length - len;
+ const newBuffer = ctx.createBuffer(1, newLen, sr);
+ const newData = newBuffer.getChannelData(0);
+ let idx = 0;
+ for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
+ for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
+ setTracks(p => p.map(tr => tr.id === contextMenu.trackId ? { ...tr, buffer: newBuffer } : tr));
+ const afterSnap = captureTrackSnapshot(contextMenu.trackId);
+ pushAction('CUT', contextMenu.trackId, beforeSnap, afterSnap);
+ closeContextMenu();
+ showToast('Đã cắt vùng chọn vào clipboard.', 'info');
+ return;
+ }
+ }
contextMenuCopy();
contextMenuDelete();
};
@@ -816,9 +845,62 @@
};
const handleCopyTrack = () => {
const t = tracks.find(x => x.id === selectedTrackId);
- if (t) { clipboardRef.current = { buffer:t.buffer, name:t.name, volume:t.volume, color:t.color }; showToast('Copied track.','info'); }
+ if (!t || !t.buffer) return;
+ const sr = t.buffer.sampleRate;
+ const data = t.buffer.getChannelData(0);
+ // If selection exists, copy only the selected region
+ if (selLeft !== null && selRight !== null && selRight > selLeft) {
+ const startSample = Math.floor(selLeft * sr);
+ const endSample = Math.min(data.length, Math.floor(selRight * sr));
+ const len = endSample - startSample;
+ if (len > 0) {
+ const ctx = getAudioContext();
+ const clipBuffer = ctx.createBuffer(1, len, sr);
+ clipBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
+ clipboardRef.current = { buffer: clipBuffer, name: t.name, volume: t.volume, color: t.color };
+ showToast('Copied selection to clipboard.', 'info');
+ return;
+ }
+ }
+ // No selection: copy entire track
+ clipboardRef.current = { buffer: t.buffer, name: t.name, volume: t.volume, color: t.color };
+ showToast('Copied track to clipboard.', 'info');
+ };
+ const handleCutTrack = () => {
+ const t = tracks.find(x => x.id === selectedTrackId);
+ if (!t || !t.buffer) return;
+ const beforeSnap = captureTrackSnapshot(selectedTrackId);
+ const sr = t.buffer.sampleRate;
+ const data = t.buffer.getChannelData(0);
+ // If selection exists, cut only the selected region
+ if (selLeft !== null && selRight !== null && selRight > selLeft) {
+ const startSample = Math.floor(selLeft * sr);
+ const endSample = Math.min(data.length, Math.floor(selRight * sr));
+ const len = endSample - startSample;
+ if (len > 0) {
+ // Copy selection to clipboard
+ const ctx = getAudioContext();
+ const clipBuffer = ctx.createBuffer(1, len, sr);
+ clipBuffer.copyToChannel(data.subarray(startSample, endSample), 0);
+ clipboardRef.current = { buffer: clipBuffer, name: t.name, volume: t.volume, color: t.color };
+ // Remove selection from track, glue the two remaining parts
+ const newLen = data.length - len;
+ const newBuffer = ctx.createBuffer(1, newLen, sr);
+ const newData = newBuffer.getChannelData(0);
+ let idx = 0;
+ for (let i = 0; i < startSample; i++) newData[idx++] = data[i];
+ for (let i = endSample; i < data.length; i++) newData[idx++] = data[i];
+ setTracks(p => p.map(tr => tr.id === selectedTrackId ? { ...tr, buffer: newBuffer } : tr));
+ const afterSnap = captureTrackSnapshot(selectedTrackId);
+ pushAction('CUT', selectedTrackId, beforeSnap, afterSnap);
+ showToast('Cut selection to clipboard.', 'info');
+ return;
+ }
+ }
+ // No selection: cut entire track (copy + delete)
+ handleCopyTrack();
+ handleDeleteTrack();
};
- const handleCutTrack = () => { handleCopyTrack(); handleDeleteTrack(); };
const handleDeleteTrack = () => {
const tid = selectedTrackId;
setTracks(p => p.filter(t => t.id !== tid));
@@ -1857,31 +1939,31 @@
{/* ── Menu Bar ── */}
{[
- { label: 'File', items: [
- { label: 'New Project', icon: 'file-plus', action: () => { setTracks([{ id:'1', name:'Track 01', buffer:null, volume:0.8, muted:false, solo:false, color:'#0f766e', markers:[], serverFileId:null }, { id:'2', name:'Track 02', buffer:null, volume:0.8, muted:false, solo:false, color:'#1d4ed8', markers:[], serverFileId:null }]); setSelectedTrackId('1'); showToast('New project created','info'); } },
- { label: 'Open Project...', icon: 'folder-open', action: () => showToast('Open project dialog','info') },
- { label: 'Save Project', icon: 'save', action: () => showToast('Project saved','success') },
- { label: 'Save As...', icon: 'save', action: () => showToast('Save as dialog','info') },
+ { label: 'File', items: [
+ { label: 'New Project', icon: 'file-plus', shortcut: 'Ctrl+N', action: () => { setTracks([{ id:'1', name:'Track 01', buffer:null, volume:0.8, muted:false, solo:false, color:'#0f766e', markers:[], serverFileId:null }, { id:'2', name:'Track 02', buffer:null, volume:0.8, muted:false, solo:false, color:'#1d4ed8', markers:[], serverFileId:null }]); setSelectedTrackId('1'); showToast('New project created','info'); } },
+ { label: 'Open Project...', icon: 'folder-open', shortcut: 'Ctrl+O', action: () => showToast('Open project dialog','info') },
+ { label: 'Save Project', icon: 'save', shortcut: 'Ctrl+S', action: () => showToast('Project saved','success') },
+ { label: 'Save As...', icon: 'save', shortcut: 'Ctrl+Alt+S', action: () => showToast('Save as dialog','info') },
{ label: 'Save to Cloud', icon: 'upload-cloud', action: () => showToast('Saving to cloud...','info') },
{ sep: true },
- { label: 'Import Audio...', icon: 'file-input', action: () => { const input = document.createElement('input'); input.type='file'; input.accept='audio/*'; input.onchange=async (e)=>{ if(e.target.files[0]){ addNewTrack(); const newId=(tracks.length+1).toString(); setTimeout(()=>loadFileOnTrack(newId,e.target.files[0]),100); } }; input.click(); showToast('Import audio','info'); } },
+ { label: 'Import Audio...', icon: 'file-input', shortcut: 'Ctrl+Alt+I', action: () => { const input = document.createElement('input'); input.type='file'; input.accept='audio/*'; input.onchange=async (e)=>{ if(e.target.files[0]){ addNewTrack(); const newId=(tracks.length+1).toString(); setTimeout(()=>loadFileOnTrack(newId,e.target.files[0]),100); } }; input.click(); showToast('Import audio','info'); } },
{ label: 'Export Mix...', icon: 'file-output', action: () => triggerWavExport() },
{ sep: true },
{ label: 'Logout', icon: 'log-out', action: () => showToast('Logged out','info') },
]},
{ label: 'Edit', items: [
- { label: 'Insert New Track', icon: 'plus', action: addNewTrack },
- { label: 'Insert Music to Track', icon: 'music', action: () => showToast('Select music file to insert','info') },
+ { label: 'Insert New Track', icon: 'plus', shortcut: 'Ctrl+I', action: addNewTrack },
+ { label: 'Insert Music to Track', icon: 'music', shortcut: 'Ctrl+Alt+I', action: () => showToast('Select music file to insert','info') },
{ sep: true },
- { label: 'Edit in New Tab', icon: 'file-edit', action: () => openTempTab() },
- { label: 'Split at Playhead', icon: 'scissors', action: () => handleSplitTrack(selectedTrackId) },
- { label: 'Merge Tracks', icon: 'combine', action: () => { handleMergeTracks(); } },
+ { label: 'Edit in New Tab', icon: 'file-edit', shortcut: 'Ctrl+E', action: () => openTempTab() },
+ { label: 'Split at Playhead', icon: 'scissors', shortcut: 'S', action: () => handleSplitTrack(selectedTrackId) },
+ { label: 'Merge Tracks', icon: 'combine', shortcut: 'Ctrl+M', action: () => { handleMergeTracks(); } },
{ sep: true },
- { label: 'Copy', icon: 'copy', action: () => { handleCopyTrack(); } },
- { label: 'Cut', icon: 'scissors', action: () => { handleCutTrack(); } },
- { label: 'Paste', icon: 'clipboard', action: contextMenuPaste },
+ { label: 'Copy', icon: 'copy', shortcut: 'Ctrl+C', action: () => { handleCopyTrack(); } },
+ { label: 'Cut', icon: 'scissors', shortcut: 'Ctrl+X', action: () => { handleCutTrack(); } },
+ { label: 'Paste', icon: 'clipboard', shortcut: 'Ctrl+V', action: contextMenuPaste },
{ sep: true },
- { label: 'Delete Track', icon: 'trash-2', action: () => { handleDeleteTrack(); } },
+ { label: 'Delete Track', icon: 'trash-2', shortcut: 'Del', action: () => { handleDeleteTrack(); } },
]},
{ label: 'View', items: [
{ label: 'Master Track', icon: 'disc', action: () => showToast('Master track view','info') },
@@ -1896,7 +1978,6 @@
]},
{ label: 'Help', items: [
{ label: 'About SonicForge', icon: 'info', action: () => showToast('SonicForge Studio v1.0 - Professional DAW','info') },
- { label: 'Keyboard Shortcuts', icon: 'keyboard', action: () => showToast('Ctrl+Z:Undo Ctrl+Y:Redo Space:Play S:Split Del:Delete Ctrl+I:NewTrack Ctrl+Alt+I:Import Ctrl+E:EditTab Ctrl+M:Merge Ctrl+C:Copy Ctrl+X:Cut Ctrl+V:Paste Ctrl+O:Open Ctrl+S:Save Ctrl+Alt+S:SaveAs','info') },
]},
].map(menu => (
@@ -1916,7 +1997,9 @@
) : (
))}
@@ -2455,30 +2538,44 @@
{/* ── Context Menu ── */}
{contextMenu && (
- e.stopPropagation()}>
)}