From 025aba326592750d1178d040801d59019939b4cd Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sat, 25 Jul 2026 21:04:06 +0700 Subject: [PATCH] fix: Ctrl+S in piano roll + ruler range selection via drag --- app/static/js/app.jsx | 47 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 379f8d3..7416e26 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -4590,6 +4590,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot React.useEffect(() => { if (!draggedNoteRef.current) setNotes(st.notes || []); }, [st.notes]); const brushVelocityRef = React.useRef(0.8); const [selectedNoteIds, setSelectedNoteIds] = React.useState([]); + const [loopStartBeat, setLoopStartBeat] = React.useState(null); + const [loopEndBeat, setLoopEndBeat] = React.useState(null); + const [isLooping, setIsLooping] = React.useState(false); + const rulerDragRef = React.useRef(null); React.useEffect(() => { const handler = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'a') { @@ -4638,6 +4642,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedo(); + } else if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + onSaveNotes(st.id, st.trackId, st.target_id, notes); + showToast('Đã lưu MIDI notes', 'info'); } else if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedNoteIds.length > 0 && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); @@ -4650,7 +4658,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [handleUndo, handleRedo, notes, selectedNoteIds]); + }, [handleUndo, handleRedo, notes, selectedNoteIds, onSaveNotes, showToast]); React.useEffect(() => { onUpdateNotes(st.id, notes); @@ -5715,14 +5723,47 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot if (clickTime >= 0) { setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s)); } - } + if (e.shiftKey) { + const beatSnap = Math.round(clickBeat / 4) * 4; + if (loopStartBeat === null) { + setLoopStartBeat(Math.max(0, beatSnap - 4)); + setLoopEndBeat(Math.max(4, beatSnap)); + } else { + setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap)); + } + return; + } + rulerDragRef.current = { startX: e.clientX, startBeat: clickBeat, scrollLeft: e.currentTarget.scrollLeft }; + }, + onMouseMove: e => { + if (!rulerDragRef.current) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left + rulerDragRef.current.scrollLeft; + const beat = Math.max(0, x / pixelsPerBeat); + if (Math.abs(e.clientX - rulerDragRef.current.startX) > 5) { + const sBeat = Math.max(0, Math.min(rulerDragRef.current.startBeat, beat)); + const eBeat = Math.max(sBeat + 1, Math.max(rulerDragRef.current.startBeat, beat)); + setLoopStartBeat(sBeat); + setLoopEndBeat(eBeat); + setIsLooping(true); + } + }, + onMouseUp: () => { rulerDragRef.current = null; }, + onMouseLeave: () => { rulerDragRef.current = null; } }, /*#__PURE__*/React.createElement("div", { style: { width: `${viewWidth}px`, height: '100%' }, className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold" - }, renderBarLabels()))), /*#__PURE__*/React.createElement("div", { + }, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", { + style: { + left: `${loopStartBeat * pixelsPerBeat}px`, + width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`, + top: 0, bottom: 0 + }, + className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400 pointer-events-none" + }))), /*#__PURE__*/React.createElement("div", { className: "flex-1 flex overflow-hidden min-h-0 relative" }, /*#__PURE__*/React.createElement("div", { ref: keybedRef,