From 60dce0135e5df3df05b44d2c68fd09e7a10a0641 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 19 Jul 2026 12:46:46 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20b=E1=BB=91=20c=E1=BB=A5c=20l=E1=BA=A1i?= =?UTF-8?q?=20UI=20v=E1=BB=9Bi=20float=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/index.html | 970 +++++++++++++++++++-------------------- 1 file changed, 474 insertions(+), 496 deletions(-) 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) */} -
- - - - -
- - - - - - -
- - {/* Transport Controls - gần toolbar */} - - - - - -
- -
-
- Snap - -
+ {/* ── 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()}> - Mô phỏng: - - -
- -
e.stopPropagation()}> -
- updateTrackVolume(track.id, v)} /> - Gain: {Math.round(track.volume * 100)}% -
- -
- loadFileOnTrack(track.id, e.target.files[0])} - /> - -
-
- - {/* 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 */} -
+ + + + +
+ + + +
- {/* ══ 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 && ( -
- -
- )} - - {/* 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 */} -
-
-
-
-
-
+ + + + + + +
+ +
+
{formatTime(currentTime)}
- {/* ── Footer ── */} -
- - {/* Export Section */} -
-
-

- Trộn & Xuất bản -

-
-
- - -
-
- - -
-
- - -
-
-
- -
+ {/* ── 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 +

+ +
+
+
+ +
- )} +
+ + +
+
+ + +
+
+
-
+ ); + if (panelId === 'ai') return ( +
+
startPanelDrag('ai', e)}> +

+ + AI +

+ +
+
+
// {analysisState.status}
+ {analysisState.data &&
BPM: {analysisState.data.bpm}
} +
+
+ + + + + +
+
+ ); + if (panelId === 'selection') return ( +
+
startPanelDrag('selection', e)}> + + Selection + + +
+
+
+ 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; + }; -
- - - + 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 + +
+
+ {/* 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()}> + Synth: + + +
+
e.stopPropagation()}> +
+ updateTrackVolume(track.id, v)} /> + Gain: {Math.round(track.volume * 100)}% +
+
+ loadFileOnTrack(track.id, e.target.files[0])} /> + +
+
+
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 && ( +
+ +
+ )} +
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')}
-
- - - - {undoStack.length}/{MAX_UNDO} -
-
-
+ {renderDock('bottom', 'Bottom')} +
+ ); + })()} {/* ── Status Bar ── */}
@@ -4562,7 +4521,26 @@ {isLoopingSelection && selectionMode === 'local' && Solo Loop} {isLoopingSelection && selectionMode !== 'local' && Master Loop}
-
+
+ + + + Scroll: Zoom