mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
fix(ui): flush playlist context submenus to parent row (#713)
* fix(ui): place playlist context submenus flush to trigger row Use left/right/top 100% instead of calc(100% + 4px) so there is no dead gap when moving the pointer from Add to playlist into the submenu. * fix(ui): defer closing playlist submenu on trigger mouseleave Use a short timer and :hover on the trigger row so slow moves across border/subpixel gaps still reach the nested submenu; cancel timer on re-enter and when the context menu closes.
This commit is contained in:
@@ -56,15 +56,41 @@ export default function ContextMenu() {
|
|||||||
const [keyboardRating, setKeyboardRating] = useState<{ kind: 'song' | 'album' | 'artist'; id: string; value: number } | null>(null);
|
const [keyboardRating, setKeyboardRating] = useState<{ kind: 'song' | 'album' | 'artist'; id: string; value: number } | null>(null);
|
||||||
const [pendingSubmenuKeyboardFocus, setPendingSubmenuKeyboardFocus] = useState(false);
|
const [pendingSubmenuKeyboardFocus, setPendingSubmenuKeyboardFocus] = useState(false);
|
||||||
|
|
||||||
|
const playlistSubmenuCloseTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const cancelPlaylistSubmenuCloseTimer = useCallback(() => {
|
||||||
|
if (playlistSubmenuCloseTimerRef.current != null) {
|
||||||
|
window.clearTimeout(playlistSubmenuCloseTimerRef.current);
|
||||||
|
playlistSubmenuCloseTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/** Delay close so a slow move across subpixel / border seams still lands on `.context-submenu` (a child of the row). */
|
||||||
|
const onPlaylistSubmenuTriggerMouseLeave = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
const cur = e.currentTarget;
|
||||||
|
const next = e.relatedTarget;
|
||||||
|
if (next instanceof Node && cur.contains(next)) return;
|
||||||
|
cancelPlaylistSubmenuCloseTimer();
|
||||||
|
playlistSubmenuCloseTimerRef.current = window.setTimeout(() => {
|
||||||
|
playlistSubmenuCloseTimerRef.current = null;
|
||||||
|
if (!cur.isConnected) return;
|
||||||
|
if (!cur.matches(':hover')) setPlaylistSubmenuOpen(false);
|
||||||
|
}, 140);
|
||||||
|
},
|
||||||
|
[cancelPlaylistSubmenuCloseTimer],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contextMenu.isOpen) {
|
if (contextMenu.isOpen) {
|
||||||
|
cancelPlaylistSubmenuCloseTimer();
|
||||||
setCoords({ x: contextMenu.x, y: contextMenu.y });
|
setCoords({ x: contextMenu.x, y: contextMenu.y });
|
||||||
setPlaylistSubmenuOpen(false);
|
setPlaylistSubmenuOpen(false);
|
||||||
setPlaylistSongIds([]);
|
setPlaylistSongIds([]);
|
||||||
setKeyboardRating(null);
|
setKeyboardRating(null);
|
||||||
setPendingSubmenuKeyboardFocus(false);
|
setPendingSubmenuKeyboardFocus(false);
|
||||||
}
|
}
|
||||||
}, [contextMenu.isOpen, contextMenu.x, contextMenu.y]);
|
}, [contextMenu.isOpen, contextMenu.x, contextMenu.y, cancelPlaylistSubmenuCloseTimer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contextMenu.isOpen && menuRef.current) {
|
if (contextMenu.isOpen && menuRef.current) {
|
||||||
@@ -84,6 +110,7 @@ export default function ContextMenu() {
|
|||||||
previousFocusRef.current = document.activeElement as HTMLElement | null;
|
previousFocusRef.current = document.activeElement as HTMLElement | null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cancelPlaylistSubmenuCloseTimer();
|
||||||
// Clean up any keyboard focus styling when menu closes
|
// Clean up any keyboard focus styling when menu closes
|
||||||
menuRef.current
|
menuRef.current
|
||||||
?.querySelectorAll<HTMLElement>('.context-menu-keyboard-active')
|
?.querySelectorAll<HTMLElement>('.context-menu-keyboard-active')
|
||||||
@@ -95,7 +122,7 @@ export default function ContextMenu() {
|
|||||||
prev.focus({ preventScroll: true });
|
prev.focus({ preventScroll: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [contextMenu.isOpen, closeContextMenu]);
|
}, [contextMenu.isOpen, closeContextMenu, cancelPlaylistSubmenuCloseTimer]);
|
||||||
|
|
||||||
|
|
||||||
const { type, item, queueIndex, playlistId, playlistSongIndex, shareKindOverride } = contextMenu;
|
const { type, item, queueIndex, playlistId, playlistSongIndex, shareKindOverride } = contextMenu;
|
||||||
@@ -173,6 +200,8 @@ export default function ContextMenu() {
|
|||||||
keyboardRating={keyboardRating}
|
keyboardRating={keyboardRating}
|
||||||
playlistSubmenuOpen={playlistSubmenuOpen}
|
playlistSubmenuOpen={playlistSubmenuOpen}
|
||||||
setPlaylistSubmenuOpen={setPlaylistSubmenuOpen}
|
setPlaylistSubmenuOpen={setPlaylistSubmenuOpen}
|
||||||
|
cancelPlaylistSubmenuCloseTimer={cancelPlaylistSubmenuCloseTimer}
|
||||||
|
onPlaylistSubmenuTriggerMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
playlistSongIds={playlistSongIds}
|
playlistSongIds={playlistSongIds}
|
||||||
setPlaylistSongIds={setPlaylistSongIds}
|
setPlaylistSongIds={setPlaylistSongIds}
|
||||||
orbitRole={orbitRole}
|
orbitRole={orbitRole}
|
||||||
|
|||||||
@@ -102,11 +102,13 @@ export function AddToPlaylistSubmenu({ songIds, onDone, dropDown, triggerId }: P
|
|||||||
onDone();
|
onDone();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Flush to the parent edge (left/right/top 100%). Actual “hole” cases are handled
|
||||||
|
// in ContextMenu via a short delayed mouseleave + :hover check on the trigger row.
|
||||||
const subStyle: React.CSSProperties = dropDown
|
const subStyle: React.CSSProperties = dropDown
|
||||||
? { top: 'calc(100% + 4px)', left: 0, right: 'auto' }
|
? { top: '100%', left: 0, right: 'auto' }
|
||||||
: flipLeft
|
: flipLeft
|
||||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
: { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-submenu" data-parent-trigger-id={triggerId ?? ''} ref={subRef} style={subStyle}>
|
<div className="context-submenu" data-parent-trigger-id={triggerId ?? ''} ref={subRef} style={subStyle}>
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
|||||||
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
||||||
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
||||||
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
||||||
playlistSubmenuOpen, setPlaylistSubmenuOpen, playlistSongIds, setPlaylistSongIds,
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
||||||
|
playlistSongIds, setPlaylistSongIds,
|
||||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||||
applySongRating, applyAlbumRating, applyArtistRating,
|
applySongRating, applyAlbumRating, applyArtistRating,
|
||||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||||
@@ -89,8 +90,8 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `album:${album.id}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `album:${album.id}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`album:${album.id}`}
|
data-playlist-trigger-id={`album:${album.id}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`album:${album.id}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`album:${album.id}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
@@ -130,8 +131,8 @@ export default function AlbumContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-album:${albumIds.join(',')}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-album:${albumIds.join(',')}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`multi-album:${albumIds.join(',')}`}
|
data-playlist-trigger-id={`multi-album:${albumIds.join(',')}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`multi-album:${albumIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`multi-album:${albumIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export default function ArtistContextItems(props: ContextMenuItemsProps) {
|
|||||||
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
||||||
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
||||||
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
||||||
playlistSubmenuOpen, setPlaylistSubmenuOpen, playlistSongIds, setPlaylistSongIds,
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
||||||
|
playlistSongIds, setPlaylistSongIds,
|
||||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||||
applySongRating, applyAlbumRating, applyArtistRating,
|
applySongRating, applyAlbumRating, applyArtistRating,
|
||||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||||
@@ -37,8 +38,8 @@ export default function ArtistContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `artist:${artist.id}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `artist:${artist.id}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`artist:${artist.id}`}
|
data-playlist-trigger-id={`artist:${artist.id}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`artist:${artist.id}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`artist:${artist.id}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
@@ -99,8 +100,8 @@ export default function ArtistContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-artist:${artistIds.join(',')}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-artist:${artistIds.join(',')}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`multi-artist:${artistIds.join(',')}`}
|
data-playlist-trigger-id={`multi-artist:${artistIds.join(',')}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`multi-artist:${artistIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`multi-artist:${artistIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ export function MultiAlbumToPlaylistSubmenu({ albumIds, onDone, triggerId: _trig
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subStyle: React.CSSProperties = flipLeft
|
const subStyle: React.CSSProperties = flipLeft
|
||||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
: { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-submenu" ref={subRef} style={{ ...subStyle, visibility: visible ? 'visible' : 'hidden' }}>
|
<div className="context-submenu" ref={subRef} style={{ ...subStyle, visibility: visible ? 'visible' : 'hidden' }}>
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ export function MultiArtistToPlaylistSubmenu({ artistIds, onDone, triggerId: _tr
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subStyle: React.CSSProperties = flipLeft
|
const subStyle: React.CSSProperties = flipLeft
|
||||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
: { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-submenu" ref={subRef} style={subStyle}>
|
<div className="context-submenu" ref={subRef} style={subStyle}>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
|||||||
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
||||||
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
||||||
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
||||||
playlistSubmenuOpen, setPlaylistSubmenuOpen, playlistSongIds, setPlaylistSongIds,
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
||||||
|
playlistSongIds, setPlaylistSongIds,
|
||||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||||
applySongRating, applyAlbumRating, applyArtistRating,
|
applySongRating, applyAlbumRating, applyArtistRating,
|
||||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||||
@@ -35,8 +36,8 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `playlist:${playlist.id}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `playlist:${playlist.id}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`playlist:${playlist.id}`}
|
data-playlist-trigger-id={`playlist:${playlist.id}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`playlist:${playlist.id}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`playlist:${playlist.id}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
@@ -79,8 +80,8 @@ export default function PlaylistContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-playlist:${playlistIds.join(',')}` ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === `multi-playlist:${playlistIds.join(',')}` ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={`multi-playlist:${playlistIds.join(',')}`}
|
data-playlist-trigger-id={`multi-playlist:${playlistIds.join(',')}`}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([`multi-playlist:${playlistIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([`multi-playlist:${playlistIds.join(',')}`]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ export function SinglePlaylistToPlaylistSubmenu({ playlist, onDone, triggerId }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subStyle: React.CSSProperties = flipLeft
|
const subStyle: React.CSSProperties = flipLeft
|
||||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
: { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
||||||
@@ -255,8 +255,8 @@ export function MultiPlaylistToPlaylistSubmenu({ playlists, onDone, triggerId }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subStyle: React.CSSProperties = flipLeft
|
const subStyle: React.CSSProperties = flipLeft
|
||||||
? { right: 'calc(100% + 4px)', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
? { right: '100%', left: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' }
|
||||||
: { left: 'calc(100% + 4px)', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
: { left: '100%', right: 'auto', top: flipUp ? 'auto' : -4, bottom: flipUp ? 0 : 'auto' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
<div ref={subRef} className="context-submenu" data-submenu-for={triggerId} style={{ ...subStyle, minWidth: 190 }}>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
|||||||
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
||||||
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
||||||
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
||||||
playlistSubmenuOpen, setPlaylistSubmenuOpen, playlistSongIds, setPlaylistSongIds,
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
||||||
|
playlistSongIds, setPlaylistSongIds,
|
||||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||||
applySongRating, applyAlbumRating, applyArtistRating,
|
applySongRating, applyAlbumRating, applyArtistRating,
|
||||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||||
@@ -42,8 +43,8 @@ export default function QueueItemContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={song.id}
|
data-playlist-trigger-id={song.id}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
|
|||||||
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
playTrack, playNext, enqueue, removeTrack, queue, currentTrack, closeContextMenu,
|
||||||
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
starredOverrides, setStarredOverride, lastfmLovedCache, setLastfmLovedForSong,
|
||||||
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
openSongInfo, userRatingOverrides, setKeyboardRating, keyboardRating,
|
||||||
playlistSubmenuOpen, setPlaylistSubmenuOpen, playlistSongIds, setPlaylistSongIds,
|
playlistSubmenuOpen, setPlaylistSubmenuOpen, cancelPlaylistSubmenuCloseTimer, onPlaylistSubmenuTriggerMouseLeave,
|
||||||
|
playlistSongIds, setPlaylistSongIds,
|
||||||
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
orbitRole, entityRatingSupport, audiomuseNavidromeEnabled,
|
||||||
applySongRating, applyAlbumRating, applyArtistRating,
|
applySongRating, applyAlbumRating, applyArtistRating,
|
||||||
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
handleAction, startRadio, startInstantMix, downloadAlbum, copyShareLink, isStarred,
|
||||||
@@ -80,8 +81,8 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={song.id}
|
data-playlist-trigger-id={song.id}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
@@ -234,8 +235,8 @@ export default function SongContextItems(props: ContextMenuItemsProps) {
|
|||||||
<div
|
<div
|
||||||
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
className={`context-menu-item context-menu-item--submenu ${playlistSubmenuOpen && playlistSongIds[0] === song.id ? 'active' : ''}`}
|
||||||
data-playlist-trigger-id={song.id}
|
data-playlist-trigger-id={song.id}
|
||||||
onMouseEnter={() => { setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
onMouseEnter={() => { cancelPlaylistSubmenuCloseTimer(); setPlaylistSongIds([song.id]); setPlaylistSubmenuOpen(true); }}
|
||||||
onMouseLeave={() => setPlaylistSubmenuOpen(false)}
|
onMouseLeave={onPlaylistSubmenuTriggerMouseLeave}
|
||||||
>
|
>
|
||||||
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
<ListMusic size={14} /> {t('contextMenu.addToPlaylist')}
|
||||||
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
<ChevronRight size={13} style={{ marginLeft: 'auto' }} />
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface ContextMenuItemsProps {
|
|||||||
keyboardRating: KeyboardRating | null;
|
keyboardRating: KeyboardRating | null;
|
||||||
playlistSubmenuOpen: boolean;
|
playlistSubmenuOpen: boolean;
|
||||||
setPlaylistSubmenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setPlaylistSubmenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
cancelPlaylistSubmenuCloseTimer: () => void;
|
||||||
|
onPlaylistSubmenuTriggerMouseLeave: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
playlistSongIds: string[];
|
playlistSongIds: string[];
|
||||||
setPlaylistSongIds: React.Dispatch<React.SetStateAction<string[]>>;
|
setPlaylistSongIds: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
orbitRole: 'host' | 'guest' | null;
|
orbitRole: 'host' | 'guest' | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user