Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions renderer/src/FileExplorerView.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo } from 'react';
import { List } from 'react-window';
import { usePlayer } from './PlayerContext.jsx';
import { artworkUrl } from './artworkUrl.js';
Expand Down Expand Up @@ -308,6 +308,10 @@ export default function FileExplorerView({ style }) {
const [selectedPaths, setSelectedPaths] = useState(new Set());
const [playlists, setPlaylists] = useState([]);
const [contextMenu, setContextMenu] = useState(null);
// Nudge applied after measuring the rendered menu so it never overflows the
// viewport bottom/right edge (#310).
const menuRef = useRef(null);
const [menuShift, setMenuShift] = useState({ x: 0, y: 0 });
const [detailsTrack, setDetailsTrack] = useState(null);
const [beatGridTrack, setBeatGridTrack] = useState(null);
const [toast, setToast] = useState(null);
Expand Down Expand Up @@ -692,6 +696,23 @@ export default function FileExplorerView({ style }) {
[selectedPaths, displayItems]
);

// Shift the menu up/left by exactly the overflow once it renders, so it
// always stays inside the window bounds (#310).
useLayoutEffect(() => {
if (!contextMenu) {
setMenuShift({ x: 0, y: 0 });
return;
}
const el = menuRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const shiftX = Math.max(0, rect.right - window.innerWidth + 8);
const shiftY = Math.max(0, rect.bottom - window.innerHeight + 8);
setMenuShift((prev) =>
prev.x === -shiftX && prev.y === -shiftY ? prev : { x: -shiftX, y: -shiftY }
);
}, [contextMenu]);

const closeMenu = useCallback(() => setContextMenu(null), []);

// ── Details save ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -944,7 +965,8 @@ export default function FileExplorerView({ style }) {
<div className="context-backdrop-invisible" onClick={closeMenu} />
<div
className={`context-menu${contextMenu.flipLeft ? ' context-menu--flip-left' : ''}${contextMenu.flipUp ? ' context-menu--flip-up' : ''}`}
style={{ top: contextMenu.y, left: contextMenu.x }}
ref={menuRef}
style={{ top: contextMenu.y + menuShift.y, left: contextMenu.x + menuShift.x }}
onMouseDown={(e) => e.stopPropagation()}
>
{menuIsDir ? (
Expand Down
27 changes: 25 additions & 2 deletions renderer/src/MusicLibrary.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
useEffect,
useLayoutEffect,
useState,
useRef,
useCallback,
Expand Down Expand Up @@ -560,6 +561,10 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) {

const [selectedIds, setSelectedIds] = useState(new Set());
const [contextMenu, setContextMenu] = useState(null); // { x, y, targetIds }
// Nudge applied after measuring the rendered menu so it never overflows the
// viewport bottom/right edge (#310).
const menuRef = useRef(null);
const [menuShift, setMenuShift] = useState({ x: 0, y: 0 });
const [toast, setToast] = useState(null); // { msg, ok } | null
const toastTimerRef = useRef(null);
const [drillStack, setDrillStack] = useState([]); // overlay drill-down stack [{ id, label, content }]
Expand Down Expand Up @@ -1112,6 +1117,23 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) {
[selectedIds]
);

// Re-measure once the menu (or its drill/submenu content) renders: shift the
// whole menu up/left by exactly the overflow so it stays inside the window.
useLayoutEffect(() => {
if (!contextMenu) {
setMenuShift({ x: 0, y: 0 });
return;
}
const el = menuRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const shiftX = Math.max(0, rect.right - window.innerWidth + 8);
const shiftY = Math.max(0, rect.bottom - window.innerHeight + 8);
setMenuShift((prev) =>
prev.x === -shiftX && prev.y === -shiftY ? prev : { x: -shiftX, y: -shiftY }
);
}, [contextMenu, drillStack, playlistSubmenu]);

const handleReanalyze = useCallback(async () => {
const targetIds = contextMenu?.targetIds ?? [];
setContextMenu(null);
Expand Down Expand Up @@ -1646,12 +1668,13 @@ function MusicLibrary({ selectedPlaylist, search, onSearchChange }) {
]
.filter(Boolean)
.join(' ')}
ref={menuRef}
style={
contextMenu.overlayMode
? undefined
: {
top: contextMenu.y,
left: contextMenu.x,
top: contextMenu.y + menuShift.y,
left: contextMenu.x + menuShift.x,
'--submenu-max-h': `${contextMenu.submenuMaxH}px`,
}
}
Expand Down
Loading