feat(now-playing): plain text instead of links in the phone mini player
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (push) Skipped
CI / e2e (push) Skipped
The bottom bar's title, artist and "Playing from X" all navigate. In a bar sized for a bar they are a few characters of text, which is not a touch target — and explore-link holds its navigation for one double-click interval and drops it if a second click arrives, a gesture that exists so double-clicking a row can play it and that means nothing on touch. Below the shell's phone breakpoint the three render as plain text. The words are unchanged: the source line still says where the queue came from, because dropping the link is the change and dropping the information would be a different and worse one. The cover art already carries the phone-only button that opens the full-screen Now Playing view, which is where the links live. This is in JS rather than in the stylesheet because what changes is the content, not its appearance — no CSS rule takes a click handler off an element. matchMedia is read in connectedCallback for the reason the reduce-motion query beside it already is, so a test can answer it first. Two smaller things. PHONE_QUERY moves out of track-list.ts into utils/breakpoints.ts: it was a private const when one component needed it, and a second reader is where a copy starts drifting from index.css. And `phone` joins geometryKey(), because crossing the breakpoint swaps a link for a bare string and the marquee travels a distance read from measuring it — the words being identical either side is not the same as the box measuring the same. Closes #61
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
isQueueSourceNavigable,
|
||||
navigateToQueueSource,
|
||||
} from '@utils/queue-source-link';
|
||||
import { PHONE_QUERY } from '@utils/breakpoints';
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { creditStore } from '@store/credit-store';
|
||||
import { QueueController } from '@store/controllers/queue-controller';
|
||||
@@ -80,6 +81,19 @@ export class NowPlaying extends LitElement {
|
||||
|
||||
private reduceMotionQuery?: MediaQueryList;
|
||||
|
||||
/**
|
||||
* Phone width, from the shell's own breakpoint.
|
||||
*
|
||||
* This is in JS rather than in the stylesheet because what changes
|
||||
* is the *content*, not its appearance: the title, artist and
|
||||
* source render as plain text instead of as links, and no CSS rule
|
||||
* can take a click handler off an element.
|
||||
*/
|
||||
@state()
|
||||
private phone = false;
|
||||
|
||||
private phoneQuery?: MediaQueryList;
|
||||
|
||||
/** Whether each field is actively mid-scroll (class toggle). */
|
||||
@state()
|
||||
private titleScrolling = false;
|
||||
@@ -341,6 +355,12 @@ export class NowPlaying extends LitElement {
|
||||
this.reduceMotion = this.reduceMotionQuery?.matches ?? false;
|
||||
this.reduceMotionQuery?.addEventListener('change', this.handleReduceMotionChange);
|
||||
|
||||
// Same reasoning as above: looked up here, not at module load,
|
||||
// so a test can install its own matchMedia first.
|
||||
this.phoneQuery = window.matchMedia?.(PHONE_QUERY);
|
||||
this.phone = this.phoneQuery?.matches ?? false;
|
||||
this.phoneQuery?.addEventListener('change', this.handlePhoneChange);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.geometryDirty = true;
|
||||
this.requestUpdate();
|
||||
@@ -364,6 +384,7 @@ export class NowPlaying extends LitElement {
|
||||
this.attachDragListeners(false);
|
||||
window.removeEventListener(SCROLL_CHANGE_EVENT, this.handleScrollModeEvent);
|
||||
this.reduceMotionQuery?.removeEventListener('change', this.handleReduceMotionChange);
|
||||
this.phoneQuery?.removeEventListener('change', this.handlePhoneChange);
|
||||
this.resizeObserver?.disconnect();
|
||||
this.stopScrollCycle('title');
|
||||
this.stopScrollCycle('artist');
|
||||
@@ -488,7 +509,7 @@ export class NowPlaying extends LitElement {
|
||||
@mouseleave=${this.handleTitleMouseLeave}
|
||||
@transitionend=${() => this.onScrollCycleEnd('title')}
|
||||
>
|
||||
<span class="scroll-content">${trackLink(track.title, track.album, track.releaseGroupMbid, track.recordingMbid) || track.title}</span>
|
||||
<span class="scroll-content">${this.phone ? track.title : trackLink(track.title, track.album, track.releaseGroupMbid, track.recordingMbid) || track.title}</span>
|
||||
</span>
|
||||
<span
|
||||
class="track-artist ${artistScrolling ? 'will-scroll' : ''} ${this.artistScrolling ? 'scrolling' : ''}"
|
||||
@@ -498,14 +519,15 @@ export class NowPlaying extends LitElement {
|
||||
@mouseleave=${this.handleArtistMouseLeave}
|
||||
@transitionend=${() => this.onScrollCycleEnd('artist')}
|
||||
>
|
||||
<span class="scroll-content">${creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'}</span>
|
||||
<span class="scroll-content">${this.phone ? track.artist || 'Unknown Artist' : creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'}</span>
|
||||
</span>
|
||||
${describeQueueSource(this.queue.source)
|
||||
? html`
|
||||
<span
|
||||
class="track-source ${isQueueSourceNavigable(this.queue.source) ? 'navigable' : ''}"
|
||||
class="track-source ${!this.phone && isQueueSourceNavigable(this.queue.source) ? 'navigable' : ''}"
|
||||
data-testid="now-playing-source"
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (this.phone) return;
|
||||
if (!isQueueSourceNavigable(this.queue.source)) return;
|
||||
navigateToQueueSource(
|
||||
e.currentTarget as EventTarget,
|
||||
@@ -571,6 +593,10 @@ export class NowPlaying extends LitElement {
|
||||
this.reduceMotion = e.matches;
|
||||
};
|
||||
|
||||
private handlePhoneChange = (e: MediaQueryListEvent): void => {
|
||||
this.phone = e.matches;
|
||||
};
|
||||
|
||||
private shouldScroll(field: 'title' | 'artist'): boolean {
|
||||
const overflows = field === 'title' ? this.titleOverflows : this.artistOverflows;
|
||||
|
||||
@@ -606,6 +632,12 @@ export class NowPlaying extends LitElement {
|
||||
track?.artist ?? '',
|
||||
this.shouldScroll('title') ? '1' : '0',
|
||||
this.shouldScroll('artist') ? '1' : '0',
|
||||
// Crossing the breakpoint swaps a link for a bare string,
|
||||
// and a link is not guaranteed to measure the same as the
|
||||
// text inside it. The marquee travels a distance read from
|
||||
// that measurement, so this belongs in the key even though
|
||||
// the words are identical either side.
|
||||
this.phone ? '1' : '0',
|
||||
].join('\u0000');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { SelectionController } from '@utils/selection-controller';
|
||||
import type { SelectionHost } from '@utils/selection-controller';
|
||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||
import { PHONE_QUERY } from '@utils/breakpoints';
|
||||
import {
|
||||
ContextMenuController,
|
||||
contextMenuStyles,
|
||||
@@ -105,9 +106,6 @@ const ROW_CHROME_WIDTH =
|
||||
const ROW_HEIGHT = 33;
|
||||
const PHONE_ROW_HEIGHT = 52;
|
||||
|
||||
/** The shell's phone breakpoint, as `index.css` and every component
|
||||
* stylesheet spells it. */
|
||||
const PHONE_QUERY = '(max-width: 599px)';
|
||||
|
||||
// Inline SVG paths for favorite icons — eliminates wa-icon shadow DOM
|
||||
// overhead (30-50 shadow roots during scroll). Font Awesome 6 paths.
|
||||
|
||||
Reference in New Issue
Block a user