diff --git a/app/templates/index.html b/app/templates/index.html index f8b14af..490cb4a 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -721,18 +721,19 @@ ctx.moveTo(leftPx, 0); ctx.lineTo(leftPx, h); ctx.moveTo(rightPx, 0); ctx.lineTo(rightPx, h); ctx.stroke(); - } + } + + // Draw playhead + if (currentTime !== null && currentTime >= 0 && currentTime <= buffer.duration) { + const playheadPx = (currentTime / buffer.duration) * w; + ctx.strokeStyle = '#ef4444'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(playheadPx, 0); + ctx.lineTo(playheadPx, h); + ctx.stroke(); + } - // Draw local playhead - if (currentTime !== null && currentTime >= 0 && currentTime <= buffer.duration) { - const playheadPx = (currentTime / buffer.duration) * w; - ctx.strokeStyle = '#ef4444'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(playheadPx, 0); - ctx.lineTo(playheadPx, h); - ctx.stroke(); - } }, [buffer, currentTime, selectionStart, selectionEnd]); const handleMouseDown = (e) => { @@ -2370,7 +2371,7 @@ timeline.addEventListener('wheel', handleWheel, { passive: false }); return () => timeline.removeEventListener('wheel', handleWheel); - }, [zoom, minZoom, maxDuration]); + }, [zoom, minZoom, maxDuration, activeTab]); // ── Update Playhead ── const updatePlayhead = () => { @@ -4102,8 +4103,8 @@ )} - {/* ── Transport Bar + Toolbar Overlay (full width, between tab bar and workspace) ── */} -
+ {/* ── Transport Bar + Toolbar Overlay ── */} +
{/* Toolbar Overlay Panel — có thể kéo thả hoặc ghim sau này */}
@@ -4208,7 +4209,7 @@
{/* ── Dock-Aware Workspace ── */} - {activeTab === 'main' && (() => { + {(() => { const dockPanels = { top: [], right: [], bottom: [], left: [] }; const addPanel = (id, pos, visible) => { if (visible) dockPanels[pos].push(id); }; addPanel('export', panelPositions.export, showExportPanel); @@ -4337,7 +4338,7 @@ }; return ( -
+
{panelDragRef.current && panelDropZone && (
{panelDropZone === 'top' &&
} @@ -4362,26 +4363,25 @@ {renderDock('left', 'Left')}
- {/* ══ LEFT COLUMN: TCP PANEL (fixed 300px) ══ */} -
+ {/* ══ LEFT COLUMN: TCP PANEL (main session, all tracks) ══ */} +
- {/* [TCP] Header Row */} + style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
Kênh
Snap
- {/* Tempo Track Row */}
@@ -4396,7 +4396,6 @@
- {/* Dynamic Track Controls */}
{tracks.map((track, idx) => { const isSelected = selectedTrackId === track.id; @@ -4442,7 +4441,7 @@
- {/* ══ RIGHT COLUMN: TIMELINE SCROLL VIEWPORT ══ */} + {/* ══ RIGHT COLUMN: TIMELINE (main session, all tracks) ══ */}
@@ -4489,8 +4488,7 @@ {track.buffer && (
+ className="px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-[9px] flex items-center gap-1 border border-zinc-700/50"> Cắt
)}
handleTrackResizeMouseDown(e, track.id)} @@ -4519,6 +4517,110 @@
+ + ) : (() => { + const st = subTabs.find(s => s.id === activeTab); + if (!st) return null; + const subTrack = tracks.find(t => t.id === st.trackId); + const vTrack = subTrack ? { ...subTrack, buffer: st.buffer, isSubTab: true } : null; + return ( + <> + {/* ══ TCP PANEL (sub-tab, single track) ══ */} +
+
+ Sub-Tab + +
+ {vTrack ? ( +
+
+
+
+ {vTrack.name} +
+
+ + +
+
+
+ updateTrackVolume(vTrack.id, v)} /> + Gain: {Math.round(vTrack.volume * 100)}% +
+
+ Duration: + {st.buffer ? formatTime(st.buffer.duration) : '0s'} +
+
+ SR: + {st.buffer ? st.buffer.sampleRate : 0} Hz +
+
+
+ + +
+
+ ) : ( +
Track not found
+ )} +
+ {/* ══ TIMELINE (sub-tab, single track) ══ */} +
+
+
+
+ {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => { + const sec = i; const x = sec * zoom; + return (
{formatTime(sec)}
); + })} +
+
+
+
+ +
+
+
+ {vTrack && ( +
+ setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, selectionStart: start, selectionEnd: end} : s))} + onPlayheadSet={(time) => setSubTabs(prev => prev.map(s => s.id === st.id ? {...s, currentTime: time} : s))} + onContextMenu={(e, clickTime) => setContextMenu({x: e.clientX, y: e.clientY, isSubTab: true, subTabId: st.id, time: clickTime})} + /> +
+ )} + {st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && ( +
+ )} +
+
+
+ + ); + })()}
{renderDock('right', 'Right')} @@ -4529,10 +4631,11 @@ })()} {/* ── Status Bar ── */} -
+
Status: {isPlaying ? 'Playing' : 'Stopped'} - Track: ID {selectedTrackId} + {activeTab !== 'main' && Sub-Tab} + {activeTab === 'main' && Track: ID {selectedTrackId}} {selectionMode === 'local' && Local Sel} {selectionMode === 'global' && Global Sel} {soloedTrackId && Solo: ID {soloedTrackId}} @@ -4569,121 +4672,6 @@
- {/* ── Sub-Tab Editor Panel (replaces workspace when active) ── */} - {activeTab !== 'main' && (() => { - const st = subTabs.find(s => s.id === activeTab); - if (!st) return null; - const fx = st.effects || {}; - return ( -
-
- - {st.label} - - ({formatTime(st.startTime)} - {formatTime(st.endTime)}) - | {st.buffer ? formatTime(st.buffer.duration) : '0s'} - | {st.buffer ? st.buffer.sampleRate : 0} Hz - -
- -
-
- setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.max(0, (s.currentTime || 0) - 1) } : s))} - onForward={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: Math.min(s.buffer?.duration || 0, (s.currentTime || 0) + 1) } : s))} - onLoop={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isLooping: !s.isLooping } : s))} - onRecord={() => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, isRecording: !s.isRecording } : s))} - onRateChange={(rate) => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, playbackRate: rate } : s))} - onCut={() => { const curTabId = activeTab; handleSubTabCut(curTabId); }} - onCopy={() => { const curTabId = activeTab; handleSubTabCopy(curTabId); }} - onPaste={() => { const curTabId = activeTab; handleSubTabPaste(curTabId); }} - onGlue={() => showToast('Glue: Gộp clip trong subtab', 'info')} - snapValue={snapValue} - onSnapChange={setSnapValue} - /> - { - setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, selectionStart: start, selectionEnd: end } : s)); - }} - onPlayheadSet={(time) => { - setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: time } : s)); - }} - onContextMenu={(e, clickTime) => { - setContextMenu({ - x: e.clientX, - y: e.clientY, - isSubTab: true, - subTabId: st.id, - time: clickTime - }); - }} - /> -
-
- Reverse - -
-
- Gain (dB) - updateSubTabEffects(st.id, { gainDb: parseFloat(e.target.value) || 0 })} - className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" /> -
-
- Fade In (ms) - updateSubTabEffects(st.id, { fadeInMs: parseInt(e.target.value) || 0 })} - className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" /> -
-
- Fade Out (ms) - updateSubTabEffects(st.id, { fadeOutMs: parseInt(e.target.value) || 0 })} - className="bg-zinc-800 text-zinc-200 text-center font-mono text-xs rounded border border-zinc-700 p-1.5 focus:outline-none focus:border-amber-600" /> -
-
-
-
- - -
-
- ); - })()} - {/* ── Context Menu ── */} {contextMenu && ( contextMenu.isSubTab ? (