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 */}
- {/* 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 ? (
+
+
+
+
+
+
+
+
+
+ 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 ? (