mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-13 20:10:07 +02:00
8a3edb71df
* 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>
231 lines
10 KiB
TypeScript
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>
|
|
);
|
|
};
|