IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll

This commit is contained in:
2026-08-07 22:10:37 +07:00
parent 393df8fd7b
commit 8e3c652551
4 changed files with 120 additions and 21 deletions
+75 -15
View File
@@ -7675,6 +7675,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
});
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);
// Follow playhead: khi PLAY playhead luôn GIA view, notes trôi sang
// trái (scroll theo st.currentTime); khi STOP scroll v đu (playhead
// v trí đu piano roll) user 09:40. Dùng C isPlaying (main) LN
// st.isPlaying (piano roll play main isPlaying=false khi tab play bug
// 09:50: effect tưng đang stop luôn v đu).
React.useEffect(function() {
const wrapper = gridScrollRef.current;
if (!wrapper) return;
const playing = isPlaying || !!st.isPlaying;
if (!playing) {
if (wrapper.scrollLeft !== 0) { wrapper.scrollLeft = 0; setRenderTick(t => t + 1); }
return;
}
const beatSec = 60.0 / (parseInt(bpm) || 120);
const phBeat = (st.currentTime || 0) / beatSec;
const midX = Math.max(0, wrapper.clientWidth / 2);
const targetLeft = Math.max(0, phBeat * pixelsPerBeat - midX);
if (Math.abs(wrapper.scrollLeft - targetLeft) > 1) {
wrapper.scrollLeft = targetLeft;
}
setRenderTick(t => t + 1);
}, [isPlaying, st.isPlaying, st.currentTime]);
React.useEffect(() => {
const scrollToC3 = () => {
if (gridScrollRef.current) {
@@ -8317,6 +8340,21 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const brushAutoScrollRef = React.useRef(null);
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
const swallowContextMenuRef = React.useRef(false);
// Status hint đng (user 09:40): theo dõi Shift/Ctrl + mouse trong piano roll
const prKeyStateRef = React.useRef({ shift: false, ctrl: false });
const prMouseInRef = React.useRef(false);
React.useEffect(function() {
const updateHint = function() {
if (!window.__setPrHint || !prMouseInRef.current) return;
const ks = prKeyStateRef.current;
window.__setPrHint(ks.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (ks.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
};
const kd = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
const ku = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
window.addEventListener('keydown', kd);
window.addEventListener('keyup', ku);
return function() { window.removeEventListener('keydown', kd); window.removeEventListener('keyup', ku); };
}, []);
const findCCNoteIndex = (b, mouseY, ccH) => {
const snapped = getSnapBeat(b, snapValue);
@@ -9266,7 +9304,16 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative"
className: "flex-1 flex overflow-hidden min-h-0 relative",
onMouseEnter: function() {
prMouseInRef.current = true;
// Status bar gi ý đng (user 09:40): trong piano roll base hint;
// gi Shift select/unselect; gi Ctrl fast copy
if (window.__setPrHint) {
window.__setPrHint(prKeyStateRef.current.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (prKeyStateRef.current.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
}
},
onMouseLeave: function() { prMouseInRef.current = false; if (window.__setPrHint) window.__setPrHint(null); }
}, /* Track column */ React.createElement("div", {
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",
style: { height: KeybedPixelHeight + 'px' }
@@ -9279,19 +9326,30 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
var track = (activeTracks || []).find(function(t) { return t.id === m._trackId; });
var isActive = m._trackId === st.trackId && m.id === st.target_id;
var isPlayOn = activePlayTrackIds && activePlayTrackIds.indexOf(m._trackId) !== -1;
els.push(React.createElement("button", {
els.push(React.createElement("div", {
key: m._trackId,
className: "flex items-center gap-0.5 mx-1 my-[2px]"
}, React.createElement("button", {
onClick: function() {
// Click nút tên track ACTIVE ghost notes ca track đó thành MAIN
// notes đ chnh sa (user 09:40)
handleSwitchMidiItem(m.id);
},
className: "flex items-center justify-center flex-1 h-[26px] min-w-0 border border-zinc-600 rounded-md cursor-pointer outline-none " + (isActive ? 'bg-yellow-600 text-black font-bold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')
}, React.createElement("span", {
className: "text-[13px] leading-none font-sans truncate px-1",
title: track ? track.name : m._trackName
}, track ? track.name : m._trackName)), React.createElement("button", {
onClick: function(e) {
e.stopPropagation();
var prevList = activePlayTrackIds || [];
var nextList = prevList.indexOf(m._trackId) !== -1 ? prevList.filter(function(id) { return id !== m._trackId; }) : prevList.concat([m._trackId]);
setActivePlayTrackIds(nextList);
if (onRealtimePlay) onRealtimePlay(nextList);
},
className: "flex items-center justify-center h-[20px] border border-zinc-600 rounded-md cursor-pointer outline-none mx-1 my-[2px] " + (isActive ? 'bg-yellow-600 text-black font-bold' : (isPlayOn ? 'bg-red-700 text-white font-semibold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'))
}, React.createElement("span", {
className: "text-[14px] font-sans truncate px-1",
title: track ? track.name : m._trackName
}, track ? track.name : m._trackName)));
title: isPlayOn ? "Unmute — ghost track play cùng main notes" : "Mute (mặc định) — click để play ghost cùng main",
className: "w-6 h-[26px] shrink-0 border border-zinc-600 rounded-md cursor-pointer text-[11px] font-bold outline-none flex items-center justify-center " + (isPlayOn ? 'bg-green-700 text-white' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-300')
}, isPlayOn ? "\u266A" : "M")));
});
return els;
}() : null)), React.createElement("div", {
@@ -14826,6 +14884,13 @@ const App = () => {
// Context Menu & Clipboard
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
// Gi ý đng PIANO ROLL trên status bar (user 09:40) piano roll set qua
// window.__setPrHint (mouse in/out + Shift/Ctrl state)
const [prHint, setPrHint] = useState(null);
React.useEffect(() => {
window.__setPrHint = (h) => setPrHint(h || null);
return () => { delete window.__setPrHint; };
}, []);
const clipboardRef = useRef(null); // { buffer, name, volume, color } for copy/paste
// Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension)
@@ -28783,14 +28848,9 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "info",
className: "w-3 h-3 text-zinc-600"
})), " Scroll: Zoom"), /*#__PURE__*/React.createElement("span", null, "|"), /*#__PURE__*/React.createElement("span", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "keyboard",
className: "w-3 h-3 text-zinc-600"
})), " Ctrl+Scroll: Playhead"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
})), prHint ? /*#__PURE__*/React.createElement("span", {
className: "text-cyan-400"
}, prHint) : " Scroll: Zoom"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
style: {
left: Math.min(contextMenu.x, window.innerWidth - 260),
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070935" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+27
View File
@@ -2898,3 +2898,30 @@
- **FIX (app.jsx):** copy-drag → `startOffsetBeat: beat - clickedNote.start_beat` (giống move thường) → kéo → delta = snap(Δ) → clones bám chuột (vị trí + pitch).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070935), `wiki.md`. Rebuild precompiled (build PASS — 2 chỗ đồng bộ).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+click note → kéo → các bản copy theo đúng vị trí con trỏ (beat + pitch).
### [2026-08-07 09:40] Task: PIANO ROLL — (1) nút track column: active ghost → main + MUTE (2) follow playhead (3) status hint động
- **Yêu cầu user:**
(1) Cột trái (nút tên track): click → active ghost notes của track đó thành MAIN notes để chỉnh sửa; cuối nút thêm nút MUTE (mặc định MUTE — unmute → play ghost cùng main notes — chức năng tương tự nút toggle cũ).
(2) Khi play → playhead di chuyển GIỮA piano roll, notes trôi sang trái; stop → playhead về ĐẦU (luôn hiển thị trong view).
(3) Status bar gợi ý: mouse trong piano roll → "Scroll: Up/Down | Drag: Draw notes"; giữ Shift → "Shift+Click: select/unselect..."; giữ Ctrl → "Ctrl+drag: fast copy notes...".
- **FIX (app.jsx):**
(1) Track column (9270): nút tên track → `handleSwitchMidiItem(m.id)` (mở item — chỉnh sửa main notes); thêm nút MUTE (w-6 — mặc định 'M' zinc, unmute '♪' green — toggle activePlayTrackIds + onRealtimePlay).
(2) Effect follow: `[isPlaying, st.currentTime]` — play → `scrollLeft = phBeat*ppb - clientWidth/2` (playhead giữa, notes trôi trái); stop → `scrollLeft = 0` (về đầu).
(3) App state `prHint` + `window.__setPrHint`; piano roll: `prKeyStateRef`/`prMouseInRef` + keydown/keyup + container onMouseEnter/Leave → set hint theo Ctrl/Shift; status bar hiển thị `prHint` (cyan) thay "Scroll: Zoom".
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070940), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — click tên track khác → mở item đó (sửa được); nút M — click → ♪ xanh (play cùng main); play → playhead giữa + trôi trái; stop → về đầu; hover trong piano roll → status bar đổi gợi ý theo Shift/Ctrl.
### [2026-08-07 09:45] Task: (1) Cân bằng tên track cột trái (2) Xóa nhãn "Ctrl+Scroll: Playhead" cố định
- **Yêu cầu user:** (1) sửa chiều cao nút tên track cột trái — tên nút cân bằng giữa nút; (2) xóa nhãn cố định "Ctrl+Scroll: Playhead" cuối status bar — để gợi ý adaptive hiển thị đủ nội dung.
- **FIX (app.jsx):**
(1) Nút tên track: `h-[20px]``h-[26px]` + `flex items-center justify-center` + text `text-[13px] leading-none`; nút MUTE đồng bộ `h-[26px]` + flex center.
(2) Status bar: xóa span "|" + span "Ctrl+Scroll: Playhead" (icon keyboard) — còn `prHint` (cyan) hoặc "Scroll: Zoom" — gợi ý adaptive có đủ chỗ.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070945), `wiki.md`. Rebuild precompiled (build PASS — Ctrl+Scroll: Playhead = 0).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — tên track nằm cân bằng giữa nút (26px); status bar cuối không còn "Ctrl+Scroll: Playhead" — gợi ý adaptive hiển thị đầy đủ.
### [2026-08-07 09:50] Task: FIX Follow playhead chưa hoạt động — dùng sai isPlaying (main)
- **Báo cáo user:** follow playhead (0940) vẫn chưa thực hiện được.
- **Nguyên nhân:** effect dùng prop `isPlaying` = MAIN play — piano roll play chỉ set `st.isPlaying` (main vẫn false — handlePlayPause sub-tab stopAllPlayback) → effect luôn rơi nhánh "stop" → scroll về đầu mãi.
- **FIX (app.jsx effect follow):** `const playing = isPlaying || !!st.isPlaying` — follow khi piano roll play LẪN main play; deps thêm `st.isPlaying`.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070950), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL → play (tab) → playhead ở giữa + notes trôi trái; stop → về đầu.