IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll
This commit is contained in:
+75
-15
@@ -7675,6 +7675,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
});
|
});
|
||||||
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);
|
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);
|
||||||
|
|
||||||
|
// Follow playhead: khi PLAY — playhead luôn ở GIỮA 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) LẪN
|
||||||
|
// 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(() => {
|
React.useEffect(() => {
|
||||||
const scrollToC3 = () => {
|
const scrollToC3 = () => {
|
||||||
if (gridScrollRef.current) {
|
if (gridScrollRef.current) {
|
||||||
@@ -8317,6 +8340,21 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
const brushAutoScrollRef = React.useRef(null);
|
const brushAutoScrollRef = React.useRef(null);
|
||||||
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
|
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
|
||||||
const swallowContextMenuRef = React.useRef(false);
|
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 findCCNoteIndex = (b, mouseY, ccH) => {
|
||||||
const snapped = getSnapBeat(b, snapValue);
|
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) */
|
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
|
||||||
React.createElement("div", {
|
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 gợi ý độ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", {
|
}, /* Track column */ React.createElement("div", {
|
||||||
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",
|
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",
|
||||||
style: { height: KeybedPixelHeight + 'px' }
|
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 track = (activeTracks || []).find(function(t) { return t.id === m._trackId; });
|
||||||
var isActive = m._trackId === st.trackId && m.id === st.target_id;
|
var isActive = m._trackId === st.trackId && m.id === st.target_id;
|
||||||
var isPlayOn = activePlayTrackIds && activePlayTrackIds.indexOf(m._trackId) !== -1;
|
var isPlayOn = activePlayTrackIds && activePlayTrackIds.indexOf(m._trackId) !== -1;
|
||||||
els.push(React.createElement("button", {
|
els.push(React.createElement("div", {
|
||||||
key: m._trackId,
|
key: m._trackId,
|
||||||
|
className: "flex items-center gap-0.5 mx-1 my-[2px]"
|
||||||
|
}, React.createElement("button", {
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
|
// Click nút tên track → ACTIVE ghost notes của track đó thành MAIN
|
||||||
|
// notes để chỉnh sửa (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 prevList = activePlayTrackIds || [];
|
||||||
var nextList = prevList.indexOf(m._trackId) !== -1 ? prevList.filter(function(id) { return id !== m._trackId; }) : prevList.concat([m._trackId]);
|
var nextList = prevList.indexOf(m._trackId) !== -1 ? prevList.filter(function(id) { return id !== m._trackId; }) : prevList.concat([m._trackId]);
|
||||||
setActivePlayTrackIds(nextList);
|
setActivePlayTrackIds(nextList);
|
||||||
if (onRealtimePlay) onRealtimePlay(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'))
|
title: isPlayOn ? "Unmute — ghost track play cùng main notes" : "Mute (mặc định) — click để play ghost cùng main",
|
||||||
}, React.createElement("span", {
|
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')
|
||||||
className: "text-[14px] font-sans truncate px-1",
|
}, isPlayOn ? "\u266A" : "M")));
|
||||||
title: track ? track.name : m._trackName
|
|
||||||
}, track ? track.name : m._trackName)));
|
|
||||||
});
|
});
|
||||||
return els;
|
return els;
|
||||||
}() : null)), React.createElement("div", {
|
}() : null)), React.createElement("div", {
|
||||||
@@ -14826,6 +14884,13 @@ const App = () => {
|
|||||||
|
|
||||||
// ── Context Menu & Clipboard ──
|
// ── Context Menu & Clipboard ──
|
||||||
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
|
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
|
||||||
|
// Gợi ý độ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
|
const clipboardRef = useRef(null); // { buffer, name, volume, color } for copy/paste
|
||||||
|
|
||||||
// ── Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension) ──
|
// ── Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension) ──
|
||||||
@@ -28783,14 +28848,9 @@ STRICT CONSTRAINTS:
|
|||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
"data-lucide": "info",
|
"data-lucide": "info",
|
||||||
className: "w-3 h-3 text-zinc-600"
|
className: "w-3 h-3 text-zinc-600"
|
||||||
})), " Scroll: Zoom"), /*#__PURE__*/React.createElement("span", null, "|"), /*#__PURE__*/React.createElement("span", {
|
})), prHint ? /*#__PURE__*/React.createElement("span", {
|
||||||
className: "flex items-center gap-1"
|
className: "text-cyan-400"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, prHint) : " Scroll: Zoom"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
|
||||||
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", {
|
|
||||||
className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
|
className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
|
||||||
style: {
|
style: {
|
||||||
left: Math.min(contextMenu.x, window.innerWidth - 260),
|
left: Math.min(contextMenu.x, window.innerWidth - 260),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></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">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -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).
|
- **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ộ).
|
- **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).
|
- **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.
|
||||||
|
|||||||
Reference in New Issue
Block a user