diff --git a/app/templates/index.html b/app/templates/index.html
index 4d05401..7905f51 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -1015,6 +1015,15 @@
const [soloedTrackId, setSoloedTrackId] = useState(null);
const [toastMessage, setToastMessage] = useState(null);
const [showAIConfig, setShowAIConfig] = useState(false);
+ const [showExportPanel, setShowExportPanel] = useState(true);
+ const [showAIPanel, setShowAIPanel] = useState(true);
+ const [showSelectionPanel, setShowSelectionPanel] = useState(true);
+ const [panelPositions, setPanelPositions] = useState({ export: 'right', ai: 'right', selection: 'right' });
+ const [panelDropZone, setPanelDropZone] = useState(null);
+ const [dragGhostPos, setDragGhostPos] = useState(null);
+ const [dragGhostPanel, setDragGhostPanel] = useState(null);
+ const panelDragRef = useRef(null);
+ const workspaceRef = useRef(null);
const [aiConfig, setAiConfig] = useState({
baseUrl: localStorage.getItem('ai_base_url') || `${API_BASE_URL}`,
apiKey: localStorage.getItem('ai_api_key') || '',
@@ -1143,6 +1152,53 @@
tcpContainerRef.current.scrollTop = e.currentTarget.scrollTop;
}
};
+ const handleTCPScroll = (e) => {
+ if (timelineWrapperRef.current) {
+ timelineWrapperRef.current.scrollTop = e.currentTarget.scrollTop;
+ }
+ };
+ const panelDropZoneRef = useRef(null);
+ const startPanelDrag = (panelId, e) => {
+ panelDragRef.current = { panelId, startX: e.clientX, startY: e.clientY };
+ setPanelDropZone(null);
+ setDragGhostPanel(panelId);
+ setDragGhostPos({ x: e.clientX - 120, y: e.clientY - 20 });
+ const onMove = (ev) => {
+ if (!panelDragRef.current || !workspaceRef.current) return;
+ const rect = workspaceRef.current.getBoundingClientRect();
+ const x = ev.clientX - rect.left;
+ const y = ev.clientY - rect.top;
+ const w = rect.width;
+ const h = rect.height;
+ const margin = 60;
+ let zone = null;
+ if (x < margin && x > 10) zone = 'left';
+ else if (x > w - margin && x < w - 10) zone = 'right';
+ else if (y < margin && y > 10) zone = 'top';
+ else if (y > h - margin && y < h - 10) zone = 'bottom';
+ panelDropZoneRef.current = zone;
+ setPanelDropZone(zone);
+ setDragGhostPos({ x: ev.clientX - 120, y: ev.clientY - 20 });
+ };
+ const onUp = () => {
+ if (panelDragRef.current) {
+ const pid = panelDragRef.current.panelId;
+ const targetZone = panelDropZoneRef.current;
+ if (targetZone && targetZone !== panelPositions[pid]) {
+ setPanelPositions(prev => ({ ...prev, [pid]: targetZone }));
+ }
+ panelDragRef.current = null;
+ panelDropZoneRef.current = null;
+ setPanelDropZone(null);
+ setDragGhostPos(null);
+ setDragGhostPanel(null);
+ }
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('mouseup', onUp);
+ };
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ };
const rulerRef = useRef(null);
const activeSourcesRef = useRef([]);
const startOffsetTimeRef = useRef(0);
@@ -4043,513 +4099,416 @@
)}
- {/* ── Workspace (Split Dual-Column — 11_REFACTOR_UI.md) ── */}
-
- {/* ══ LEFT COLUMN: TCP PANEL (fixed 300px, overflow hidden, z-30, solid bg) ══ */}
-
- {/* [TCP] Header Row (Channels & Tools) — height 40px (h-10) */}
-
-
Kênh
-
- {/* Timeline Toolbar (scaled up buttons, height locked at 40px) */}
-
-
{ setActiveTool('select'); showToast('Công cụ chọn (Select Tool) đã kích hoạt.', 'info'); }}
- className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'select' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-800/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
- title="Select Tool: Chọn khoảng, Đặt playhead (Alt+Kéo để di chuyển clip, Ctrl+Kéo để nhân bản)"
- >
-
-
-
{ setActiveTool('grab'); showToast('Công cụ di chuyển (Hand Tool) đã kích hoạt.', 'info'); }}
- className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'grab' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
- title="Grab Tool: Click kéo trực tiếp để di chuyển clip"
- >
-
-
-
{ setActiveTool('razor'); showToast('Công cụ chia đoạn (Razor Tool) đã kích hoạt.', 'info'); }}
- className={`p-1 rounded transition text-sm flex items-center justify-center ${activeTool === 'razor' ? 'bg-cyan-950/80 text-cyan-400 border border-cyan-700/50 font-bold' : 'text-zinc-400 hover:bg-zinc-800 border border-transparent'}`}
- title="Razor Tool: Click trên clip để chia nhỏ tại điểm click"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Transport Controls - gần toolbar */}
-
setCurrentTime(0)}
- 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 transition"
- title="Quay lại đầu">
-
-
-
-
-
setCurrentTime(maxDuration)}
- 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 transition"
- title="Đến cuối">
-
setIsLoopingSelection(prev => !prev)}
- className={`w-6 h-6 flex items-center justify-center rounded border transition ${
- isLoopingSelection
- ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
- : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
- }`} title="Bật/Tắt Lặp vùng chọn">
-
-
-
-
-
-
- Snap
- setSnapValue(e.target.value)}
- className="bg-zinc-850 text-zinc-300 text-[10px] px-1 py-0.5 rounded border border-zinc-800 focus:outline-none focus:border-cyan-550 font-mono"
- >
- Free
- 1
- 1/2
- 1/4
- 1/8
- 1/16
- 1/32
-
-
+ {/* ── Transport Bar + Toolbar Overlay (full width, between tab bar and workspace) ── */}
+
+ {/* Toolbar Overlay Panel — có thể kéo thả hoặc ghim sau này */}
+
+
+
-
- {/* [TCP] Tempo Track Row — height 40px (h-[40px]) */}
-
-
-
- TM
- Tempo Track
-
-
- setBpm(e.target.value)}
- onBlur={() => localStorage.setItem('studio_bpm', bpm)}
- className="w-12 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500"
- min="40"
- max="300"
- />
- BPM
-
-
-
-
- {/* [TCP] Dynamic Track Controls */}
-
- {tracks.map((track, idx) => {
- const isSelected = selectedTrackId === track.id;
- return (
-
setSelectedTrackId(track.id)}
- >
-
-
-
{(idx+1).toString().padStart(2, '0')}
-
-
- {track.name}
-
-
-
-
- { e.stopPropagation(); toggleTrackMute(track.id); }}
- className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
- track.muted
- ? 'bg-red-950 text-red-400 border-red-700'
- : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
- }`}
- >M
- { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
- className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${
- soloedTrackId === track.id || track.solo
- ? 'bg-amber-950 text-amber-400 border-amber-600'
- : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'
- }`}
- title="Solo nghe thử"
- >S
- { e.stopPropagation(); deleteTrack(track.id); }}
- className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition flex items-center justify-center"
- title="Xóa Track"
- >
-
-
-
-
-
-
e.stopPropagation()}>
- Mô phỏng:
- generateSynthToTrack(track.id, 'kick')}
- className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
- >Kick
- generateSynthToTrack(track.id, 'synth')}
- className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700"
- >Synth
-
-
-
e.stopPropagation()}>
-
- updateTrackVolume(track.id, v)} />
- Gain: {Math.round(track.volume * 100)}%
-
-
-
- loadFileOnTrack(track.id, e.target.files[0])}
- />
-
- Tải file
-
-
-
-
- {/* Resize handle at bottom of TCP */}
-
handleTrackResizeMouseDown(e, track.id)}
- className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors"
- onClick={e => e.stopPropagation()}
- />
-
- );
- })}
-
-
- {/* [TCP] Bottom Drop Zone spacer */}
-
+
{ setActiveTool('select'); showToast('Select Tool', 'info'); }}
+ className={`p-0.5 rounded ${activeTool === 'select' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
+ title="Select Tool">
+
+
+
{ setActiveTool('grab'); showToast('Grab Tool', 'info'); }}
+ className={`p-0.5 rounded ${activeTool === 'grab' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
+ title="Grab Tool">
+
+
+
{ setActiveTool('razor'); showToast('Razor Tool', 'info'); }}
+ className={`p-0.5 rounded ${activeTool === 'razor' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
+ title="Razor Tool">
+
+
+
+
+
+
+
{ setActiveTool('pen'); showToast('Pen Tool', 'info'); }}
+ className={`p-0.5 rounded ${activeTool === 'pen' ? 'bg-cyan-700 text-white' : 'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`}
+ title="Pen Tool">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {/* ══ RIGHT COLUMN: TIMELINE SCROLL VIEWPORT ══ */}
-
-
-
- {/* [ZONE A] Time Ruler — height 40px (h-10) */}
-
-
- {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
- const sec = i;
- const x = sec * zoom;
- return (
-
- {formatTime(sec)}
-
- );
- })}
-
-
+
- {/* [ZONE A] Tempo Lane — height 40px (h-[40px]) */}
-
-
- {/* [ZONE B] Dynamic Waveform Lanes */}
-
- {tracks.map((track, idx) => {
- const isSelected = selectedTrackId === track.id;
- return (
-
e.preventDefault()}
- onDrop={(e) => {
- e.preventDefault();
- if (e.dataTransfer.files[0]) {
- loadFileOnTrack(track.id, e.dataTransfer.files[0]);
- }
- }}
- onMouseEnter={() => setHoveredTrackId(track.id)}
- >
-
-
- {track.buffer && (
-
- handleSplitTrack(track.id)}
- 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"
- title="Cắt đoạn tại Playhead">
- Cắt
-
-
- )}
-
- {/* Resize handle at bottom of Waveform Lane */}
-
handleTrackResizeMouseDown(e, track.id)}
- className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors"
- onClick={e => e.stopPropagation()}
- />
-
- );
- })}
-
- {/* Bottom Drop Zone to create new track */}
-
{
- if (draggedClipRef.current) {
- const newId = addNewTrack();
- setHoveredTrackId(newId);
- }
- }}
- onClick={addNewTrack}
- >
-
- Kéo clip xuống đây hoặc Click để tạo Track mới
-
-
-
- {/* Selection Overlay */}
- {selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
-
-
handleHandleDragStart(e, 'left')}
- title="Kéo giãn mốc bắt đầu">
-
-
-
handleHandleDragStart(e, 'right')}
- title="Kéo giãn mốc kết thúc">
-
-
-
- )}
-
- {/* Playhead */}
-
-
-
-
+
setCurrentTime(0)}
+ className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
+ title="Quay lại đầu">
+
{ if (selLeft !== null) setCurrentTime(selLeft); }}
+ className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
+ title="Đầu vùng chọn">
+
+
+
+
+
{ if (selRight !== null) setCurrentTime(selRight); }}
+ className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
+ title="Cuối vùng chọn">
+
setCurrentTime(maxDuration)}
+ className="w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition"
+ title="Đến cuối">
+
+
setIsLoopingSelection(prev => !prev)}
+ className={`w-7 h-7 flex items-center justify-center rounded border transition ${
+ isLoopingSelection
+ ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500'
+ : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'
+ }`} title="Bật/Tắt Lặp vùng chọn">
+
+
+
+
{formatTime(currentTime)}
- {/* ── Footer ── */}
-
-
- {/* Export Section */}
-
-
-
- Trộn & Xuất bản
-
-
-
- Sample Rate
- setExportSettings(prev => ({ ...prev, sampleRate: e.target.value }))}
- className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
- 22.5 kHz
- 44.1 kHz
- 48.0 kHz
-
-
-
- Bit Depth
- setExportSettings(prev => ({ ...prev, bitDepth: e.target.value }))}
- className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
- 8-bit
- 16-bit
- 24-bit
-
-
-
- Mã Hóa
- setExportSettings(prev => ({ ...prev, format: e.target.value }))}
- className="w-full bg-[#141414] border border-zinc-800 rounded px-1.5 py-1 text-[10px] text-zinc-300 focus:outline-none">
- WAV (.wav)
- MP3 (.mp3)
- OGG (.ogg)
-
-
-
-
-
-
- {isExporting ? 'Đang xuất...' : 'Xuất bản bản phối'}
-
-
+ {/* ── 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);
+ addPanel('ai', panelPositions.ai, showAIPanel);
+ addPanel('selection', panelPositions.selection, showSelectionPanel);
- {/* Selection Info Section */}
-
-
-
Selection Info
-
{formatTime(currentTime)}
-
+ const closePanel = (id) => {
+ if (id === 'export') setShowExportPanel(false);
+ else if (id === 'ai') setShowAIPanel(false);
+ else if (id === 'selection') setShowSelectionPanel(false);
+ };
-
-
- Vùng Chọn Đầu
- handleSelectionInputChange('start', e.target.value)}
- className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[11px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
-
-
- Vùng Chọn Cuối
- handleSelectionInputChange('end', e.target.value)}
- className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[11px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
-
-
- Thời Lượng
- {selectionStats.length}s
-
-
-
-
- {/* AI Analysis & Edit Section */}
-
-
-
- Edit & AI Engine
-
-
-
// Status: {analysisState.status}
- {analysisState.data && (
-
- BPM: {analysisState.data.bpm} | Bars: {analysisState.data.bars}
+ const renderPanelContent = (panelId) => {
+ const h = (id) => (e) => { startPanelDrag(id, e); };
+ if (panelId === 'export') return (
+
+
startPanelDrag('export', e)}>
+
+
+ Export
+
+ closePanel('export')} className="text-zinc-600 hover:text-zinc-300">
+
+
+
+ SR
+ setExportSettings(p => ({...p, sampleRate: e.target.value}))}
+ className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
+ 44.1k 48k
+
- )}
+
+ Bit
+ setExportSettings(p => ({...p, bitDepth: e.target.value}))}
+ className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
+ 16 24
+
+
+
+ Fmt
+ setExportSettings(p => ({...p, format: e.target.value}))}
+ className="w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-[9px] text-zinc-300 focus:outline-none">
+ WAV
+
+
+
+
+
+ {isExporting ? '...' : 'Export'}
+
-
+ );
+ if (panelId === 'ai') return (
+
+
startPanelDrag('ai', e)}>
+
+
+ AI
+
+ closePanel('ai')} className="text-zinc-600 hover:text-zinc-300">
+
+
+
// {analysisState.status}
+ {analysisState.data &&
BPM: {analysisState.data.bpm}
}
+
+
+ Mark
+ Cut
+
+
+
+
+
+ );
+ if (panelId === 'selection') return (
+
+
startPanelDrag('selection', e)}>
+
+ Selection
+
+ closePanel('selection')} className="text-zinc-600 hover:text-zinc-300">
+
+
+
+ Start
+ handleSelectionInputChange('start', e.target.value)}
+ className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
+
+
+ End
+ handleSelectionInputChange('end', e.target.value)}
+ className="w-full bg-[#242424] text-amber-400 text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
+
+
+ Len
+ {selectionStats.length}s
+
+
+
+ );
+ return null;
+ };
-
-
- Mark
-
-
- AI Cut
-
-
-
-
+ const renderDock = (pos, title) => {
+ const panels = dockPanels[pos];
+ if (panels.length === 0) return null;
+ const isSide = pos === 'left' || pos === 'right';
+ const sideClass = isSide ? 'w-56 shrink-0 overflow-y-auto border-zinc-800' : 'shrink-0';
+ const borderClass = pos === 'left' ? 'border-r' : pos === 'right' ? 'border-l' : pos === 'top' ? 'border-b' : 'border-t';
+ const bgClass = 'bg-[#1e1e1e]';
+ const highlight = panelDragRef.current && panelDropZone === pos;
+ return (
+
+ {panels.map(p => (
+
+ {renderPanelContent(p)}
+
+ ))}
+
+ );
+ };
+
+ return (
+
+ {panelDragRef.current && panelDropZone && (
+
+ {panelDropZone === 'top' &&
}
+ {panelDropZone === 'bottom' &&
}
+ {panelDropZone === 'left' &&
}
+ {panelDropZone === 'right' &&
}
+
+ )}
+ {/* Drag Ghost */}
+ {dragGhostPanel && dragGhostPos && (
+
+
+
+ {dragGhostPanel === 'export' ? 'Export Panel' : dragGhostPanel === 'ai' ? 'AI Panel' : 'Selection Panel'}
+
+
Drop at edge to dock
+
+ )}
+ {renderDock('top', 'Top')}
+
+ {renderDock('left', 'Left')}
+
+
+ {/* ══ LEFT COLUMN: TCP PANEL (fixed 300px) ══ */}
+
+ {/* [TCP] Header Row */}
+
+
Kênh
+
+ Snap
+ setSnapValue(e.target.value)}
+ className="bg-zinc-850 text-zinc-300 text-[10px] px-1 py-0.5 rounded border border-zinc-800 focus:outline-none focus:border-cyan-550 font-mono">
+ Free 1 1/2
+ 1/4 1/8 1/16 1/32
+
+
+
+ {/* Tempo Track Row */}
+
+
+
+ TM
+ Tempo
+
+
+ setBpm(e.target.value)}
+ onBlur={() => localStorage.setItem('studio_bpm', bpm)}
+ className="w-12 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500" min="40" max="300" />
+ BPM
+
+
+
+ {/* Dynamic Track Controls */}
+
+ {tracks.map((track, idx) => {
+ const isSelected = selectedTrackId === track.id;
+ return (
+
setSelectedTrackId(track.id)}>
+
+
+
{(idx+1).toString().padStart(2, '0')}
+
+
{track.name}
+
+
+ { e.stopPropagation(); toggleTrackMute(track.id); }}
+ className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${track.muted ? 'bg-red-950 text-red-400 border-red-700' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>M
+ { e.stopPropagation(); toggleTrackSoloEvaluate(track.id); }}
+ className={`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`}>S
+ { e.stopPropagation(); deleteTrack(track.id); }}
+ className="p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition">
+
+
+
e.stopPropagation()}>
+ Synth:
+ generateSynthToTrack(track.id, 'kick')} className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700">Kick
+ generateSynthToTrack(track.id, 'synth')} className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700">Synth
+
+
e.stopPropagation()}>
+
+ updateTrackVolume(track.id, v)} />
+ Gain: {Math.round(track.volume * 100)}%
+
+
+ loadFileOnTrack(track.id, e.target.files[0])} />
+ File
+
+
+
handleTrackResizeMouseDown(e, track.id)}
+ className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
+
+ );
+ })}
+
+
+
+ {/* ══ RIGHT COLUMN: TIMELINE SCROLL VIEWPORT ══ */}
+
+
+
+
+ {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
+ const sec = i; const x = sec * zoom;
+ return (
{formatTime(sec)}
);
+ })}
+
+
+
+
+ {tracks.map((track, idx) => {
+ const isSelected = selectedTrackId === track.id;
+ return (
+
e.preventDefault()}
+ onDrop={e => { e.preventDefault(); if (e.dataTransfer.files[0]) loadFileOnTrack(track.id, e.dataTransfer.files[0]); }}
+ onMouseEnter={() => setHoveredTrackId(track.id)}>
+
+ {track.buffer && (
+
+ handleSplitTrack(track.id)}
+ 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)}
+ className="absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors" onClick={e => e.stopPropagation()} />
+
+ );
+ })}
+
{ if (draggedClipRef.current) { setHoveredTrackId(addNewTrack()); } }}
+ onClick={addNewTrack}>
+ Kéo clip xuống hoặc Click tạo Track
+
+ {selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && (
+
+
handleHandleDragStart(e, 'left')}>
+
handleHandleDragStart(e, 'right')}>
+
+ )}
+
+
+
+
+
+
+ {renderDock('right', 'Right')}
-
-
- Edit in Temp Tab
-
-
-
-
-
-
-
- {undoStack.length}/{MAX_UNDO}
-
-
-
+ {renderDock('bottom', 'Bottom')}
+
+ );
+ })()}
{/* ── Status Bar ── */}
@@ -4562,7 +4521,26 @@
{isLoopingSelection && selectionMode === 'local' && Solo Loop }
{isLoopingSelection && selectionMode !== 'local' && Master Loop }
-
+
+ setShowExportPanel(p => !p)}
+ className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showExportPanel ? 'bg-cyan-900 text-cyan-300' : 'text-zinc-500 hover:text-zinc-300'}`}
+ title={`Export Panel (${panelPositions.export})`}>
+
+ {showExportPanel ? panelPositions.export[0].toUpperCase() : ''}
+
+ setShowSelectionPanel(p => !p)}
+ className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showSelectionPanel ? 'bg-amber-900 text-amber-300' : 'text-zinc-500 hover:text-zinc-300'}`}
+ title={`Selection Panel (${panelPositions.selection})`}>
+
+ {showSelectionPanel ? panelPositions.selection[0].toUpperCase() : ''}
+
+ setShowAIPanel(p => !p)}
+ className={`px-1.5 py-0.5 rounded text-[9px] font-semibold transition flex items-center gap-1 ${showAIPanel ? 'bg-purple-900 text-purple-300' : 'text-zinc-500 hover:text-zinc-300'}`}
+ title={`AI Panel (${panelPositions.ai})`}>
+
+ {showAIPanel ? panelPositions.ai[0].toUpperCase() : ''}
+
+
Scroll: Zoom