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:
+101
-5
@@ -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
Reference in New Issue
Block a user