player bar: the overflow menu out from under the bar's own clip, and Lyrics wired for real
This commit is contained in:
1 parent
df3d15553a
commit
f4f26d6d34
1 file changed
+79
-36
@@ -215,25 +215,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- the overflow menu -------------------------------------------------
|
// ---- the overflow menu -------------------------------------------------
|
||||||
|
// The popup renders OUTSIDE the footer and position:fixed (the SaveMenu
|
||||||
|
// idiom): the bar is overflow:hidden, so an absolute menu inside it got
|
||||||
|
// amputated at the bar's top edge — and what poked out still lost to the
|
||||||
|
// Now Playing panel's z-indexed layers. Fixed + measured coordinates
|
||||||
|
// escape both the clip and the stacking war. It must NOT live inside
|
||||||
|
// .extras: that element's centering transform would become a fixed
|
||||||
|
// element's containing block and hand it right back to the clip.
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
let menuWrap = $state(null);
|
let menuWrap = $state(null);
|
||||||
|
let menuEl = $state(null);
|
||||||
|
let menuPos = $state({ right: 0, bottom: 0 });
|
||||||
|
function toggleMenu() {
|
||||||
|
if (!menuOpen && menuWrap) {
|
||||||
|
const r = menuWrap.getBoundingClientRect();
|
||||||
|
// Right-aligned over the button, opening upward — the same seat
|
||||||
|
// the old absolute placement (bottom + right of the wrap) gave it.
|
||||||
|
menuPos = {
|
||||||
|
right: Math.max(8, window.innerWidth - r.right - 6),
|
||||||
|
bottom: window.innerHeight - r.top + 6,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
menuOpen = !menuOpen;
|
||||||
|
}
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!menuOpen) return;
|
if (!menuOpen) return;
|
||||||
const away = (e) => {
|
const away = (e) => {
|
||||||
if (menuWrap && !menuWrap.contains(e.target)) menuOpen = false;
|
const inside =
|
||||||
|
(menuWrap && menuWrap.contains(e.target)) ||
|
||||||
|
(menuEl && menuEl.contains(e.target));
|
||||||
|
if (!inside) menuOpen = false;
|
||||||
};
|
};
|
||||||
const esc = (e) => e.key === "Escape" && (menuOpen = false);
|
const esc = (e) => e.key === "Escape" && (menuOpen = false);
|
||||||
|
// The menu is anchored to coordinates measured at open; a resize
|
||||||
|
// moves the anchor out from under them, so it closes instead.
|
||||||
|
const close = () => (menuOpen = false);
|
||||||
window.addEventListener("pointerdown", away);
|
window.addEventListener("pointerdown", away);
|
||||||
window.addEventListener("keydown", esc);
|
window.addEventListener("keydown", esc);
|
||||||
|
window.addEventListener("resize", close);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("pointerdown", away);
|
window.removeEventListener("pointerdown", away);
|
||||||
window.removeEventListener("keydown", esc);
|
window.removeEventListener("keydown", esc);
|
||||||
|
window.removeEventListener("resize", close);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
function openQueue() {
|
function openQueue() {
|
||||||
menuOpen = false;
|
menuOpen = false;
|
||||||
showNowPlaying(); // the queue lives in the panel; always reveal it
|
showNowPlaying(); // the queue lives in the panel; always reveal it
|
||||||
}
|
}
|
||||||
|
function openLyrics() {
|
||||||
|
menuOpen = false;
|
||||||
|
ui.lyricsFull = true; // the fullscreen overlay (LyricsOverlay.svelte)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Save to library, from the ⋯ menu ---------------------------------
|
// ---- Save to library, from the ⋯ menu ---------------------------------
|
||||||
// Only a stream can be saved; a single queued stream has one destination
|
// Only a stream can be saved; a single queued stream has one destination
|
||||||
@@ -1224,40 +1257,10 @@
|
|||||||
title="More"
|
title="More"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={menuOpen}
|
aria-expanded={menuOpen}
|
||||||
onclick={() => (menuOpen = !menuOpen)}
|
onclick={toggleMenu}
|
||||||
>
|
>
|
||||||
<Icon name="more-horizontal" size={17} />
|
<Icon name="more-horizontal" size={17} />
|
||||||
</button>
|
</button>
|
||||||
{#if menuOpen}
|
|
||||||
<div class="menu" role="menu">
|
|
||||||
<button role="menuitem" onclick={openQueue}>
|
|
||||||
<Icon name="playlist" size={15} /> Queue
|
|
||||||
</button>
|
|
||||||
<!-- Save's new home. The bar's download button became the
|
|
||||||
expand toggle, and this menu is where everything past
|
|
||||||
Save was always going to live. Only a stream can be
|
|
||||||
saved, so the items appear only for one. -->
|
|
||||||
{#if canSave}
|
|
||||||
<button role="menuitem" onclick={() => saveFromMenu("track")}>
|
|
||||||
<Icon name="music" size={15} /> Save this track
|
|
||||||
</button>
|
|
||||||
{#if savingAlbum}
|
|
||||||
<button role="menuitem" onclick={() => saveFromMenu("album")}>
|
|
||||||
<Icon name="library" size={15} /> Save whole album
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<button role="menuitem" disabled title="Not built yet">
|
|
||||||
<Icon name="lyrics" size={15} /> Lyrics <span class="soon">soon</span>
|
|
||||||
</button>
|
|
||||||
<button role="menuitem" disabled title="Not built yet">
|
|
||||||
<Icon name="panel-right" size={15} /> Mini player <span class="soon">soon</span>
|
|
||||||
</button>
|
|
||||||
<button role="menuitem" disabled title="Not built yet">
|
|
||||||
<Icon name="volume" size={15} /> Output device <span class="soon">soon</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="vol-icon" title={player.muted ? "Unmute" : "Mute"} onclick={toggleMute}>
|
<button class="vol-icon" title={player.muted ? "Unmute" : "Mute"} onclick={toggleMute}>
|
||||||
@@ -1283,6 +1286,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Outside the footer on purpose: see the note at menuOpen. -->
|
||||||
|
{#if menuOpen}
|
||||||
|
<div
|
||||||
|
class="menu"
|
||||||
|
role="menu"
|
||||||
|
bind:this={menuEl}
|
||||||
|
style="right: {menuPos.right}px; bottom: {menuPos.bottom}px;"
|
||||||
|
>
|
||||||
|
<button role="menuitem" onclick={openQueue}>
|
||||||
|
<Icon name="playlist" size={15} /> Queue
|
||||||
|
</button>
|
||||||
|
<!-- Save's new home. The bar's download button became the expand
|
||||||
|
toggle, and this menu is where everything past Save was always
|
||||||
|
going to live. Only a stream can be saved, so the items appear
|
||||||
|
only for one. -->
|
||||||
|
{#if canSave}
|
||||||
|
<button role="menuitem" onclick={() => saveFromMenu("track")}>
|
||||||
|
<Icon name="music" size={15} /> Save this track
|
||||||
|
</button>
|
||||||
|
{#if savingAlbum}
|
||||||
|
<button role="menuitem" onclick={() => saveFromMenu("album")}>
|
||||||
|
<Icon name="library" size={15} /> Save whole album
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<!-- Enabled only with a track: the overlay closes itself the moment
|
||||||
|
player.track is empty, so opening it then would just blink. -->
|
||||||
|
<button role="menuitem" disabled={!currentItem} onclick={openLyrics}>
|
||||||
|
<Icon name="lyrics" size={15} /> Lyrics
|
||||||
|
</button>
|
||||||
|
<button role="menuitem" disabled title="Not built yet">
|
||||||
|
<Icon name="panel-right" size={15} /> Mini player <span class="soon">soon</span>
|
||||||
|
</button>
|
||||||
|
<button role="menuitem" disabled title="Not built yet">
|
||||||
|
<Icon name="volume" size={15} /> Output device <span class="soon">soon</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* FOUR blocks, absolutely placed, so the collapsed row and the expanded
|
/* FOUR blocks, absolutely placed, so the collapsed row and the expanded
|
||||||
stack are the same layout at two sets of coordinates and the bar can
|
stack are the same layout at two sets of coordinates and the bar can
|
||||||
@@ -1708,10 +1750,11 @@
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
.menu {
|
.menu {
|
||||||
position: absolute;
|
/* Fixed positioning escapes the bar's overflow clipping and every
|
||||||
bottom: 30px;
|
panel stacking context (the SaveMenu idiom, same tier). The
|
||||||
right: -6px;
|
coordinates come measured from the trigger at open. */
|
||||||
z-index: 40;
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|||||||
Reference in new issue
Block a user