Files
feishin/src/renderer/features/player/components/left-controls.tsx
T
Malachi Soord 8a3edb71df 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>
2025-09-23 12:44:22 -07:00

231 lines
10 KiB
TypeScript

import { useHotkeys } from '@mantine/hooks';
import clsx from 'clsx';
import { AnimatePresence, LayoutGroup, motion } from 'motion/react';
import React, { MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, Link } from 'react-router-dom';
import styles from './left-controls.module.css';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { AppRoute } from '/@/renderer/router/routes';
import {
useAppStoreActions,
useCurrentSong,
useFullScreenPlayerStore,
useHotkeySettings,
useSetFullScreenPlayerStore,
useSidebarStore,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Image } from '/@/shared/components/image/image';
import { Separator } from '/@/shared/components/separator/separator';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { LibraryItem } from '/@/shared/types/domain-types';
export const LeftControls = () => {
const { t } = useTranslation();
const { setSideBar } = useAppStoreActions();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
const { collapsed, image } = useSidebarStore();
const hideImage = image && !collapsed;
const currentSong = useCurrentSong();
const title = currentSong?.name;
const artists = currentSong?.artists;
const { bindings } = useHotkeySettings();
const isSongDefined = Boolean(currentSong?.id);
const handleGeneralContextMenu = useHandleGeneralContextMenu(
LibraryItem.SONG,
SONG_CONTEXT_MENU_ITEMS,
);
const handleToggleFullScreenPlayer = (e?: KeyboardEvent | MouseEvent<HTMLDivElement>) => {
// don't toggle if right click
if (e && 'button' in e && e.button === 2) {
return;
}
e?.stopPropagation();
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
};
const handleToggleSidebarImage = (e?: MouseEvent<HTMLButtonElement>) => {
e?.stopPropagation();
setSideBar({ image: true });
};
const handleToggleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
if (isSongDefined && !isFullScreenPlayerExpanded) {
handleGeneralContextMenu(e, [currentSong!]);
}
};
const stopPropagation = (e?: MouseEvent) => e?.stopPropagation();
useHotkeys([
[
bindings.toggleFullscreenPlayer.allowGlobal
? ''
: bindings.toggleFullscreenPlayer.hotkey,
handleToggleFullScreenPlayer,
],
]);
return (
<div className={styles.leftControlsContainer}>
<LayoutGroup>
<AnimatePresence initial={false} mode="popLayout">
{!hideImage && (
<div className={styles.imageWrapper}>
<motion.div
animate={{ opacity: 1, scale: 1, x: 0 }}
className={styles.image}
exit={{ opacity: 0, x: -50 }}
initial={{ opacity: 0, x: -50 }}
key="playerbar-image"
onClick={handleToggleFullScreenPlayer}
onContextMenu={handleToggleContextMenu}
role="button"
transition={{ duration: 0.2, ease: 'easeIn' }}
>
<Tooltip
label={t('player.toggleFullscreenPlayer', {
postProcess: 'sentenceCase',
})}
openDelay={500}
>
<Image
className={clsx(
styles.playerbarImage,
PlaybackSelectors.playerCoverArt,
)}
loading="eager"
src={currentSong?.imageUrl ?? ''}
/>
</Tooltip>
{!collapsed && (
<ActionIcon
icon="arrowUpS"
iconProps={{ size: 'xl' }}
onClick={handleToggleSidebarImage}
opacity={0.8}
radius="md"
size="xs"
style={{
cursor: 'default',
position: 'absolute',
right: 2,
top: 2,
}}
tooltip={{
label: t('common.expand', {
postProcess: 'titleCase',
}),
openDelay: 500,
}}
/>
)}
</motion.div>
</div>
)}
</AnimatePresence>
<motion.div className={styles.metadataStack} layout="position">
<div className={styles.lineItem} onClick={stopPropagation}>
<Group align="center" gap="xs" wrap="nowrap">
<Text
className={PlaybackSelectors.songTitle}
component={Link}
fw={500}
isLink
onContextMenu={handleToggleContextMenu} // Ajout du clic droit
overflow="hidden"
to={AppRoute.NOW_PLAYING}
>
{title || '—'}
</Text>
{isSongDefined && (
<ActionIcon
icon="ellipsisVertical"
onClick={(e) => handleGeneralContextMenu(e, [currentSong!])}
size="xs"
styles={{
root: {
'--ai-size-xs': '1.15rem',
},
}}
variant="subtle"
/>
)}
</Group>
</div>
<div
className={clsx(
styles.lineItem,
styles.secondary,
PlaybackSelectors.songArtist,
)}
onClick={stopPropagation}
>
{artists?.map((artist, index) => (
<React.Fragment key={`bar-${artist.id}`}>
{index > 0 && <Separator />}
<Text
component={artist.id ? Link : undefined}
fw={500}
isLink={artist.id !== ''}
overflow="hidden"
size="md"
to={
artist.id
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})
: undefined
}
>
{artist.name || '—'}
</Text>
</React.Fragment>
))}
</div>
<div
className={clsx(
styles.lineItem,
styles.secondary,
PlaybackSelectors.songAlbum,
)}
onClick={stopPropagation}
>
<Text
component={Link}
fw={500}
isLink
overflow="hidden"
size="md"
to={
currentSong?.albumId
? generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: currentSong.albumId,
})
: ''
}
>
{currentSong?.album || '—'}
</Text>
</div>
</motion.div>
</LayoutGroup>
</div>
);
};