fix: đã sửa lỗi bị mất không hiển thị midi piano grid
This commit is contained in:
@@ -0,0 +1,212 @@
|
|||||||
|
# Piano Roll: 4 tính năng
|
||||||
|
|
||||||
|
## 1. Auto-scroll brush khi drag gần cạnh
|
||||||
|
|
||||||
|
**File**: `app/static/js/app.jsx`
|
||||||
|
|
||||||
|
**Vị trí**: Trong `handleGridMouseMove`, cuối block `draggedNote.mode === 'draw'` (trước `return;` ở dòng ~5237).
|
||||||
|
|
||||||
|
**Code thêm** (sau visitedPitches/brushIds logic, trước `return;`):
|
||||||
|
```js
|
||||||
|
const container = gridScrollRef.current;
|
||||||
|
if (container) {
|
||||||
|
const cr = container.getBoundingClientRect();
|
||||||
|
const edgeThreshold = 30;
|
||||||
|
const scrollStep = 6;
|
||||||
|
if (e.clientY < cr.top + edgeThreshold) {
|
||||||
|
container.scrollTop = Math.max(0, container.scrollTop - scrollStep);
|
||||||
|
} else if (e.clientY > cr.bottom - edgeThreshold) {
|
||||||
|
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠ Edge case**: Nếu chuột dừng tại mép, `mousemove` ngưng → cuộn dừng. Để cuộn liên tục, dùng `setInterval` khi vào threshold. Tạm thời chấp nhập giới hạn này.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ctrl+drag velocity với selected notes
|
||||||
|
|
||||||
|
**File**: `app/static/js/app.jsx`
|
||||||
|
|
||||||
|
### 2a. `handleCCMouseDown` (dòng ~5360)
|
||||||
|
Thay block `if (e.ctrlKey)` hiện tại:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
if (selectedNoteIds.length > 0) {
|
||||||
|
selectedNoteIds.forEach(id => {
|
||||||
|
const idx = notes.findIndex(n => n.id === id);
|
||||||
|
if (idx !== -1) paintNote(idx, val);
|
||||||
|
});
|
||||||
|
ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: selectedNoteIds.map(id => notes.findIndex(n => n.id === id)).filter(i => i !== -1) };
|
||||||
|
} else {
|
||||||
|
if (noteIdx !== -1) paintNote(noteIdx, val);
|
||||||
|
ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] };
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2b. `handleCCMouseMove` (sau dòng ~5381)
|
||||||
|
Thêm block đầu `handleCCMouseMove` (SAU khi lấy `drag`, `painted`, TRƯỚC `candidateIdx`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (drag.selectedMode && selectedNoteIds.length > 0) {
|
||||||
|
selectedNoteIds.forEach(id => {
|
||||||
|
const idx = notes.findIndex(n => n.id === id);
|
||||||
|
if (idx !== -1 && !painted.includes(idx)) {
|
||||||
|
setNotes(prev => prev.map((n, i) => {
|
||||||
|
if (i !== idx) return n;
|
||||||
|
if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 };
|
||||||
|
return { ...n, velocity: val };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Cập nhật lastPainted TRỰC TIẾP trên ref (không qua setNotes callback)
|
||||||
|
const newPainted = selectedNoteIds
|
||||||
|
.map(id => notes.findIndex(n => n.id === id))
|
||||||
|
.filter(i => i !== -1 && !painted.includes(i));
|
||||||
|
ccDragRef.current.lastPainted = [...painted, ...newPainted];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠ **Không gán `drag.lastPainted` bên trong `setNotes` callback** — `drag` là `ccDragRef.current`, gán trực tiếp vào ref ngoài callback để tránh stale closure.
|
||||||
|
|
||||||
|
### 2b2. Cleanup `selectedMode` khi mouseup (dòng ~5409)
|
||||||
|
Trong `handleCCMouseUp` (và `onMouseLeave`), thêm reset:
|
||||||
|
```js
|
||||||
|
if (ccDragRef.current) ccDragRef.current.selectedMode = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2c. CC canvas rendering (dòng ~4911-4932)
|
||||||
|
Thêm `isSelected` vào loop notes; đổi màu xanh dương `#3b82f6` khi selected:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isSelected = selectedNoteIds.includes(note.id);
|
||||||
|
// ...
|
||||||
|
ctx.strokeStyle = ccMode === 'pan' ? (isSelected ? '#60a5fa' : '#a78bfa') : (isSelected ? '#3b82f6' : '#fbbf24');
|
||||||
|
ctx.fillStyle = ccMode === 'pan' ? (isSelected ? '#3b82f6' : '#c084fc') : (isSelected ? '#3b82f6' : '#fbbf24');
|
||||||
|
```
|
||||||
|
|
||||||
|
Thêm `selectedNoteIds` vào dependency array của effect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SNAP trong MIDI tab
|
||||||
|
|
||||||
|
**Ghi chú**: `getSnapBeat(beat, mode)` đã xử lý `mode === 'free'` bằng cách return `beat` không đổi. Không cần check `snapVal !== 'free'` riêng.
|
||||||
|
|
||||||
|
**File**: `app/static/js/app.jsx`
|
||||||
|
|
||||||
|
### 3a. Selection marquee — create (dòng ~5003-5008)
|
||||||
|
Snap `startBeat` khi tạo marquee:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const snapStart = getSnapBeat(beat, snapVal);
|
||||||
|
setSelectionMarquee({
|
||||||
|
startBeat: snapStart, startPitch: pitch,
|
||||||
|
currentBeat: snapStart, currentPitch: pitch
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3a2. Selection marquee — drag update (dòng ~5140-5144)
|
||||||
|
Snap `currentBeat` khi kéo marquee:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const snappedBeat = getSnapBeat(beat, snapVal);
|
||||||
|
const marquee = {
|
||||||
|
...selectionMarquee,
|
||||||
|
currentBeat: snappedBeat,
|
||||||
|
currentPitch: pitch
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3b. Ruler drag loop range (dòng ~5742-5763)
|
||||||
|
Snap `clickBeat` khởi tạo, snap `beat` trong onMove:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
||||||
|
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
|
||||||
|
// ...
|
||||||
|
const rawBeat = Math.max(0, bx / pixelsPerBeat);
|
||||||
|
const beat = getSnapBeat(rawBeat, snapVal);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3c. Shift+Click ruler loop (dòng ~5732-5739)
|
||||||
|
Đổi `Math.round(clickBeat / 4) * 4` thành `getSnapBeat(clickBeat, snapVal)`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const beatSnap = getSnapBeat(clickBeat, snapVal);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3d. Ruler loop handles (dòng ~5791, ~5808, ~5829)
|
||||||
|
Đổi `Math.round(bx / pixelsPerBeat / 4) * 4` thành `getSnapBeat(bx / pixelsPerBeat, snapVal)` ở cả 3 handle (left resize, right resize, grab body).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Synth button trong MIDI tab toolbar
|
||||||
|
|
||||||
|
**File**: `app/static/js/app.jsx`
|
||||||
|
|
||||||
|
### 4a. Prop `onInstrumentSelect` (dòng ~4546)
|
||||||
|
Thêm `onInstrumentSelect` vào props destructuring.
|
||||||
|
|
||||||
|
### 4b. Button synth trong toolbar (dòng ~5662-5700)
|
||||||
|
Chèn button sau MIDI input select, trước transport buttons:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
React.createElement("button", {
|
||||||
|
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
|
||||||
|
title: st.instrumentName || "Synth",
|
||||||
|
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
|
||||||
|
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4c. Pass callback từ App (dòng ~15631-15652)
|
||||||
|
Thêm `onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); }`.
|
||||||
|
|
||||||
|
### 4d. Đồng bộ subTab instrument (dòng ~6343-6353)
|
||||||
|
Trong `setTrackInstrumentWithProgram`, thêm cập nhật `subTabs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
setSubTabs(prev => prev.map(s => {
|
||||||
|
if (s.trackId !== trackId) return s;
|
||||||
|
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Layout fix: thêm `h-full` (QUAN TRỌNG)
|
||||||
|
|
||||||
|
**File**: `app/static/js/app.jsx`, dòng ~5623
|
||||||
|
|
||||||
|
Đổi `className` của outer div từ:
|
||||||
|
```
|
||||||
|
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"
|
||||||
|
```
|
||||||
|
thành:
|
||||||
|
```
|
||||||
|
"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lý do**: `h-full` cung cấp height tham chiếu cho flex chain, tránh content area cao 0px.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thứ tự thực hiện
|
||||||
|
|
||||||
|
1. Sửa layout: thêm `h-full`
|
||||||
|
2. Feature 3: SNAP (4 edits nhỏ — dễ verify)
|
||||||
|
3. Feature 2: Velocity selected notes
|
||||||
|
4. Feature 1: Auto-scroll brush
|
||||||
|
5. Feature 4: Synth button (liên quan nhiều component nhất)
|
||||||
|
|
||||||
|
## Kiểm tra
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/locpham/SonicForgeStudio && npm run build
|
||||||
|
```
|
||||||
|
Build phải pass. Nếu lỗi paren, kiểm tra đóng `()` tại cuối return statement.
|
||||||
+306
-291
@@ -5657,306 +5657,321 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
return labels;
|
return labels;
|
||||||
};
|
};
|
||||||
|
|
||||||
const beatSec = 60.0 / (parseInt(bpm) || 120);
|
const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||||
const playHeadX = (st.currentTime || 0) / beatSec * pixelsPerBeat;
|
const playHeadX = (st.currentTime || 0) / beatSec * pixelsPerBeat;
|
||||||
|
|
||||||
return React.createElement("div", {
|
return React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
className: "flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"
|
||||||
}, React.createElement("div", {
|
},
|
||||||
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
|
/* 1. TOOLBAR HEADER */
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
React.createElement("div", {
|
||||||
className: "flex items-center gap-4"
|
className: "h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}, React.createElement("div", {
|
||||||
className: "text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"
|
className: "flex items-center gap-4"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, React.createElement("span", {
|
||||||
"data-lucide": "music",
|
className: "text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"
|
||||||
className: "w-3.5 h-3.5"
|
}, React.createElement("i", {
|
||||||
}), st.label), /*#__PURE__*/React.createElement("div", {
|
"data-lucide": "music",
|
||||||
className: "flex items-center gap-1 text-xs"
|
className: "w-3.5 h-3.5"
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}), st.label), React.createElement("div", {
|
||||||
className: "text-zinc-500 font-semibold"
|
className: "flex items-center gap-1 text-xs"
|
||||||
}, "Snap to Scale"), /*#__PURE__*/React.createElement("button", {
|
}, React.createElement("span", {
|
||||||
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
|
className: "text-zinc-500 font-semibold"
|
||||||
className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
|
}, "Snap to Scale"), React.createElement("button", {
|
||||||
style: { padding: 0 }
|
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)),
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`,
|
||||||
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
|
style: { padding: 0 }
|
||||||
}))), /*#__PURE__*/React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
className: "flex items-center gap-1 text-xs"
|
className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}`
|
||||||
}, /*#__PURE__*/React.createElement("span", {
|
}))), React.createElement("div", {
|
||||||
className: "text-zinc-500 font-semibold"
|
className: "flex items-center gap-1 text-xs"
|
||||||
}, "Snap:"), /*#__PURE__*/React.createElement("select", {
|
}, React.createElement("span", {
|
||||||
value: snapVal,
|
className: "text-zinc-500 font-semibold"
|
||||||
onChange: e => setSnapVal(e.target.value),
|
}, "Snap:"), React.createElement("select", {
|
||||||
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
|
value: snapVal,
|
||||||
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => /*#__PURE__*/React.createElement("option", {
|
onChange: e => setSnapVal(e.target.value),
|
||||||
key: v,
|
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"
|
||||||
value: v
|
}, ['free', '4', '1', '1/2', '1/4', '1/8', '1/16', '1/32'].map(v => React.createElement("option", {
|
||||||
}, v)))), /*#__PURE__*/React.createElement("button", {
|
key: v,
|
||||||
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
|
value: v
|
||||||
className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
|
}, v)))), React.createElement("button", {
|
||||||
}, "ARM"), /*#__PURE__*/React.createElement("select", {
|
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isArmed: !s.isArmed } : s)),
|
||||||
value: selectedMidiInputId || '',
|
className: `px-2 py-1 rounded text-xs font-bold ${st.isArmed ? 'bg-red-600 text-white' : 'bg-zinc-800 text-zinc-400'}`
|
||||||
onChange: e => onMidiInputSelect(e.target.value),
|
}, "ARM"), React.createElement("select", {
|
||||||
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
|
value: selectedMidiInputId || '',
|
||||||
}, /*#__PURE__*/React.createElement("option", { value: "" }, "Input"), /*#__PURE__*/React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => /*#__PURE__*/React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), /*#__PURE__*/React.createElement("button", {
|
onChange: e => onMidiInputSelect(e.target.value),
|
||||||
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
|
className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"
|
||||||
title: st.instrumentName || "Synth",
|
}, React.createElement("option", { value: "" }, "Input"), React.createElement("option", { value: "ALL" }, "Omni"), (midiDevices || []).map(d => React.createElement("option", { key: d.id, value: d.id }, d.name || d.id))), React.createElement("button", {
|
||||||
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
|
onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId),
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), /*#__PURE__*/React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), /*#__PURE__*/React.createElement("div", {
|
title: st.instrumentName || "Synth",
|
||||||
className: "flex items-center gap-0.5 ml-1"
|
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, st.instrumentName || 'Synth')), React.createElement("div", {
|
||||||
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
|
className: "flex items-center gap-0.5 ml-1"
|
||||||
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
}, React.createElement("button", {
|
||||||
title: "Back 1 bar"
|
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (st.currentTime || 0) - beatSec * 4) } : s)),
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", {
|
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
||||||
onClick: onPlayPause,
|
title: "Back 1 bar"
|
||||||
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
|
}, React.createElement("i", { "data-lucide": "step-back", className: "w-3 h-3" })), React.createElement("button", {
|
||||||
title: isPlaying ? "Pause" : "Play"
|
onClick: onPlayPause,
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
|
className: `w-6 h-6 flex items-center justify-center rounded border ${isPlaying ? 'bg-emerald-600 text-black' : 'bg-cyan-600 text-white'} border-cyan-500`,
|
||||||
onClick: onStop,
|
title: isPlaying ? "Pause" : "Play"
|
||||||
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
}, React.createElement("i", { "data-lucide": isPlaying ? "pause" : "play", className: "w-3 h-3 fill-current" })), React.createElement("button", {
|
||||||
title: "Stop"
|
onClick: onStop,
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
|
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
||||||
onClick: onRecord,
|
title: "Stop"
|
||||||
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
|
}, React.createElement("i", { "data-lucide": "square", className: "w-3 h-3 fill-current" })), React.createElement("button", {
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), /*#__PURE__*/React.createElement("button", {
|
onClick: onRecord,
|
||||||
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
|
className: `w-6 h-6 flex items-center justify-center rounded border ${recordingState === 'RECORDING' ? 'bg-red-600 text-white border-red-500 animate-pulse' : 'bg-zinc-800 text-red-500 border-zinc-700'}`
|
||||||
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
}, React.createElement("i", { "data-lucide": "circle", className: "w-3 h-3 fill-current" })), React.createElement("button", {
|
||||||
title: "Forward 1 bar"
|
onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: (st.currentTime || 0) + beatSec * 4 } : s)),
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
|
className: "w-6 h-6 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700",
|
||||||
className: "flex items-center gap-1 ml-1 text-xs"
|
title: "Forward 1 bar"
|
||||||
}, /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "AI:"), /*#__PURE__*/React.createElement("input", {
|
}, React.createElement("i", { "data-lucide": "step-forward", className: "w-3 h-3" }))), React.createElement("div", {
|
||||||
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
|
className: "flex items-center gap-1 ml-1 text-xs"
|
||||||
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
|
}, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", {
|
||||||
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "-"), /*#__PURE__*/React.createElement("input", {
|
type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0),
|
||||||
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
|
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
|
||||||
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
|
}), React.createElement("span", { className: "text-zinc-500" }, "-"), React.createElement("input", {
|
||||||
}), /*#__PURE__*/React.createElement("span", { className: "text-zinc-500" }, "bar"), /*#__PURE__*/React.createElement("button", {
|
type: "number", value: aiBarEnd, onChange: e => setAiBarEnd(parseInt(e.target.value) || 1),
|
||||||
onClick: () => {
|
className: "w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"
|
||||||
setIsLooping(!isLooping);
|
}), React.createElement("span", { className: "text-zinc-500" }, "bar"), React.createElement("button", {
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !(s.isLooping || false) } : s));
|
onClick: () => {
|
||||||
},
|
setIsLooping(!isLooping);
|
||||||
className: `px-2 py-0.5 rounded text-xs ${isLooping ? 'bg-emerald-700 text-white border border-emerald-500' : 'bg-zinc-800 text-zinc-400'}`
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !(s.isLooping || false) } : s));
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), /*#__PURE__*/React.createElement("div", {
|
},
|
||||||
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
|
className: `px-2 py-0.5 rounded text-xs ${isLooping ? 'bg-emerald-700 text-white border border-emerald-500' : 'bg-zinc-800 text-zinc-400'}`
|
||||||
}, ['velocity', 'pan'].map(mode => /*#__PURE__*/React.createElement("button", {
|
}, React.createElement("i", { "data-lucide": "repeat", className: "w-3 h-3" }))), React.createElement("div", {
|
||||||
key: mode,
|
className: "flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"
|
||||||
onClick: () => setCcMode(mode),
|
}, ['velocity', 'pan'].map(mode => React.createElement("button", {
|
||||||
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
|
key: mode,
|
||||||
}, mode)))), /*#__PURE__*/React.createElement("button", {
|
onClick: () => setCcMode(mode),
|
||||||
onClick: () => setShowCC(!showCC),
|
className: `px-2.5 py-1 rounded capitalize ${ccMode === mode ? 'bg-purple-900/60 text-purple-300 font-bold border border-purple-700' : 'text-zinc-400 hover:text-zinc-200'}`
|
||||||
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
|
}, mode)))), React.createElement("button", {
|
||||||
}, ccMode === 'pan' ? 'Pan' : 'Vel'), /*#__PURE__*/React.createElement("div", {
|
onClick: () => setShowCC(!showCC),
|
||||||
className: "flex items-center gap-1"
|
className: `px-2 py-1 rounded text-xs ${showCC ? 'bg-purple-900/60 text-purple-300 border border-purple-700' : 'text-zinc-500 hover:text-zinc-300'}`
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, ccMode === 'pan' ? 'Pan' : 'Vel'), React.createElement("div", {
|
||||||
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
className: "flex items-center gap-1"
|
||||||
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
}, React.createElement("button", {
|
||||||
}, /*#__PURE__*/React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), /*#__PURE__*/React.createElement("button", {
|
onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes),
|
||||||
onClick: onClose,
|
className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"
|
||||||
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
|
}, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "Lưu"), React.createElement("button", {
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
onClick: onClose,
|
||||||
"data-lucide": "x",
|
className: "px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"
|
||||||
className: "w-3 h-3"
|
}, React.createElement("i", {
|
||||||
}), "Đóng"))), /*#__PURE__*/React.createElement("div", {
|
"data-lucide": "x",
|
||||||
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
|
className: "w-3 h-3"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}), "Đóng"))),
|
||||||
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
|
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
/* 2. BAR RULER (ĐÃ SỬA LẠI ĐÓNG NGOẶC ĐÚNG TẠI ĐÂY) */
|
||||||
ref: rulerScrollRef,
|
React.createElement("div", {
|
||||||
className: "flex-1 overflow-hidden",
|
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
|
||||||
onMouseDown: (e) => {
|
}, React.createElement("div", {
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
|
||||||
const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
|
}), React.createElement("div", {
|
||||||
const clickBeat = x / pixelsPerBeat;
|
ref: rulerScrollRef,
|
||||||
const clickTime = clickBeat * beatSec;
|
className: "flex-1 overflow-hidden",
|
||||||
if (clickTime >= 0) {
|
onMouseDown: (e) => {
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
}
|
const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
|
||||||
if (e.shiftKey) {
|
const clickBeat = x / pixelsPerBeat;
|
||||||
const beatSnap = getSnapBeat(clickBeat, snapVal);
|
const clickTime = clickBeat * beatSec;
|
||||||
if (loopStartBeat === null) {
|
if (clickTime >= 0) {
|
||||||
setLoopStartBeat(Math.max(0, beatSnap - 4));
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
|
||||||
setLoopEndBeat(Math.max(4, beatSnap));
|
|
||||||
} else {
|
|
||||||
setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap));
|
|
||||||
}
|
}
|
||||||
return;
|
if (e.shiftKey) {
|
||||||
}
|
const beatSnap = getSnapBeat(clickBeat, snapVal);
|
||||||
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
if (loopStartBeat === null) {
|
||||||
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
|
setLoopStartBeat(Math.max(0, beatSnap - 4));
|
||||||
rulerDragRef.current = startData;
|
setLoopEndBeat(Math.max(4, beatSnap));
|
||||||
const onMove = (ev) => {
|
} else {
|
||||||
const r = rulerScrollRef.current;
|
setLoopEndBeat(Math.max(loopStartBeat + 4, beatSnap));
|
||||||
if (!r || !rulerDragRef.current) return;
|
}
|
||||||
const rRect = r.getBoundingClientRect();
|
return;
|
||||||
const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
|
|
||||||
const rawBeat = Math.max(0, bx / pixelsPerBeat);
|
|
||||||
const beat = getSnapBeat(rawBeat, snapVal);
|
|
||||||
if (Math.abs(ev.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);
|
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? {
|
|
||||||
...s,
|
|
||||||
selectionStart: sBeat * beatSec,
|
|
||||||
selectionEnd: eBeat * beatSec,
|
|
||||||
isLooping: true
|
|
||||||
} : s));
|
|
||||||
}
|
}
|
||||||
};
|
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
||||||
const onUp = () => { rulerDragRef.current = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
const startData = { startX: e.clientX, startBeat: snappedStartBeat, scrollLeft: e.currentTarget.scrollLeft };
|
||||||
document.addEventListener('mousemove', onMove);
|
rulerDragRef.current = startData;
|
||||||
document.addEventListener('mouseup', onUp);
|
const onMove = (ev) => {
|
||||||
}
|
const r = rulerScrollRef.current;
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
if (!r || !rulerDragRef.current) return;
|
||||||
style: {
|
const rRect = r.getBoundingClientRect();
|
||||||
width: `${viewWidth}px`,
|
const bx = ev.clientX - rRect.left + rulerDragRef.current.scrollLeft;
|
||||||
height: '100%'
|
const rawBeat = Math.max(0, bx / pixelsPerBeat);
|
||||||
},
|
const beat = getSnapBeat(rawBeat, snapVal);
|
||||||
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
|
if (Math.abs(ev.clientX - rulerDragRef.current.startX) > 5) {
|
||||||
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
|
const sBeat = Math.max(0, Math.min(rulerDragRef.current.startBeat, beat));
|
||||||
style: {
|
const eBeat = Math.max(sBeat + 1, Math.max(rulerDragRef.current.startBeat, beat));
|
||||||
left: `${loopStartBeat * pixelsPerBeat}px`,
|
setLoopStartBeat(sBeat);
|
||||||
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
setLoopEndBeat(eBeat);
|
||||||
top: 0, bottom: 0
|
setIsLooping(true);
|
||||||
},
|
setSubTabs(prev => prev.map(s => s.id === st.id ? {
|
||||||
className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
|
...s,
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
selectionStart: sBeat * beatSec,
|
||||||
style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
|
selectionEnd: eBeat * beatSec,
|
||||||
onMouseDown: (e) => {
|
isLooping: true
|
||||||
e.stopPropagation();
|
} : s));
|
||||||
const startBeat = loopStartBeat;
|
}
|
||||||
const onMove = (ev) => {
|
};
|
||||||
const r = rulerScrollRef.current;
|
const onUp = () => { rulerDragRef.current = null; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
if (!r) return;
|
document.addEventListener('mousemove', onMove);
|
||||||
const rRect = r.getBoundingClientRect();
|
document.addEventListener('mouseup', onUp);
|
||||||
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
}
|
||||||
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapVal)));
|
}, React.createElement("div", {
|
||||||
setLoopStartBeat(nBeat);
|
style: {
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
|
width: `${viewWidth}px`,
|
||||||
};
|
height: '100%'
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
},
|
||||||
document.addEventListener('mousemove', onMove);
|
className: "relative h-full font-mono text-[9px] text-zinc-500 font-bold"
|
||||||
document.addEventListener('mouseup', onUp);
|
}, renderBarLabels(), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
|
||||||
}
|
style: {
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
left: `${loopStartBeat * pixelsPerBeat}px`,
|
||||||
style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
|
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
||||||
onMouseDown: (e) => {
|
top: 0, bottom: 0
|
||||||
e.stopPropagation();
|
},
|
||||||
const onMove = (ev) => {
|
className: "absolute bg-emerald-500/15 border-l border-r border-emerald-400"
|
||||||
const r = rulerScrollRef.current;
|
}, React.createElement("div", {
|
||||||
if (!r) return;
|
style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
|
||||||
const rRect = r.getBoundingClientRect();
|
onMouseDown: (e) => {
|
||||||
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
e.stopPropagation();
|
||||||
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapVal));
|
const startBeat = loopStartBeat;
|
||||||
setLoopEndBeat(nBeat);
|
const onMove = (ev) => {
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
|
const r = rulerScrollRef.current;
|
||||||
};
|
if (!r) return;
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
const rRect = r.getBoundingClientRect();
|
||||||
document.addEventListener('mousemove', onMove);
|
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
||||||
document.addEventListener('mouseup', onUp);
|
const nBeat = Math.max(0, Math.min(loopEndBeat - 1, getSnapBeat(bx / pixelsPerBeat, snapVal)));
|
||||||
}
|
setLoopStartBeat(nBeat);
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: nBeat * beatSec } : s));
|
||||||
style: { position: 'absolute', left: '4px', right: '4px', top: 0, bottom: 0, cursor: 'grab' },
|
};
|
||||||
onMouseDown: (e) => {
|
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
e.stopPropagation();
|
document.addEventListener('mousemove', onMove);
|
||||||
const startBeat = loopStartBeat;
|
document.addEventListener('mouseup', onUp);
|
||||||
const range = loopEndBeat - loopStartBeat;
|
}
|
||||||
const offsetBeat = startBeat + range / 2;
|
}), React.createElement("div", {
|
||||||
const onMove = (ev) => {
|
style: { position: 'absolute', right: 0, top: 0, bottom: 0, width: '4px', cursor: 'ew-resize' },
|
||||||
const r = rulerScrollRef.current;
|
onMouseDown: (e) => {
|
||||||
if (!r) return;
|
e.stopPropagation();
|
||||||
const rRect = r.getBoundingClientRect();
|
const onMove = (ev) => {
|
||||||
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
const r = rulerScrollRef.current;
|
||||||
const centerBeat = getSnapBeat(bx / pixelsPerBeat, snapVal);
|
if (!r) return;
|
||||||
const halfRange = range / 2;
|
const rRect = r.getBoundingClientRect();
|
||||||
const newStart = Math.max(0, centerBeat - halfRange);
|
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
||||||
setLoopStartBeat(newStart);
|
const nBeat = Math.max(loopStartBeat + 1, getSnapBeat(bx / pixelsPerBeat, snapVal));
|
||||||
setLoopEndBeat(newStart + range);
|
setLoopEndBeat(nBeat);
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: newStart * beatSec, selectionEnd: (newStart + range) * beatSec } : s));
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionEnd: nBeat * beatSec } : s));
|
||||||
};
|
};
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp);
|
||||||
}
|
}
|
||||||
})))), /*#__PURE__*/React.createElement("div", {
|
}), React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden min-h-0 relative"
|
style: { position: 'absolute', left: '4px', right: '4px', top: 0, bottom: 0, cursor: 'grab' },
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
onMouseDown: (e) => {
|
||||||
ref: keybedRef,
|
e.stopPropagation();
|
||||||
onScroll: handleKeybedScroll,
|
const startBeat = loopStartBeat;
|
||||||
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
|
const range = loopEndBeat - loopStartBeat;
|
||||||
style: {
|
const offsetBeat = startBeat + range / 2;
|
||||||
scrollbarWidth: 'none',
|
const onMove = (ev) => {
|
||||||
msOverflowStyle: 'none'
|
const r = rulerScrollRef.current;
|
||||||
}
|
if (!r) return;
|
||||||
}, renderKeybed()), /*#__PURE__*/React.createElement("div", {
|
const rRect = r.getBoundingClientRect();
|
||||||
ref: gridScrollRef,
|
const bx = ev.clientX - rRect.left + r.scrollLeft;
|
||||||
onScroll: handleScroll,
|
const centerBeat = getSnapBeat(bx / pixelsPerBeat, snapVal);
|
||||||
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
|
const halfRange = range / 2;
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
const newStart = Math.max(0, centerBeat - halfRange);
|
||||||
|
setLoopStartBeat(newStart);
|
||||||
|
setLoopEndBeat(newStart + range);
|
||||||
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: newStart * beatSec, selectionEnd: (newStart + range) * beatSec } : s));
|
||||||
|
};
|
||||||
|
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
|
document.addEventListener('mousemove', onMove);
|
||||||
|
document.addEventListener('mouseup', onUp);
|
||||||
|
}
|
||||||
|
}))))),
|
||||||
|
|
||||||
|
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + CANVAS) */
|
||||||
|
React.createElement("div", {
|
||||||
|
className: "flex-1 flex overflow-hidden min-h-0 relative"
|
||||||
|
}, React.createElement("div", {
|
||||||
|
ref: keybedRef,
|
||||||
|
onScroll: handleKeybedScroll,
|
||||||
|
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
|
||||||
|
style: {
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
msOverflowStyle: 'none'
|
||||||
|
}
|
||||||
|
}, renderKeybed()), React.createElement("div", {
|
||||||
|
ref: gridScrollRef,
|
||||||
|
onScroll: handleScroll,
|
||||||
|
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
|
||||||
|
}, React.createElement("div", {
|
||||||
style: {
|
style: {
|
||||||
width: `${viewWidth}px`,
|
width: `${viewWidth}px`,
|
||||||
height: `${(128 - PITCH_START) * NoteHeight}px`
|
height: `${(128 - PITCH_START) * NoteHeight}px`
|
||||||
},
|
},
|
||||||
className: "relative"
|
className: "relative"
|
||||||
}, /*#__PURE__*/React.createElement("canvas", {
|
}, React.createElement("canvas", {
|
||||||
ref: canvasRef,
|
ref: canvasRef,
|
||||||
onMouseDown: handleGridMouseDown,
|
onMouseDown: handleGridMouseDown,
|
||||||
onMouseMove: handleGridMouseMove,
|
onMouseMove: handleGridMouseMove,
|
||||||
onMouseUp: handleGridMouseUp,
|
onMouseUp: handleGridMouseUp,
|
||||||
onMouseLeave: handleGridMouseUp,
|
onMouseLeave: handleGridMouseUp,
|
||||||
onContextMenu: handleContextMenu,
|
onContextMenu: handleContextMenu,
|
||||||
className: "absolute inset-0 cursor-crosshair"
|
className: "absolute inset-0 cursor-crosshair"
|
||||||
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
|
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
|
||||||
style: {
|
style: {
|
||||||
left: `${loopStartBeat * pixelsPerBeat}px`,
|
left: `${loopStartBeat * pixelsPerBeat}px`,
|
||||||
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
||||||
top: 0, bottom: 0
|
top: 0, bottom: 0
|
||||||
},
|
},
|
||||||
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
|
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
|
||||||
})))), showCC && /*#__PURE__*/React.createElement("div", {
|
})))),
|
||||||
style: { height: `${ccHeight}px` },
|
|
||||||
className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
|
/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
showCC && React.createElement("div", {
|
||||||
onMouseDown: e => {
|
style: { height: `${ccHeight}px` },
|
||||||
e.preventDefault();
|
className: "bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"
|
||||||
const startY = e.clientY;
|
}, React.createElement("div", {
|
||||||
const startH = ccHeight;
|
onMouseDown: e => {
|
||||||
const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
|
e.preventDefault();
|
||||||
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
const startY = e.clientY;
|
||||||
document.addEventListener('mousemove', onMove);
|
const startH = ccHeight;
|
||||||
document.addEventListener('mouseup', onUp);
|
const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
|
||||||
},
|
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
|
document.addEventListener('mousemove', onMove);
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
document.addEventListener('mouseup', onUp);
|
||||||
className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
|
},
|
||||||
}, ccMode.toUpperCase()), /*#__PURE__*/React.createElement("div", {
|
className: "absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"
|
||||||
ref: ccWrapperRef,
|
}), React.createElement("div", {
|
||||||
className: "flex-1 overflow-x-hidden min-w-0"
|
className: "w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, ccMode.toUpperCase()), React.createElement("div", {
|
||||||
style: {
|
ref: ccWrapperRef,
|
||||||
width: `${viewWidth}px`,
|
className: "flex-1 overflow-x-hidden min-w-0"
|
||||||
height: '100%'
|
}, React.createElement("div", {
|
||||||
},
|
style: {
|
||||||
className: "relative"
|
width: `${viewWidth}px`,
|
||||||
}, /*#__PURE__*/React.createElement("canvas", {
|
height: '100%'
|
||||||
ref: ccCanvasRef,
|
},
|
||||||
onMouseDown: handleCCMouseDown,
|
className: "relative"
|
||||||
onMouseMove: handleCCMouseMove,
|
}, React.createElement("canvas", {
|
||||||
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
ref: ccCanvasRef,
|
||||||
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
onMouseDown: handleCCMouseDown,
|
||||||
className: "absolute inset-0"
|
onMouseMove: handleCCMouseMove,
|
||||||
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && /*#__PURE__*/React.createElement("div", {
|
onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
||||||
style: {
|
onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; },
|
||||||
left: `${loopStartBeat * pixelsPerBeat}px`,
|
className: "absolute inset-0"
|
||||||
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
}), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", {
|
||||||
top: 0, bottom: 0
|
style: {
|
||||||
},
|
left: `${loopStartBeat * pixelsPerBeat}px`,
|
||||||
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
|
width: `${(loopEndBeat - loopStartBeat) * pixelsPerBeat}px`,
|
||||||
})))), scaleMenuPos && renderScaleContextMenu());
|
top: 0, bottom: 0
|
||||||
|
},
|
||||||
|
className: "absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"
|
||||||
|
})))),
|
||||||
|
|
||||||
|
/* 5. OVERLAY / CONTEXT MENU */
|
||||||
|
scaleMenuPos && renderScaleContextMenu()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeTracksList = (tracksList, secondsPerBar) => {
|
const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Reference in New Issue
Block a user