fix: chỉnh sửa hiển thị của audio clip trong subtab
This commit is contained in:
+248
-117
@@ -668,7 +668,7 @@
|
||||
};
|
||||
|
||||
// ── Sub-Tab Waveform Component (LOOP_EDITOR_2.md §1.2 & SUB_EDITOR.md) ──
|
||||
const SubTabWaveform = ({ buffer, subTabId, activeTab, currentTime, selectionStart, selectionEnd, onSelectRange, onPlayheadSet, onContextMenu, activeTool, zoom, timelineWidth }) => {
|
||||
const SubTabWaveform = ({ buffer, subTabId, activeTab, currentTime, selectionStart, selectionEnd, onSelectRange, onPlayheadSet, onContextMenu, activeTool, zoom, timelineWidth, color, name }) => {
|
||||
const canvasRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -689,10 +689,33 @@
|
||||
const len = data.length;
|
||||
if (len === 0) return;
|
||||
|
||||
// Draw waveform only in viewport range for performance & zero-line accuracy
|
||||
// Draw clip container (like main session clips)
|
||||
const xStart = 0;
|
||||
const wClip = buffer.duration * zoom;
|
||||
const xEnd = xStart + wClip;
|
||||
const clipTop = 8;
|
||||
const clipHeight = h - 16;
|
||||
const clipColor = color || '#06b6d4';
|
||||
|
||||
ctx.fillStyle = clipColor + '22';
|
||||
ctx.strokeStyle = clipColor;
|
||||
ctx.lineWidth = 1.5;
|
||||
if (ctx.roundRect) {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(xStart, clipTop, wClip, clipHeight, 4);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fillRect(xStart, clipTop, wClip, clipHeight);
|
||||
ctx.strokeRect(xStart, clipTop, wClip, clipHeight);
|
||||
}
|
||||
|
||||
// Draw clip label
|
||||
ctx.fillStyle = '#e4e4e7';
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.fillText(name || 'Audio Clip', xStart + 8, clipTop + 14);
|
||||
|
||||
// Draw waveform inside clip
|
||||
const drawXStart = Math.max(0, Math.floor(xStart));
|
||||
const drawXEnd = Math.min(w, Math.ceil(xEnd));
|
||||
const samplesPerPixel = buffer.sampleRate / zoom;
|
||||
@@ -715,8 +738,8 @@
|
||||
if (val > maxVal) maxVal = val;
|
||||
if (val < minVal) minVal = val;
|
||||
}
|
||||
const yTop = mid + (minVal * (h * 0.45));
|
||||
const yBottom = mid + (maxVal * (h * 0.45));
|
||||
const yTop = mid + (minVal * (clipHeight * 0.45));
|
||||
const yBottom = mid + (maxVal * (clipHeight * 0.45));
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px, yTop);
|
||||
ctx.lineTo(px, yBottom);
|
||||
@@ -750,24 +773,27 @@
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
}, [buffer, currentTime, selectionStart, selectionEnd, zoom, timelineWidth]);
|
||||
}, [buffer, currentTime, selectionStart, selectionEnd, zoom, timelineWidth, color, name]);
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
if (e.button === 2) return; // ignore right click
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const startX = e.clientX - rect.left;
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const parent = canvas.parentElement;
|
||||
const scrollContainer = parent ? parent.parentElement : null;
|
||||
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
|
||||
const duration = buffer.duration;
|
||||
const startTime = (startX / rect.width) * duration;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const startTime = Math.max(0, Math.min(duration, mouseX / zoom));
|
||||
|
||||
// Tool-specific behavior
|
||||
if (activeTool === 'grab') {
|
||||
// Grab tool: move playhead on click/drag
|
||||
onPlayheadSet(startTime);
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const currentX = moveEvent.clientX - rect.left;
|
||||
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration));
|
||||
onPlayheadSet(currentTime);
|
||||
const currentX = moveEvent.clientX - rect.left + scrollLeft;
|
||||
const ct = Math.max(0, Math.min(duration, currentX / zoom));
|
||||
onPlayheadSet(ct);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
@@ -781,29 +807,25 @@
|
||||
}
|
||||
|
||||
if (activeTool === 'razor') {
|
||||
// Razor tool: add a cut marker at click position
|
||||
// For sub-tab, we can store cut points in the subTab state
|
||||
onPlayheadSet(startTime);
|
||||
showToast(`Cut point at ${formatTime(startTime)}`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTool === 'pen') {
|
||||
// Pen tool: for volume automation drawing
|
||||
// For now, just move playhead and show indicator
|
||||
onPlayheadSet(startTime);
|
||||
canvasRef.current.style.cursor = 'crosshair';
|
||||
canvas.style.cursor = 'crosshair';
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const currentX = moveEvent.clientX - rect.left;
|
||||
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration));
|
||||
onPlayheadSet(currentTime);
|
||||
const currentX = moveEvent.clientX - rect.left + scrollLeft;
|
||||
const ct = Math.max(0, Math.min(duration, currentX / zoom));
|
||||
onPlayheadSet(ct);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
canvasRef.current.style.cursor = 'crosshair';
|
||||
canvas.style.cursor = 'crosshair';
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@@ -816,9 +838,9 @@
|
||||
onPlayheadSet(startTime);
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const currentX = moveEvent.clientX - rect.left;
|
||||
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration));
|
||||
onSelectRange(startTime, currentTime);
|
||||
const currentX = moveEvent.clientX - rect.left + scrollLeft;
|
||||
const ct = Math.max(0, Math.min(duration, currentX / zoom));
|
||||
onSelectRange(startTime, ct);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
@@ -833,16 +855,20 @@
|
||||
const handleContextMenuInternal = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const clickTime = (x / rect.width) * buffer.duration;
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const parent = canvas.parentElement;
|
||||
const scrollContainer = parent ? parent.parentElement : null;
|
||||
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
|
||||
const x = e.clientX - rect.left + scrollLeft;
|
||||
const clickTime = Math.max(0, Math.min(buffer.duration, x / zoom));
|
||||
onContextMenu(e, clickTime);
|
||||
};
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-48 rounded border border-zinc-800 cursor-crosshair"
|
||||
className="w-full h-full rounded border border-zinc-800 cursor-crosshair"
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenuInternal}
|
||||
/>
|
||||
@@ -1031,6 +1057,7 @@
|
||||
const [beginBar, setBeginBar] = useState(1);
|
||||
const [endBar, setEndBar] = useState(1);
|
||||
const [numberBar, setNumberBar] = useState(1);
|
||||
const [subTabHeight, setSubTabHeight] = useState(96);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [soloedTrackId, setSoloedTrackId] = useState(null);
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
@@ -1038,7 +1065,7 @@
|
||||
const [showExportPanel, setShowExportPanel] = useState(true);
|
||||
const [showAIPanel, setShowAIPanel] = useState(true);
|
||||
const [showSelectionPanel, setShowSelectionPanel] = useState(true);
|
||||
const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', selection: 'right' });
|
||||
const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', selection: 'bottom' });
|
||||
const [panelDropZone, setPanelDropZone] = useState(null);
|
||||
const [dragGhostPos, setDragGhostPos] = useState(null);
|
||||
const [dragGhostPanel, setDragGhostPanel] = useState(null);
|
||||
@@ -1630,6 +1657,15 @@
|
||||
showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a subtab for this track+range already exists
|
||||
const existing = subTabs.find(s => s.trackId === trackId && s.startTime === selLeft && s.endTime === selRight);
|
||||
if (existing) {
|
||||
setActiveTab(existing.id);
|
||||
showToast(`Sub Tab already open.`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const sr = t.buffer.sampleRate;
|
||||
const trackStart = t.startTime || 0;
|
||||
const relSelLeft = Math.max(0, selLeft - trackStart);
|
||||
@@ -1666,46 +1702,55 @@
|
||||
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info');
|
||||
};
|
||||
|
||||
const handleEditClipInSubTab = (trackId, clipId) => {
|
||||
const track = tracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
|
||||
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{
|
||||
id: 'default_' + track.id,
|
||||
buffer: track.buffer,
|
||||
startTime: track.startTime || 0,
|
||||
name: track.name
|
||||
}] : []);
|
||||
|
||||
const clip = clips.find(c => c.id === clipId || (clipId === 'default' && c.id === 'default_' + trackId));
|
||||
if (!clip || !clip.buffer) return;
|
||||
const handleEditClipInSubTab = (trackId, clipId) => {
|
||||
const track = tracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
|
||||
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{
|
||||
id: 'default_' + track.id,
|
||||
buffer: track.buffer,
|
||||
startTime: track.startTime || 0,
|
||||
name: track.name
|
||||
}] : []);
|
||||
|
||||
const clip = clips.find(c => c.id === clipId || (clipId === 'default' && c.id === 'default_' + trackId));
|
||||
if (!clip || !clip.buffer) return;
|
||||
|
||||
const sr = clip.buffer.sampleRate;
|
||||
const len = clip.buffer.length;
|
||||
const ctx = getAudioContext();
|
||||
const subBuffer = ctx.createBuffer(1, len, sr);
|
||||
subBuffer.copyToChannel(clip.buffer.getChannelData(0), 0);
|
||||
const resolvedClipId = clip.id === 'default' ? 'default_' + trackId : clip.id;
|
||||
// Check if a subtab for this clip already exists
|
||||
const existing = subTabs.find(s => s.clipId === resolvedClipId && s.trackId === trackId);
|
||||
if (existing) {
|
||||
setActiveTab(existing.id);
|
||||
showToast(`Sub Tab for "${clip.name}" already open.`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = 'subtab_' + Date.now();
|
||||
const tabLabel = `Edit_${clip.name.replace('.wav','').slice(0,10)}`;
|
||||
const sr = clip.buffer.sampleRate;
|
||||
const len = clip.buffer.length;
|
||||
const ctx = getAudioContext();
|
||||
const subBuffer = ctx.createBuffer(1, len, sr);
|
||||
subBuffer.copyToChannel(clip.buffer.getChannelData(0), 0);
|
||||
|
||||
setSubTabs(prev => [...prev, {
|
||||
id: tabId,
|
||||
label: tabLabel,
|
||||
trackId: trackId,
|
||||
clipId: clip.id === 'default' ? 'default_' + trackId : clip.id,
|
||||
startTime: clip.startTime,
|
||||
endTime: clip.startTime + clip.buffer.duration,
|
||||
buffer: subBuffer,
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 },
|
||||
currentTime: 0,
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
isPlaying: false
|
||||
}]);
|
||||
setActiveTab(tabId);
|
||||
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info');
|
||||
};
|
||||
const tabId = 'subtab_' + Date.now();
|
||||
const tabLabel = `Edit_${clip.name.replace('.wav','').slice(0,10)}`;
|
||||
|
||||
setSubTabs(prev => [...prev, {
|
||||
id: tabId,
|
||||
label: tabLabel,
|
||||
trackId: trackId,
|
||||
clipId: resolvedClipId,
|
||||
startTime: clip.startTime,
|
||||
endTime: clip.startTime + clip.buffer.duration,
|
||||
buffer: subBuffer,
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 },
|
||||
currentTime: 0,
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
isPlaying: false
|
||||
}]);
|
||||
setActiveTab(tabId);
|
||||
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info');
|
||||
};
|
||||
|
||||
// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
|
||||
const applySubTab = (tabId) => {
|
||||
@@ -2710,6 +2755,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubTabResizeMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const startY = e.clientY;
|
||||
const startHeight = subTabHeight;
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const deltaY = moveEvent.clientY - startY;
|
||||
const newHeight = Math.max(48, Math.min(400, startHeight + deltaY));
|
||||
setSubTabHeight(newHeight);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// ── Selection ──
|
||||
const clearLocalSelection = () => {
|
||||
setSelectionMode(null);
|
||||
@@ -2718,36 +2784,38 @@
|
||||
setLocalSelectionEnd(null);
|
||||
};
|
||||
|
||||
const handleRulerMouseDown = (e) => {
|
||||
const wrapper = timelineWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const time = mouseX / zoom;
|
||||
|
||||
clearLocalSelection();
|
||||
setSelectionMode('global');
|
||||
rulerDragStartRef.current = time;
|
||||
isDraggingRulerRef.current = true;
|
||||
setSelectionStart(time);
|
||||
setSelectionEnd(time);
|
||||
setCurrentTime(time);
|
||||
};
|
||||
const handleRulerMouseDown = (e) => {
|
||||
const wrapper = timelineWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const rawTime = mouseX / zoom;
|
||||
const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime;
|
||||
|
||||
clearLocalSelection();
|
||||
setSelectionMode('global');
|
||||
rulerDragStartRef.current = time;
|
||||
isDraggingRulerRef.current = true;
|
||||
setSelectionStart(time);
|
||||
setSelectionEnd(time);
|
||||
setCurrentTime(time);
|
||||
};
|
||||
|
||||
// Global Ruler mousemove is tracked via document listener set up in useEffect
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDraggingRulerRef.current) return;
|
||||
const wrapper = timelineWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const time = Math.max(0, Math.min(maxDuration, mouseX / zoom));
|
||||
|
||||
setSelectionEnd(time);
|
||||
};
|
||||
// Global Ruler mousemove is tracked via document listener set up in useEffect
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDraggingRulerRef.current) return;
|
||||
const wrapper = timelineWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollLeft = wrapper.scrollLeft;
|
||||
const mouseX = e.clientX - rect.left + scrollLeft;
|
||||
const rawTime = Math.max(0, Math.min(maxDuration, mouseX / zoom));
|
||||
const time = snapValueRef.current !== 'free' ? snapTime(rawTime, snapValueRef.current, bpmRef.current) : rawTime;
|
||||
|
||||
setSelectionEnd(time);
|
||||
};
|
||||
const handleMouseUp = () => {
|
||||
if (isDraggingRulerRef.current) {
|
||||
isDraggingRulerRef.current = false;
|
||||
@@ -4302,41 +4370,70 @@
|
||||
}`} title="Bật/Tắt Lặp vùng chọn">
|
||||
<i data-lucide="repeat" className="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<span className="text-[9px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
|
||||
<span className="text-[14px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
|
||||
<select value={snapValue} onChange={e => setSnapValue(e.target.value)}
|
||||
className="bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer">
|
||||
className="bg-black text-white text-[14px] px-1.5 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer">
|
||||
<option value="free">Free</option><option value="1">1</option>
|
||||
<option value="1/2">1/2</option><option value="1/4">1/4</option>
|
||||
<option value="1/8">1/8</option><option value="1/16">1/16</option><option value="1/32">1/32</option>
|
||||
</select>
|
||||
<div className="w-[1px] h-5 bg-zinc-800 mx-1"></div>
|
||||
<span className="text-[9px] text-zinc-500 font-bold">Bars:</span>
|
||||
<div className="w-[1px] h-6 bg-zinc-800 mx-1.5"></div>
|
||||
<span className="text-[14px] text-zinc-500 font-bold">Bars:</span>
|
||||
<input type="number" min="1" value={beginBar} onChange={e => {
|
||||
const b = parseInt(e.target.value) || 1;
|
||||
setBeginBar(b);
|
||||
const beatDuration = 60 / parseInt(bpm || 120);
|
||||
const t = (b - 1) * beatDuration * 4;
|
||||
setSelectionStart(t); setSelectionEnd(Math.max(t, selRight || t));
|
||||
}} className="w-10 bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[9px] text-zinc-500">-</span>
|
||||
clearLocalSelection();
|
||||
setSelectionMode('global');
|
||||
setSelectionStart(t);
|
||||
setSelectionEnd(t + beatDuration * 4);
|
||||
}} className="w-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[14px] text-zinc-500">-</span>
|
||||
<input type="number" min="1" value={endBar} onChange={e => {
|
||||
const b = parseInt(e.target.value) || 1;
|
||||
setEndBar(b);
|
||||
const beatDuration = 60 / parseInt(bpm || 120);
|
||||
const t = (b - 1) * beatDuration * 4;
|
||||
setSelectionEnd(t);
|
||||
setSelectionEnd(t + beatDuration * 4);
|
||||
setNumberBar(b - beginBar + 1);
|
||||
}} className="w-10 bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[9px] text-zinc-500">#</span>
|
||||
}} className="w-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[14px] text-zinc-500">#</span>
|
||||
<input type="number" min="1" value={numberBar} readOnly
|
||||
className="w-10 bg-black text-zinc-400 text-[9px] px-1 py-0.5 rounded border border-zinc-800 text-center font-mono" />
|
||||
<div className="w-[1px] h-5 bg-zinc-800 mx-1"></div>
|
||||
<span className="text-[9px] text-zinc-500">Start:</span>
|
||||
<span className="text-[9px] font-mono text-zinc-200 w-14">{selLeft !== null ? formatTime(selLeft) : '--:--'}</span>
|
||||
<span className="text-[9px] text-zinc-500">End:</span>
|
||||
<span className="text-[9px] font-mono text-zinc-200 w-14">{selRight !== null ? formatTime(selRight) : '--:--'}</span>
|
||||
<span className="text-[9px] text-zinc-500">Len:</span>
|
||||
<span className="text-[9px] font-mono text-amber-400 w-14">{selLeft !== null && selRight !== null ? formatTime(Math.abs(selRight - selLeft)) : '--:--'}</span>
|
||||
className="w-14 bg-black text-zinc-400 text-[14px] px-1 py-0.5 rounded border border-zinc-800 text-center font-mono" />
|
||||
<div className="w-[1px] h-6 bg-zinc-800 mx-1.5"></div>
|
||||
<span className="text-[14px] text-zinc-500">Start:</span>
|
||||
<input type="text" value={selLeft !== null ? formatTime(selLeft) : ''}
|
||||
onChange={e => {
|
||||
const parts = e.target.value.split(/[:.]/);
|
||||
if (parts.length === 3) {
|
||||
const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
|
||||
clearLocalSelection();
|
||||
setSelectionMode('global');
|
||||
setSelectionStart(secs);
|
||||
}
|
||||
}}
|
||||
className="w-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[14px] text-zinc-500">End:</span>
|
||||
<input type="text" value={selRight !== null ? formatTime(selRight) : ''}
|
||||
onChange={e => {
|
||||
const parts = e.target.value.split(/[:.]/);
|
||||
if (parts.length === 3) {
|
||||
const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
|
||||
setSelectionEnd(secs);
|
||||
}
|
||||
}}
|
||||
className="w-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<span className="text-[14px] text-zinc-500">Len:</span>
|
||||
<input type="text" value={selLeft !== null && selRight !== null ? formatTime(Math.abs(selRight - selLeft)) : ''}
|
||||
onChange={e => {
|
||||
const parts = e.target.value.split(/[:.]/);
|
||||
if (parts.length === 3 && selLeft !== null) {
|
||||
const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
|
||||
setSelectionEnd(selLeft + secs);
|
||||
}
|
||||
}}
|
||||
className="w-20 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
|
||||
<div className="flex-1"></div>
|
||||
<div className="text-xs font-bold font-mono text-zinc-100">{formatTime(currentTime)}</div>
|
||||
</div>
|
||||
@@ -4446,6 +4543,36 @@
|
||||
<span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{selectionStats.length}s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800 mt-1">
|
||||
<div>
|
||||
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Begin Bar</span>
|
||||
<input type="number" min="1" value={beginBar} onChange={e => {
|
||||
const b = parseInt(e.target.value) || 1;
|
||||
setBeginBar(b);
|
||||
const beatDuration = 60 / parseInt(bpm || 120);
|
||||
const t = (b - 1) * beatDuration * 4;
|
||||
clearLocalSelection();
|
||||
setSelectionMode('global');
|
||||
setSelectionStart(t);
|
||||
setSelectionEnd(t + beatDuration * 4);
|
||||
}} className="w-full bg-[#242424] text-white text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">End Bar</span>
|
||||
<input type="number" min="1" value={endBar} onChange={e => {
|
||||
const b = parseInt(e.target.value) || 1;
|
||||
setEndBar(b);
|
||||
const beatDuration = 60 / parseInt(bpm || 120);
|
||||
const t = (b - 1) * beatDuration * 4;
|
||||
setSelectionEnd(t + beatDuration * 4);
|
||||
setNumberBar(b - beginBar + 1);
|
||||
}} className="w-full bg-[#242424] text-white text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-[7px] text-zinc-500 font-bold uppercase"># Bars</span>
|
||||
<span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{numberBar}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
@@ -4591,7 +4718,7 @@
|
||||
<div ref={rulerRef} className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
|
||||
{Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
|
||||
const sec = i; const x = sec * zoom;
|
||||
return (<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
|
||||
return (<div key={i} className="absolute h-full border-l border-zinc-700 pl-1 pt-1 text-[14px] font-mono text-zinc-300 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -4748,10 +4875,10 @@
|
||||
<div ref={timelineWrapperRef} className="flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0">
|
||||
<div style={{ width: `${subTabTimelineWidth}px` }} className="relative flex flex-col min-h-full">
|
||||
<div className="sticky top-0 z-45 flex h-10 border-b border-zinc-900 bg-[#242424] shrink-0">
|
||||
<div className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden">
|
||||
<div className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
|
||||
{Array.from({ length: Math.ceil(st.buffer ? st.buffer.duration : 0) }).map((_, i) => {
|
||||
const sec = i; const x = sec * zoom;
|
||||
return (<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
|
||||
return (<div key={i} className="absolute h-full border-l border-zinc-700 pl-1 pt-1 text-[14px] font-mono text-zinc-300 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -4763,7 +4890,7 @@
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col relative bg-[#111111] min-h-full">
|
||||
{vTrack && (
|
||||
<div className="flex-1 relative overflow-hidden border-b border-[#141414]">
|
||||
<div style={{ height: `${subTabHeight}px` }} className="relative overflow-hidden border-2 border-purple-500/40 rounded bg-[#151515] mx-1 mt-1 shrink-0">
|
||||
<SubTabWaveform
|
||||
buffer={st.buffer}
|
||||
subTabId={st.id}
|
||||
@@ -4777,7 +4904,11 @@
|
||||
onContextMenu={(e, clickTime) => setContextMenu({x: e.clientX, y: e.clientY, isSubTab: true, subTabId: st.id, time: clickTime})}
|
||||
zoom={zoom}
|
||||
timelineWidth={subTabTimelineWidth}
|
||||
color={vTrack.color}
|
||||
name={vTrack.name}
|
||||
/>
|
||||
<div onMouseDown={handleSubTabResizeMouseDown}
|
||||
className="absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors" />
|
||||
</div>
|
||||
)}
|
||||
{st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && (
|
||||
|
||||
Reference in New Issue
Block a user