FIX: Media Explorer panel chọn tập tin ở thư mục nào thì di chuyển đến thư mục đó

This commit is contained in:
2026-08-02 21:55:52 +07:00
parent 73025fc387
commit adccf6430a
2 changed files with 106 additions and 7 deletions
+101 -5
View File
@@ -9037,6 +9037,9 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
}); });
const [zoom, setZoom] = React.useState(1.0); const [zoom, setZoom] = React.useState(1.0);
const [scrollOffset, setScrollOffset] = React.useState(0);
const scrollOffsetRef = React.useRef(0);
scrollOffsetRef.current = scrollOffset;
const [selStart, setSelStart] = React.useState(null); const [selStart, setSelStart] = React.useState(null);
const [selEnd, setSelEnd] = React.useState(null); const [selEnd, setSelEnd] = React.useState(null);
const [isDragging, setIsDragging] = React.useState(false); const [isDragging, setIsDragging] = React.useState(false);
@@ -9072,6 +9075,16 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
tempoRef.current = tempo; tempoRef.current = tempo;
currentTimeRef.current = currentTime; currentTimeRef.current = currentTime;
React.useEffect(() => {
setScrollOffset(0);
}, [selected]);
React.useEffect(() => {
return () => {
window.mediaExplorerActive = false;
};
}, []);
const getCanvasLayout = () => { const getCanvasLayout = () => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return { w: 300, h: 100, pxPerBeat: 42, pxPerSec: 84, offset: 0, dur: 0, contentW: 0 }; if (!canvas) return { w: 300, h: 100, pxPerBeat: 42, pxPerSec: 84, offset: 0, dur: 0, contentW: 0 };
@@ -9093,9 +9106,11 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
contentW = Math.max(1, dur * pxPerSec); contentW = Math.max(1, dur * pxPerSec);
} }
let offset = 0; let offset = scrollOffset;
if (isPlayingRef.current && contentW > w && dur > 0) { if (isPlayingRef.current && contentW > w && dur > 0) {
offset = Math.max(0, Math.min(contentW - w, currentTimeRef.current * pxPerSec - w / 2)); offset = Math.max(0, Math.min(contentW - w, currentTimeRef.current * pxPerSec - w / 2));
} else {
offset = Math.max(0, Math.min(contentW - w, scrollOffset));
} }
return { w, h, pxPerBeat, pxPerSec, offset, dur, contentW }; return { w, h, pxPerBeat, pxPerSec, offset, dur, contentW };
}; };
@@ -9119,6 +9134,16 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
const handleCanvasWheel = (e) => { const handleCanvasWheel = (e) => {
if (!selected) return; if (!selected) return;
e.preventDefault(); e.preventDefault();
if (e.shiftKey) {
const scrollSpeed = 45;
const direction = e.deltaY > 0 ? 1 : -1;
setScrollOffset(prev => {
const { contentW, w } = getCanvasLayout();
const maxScroll = Math.max(0, contentW - w);
return Math.max(0, Math.min(maxScroll, prev + direction * scrollSpeed));
});
return;
}
const zoomFactor = 1.15; const zoomFactor = 1.15;
if (e.deltaY < 0) { if (e.deltaY < 0) {
setZoom(prev => Math.min(10.0, prev * zoomFactor)); setZoom(prev => Math.min(10.0, prev * zoomFactor));
@@ -9278,6 +9303,34 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
return () => window.removeEventListener('click', handleGlobalClick); return () => window.removeEventListener('click', handleGlobalClick);
}, []); }, []);
React.useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === ' ' || e.code === 'Space') {
if (window.mediaExplorerActive) {
const target = e.target;
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
return;
}
e.preventDefault();
e.stopPropagation();
if (isPlayingRef.current) {
stopMediaPlayback();
} else {
const cur = selectedRef.current;
if (cur) {
selectTokenRef.current++;
playSelected(cur, selectTokenRef.current);
}
}
}
}
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true });
};
}, []);
const [computerRoots, setComputerRoots] = React.useState(null); const [computerRoots, setComputerRoots] = React.useState(null);
const [computerTree, setComputerTree] = React.useState({}); const [computerTree, setComputerTree] = React.useState({});
const [computerPath, setComputerPath] = React.useState(null); const [computerPath, setComputerPath] = React.useState(null);
@@ -10081,9 +10134,12 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
const beats = Math.max(totalBeats, 4); const beats = Math.max(totalBeats, 4);
const contentW = Math.max(w, beats * pxPerBeat); const contentW = Math.max(w, beats * pxPerBeat);
let offset = 0; const offsetVal = scrollOffsetRef.current;
let offset = offsetVal;
if (playing && contentW > w && dur > 0) { if (playing && contentW > w && dur > 0) {
offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2)); offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2));
} else {
offset = Math.max(0, Math.min(contentW - w, offsetVal));
} }
// bar lines (every 4 beats) // bar lines (every 4 beats)
@@ -10138,9 +10194,12 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
const pk = curPeaks && curPeaks.length > 0 ? curPeaks : null; const pk = curPeaks && curPeaks.length > 0 ? curPeaks : null;
if (pk) { if (pk) {
const contentW = Math.max(1, dur * pxPerSec); const contentW = Math.max(1, dur * pxPerSec);
let offset = 0; const offsetVal = scrollOffsetRef.current;
let offset = offsetVal;
if (playing && contentW > w && dur > 0) { if (playing && contentW > w && dur > 0) {
offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2)); offset = Math.max(0, Math.min(contentW - w, t * pxPerSec - w / 2));
} else {
offset = Math.max(0, Math.min(contentW - w, offsetVal));
} }
const mid = h / 2; const mid = h / 2;
ctx.fillStyle = '#22c55e'; ctx.fillStyle = '#22c55e';
@@ -10193,11 +10252,46 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
} }
}; };
React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, audioDuration, midiNotes, selected, folder, isPlaying, zoom, selStart, selEnd]); React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, audioDuration, midiNotes, selected, folder, isPlaying, zoom, selStart, selEnd, scrollOffset]);
React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []); React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []);
const handleSelect = (f) => { const handleSelect = (f) => {
if (!f || f.is_dir) return; if (!f || f.is_dir) return;
// Find parent path of selected file and scroll it into view in Tree pane
if (f.path) {
const lastSlash = f.path.lastIndexOf('/');
if (lastSlash > 0) {
const parentPath = f.path.substring(0, lastSlash);
setComputerPath(parentPath);
// Expand all parent nodes in computerTree
setComputerTree(prev => {
const next = { ...prev };
let current = parentPath;
while (current) {
if (!next[current]) {
next[current] = { dirs: [], expanded: true };
} else {
next[current] = { ...next[current], expanded: true };
}
const idx = current.lastIndexOf('/');
if (idx <= 0) break;
current = current.substring(0, idx);
}
return next;
});
// Scroll the parent tree element into view
setTimeout(() => {
const treeNodeEl = document.querySelector(`[data-tree-path="${parentPath}"]`);
if (treeNodeEl) {
treeNodeEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
}
}
selectTokenRef.current++; selectTokenRef.current++;
const token = selectTokenRef.current; const token = selectTokenRef.current;
setSelected(f); setSelected(f);
@@ -10224,7 +10318,7 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
const fav = isFavorite(entry); const fav = isFavorite(entry);
return ( return (
<React.Fragment key={nodePath}> <React.Fragment key={nodePath}>
<div className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath === nodePath ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`} <div data-tree-path={nodePath} className={`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath === nodePath ? 'bg-slate-300 text-slate-900' : 'hover:bg-slate-200 text-slate-800'}`}
style={{ paddingLeft: pad }} style={{ paddingLeft: pad }}
onClick={() => browseComputerDir(entry)} onClick={() => browseComputerDir(entry)}
onDoubleClick={e => { e.stopPropagation(); toggleComputerDir(entry); }} onDoubleClick={e => { e.stopPropagation(); toggleComputerDir(entry); }}
@@ -10315,6 +10409,7 @@ const MediaExplorerPanel = ({ height, clipboardRef }) => {
<div className="pl-3 space-y-0.5"> <div className="pl-3 space-y-0.5">
{favorites.map((fav, fi) => ( {favorites.map((fav, fi) => (
<div key={fav.path + fi} <div key={fav.path + fi}
data-tree-path={fav.path}
className="flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm hover:bg-amber-100 text-slate-800" className="flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm hover:bg-amber-100 text-slate-800"
style={{ paddingLeft: 20 }} style={{ paddingLeft: 20 }}
onClick={() => openFavorite(fav)} onClick={() => openFavorite(fav)}
@@ -12666,6 +12761,7 @@ const App = () => {
// Global space play/pause shortcut for transport // Global space play/pause shortcut for transport
if (e.key === ' ' || e.code === 'Space') { if (e.key === ' ' || e.code === 'Space') {
if (window.mediaExplorerActive) return;
e.preventDefault(); e.preventDefault();
if (recordingStateRef.current === 'RECORDING' || recordingStateRef.current === 'COUNT_IN') { if (recordingStateRef.current === 'RECORDING' || recordingStateRef.current === 'COUNT_IN') {
handleRecordClickRef.current(); handleRecordClickRef.current();
File diff suppressed because one or more lines are too long