fix: time selection show handles

This commit is contained in:
2026-07-20 16:25:46 +07:00
parent 9e936144e1
commit 271f0583f4
2 changed files with 60 additions and 46 deletions
Binary file not shown.
+53 -39
View File
@@ -200,6 +200,8 @@
markers, markers,
selectionMode, selectionMode,
localSelectionTrackId, localSelectionTrackId,
localSelectionStart,
currentTime,
localSelLeft, localSelLeft,
localSelRight, localSelRight,
onTrackLaneMouseDown, onTrackLaneMouseDown,
@@ -294,7 +296,7 @@
// 1. Draw Clip Layer Background & Border // 1. Draw Clip Layer Background & Border
ctx.fillStyle = isClipSelected ? (track.color ? track.color + '44' : 'rgba(6, 182, 212, 0.30)') : (track.color ? track.color + '22' : 'rgba(6, 182, 212, 0.12)'); ctx.fillStyle = isClipSelected ? (track.color ? track.color + '44' : 'rgba(6, 182, 212, 0.30)') : (track.color ? track.color + '22' : 'rgba(6, 182, 212, 0.12)');
ctx.strokeStyle = isClipSelected ? '#fbbf24' : (track.color || '#06b6d4'); ctx.strokeStyle = isClipSelected ? '#fbbf24' : (track.color || '#06b6d4');
ctx.lineWidth = isClipSelected ? 3 : 1.5; ctx.lineWidth = isClipSelected ? 1 : 1.5;
const clipTop = 8; const clipTop = 8;
const clipHeight = height - 16; const clipHeight = height - 16;
@@ -379,7 +381,7 @@
ctx.fillStyle = 'rgba(245, 158, 11, 0.15)'; ctx.fillStyle = 'rgba(245, 158, 11, 0.15)';
ctx.fillRect(hlLeft, 0, hlWidth, height); ctx.fillRect(hlLeft, 0, hlWidth, height);
ctx.strokeStyle = '#f59e0b'; ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 1.5; ctx.lineWidth = 1;
ctx.strokeRect(hlLeft, 0, hlWidth, height); ctx.strokeRect(hlLeft, 0, hlWidth, height);
} }
@@ -537,7 +539,7 @@
onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey); onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey);
} }
} else { } else {
onPlayheadSet(time, e.shiftKey); onPlayheadSet(time);
} }
return; return;
} }
@@ -564,7 +566,22 @@
return; return;
} }
onPlayheadSet(time, e.shiftKey); if (e.shiftKey) {
// Shift+click on track: extend LOCAL selection from current start to clicked position
const prevStart = localSelectionStart !== null ? localSelectionStart : currentTime;
const selS = Math.min(prevStart, time);
const selE = Math.max(prevStart, time);
setSelectionMode('local');
setLocalSelectionTrackId(track.id);
setLocalSelectionStart(selS);
setLocalSelectionEnd(selE);
setSelectionStart(selS);
setSelectionEnd(selE);
setCurrentTime(time);
e.stopPropagation();
return;
}
onPlayheadSet(time);
if (onTrackLaneMouseDown) { if (onTrackLaneMouseDown) {
onTrackLaneMouseDown(track.id, time, e); onTrackLaneMouseDown(track.id, time, e);
} }
@@ -609,7 +626,7 @@
); );
}; };
const TempoTrackLane = ({ bpm, zoom, timelineWidth, onPlayheadSet, snapValue }) => { const TempoTrackLane = ({ bpm, zoom, timelineWidth, onPlayheadSet, snapValue, onRulerMouseDown }) => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -705,7 +722,15 @@
const rect = canvasRef.current.getBoundingClientRect(); const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left + scrollLeft; const x = e.clientX - rect.left + scrollLeft;
const time = Math.max(0, x / zoom); const time = Math.max(0, x / zoom);
onPlayheadSet(time, e.shiftKey); if (e.shiftKey) {
// Shift+click on tempo track: extend global selection from currentTime to clicked position
if (onRulerMouseDown) onRulerMouseDown({ ...e, shiftKey: true });
else onPlayheadSet(time);
} else {
// Click on tempo track: set playhead and start global selection point
if (onRulerMouseDown) onRulerMouseDown(e);
else onPlayheadSet(time);
}
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
@@ -4519,35 +4544,9 @@
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
// ── Playhead set with seek+play & shift+click selection ── // ── Playhead set with seek+play ──
const handlePlayheadSet = (time, shiftKey) => { const handlePlayheadSet = (time, shiftKey) => {
if (shiftKey) {
// Shift+click: extend selection from currentTime to clicked position
const selStart = Math.min(currentTime, time);
const selEnd = Math.max(currentTime, time);
setSelectionStart(selStart);
setSelectionEnd(selEnd);
setSelectionMode('global');
clearLocalSelection();
if (isLoopingSelection) {
// If looping, restart loop from new selection start
stopAllPlayback();
setCurrentTime(selStart);
if (isPlaying) { if (isPlaying) {
setTimeout(() => {
startOffsetTimeRef.current = selStart;
startAudioTimeRef.current = getAudioContext().currentTime;
startTrackPlayback(selStart);
setIsPlaying(true);
}, 50);
}
} else if (isPlaying) {
// Continue playing, just update the playhead visually
setCurrentTime(time);
} else {
setCurrentTime(time);
}
} else if (isPlaying) {
// Click during playback: seek to position and continue playing // Click during playback: seek to position and continue playing
setCurrentTime(time); setCurrentTime(time);
stopAllPlayback(); stopAllPlayback();
@@ -4582,9 +4581,15 @@
setSelectionMode('global'); setSelectionMode('global');
rulerDragStartRef.current = time; rulerDragStartRef.current = time;
isDraggingRulerRef.current = true; isDraggingRulerRef.current = true;
if (e.shiftKey) {
// Shift+click on ruler: extend global selection from currentTime to clicked position
setSelectionStart(Math.min(currentTime, time));
setSelectionEnd(Math.max(currentTime, time));
} else {
setSelectionStart(time); setSelectionStart(time);
setSelectionEnd(time); setSelectionEnd(time);
handlePlayheadSet(time, e.shiftKey); }
handlePlayheadSet(time);
}; };
// Global Ruler mousemove is tracked via document listener set up in useEffect // Global Ruler mousemove is tracked via document listener set up in useEffect
@@ -6912,10 +6917,17 @@
})} })}
</div> </div>
</div> </div>
{/* Global selection overlay for main session */}
{selectionMode === 'global' && selLeft !== null && selRight !== null && selRight > selLeft && (
<div className="absolute inset-0 pointer-events-none z-20"
style={{ left: `${selLeft * zoom}px`, width: `${(selRight - selLeft) * zoom}px`, top: '40px' }}>
<div className="w-full h-full bg-amber-500/10" style={{ borderLeft: '1px solid #f59e0b', borderRight: '1px solid #f59e0b' }}></div>
</div>
)}
<div className="sticky top-10 z-35 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] shrink-0"> <div className="sticky top-10 z-35 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] shrink-0">
<div className="flex-1 relative h-full overflow-hidden bg-[#1a1a2e]"> <div className="flex-1 relative h-full overflow-hidden bg-[#1a1a2e]">
<TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth} <TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth}
onPlayheadSet={handlePlayheadSet} snapValue={snapValue} /> onPlayheadSet={handlePlayheadSet} snapValue={snapValue} onRulerMouseDown={handleRulerMouseDown} />
</div> </div>
</div> </div>
<div className="flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full"> <div className="flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full">
@@ -6943,6 +6955,8 @@
snapValue={snapValue} bpm={bpm} snapValue={snapValue} bpm={bpm}
selectionMode={selectionMode} selectionMode={selectionMode}
localSelectionTrackId={localSelectionTrackId} localSelectionTrackId={localSelectionTrackId}
localSelectionStart={localSelectionStart}
currentTime={currentTime}
localSelLeft={localSelectionStart !== null && localSelectionEnd !== null ? Math.min(localSelectionStart, localSelectionEnd) : null} localSelLeft={localSelectionStart !== null && localSelectionEnd !== null ? Math.min(localSelectionStart, localSelectionEnd) : null}
localSelRight={localSelectionStart !== null && localSelectionEnd !== null ? Math.max(localSelectionStart, localSelectionEnd) : null} /> localSelRight={localSelectionStart !== null && localSelectionEnd !== null ? Math.max(localSelectionStart, localSelectionEnd) : null} />
{track.buffer && ( {track.buffer && (
@@ -6965,10 +6979,10 @@
<div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing" <div className="absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing"
style={{ left: `${selLeft * zoom}px`, width: `${(selRight - selLeft) * zoom}px` }} style={{ left: `${selLeft * zoom}px`, width: `${(selRight - selLeft) * zoom}px` }}
onMouseDown={handleSelectionBodyDragStart}> onMouseDown={handleSelectionBodyDragStart}>
<div className="absolute -left-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30" <div className="absolute -left-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30"
onMouseDown={e => handleHandleDragStart(e, 'left')}><div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div></div> onMouseDown={e => handleHandleDragStart(e, 'left')}></div>
<div className="absolute -right-1.5 top-0 bottom-0 w-3 bg-amber-500 hover:bg-amber-400 cursor-ew-resize flex items-center justify-center z-30" <div className="absolute -right-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30"
onMouseDown={e => handleHandleDragStart(e, 'right')}><div className="w-[1.5px] h-4 bg-zinc-950/70 rounded"></div></div> onMouseDown={e => handleHandleDragStart(e, 'right')}></div>
</div> </div>
)} )}
<div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none" style={{ left: `${playheadLeftPos}px` }}> <div className="absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none" style={{ left: `${playheadLeftPos}px` }}>