feat: add semantic selectors for now-playing media (#1138)

* feat: add semantic selectors for now-playing media

This change adds unique class names to the elements that display the currently playing media information. This makes it easier for extension developers to parse the DOM and understand what media is playing.

The following classes have been added:
- `media-player`: The main player container.
- `player-cover-art`: The cover art of the playing track.
- `song-title`: The title of the playing track.
- `song-artist`: The artist of the playing track.
- `song-album`: The album of the playing track.
- `player-state-playing`/`player-state-paused`: The state of the player.
- `elapsed-time`: The elapsed time of the playing track.
- `total-duration`: The total duration of the playing track.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Malachi Soord 2025-09-23 21:44:22 +02:00 committed by GitHub
parent 55e35e9b24
commit 8a3edb71df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 7 deletions

View file

@ -28,6 +28,7 @@ import {
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types'; import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
interface CenterControlsProps { interface CenterControlsProps {
@ -253,7 +254,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
</div> </div>
<div className={styles.sliderContainer}> <div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapper}>
<Text fw={600} isMuted isNoSelect size="xs"> <Text
className={PlaybackSelectors.elapsedTime}
fw={600}
isMuted
isNoSelect
size="xs"
>
{formattedTime} {formattedTime}
</Text> </Text>
</div> </div>
@ -281,7 +288,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
</div> </div>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapper}>
<Text fw={600} isMuted isNoSelect size="xs"> <Text
className={PlaybackSelectors.totalDuration}
fw={600}
isMuted
isNoSelect
size="xs"
>
{duration} {duration}
</Text> </Text>
</div> </div>

View file

@ -24,6 +24,7 @@ import { Image } from '/@/shared/components/image/image';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip'; import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
export const LeftControls = () => { export const LeftControls = () => {
@ -104,7 +105,10 @@ export const LeftControls = () => {
openDelay={500} openDelay={500}
> >
<Image <Image
className={styles.playerbarImage} className={clsx(
styles.playerbarImage,
PlaybackSelectors.playerCoverArt,
)}
loading="eager" loading="eager"
src={currentSong?.imageUrl ?? ''} src={currentSong?.imageUrl ?? ''}
/> />
@ -139,6 +143,7 @@ export const LeftControls = () => {
<div className={styles.lineItem} onClick={stopPropagation}> <div className={styles.lineItem} onClick={stopPropagation}>
<Group align="center" gap="xs" wrap="nowrap"> <Group align="center" gap="xs" wrap="nowrap">
<Text <Text
className={PlaybackSelectors.songTitle}
component={Link} component={Link}
fw={500} fw={500}
isLink isLink
@ -164,7 +169,11 @@ export const LeftControls = () => {
</Group> </Group>
</div> </div>
<div <div
className={clsx(styles.lineItem, styles.secondary)} className={clsx(
styles.lineItem,
styles.secondary,
PlaybackSelectors.songArtist,
)}
onClick={stopPropagation} onClick={stopPropagation}
> >
{artists?.map((artist, index) => ( {artists?.map((artist, index) => (
@ -190,7 +199,11 @@ export const LeftControls = () => {
))} ))}
</div> </div>
<div <div
className={clsx(styles.lineItem, styles.secondary)} className={clsx(
styles.lineItem,
styles.secondary,
PlaybackSelectors.songAlbum,
)}
onClick={stopPropagation} onClick={stopPropagation}
> >
<Text <Text

View file

@ -6,6 +6,7 @@ import styles from './player-button.module.css';
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
import { Tooltip, TooltipProps } from '/@/shared/components/tooltip/tooltip'; import { Tooltip, TooltipProps } from '/@/shared/components/tooltip/tooltip';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> { interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
icon: ReactNode; icon: ReactNode;
@ -62,9 +63,13 @@ interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>( export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
({ isPaused, onClick, ...props }: PlayButtonProps, ref) => { ({ isPaused, onClick, ...props }: PlayButtonProps, ref) => {
const playerStateClass = isPaused
? PlaybackSelectors.playerStatePaused
: PlaybackSelectors.playerStatePlaying;
return ( return (
<ActionIcon <ActionIcon
className={styles.main} className={clsx(styles.main, playerStateClass)}
icon={isPaused ? 'mediaPlay' : 'mediaPause'} icon={isPaused ? 'mediaPlay' : 'mediaPause'}
iconProps={{ iconProps={{
size: 'lg', size: 'lg',

View file

@ -1,3 +1,4 @@
import clsx from 'clsx';
import { MouseEvent, useCallback } from 'react'; import { MouseEvent, useCallback } from 'react';
import styles from './playerbar.module.css'; import styles from './playerbar.module.css';
@ -25,6 +26,7 @@ import {
usePlaybackType, usePlaybackType,
useSettingsStore, useSettingsStore,
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { PlaybackType } from '/@/shared/types/types'; import { PlaybackType } from '/@/shared/types/types';
export const Playerbar = () => { export const Playerbar = () => {
@ -56,7 +58,7 @@ export const Playerbar = () => {
return ( return (
<div <div
className={styles.container} className={clsx(styles.container, PlaybackSelectors.mediaPlayer)}
onClick={playerbarOpenDrawer ? handleToggleFullScreenPlayer : undefined} onClick={playerbarOpenDrawer ? handleToggleFullScreenPlayer : undefined}
> >
<div className={styles.controlsGrid}> <div className={styles.controlsGrid}>

View file

@ -0,0 +1,14 @@
// Defines the selectors used to identify playback-related elements in the UI.
// Can be used by browser extensions for accessing meta data around currently playing media.
export const PlaybackSelectors = {
elapsedTime: 'elapsed-time',
mediaPlayer: 'media-player',
playerCoverArt: 'player-cover-art',
playerStatePaused: 'player-state-paused',
playerStatePlaying: 'player-state-playing',
songAlbum: 'song-album',
songArtist: 'song-artist',
songTitle: 'song-title',
totalDuration: 'total-duration',
} as const;