mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration * various design changes and improvements
This commit is contained in:
parent
bea55d48a8
commit
c1330d92b2
473 changed files with 12469 additions and 11607 deletions
|
|
@ -0,0 +1,47 @@
|
|||
.buttons-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
width: 95%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.slider-value-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
max-width: 50px;
|
||||
|
||||
@media (width < 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
flex: 6;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 35px;
|
||||
|
||||
@media (width < 768px) {
|
||||
.buttons-container {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.slider-value-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,22 +4,9 @@ import formatDuration from 'format-duration';
|
|||
import isElectron from 'is-electron';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsDice3 } from 'react-icons/bs';
|
||||
import { IoIosPause } from 'react-icons/io';
|
||||
import {
|
||||
RiPlayFill,
|
||||
RiRepeat2Line,
|
||||
RiRepeatOneLine,
|
||||
RiRewindFill,
|
||||
RiShuffleFill,
|
||||
RiSkipBackFill,
|
||||
RiSkipForwardFill,
|
||||
RiSpeedFill,
|
||||
RiStopFill,
|
||||
} from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Text } from '/@/renderer/components';
|
||||
import styles from './center-controls.module.css';
|
||||
|
||||
import { PlayerButton } from '/@/renderer/features/player/components/player-button';
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
import { openShuffleAllModal } from '/@/renderer/features/player/components/shuffle-all-modal';
|
||||
|
|
@ -39,60 +26,14 @@ import {
|
|||
usePlaybackType,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
interface CenterControlsProps {
|
||||
playersRef: any;
|
||||
}
|
||||
|
||||
const ButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
width: 95%;
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
const SliderValueWrapper = styled.div<{ $position: 'left' | 'right' }>`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
max-width: 50px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SliderWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 6;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ControlsContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 35px;
|
||||
|
||||
@media (width <= 768px) {
|
||||
${ButtonsContainer} {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
${SliderValueWrapper} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -171,10 +112,16 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ControlsContainer>
|
||||
<ButtonsContainer>
|
||||
<div className={styles.controlsContainer}>
|
||||
<div className={styles.buttonsContainer}>
|
||||
<PlayerButton
|
||||
icon={<RiStopFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStop"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={handleStop}
|
||||
tooltip={{
|
||||
label: t('player.stop', { postProcess: 'sentenceCase' }),
|
||||
|
|
@ -182,8 +129,14 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
variant="tertiary"
|
||||
/>
|
||||
<PlayerButton
|
||||
$isActive={shuffle !== PlayerShuffle.NONE}
|
||||
icon={<RiShuffleFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill={shuffle === PlayerShuffle.NONE ? 'default' : 'primary'}
|
||||
icon="mediaShuffle"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
isActive={shuffle !== PlayerShuffle.NONE}
|
||||
onClick={handleToggleShuffle}
|
||||
tooltip={{
|
||||
label:
|
||||
|
|
@ -197,7 +150,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
variant="tertiary"
|
||||
/>
|
||||
<PlayerButton
|
||||
icon={<RiSkipBackFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaPrevious"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={handlePrevTrack}
|
||||
tooltip={{
|
||||
label: t('player.previous', { postProcess: 'sentenceCase' }),
|
||||
|
|
@ -206,7 +165,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
/>
|
||||
{skip?.enabled && (
|
||||
<PlayerButton
|
||||
icon={<RiRewindFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStepBackward"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
|
||||
tooltip={{
|
||||
label: t('player.skip', {
|
||||
|
|
@ -221,9 +186,15 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
disabled={currentSong?.id === undefined}
|
||||
icon={
|
||||
status === PlayerStatus.PAUSED ? (
|
||||
<RiPlayFill size={buttonSize} />
|
||||
<Icon
|
||||
icon="mediaPlay"
|
||||
size={buttonSize}
|
||||
/>
|
||||
) : (
|
||||
<IoIosPause size={buttonSize} />
|
||||
<Icon
|
||||
icon="mediaPause"
|
||||
size={buttonSize}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handlePlayPause}
|
||||
|
|
@ -237,7 +208,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
/>
|
||||
{skip?.enabled && (
|
||||
<PlayerButton
|
||||
icon={<RiSpeedFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaStepForward"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
|
||||
tooltip={{
|
||||
label: t('player.skip', {
|
||||
|
|
@ -249,7 +226,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
/>
|
||||
)}
|
||||
<PlayerButton
|
||||
icon={<RiSkipForwardFill size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaNext"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={handleNextTrack}
|
||||
tooltip={{
|
||||
label: t('player.next', { postProcess: 'sentenceCase' }),
|
||||
|
|
@ -257,14 +240,22 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
variant="secondary"
|
||||
/>
|
||||
<PlayerButton
|
||||
$isActive={repeat !== PlayerRepeat.NONE}
|
||||
icon={
|
||||
repeat === PlayerRepeat.ONE ? (
|
||||
<RiRepeatOneLine size={buttonSize} />
|
||||
<Icon
|
||||
fill="primary"
|
||||
icon="mediaRepeatOne"
|
||||
size={buttonSize}
|
||||
/>
|
||||
) : (
|
||||
<RiRepeat2Line size={buttonSize} />
|
||||
<Icon
|
||||
fill={repeat === PlayerRepeat.NONE ? 'default' : 'primary'}
|
||||
icon="mediaRepeat"
|
||||
size={buttonSize}
|
||||
/>
|
||||
)
|
||||
}
|
||||
isActive={repeat !== PlayerRepeat.NONE}
|
||||
onClick={handleToggleRepeat}
|
||||
tooltip={{
|
||||
label: `${
|
||||
|
|
@ -288,7 +279,13 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
/>
|
||||
|
||||
<PlayerButton
|
||||
icon={<BsDice3 size={buttonSize} />}
|
||||
icon={
|
||||
<Icon
|
||||
fill="default"
|
||||
icon="mediaRandom"
|
||||
size={buttonSize}
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
openShuffleAllModal({
|
||||
handlePlayQueueAdd,
|
||||
|
|
@ -300,20 +297,20 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
}}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</ButtonsContainer>
|
||||
</ControlsContainer>
|
||||
<SliderContainer>
|
||||
<SliderValueWrapper $position="left">
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
weight={600}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</SliderValueWrapper>
|
||||
<SliderWrapper>
|
||||
</div>
|
||||
<div className={styles.sliderWrapper}>
|
||||
<PlayerbarSlider
|
||||
label={(value) => formatDuration(value * 1000)}
|
||||
max={songDuration}
|
||||
|
|
@ -335,18 +332,18 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
|||
value={!isSeeking ? currentTime : seekValue}
|
||||
w="100%"
|
||||
/>
|
||||
</SliderWrapper>
|
||||
<SliderValueWrapper $position="right">
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
weight={600}
|
||||
>
|
||||
{duration}
|
||||
</Text>
|
||||
</SliderValueWrapper>
|
||||
</SliderContainer>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
.image {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
object-position: 50% 100%;
|
||||
border-radius: 5px;
|
||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
height: 65%;
|
||||
aspect-ratio: 1/1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metadata-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
|
||||
h1 {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
.player-container {
|
||||
@media screen and (height < 640px) {
|
||||
.full-screen-player-image-metadata {
|
||||
display: none;
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,27 @@
|
|||
import { Center, Flex, Group, Stack } from '@mantine/core';
|
||||
import { useSetState } from '@mantine/hooks';
|
||||
import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'framer-motion';
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence, HTMLMotionProps, motion, Variants } from 'motion/react';
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { RiAlbumFill } from 'react-icons/ri';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Badge, Text, TextTitle } from '/@/renderer/components';
|
||||
import styles from './full-screen-player-image.module.css';
|
||||
|
||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useFullScreenPlayerStore, usePlayerData, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerData, usePlayerStore } from '/@/renderer/store';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { PlayerData, QueueSong } from '/@/shared/types/domain-types';
|
||||
|
||||
const Image = styled(motion.img)<any>`
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
object-fit: ${({ $useAspectRatio }) => ($useAspectRatio ? 'contain' : 'cover')};
|
||||
object-position: 50% 100%;
|
||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const ImageContainer = styled(motion.div)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
height: 65%;
|
||||
aspect-ratio: 1/1;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
interface TransparentMetadataContainer {
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
const MetadataContainer = styled(Stack)<TransparentMetadataContainer>`
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
|
||||
h1 {
|
||||
font-size: 3.5vh;
|
||||
}
|
||||
`;
|
||||
|
||||
const PlayerContainer = styled(Flex)`
|
||||
@media screen and (height <= 640px) {
|
||||
.full-screen-player-image-metadata {
|
||||
display: none;
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
${ImageContainer} {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const imageVariants: Variants = {
|
||||
closed: {
|
||||
opacity: 0,
|
||||
|
|
@ -93,22 +52,22 @@ const scaleImageUrl = (imageSize: number, url?: null | string) => {
|
|||
.replace(/&height=\d+/, `&height=${imageSize}`);
|
||||
};
|
||||
|
||||
const ImageWithPlaceholder = ({
|
||||
useAspectRatio,
|
||||
...props
|
||||
}: HTMLMotionProps<'img'> & { placeholder?: string; useAspectRatio: boolean }) => {
|
||||
const MotionImage = motion.create(Image);
|
||||
|
||||
const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'> & { placeholder?: string }) => {
|
||||
if (!props.src) {
|
||||
return (
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
borderRadius: 'var(--card-default-radius)',
|
||||
style={{
|
||||
background: 'var(--theme-colors-surface)',
|
||||
borderRadius: 'var(--theme-card-default-radius)',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<RiAlbumFill
|
||||
color="var(--placeholder-fg)"
|
||||
<Icon
|
||||
color="muted"
|
||||
icon="itemAlbum"
|
||||
size="25%"
|
||||
/>
|
||||
</Center>
|
||||
|
|
@ -116,8 +75,8 @@ const ImageWithPlaceholder = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
$useAspectRatio={useAspectRatio}
|
||||
<MotionImage
|
||||
className={styles.image}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -130,7 +89,6 @@ export const FullScreenPlayerImage = () => {
|
|||
const albumArtRes = useSettingsStore((store) => store.general.albumArtRes);
|
||||
|
||||
const { queue } = usePlayerData();
|
||||
const { useImageAspectRatio } = useFullScreenPlayerStore();
|
||||
const currentSong = queue.current;
|
||||
const { background } = useFastAverageColor({
|
||||
algorithm: 'dominant',
|
||||
|
|
@ -195,14 +153,17 @@ export const FullScreenPlayerImage = () => {
|
|||
}, [imageState, mainImageDimensions.idealSize, queue, setImageState]);
|
||||
|
||||
return (
|
||||
<PlayerContainer
|
||||
<Flex
|
||||
align="center"
|
||||
className="full-screen-player-image-container"
|
||||
className={clsx(styles.playerContainer, 'full-screen-player-image-container')}
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
p="1rem"
|
||||
>
|
||||
<ImageContainer ref={mainImageRef}>
|
||||
<div
|
||||
className={styles.imageContainer}
|
||||
ref={mainImageRef}
|
||||
>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="sync"
|
||||
|
|
@ -216,9 +177,8 @@ export const FullScreenPlayerImage = () => {
|
|||
exit="closed"
|
||||
initial="closed"
|
||||
key={imageKey}
|
||||
placeholder="var(--placeholder-bg)"
|
||||
placeholder="var(--theme-colors-foreground-muted)"
|
||||
src={imageState.topImage || ''}
|
||||
useAspectRatio={useImageAspectRatio}
|
||||
variants={imageVariants}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -232,62 +192,55 @@ export const FullScreenPlayerImage = () => {
|
|||
exit="closed"
|
||||
initial="closed"
|
||||
key={imageKey}
|
||||
placeholder="var(--placeholder-bg)"
|
||||
placeholder="var(--theme-colors-foreground-muted)"
|
||||
src={imageState.bottomImage || ''}
|
||||
useAspectRatio={useImageAspectRatio}
|
||||
variants={imageVariants}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ImageContainer>
|
||||
<MetadataContainer
|
||||
className="full-screen-player-image-metadata"
|
||||
</div>
|
||||
<Stack
|
||||
className={styles.metadataContainer}
|
||||
gap="xs"
|
||||
maw="100%"
|
||||
spacing="xs"
|
||||
>
|
||||
<TextTitle
|
||||
align="center"
|
||||
fw={900}
|
||||
order={1}
|
||||
overflow="hidden"
|
||||
style={{
|
||||
textShadow: 'var(--fullscreen-player-text-shadow)',
|
||||
}}
|
||||
w="100%"
|
||||
weight={900}
|
||||
>
|
||||
{currentSong?.name}
|
||||
</TextTitle>
|
||||
<TextTitle
|
||||
$link
|
||||
align="center"
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
order={3}
|
||||
overflow="hidden"
|
||||
style={{
|
||||
textShadow: 'var(--fullscreen-player-text-shadow)',
|
||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
||||
}}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||
albumId: currentSong?.albumId || '',
|
||||
})}
|
||||
w="100%"
|
||||
weight={600}
|
||||
>
|
||||
{currentSong?.album}{' '}
|
||||
</TextTitle>
|
||||
<TextTitle
|
||||
align="center"
|
||||
key="fs-artists"
|
||||
order={3}
|
||||
style={{
|
||||
textShadow: 'var(--fullscreen-player-text-shadow)',
|
||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
||||
}}
|
||||
>
|
||||
{currentSong?.artists?.map((artist, index) => (
|
||||
<Fragment key={`fs-artist-${artist.id}`}>
|
||||
{index > 0 && (
|
||||
<Text
|
||||
$secondary
|
||||
sx={{
|
||||
isMuted
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 0.5rem',
|
||||
}}
|
||||
|
|
@ -296,16 +249,16 @@ export const FullScreenPlayerImage = () => {
|
|||
</Text>
|
||||
)}
|
||||
<Text
|
||||
$link
|
||||
$secondary
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
isMuted
|
||||
style={{
|
||||
textShadow: 'var(--fullscreen-player-text-shadow)',
|
||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
||||
}}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
weight={600}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
|
|
@ -313,8 +266,8 @@ export const FullScreenPlayerImage = () => {
|
|||
))}
|
||||
</TextTitle>
|
||||
<Group
|
||||
justify="center"
|
||||
mt="sm"
|
||||
position="center"
|
||||
>
|
||||
{currentSong?.container && (
|
||||
<Badge size="lg">
|
||||
|
|
@ -325,7 +278,7 @@ export const FullScreenPlayerImage = () => {
|
|||
<Badge size="lg">{currentSong?.releaseYear}</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</MetadataContainer>
|
||||
</PlayerContainer>
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
.queue-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
:global(.ag-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.ag-theme-alpine-dark) {
|
||||
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
|
||||
--ag-background-color: rgb(0 0 0 / 0%) !important;
|
||||
--ag-odd-row-background-color: rgb(0 0 0 / 0%) !important;
|
||||
}
|
||||
|
||||
:global(.ag-row) {
|
||||
&::before {
|
||||
background: rgb(0 0 0 / 10%) !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ag-row-hover) {
|
||||
background: rgb(0 0 0 / 10%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.active-tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--theme-colors-foreground);
|
||||
}
|
||||
|
||||
.header-item-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1rem;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: var(--theme-colors-background);
|
||||
border-radius: 5px;
|
||||
opacity: var(--opacity, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import { Group } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { lazy, Suspense, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import { CSSProperties, lazy, Suspense, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HiOutlineQueueList } from 'react-icons/hi2';
|
||||
import { RiFileMusicLine, RiFileTextLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button } from '/@/renderer/components';
|
||||
import styles from './full-screen-player-queue.module.css';
|
||||
|
||||
import { Lyrics } from '/@/renderer/features/lyrics/lyrics';
|
||||
import { PlayQueue } from '/@/renderer/features/now-playing';
|
||||
import { FullScreenSimilarSongs } from '/@/renderer/features/player/components/full-screen-similar-songs';
|
||||
|
|
@ -15,6 +13,8 @@ import {
|
|||
useFullScreenPlayerStore,
|
||||
useFullScreenPlayerStoreActions,
|
||||
} from '/@/renderer/store/full-screen-player.store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
|
||||
const Visualizer = lazy(() =>
|
||||
|
|
@ -23,50 +23,6 @@ const Visualizer = lazy(() =>
|
|||
})),
|
||||
);
|
||||
|
||||
const QueueContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.ag-theme-alpine-dark {
|
||||
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
|
||||
--ag-background-color: rgb(0 0 0 / 0%) !important;
|
||||
--ag-odd-row-background-color: rgb(0 0 0 / 0%) !important;
|
||||
}
|
||||
|
||||
.ag-header {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActiveTabIndicator = styled(motion.div)`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--main-fg);
|
||||
`;
|
||||
|
||||
const HeaderItemWrapper = styled.div`
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
interface TransparentGridContainerProps {
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
const GridContainer = styled.div<TransparentGridContainerProps>`
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1rem;
|
||||
/* stylelint-disable-next-line color-function-notation */
|
||||
background: rgb(var(--main-bg-transparent), ${({ opacity }) => opacity}%);
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
export const FullScreenPlayerQueue = () => {
|
||||
const { t } = useTranslation();
|
||||
const { activeTab, opacity } = useFullScreenPlayerStore();
|
||||
|
|
@ -77,19 +33,16 @@ export const FullScreenPlayerQueue = () => {
|
|||
const items = [
|
||||
{
|
||||
active: activeTab === 'queue',
|
||||
icon: <RiFileMusicLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.upNext'),
|
||||
onClick: () => setStore({ activeTab: 'queue' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'related',
|
||||
icon: <HiOutlineQueueList size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.related'),
|
||||
onClick: () => setStore({ activeTab: 'related' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'lyrics',
|
||||
icon: <RiFileTextLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.lyrics'),
|
||||
onClick: () => setStore({ activeTab: 'lyrics' }),
|
||||
},
|
||||
|
|
@ -98,7 +51,6 @@ export const FullScreenPlayerQueue = () => {
|
|||
if (type === PlaybackType.WEB && webAudio) {
|
||||
items.push({
|
||||
active: activeTab === 'visualizer',
|
||||
icon: <RiFileTextLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
|
||||
onClick: () => setStore({ activeTab: 'visualizer' }),
|
||||
});
|
||||
|
|
@ -108,48 +60,54 @@ export const FullScreenPlayerQueue = () => {
|
|||
}, [activeTab, setStore, t, type, webAudio]);
|
||||
|
||||
return (
|
||||
<GridContainer
|
||||
className="full-screen-player-queue-container"
|
||||
opacity={opacity}
|
||||
<div
|
||||
className={clsx(styles.gridContainer, 'full-screen-player-queue-container')}
|
||||
style={
|
||||
{
|
||||
'--opacity': opacity / 100,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<Group
|
||||
align="center"
|
||||
className="full-screen-player-queue-header"
|
||||
gap={0}
|
||||
grow
|
||||
position="center"
|
||||
justify="center"
|
||||
>
|
||||
{headerItems.map((item) => (
|
||||
<HeaderItemWrapper key={`tab-${item.label}`}>
|
||||
<div
|
||||
className={styles.headerItemWrapper}
|
||||
key={`tab-${item.label}`}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
flex={1}
|
||||
fw="600"
|
||||
onClick={item.onClick}
|
||||
pos="relative"
|
||||
size="lg"
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
color: item.active
|
||||
? 'var(--main-fg) !important'
|
||||
: 'var(--main-fg-secondary) !important',
|
||||
letterSpacing: '1px',
|
||||
}}
|
||||
uppercase
|
||||
variant="subtle"
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
{item.active ? <ActiveTabIndicator layoutId="underline" /> : null}
|
||||
</HeaderItemWrapper>
|
||||
{item.active ? (
|
||||
<motion.div
|
||||
className={styles.activeTabIndicator}
|
||||
layoutId="underline"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</Group>
|
||||
{activeTab === 'queue' ? (
|
||||
<QueueContainer>
|
||||
<div className={styles.queueContainer}>
|
||||
<PlayQueue type="fullScreen" />
|
||||
</QueueContainer>
|
||||
</div>
|
||||
) : activeTab === 'related' ? (
|
||||
<QueueContainer>
|
||||
<div className={styles.queueContainer}>
|
||||
<FullScreenSimilarSongs />
|
||||
</QueueContainer>
|
||||
</div>
|
||||
) : activeTab === 'lyrics' ? (
|
||||
<Lyrics />
|
||||
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
|
||||
|
|
@ -157,6 +115,6 @@ export const FullScreenPlayerQueue = () => {
|
|||
<Visualizer />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</GridContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
.container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
padding: 2rem 2rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-container {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
max-width: 2560px;
|
||||
margin-top: 5rem;
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.background-image-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--theme-overlay-header);
|
||||
backdrop-filter: blur(var(--image-blur));
|
||||
}
|
||||
|
|
@ -1,21 +1,11 @@
|
|||
import { Divider, Group } from '@mantine/core';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { motion, Variants } from 'motion/react';
|
||||
import { CSSProperties, useLayoutEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
|
||||
import { useLocation } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
Button,
|
||||
NumberInput,
|
||||
Option,
|
||||
Popover,
|
||||
Select,
|
||||
Slider,
|
||||
Switch,
|
||||
} from '/@/renderer/components';
|
||||
import styles from './full-screen-player.module.css';
|
||||
|
||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
|
||||
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
|
||||
|
|
@ -29,54 +19,18 @@ import {
|
|||
useSettingsStoreActions,
|
||||
useWindowSettings,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Option } from '/@/shared/components/option/option';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
|
||||
const Container = styled(motion.div)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
padding: 2rem 2rem 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponsiveContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 2rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 2560px;
|
||||
margin-top: 5rem;
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface BackgroundImageOverlayProps {
|
||||
$blur: number;
|
||||
}
|
||||
|
||||
const BackgroundImageOverlay = styled.div<BackgroundImageOverlayProps>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-header-overlay);
|
||||
backdrop-filter: blur(${({ $blur }) => $blur}rem);
|
||||
`;
|
||||
|
||||
const mainBackground = 'var(--main-bg)';
|
||||
const mainBackground = 'var(--theme-colors-background)';
|
||||
|
||||
const Controls = () => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -109,34 +63,30 @@ const Controls = () => {
|
|||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
p="1rem"
|
||||
pos="absolute"
|
||||
spacing="sm"
|
||||
sx={{
|
||||
background: `rgb(var(--main-bg-transparent), ${opacity}%)`,
|
||||
style={{
|
||||
background: `rgb(var(--theme-colors-background-transparent), ${opacity}%)`,
|
||||
left: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
compact
|
||||
<ActionIcon
|
||||
icon="arrowDownS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleToggleFullScreenPlayer}
|
||||
size="sm"
|
||||
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiArrowDownSLine size="2rem" />
|
||||
</Button>
|
||||
/>
|
||||
<Popover position="bottom-start">
|
||||
<Popover.Target>
|
||||
<Button
|
||||
compact
|
||||
size="sm"
|
||||
<ActionIcon
|
||||
icon="settings"
|
||||
iconProps={{ size: 'lg' }}
|
||||
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Line size="1.5rem" />
|
||||
</Button>
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Option>
|
||||
|
|
@ -285,8 +235,8 @@ const Controls = () => {
|
|||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Group
|
||||
noWrap
|
||||
w="100%"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Slider
|
||||
defaultValue={lyricConfig.fontSize}
|
||||
|
|
@ -325,8 +275,8 @@ const Controls = () => {
|
|||
</Option.Label>
|
||||
<Option.Control>
|
||||
<Group
|
||||
noWrap
|
||||
w="100%"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Slider
|
||||
defaultValue={lyricConfig.gap}
|
||||
|
|
@ -485,8 +435,9 @@ export const FullScreenPlayer = () => {
|
|||
: mainBackground;
|
||||
|
||||
return (
|
||||
<Container
|
||||
<motion.div
|
||||
animate="open"
|
||||
className={styles.container}
|
||||
custom={{ background, backgroundImage, dynamicBackground, windowBarStyle }}
|
||||
exit="closed"
|
||||
initial="closed"
|
||||
|
|
@ -494,11 +445,20 @@ export const FullScreenPlayer = () => {
|
|||
variants={containerVariants}
|
||||
>
|
||||
<Controls />
|
||||
{dynamicBackground && <BackgroundImageOverlay $blur={dynamicImageBlur} />}
|
||||
<ResponsiveContainer>
|
||||
{dynamicBackground && (
|
||||
<div
|
||||
className={styles.backgroundImageOverlay}
|
||||
style={
|
||||
{
|
||||
'--image-blur': `${dynamicImageBlur}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.responsiveContainer}>
|
||||
<FullScreenPlayerImage />
|
||||
<FullScreenPlayerQueue />
|
||||
</ResponsiveContainer>
|
||||
</Container>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
.image-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md) 0;
|
||||
}
|
||||
|
||||
.metadata-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-xs);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
animation: fadein 0.2s ease-in-out;
|
||||
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.playerbar-image {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
}
|
||||
|
||||
.line-item {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 20vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.line-item.secondary {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
|
||||
a {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.left-controls-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: 1rem;
|
||||
|
||||
@media (width < 640px) {
|
||||
.image-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import { Center, Group } from '@mantine/core';
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'motion/react';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button, Text, Tooltip } from '/@/renderer/components';
|
||||
import { Separator } from '/@/renderer/components/separator';
|
||||
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';
|
||||
|
|
@ -20,80 +18,14 @@ import {
|
|||
useSetFullScreenPlayerStore,
|
||||
useSidebarStore,
|
||||
} from '/@/renderer/store';
|
||||
import { fadeIn } from '/@/renderer/styles';
|
||||
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 { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
`;
|
||||
|
||||
const MetadataStack = styled(motion.div)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Image = styled(motion.div)`
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
background-color: var(--placeholder-bg);
|
||||
filter: drop-shadow(0 5px 6px rgb(0 0 0 / 50%));
|
||||
|
||||
${fadeIn};
|
||||
animation: fadein 0.2s ease-in-out;
|
||||
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const PlayerbarImage = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--image-fit);
|
||||
`;
|
||||
|
||||
const LineItem = styled.div<{ $secondary?: boolean }>`
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 20vw;
|
||||
overflow: hidden;
|
||||
line-height: 1.3;
|
||||
color: ${(props) => props.$secondary && 'var(--main-fg-secondary)'};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
color: ${(props) => props.$secondary && 'var(--text-secondary)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const LeftControlsContainer = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-left: 1rem;
|
||||
|
||||
@media (width <= 640px) {
|
||||
${ImageWrapper} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeftControls = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setSideBar } = useAppStoreActions();
|
||||
|
|
@ -135,16 +67,17 @@ export const LeftControls = () => {
|
|||
]);
|
||||
|
||||
return (
|
||||
<LeftControlsContainer>
|
||||
<div className={styles.leftControlsContainer}>
|
||||
<LayoutGroup>
|
||||
<AnimatePresence
|
||||
initial={false}
|
||||
mode="wait"
|
||||
>
|
||||
{!hideImage && (
|
||||
<ImageWrapper>
|
||||
<Image
|
||||
<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"
|
||||
|
|
@ -158,34 +91,21 @@ export const LeftControls = () => {
|
|||
})}
|
||||
openDelay={500}
|
||||
>
|
||||
{currentSong?.imageUrl ? (
|
||||
<PlayerbarImage
|
||||
loading="eager"
|
||||
src={currentSong?.imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<RiDiscLine
|
||||
color="var(--placeholder-fg)"
|
||||
size={50}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
<Image
|
||||
className={styles.playerbarImage}
|
||||
loading="eager"
|
||||
src={currentSong?.imageUrl ?? ''}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{!collapsed && (
|
||||
<Button
|
||||
compact
|
||||
<ActionIcon
|
||||
icon="arrowUpS"
|
||||
iconProps={{ size: 'xl' }}
|
||||
onClick={handleToggleSidebarImage}
|
||||
opacity={0.8}
|
||||
radius={50}
|
||||
size="md"
|
||||
sx={{
|
||||
radius="md"
|
||||
size="xs"
|
||||
style={{
|
||||
cursor: 'default',
|
||||
position: 'absolute',
|
||||
right: 2,
|
||||
|
|
@ -197,56 +117,60 @@ export const LeftControls = () => {
|
|||
}),
|
||||
openDelay: 500,
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
<RiArrowUpSLine
|
||||
color="white"
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</Image>
|
||||
</ImageWrapper>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<MetadataStack layout="position">
|
||||
<LineItem onClick={stopPropagation}>
|
||||
<motion.div
|
||||
className={styles.metadataStack}
|
||||
layout="position"
|
||||
>
|
||||
<div
|
||||
className={styles.lineItem}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Group
|
||||
align="flex-start"
|
||||
noWrap
|
||||
spacing="xs"
|
||||
align="center"
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Text
|
||||
$link
|
||||
component={Link}
|
||||
fw={500}
|
||||
isLink
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={AppRoute.NOW_PLAYING}
|
||||
weight={500}
|
||||
>
|
||||
{title || '—'}
|
||||
</Text>
|
||||
{isSongDefined && (
|
||||
<Button
|
||||
compact
|
||||
<ActionIcon
|
||||
icon="ellipsisVertical"
|
||||
onClick={(e) => handleGeneralContextMenu(e, [currentSong!])}
|
||||
size="xs"
|
||||
styles={{
|
||||
root: {
|
||||
'--ai-size-xs': '1.15rem',
|
||||
},
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMore2Fill size="1.2rem" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</LineItem>
|
||||
<LineItem
|
||||
$secondary
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.lineItem, styles.secondary)}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{artists?.map((artist, index) => (
|
||||
<React.Fragment key={`bar-${artist.id}`}>
|
||||
{index > 0 && <Separator />}
|
||||
<Text
|
||||
$link={artist.id !== ''}
|
||||
component={artist.id ? Link : undefined}
|
||||
fw={500}
|
||||
isLink={artist.id !== ''}
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={
|
||||
|
|
@ -256,20 +180,20 @@ export const LeftControls = () => {
|
|||
})
|
||||
: undefined
|
||||
}
|
||||
weight={500}
|
||||
>
|
||||
{artist.name || '—'}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</LineItem>
|
||||
<LineItem
|
||||
$secondary
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.lineItem, styles.secondary)}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Text
|
||||
$link
|
||||
component={Link}
|
||||
fw={500}
|
||||
isLink
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={
|
||||
|
|
@ -279,13 +203,12 @@ export const LeftControls = () => {
|
|||
})
|
||||
: ''
|
||||
}
|
||||
weight={500}
|
||||
>
|
||||
{currentSong?.album || '—'}
|
||||
</Text>
|
||||
</LineItem>
|
||||
</MetadataStack>
|
||||
</div>
|
||||
</motion.div>
|
||||
</LayoutGroup>
|
||||
</LeftControlsContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
.motion-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.motion-wrapper.main {
|
||||
display: flex;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.player-button {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
overflow: visible;
|
||||
cursor: default;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px var(--theme-colors-primary-filled) solid;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.player-button.active {
|
||||
svg {
|
||||
fill: var(--theme-colors-primary-filled);
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
background: var(--theme-colors-foreground);
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
color: var(--theme-colors-background);
|
||||
fill: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--theme-colors-foreground);
|
||||
|
||||
svg {
|
||||
color: var(--theme-colors-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
svg {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +1,72 @@
|
|||
import type { TooltipProps, UnstyledButtonProps } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
|
||||
import { UnstyledButton } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import styles from './player-button.module.css';
|
||||
|
||||
import { Tooltip } from '/@/renderer/components';
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Tooltip, TooltipProps } from '/@/shared/components/tooltip/tooltip';
|
||||
|
||||
type MantineButtonProps = ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
||||
interface PlayerButtonProps extends MantineButtonProps {
|
||||
$isActive?: boolean;
|
||||
interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
|
||||
icon: ReactNode;
|
||||
isActive?: boolean;
|
||||
tooltip?: Omit<TooltipProps, 'children'>;
|
||||
variant: 'main' | 'secondary' | 'tertiary';
|
||||
}
|
||||
|
||||
const WrapperMainVariant = css`
|
||||
margin: 0 0.5rem;
|
||||
`;
|
||||
|
||||
type MotionWrapperProps = { variant: PlayerButtonProps['variant'] };
|
||||
|
||||
const MotionWrapper = styled(motion.div)<MotionWrapperProps>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${({ variant }) => variant === 'main' && WrapperMainVariant};
|
||||
`;
|
||||
|
||||
const ButtonMainVariant = css`
|
||||
padding: 0.5rem;
|
||||
background: var(--playerbar-btn-main-bg);
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
fill: var(--playerbar-btn-main-fg);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: var(--playerbar-btn-main-bg-hover);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--playerbar-btn-main-bg-hover);
|
||||
|
||||
svg {
|
||||
fill: var(--playerbar-btn-main-fg-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonSecondaryVariant = css`
|
||||
padding: 0.5rem;
|
||||
`;
|
||||
|
||||
const ButtonTertiaryVariant = css`
|
||||
padding: 0.5rem;
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
svg {
|
||||
fill: var(--playerbar-btn-fg-hover);
|
||||
stroke: var(--playerbar-btn-fg-hover);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type StyledPlayerButtonProps = Omit<PlayerButtonProps, 'icon'>;
|
||||
|
||||
const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
overflow: visible;
|
||||
cursor: default;
|
||||
background: var(--playerbar-btn-bg-hover);
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: var(--playerbar-btn-bg-hover);
|
||||
outline: 1px var(--primary-color) solid;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
fill: ${({ $isActive }) =>
|
||||
$isActive ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)'};
|
||||
stroke: var(--playerbar-btn-fg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--playerbar-btn-fg-hover);
|
||||
background: var(--playerbar-btn-bg-hover);
|
||||
|
||||
svg {
|
||||
fill: ${({ $isActive }) =>
|
||||
$isActive ? 'var(--primary-color)' : 'var(--playerbar-btn-fg-hover)'};
|
||||
}
|
||||
}
|
||||
|
||||
${({ variant }) =>
|
||||
variant === 'main'
|
||||
? ButtonMainVariant
|
||||
: variant === 'secondary'
|
||||
? ButtonSecondaryVariant
|
||||
: ButtonTertiaryVariant};
|
||||
`;
|
||||
|
||||
export const PlayerButton = forwardRef<HTMLDivElement, PlayerButtonProps>(
|
||||
({ icon, tooltip, variant, ...rest }: PlayerButtonProps, ref) => {
|
||||
({ icon, isActive, tooltip, variant, ...rest }: PlayerButtonProps, ref) => {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip {...tooltip}>
|
||||
<MotionWrapper
|
||||
<motion.div
|
||||
className={clsx({
|
||||
[styles.main]: variant === 'main',
|
||||
[styles.motionWrapper]: true,
|
||||
})}
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
>
|
||||
<StyledPlayerButton
|
||||
variant={variant}
|
||||
<ActionIcon
|
||||
className={clsx(styles.playerButton, styles[variant], {
|
||||
[styles.active]: isActive,
|
||||
})}
|
||||
{...rest}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
rest.onClick?.(e);
|
||||
}}
|
||||
variant="transparent"
|
||||
>
|
||||
{icon}
|
||||
</StyledPlayerButton>
|
||||
</MotionWrapper>
|
||||
</ActionIcon>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionWrapper
|
||||
<motion.div
|
||||
className={clsx({
|
||||
[styles.main]: variant === 'main',
|
||||
[styles.motionWrapper]: true,
|
||||
})}
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
>
|
||||
<StyledPlayerButton
|
||||
variant={variant}
|
||||
<ActionIcon
|
||||
className={clsx(styles.playerButton, styles[variant], {
|
||||
[styles.active]: isActive,
|
||||
})}
|
||||
{...rest}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
rest.onClick?.(e);
|
||||
}}
|
||||
size="compact-md"
|
||||
variant="transparent"
|
||||
>
|
||||
{icon}
|
||||
</StyledPlayerButton>
|
||||
</MotionWrapper>
|
||||
</ActionIcon>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
.bar {
|
||||
background-color: var(--theme-colors-foreground);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.label {
|
||||
max-width: 200px;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
font-size: var(--theme-font-size-md);
|
||||
font-weight: 550;
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
box-shadow: 4px 4px 10px 0 rgb(0 0 0 / 20%);
|
||||
}
|
||||
|
||||
.root {
|
||||
&:hover {
|
||||
.bar {
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.bar {
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-color: var(--theme-colors-primary-filled);
|
||||
border-width: 1px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.track {
|
||||
&::before {
|
||||
right: calc(0.1rem * -1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,16 @@
|
|||
import { rem, Slider, SliderProps } from '@mantine/core';
|
||||
import styles from './playerbar-slider.module.css';
|
||||
|
||||
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
|
||||
|
||||
export const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
return (
|
||||
<Slider
|
||||
styles={{
|
||||
bar: {
|
||||
backgroundColor: 'var(--playerbar-slider-track-progress-bg)',
|
||||
transition: 'background-color 0.2s ease',
|
||||
},
|
||||
label: {
|
||||
backgroundColor: 'var(--tooltip-bg)',
|
||||
color: 'var(--tooltip-fg)',
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 600,
|
||||
padding: '0 1rem',
|
||||
},
|
||||
root: {
|
||||
'&:hover': {
|
||||
'& .mantine-Slider-bar': {
|
||||
backgroundColor: 'var(--primary-color)',
|
||||
},
|
||||
'& .mantine-Slider-thumb': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
thumb: {
|
||||
backgroundColor: 'var(--slider-thumb-bg)',
|
||||
borderColor: 'var(--primary-color)',
|
||||
borderWidth: rem(1),
|
||||
height: '1rem',
|
||||
opacity: 0,
|
||||
width: '1rem',
|
||||
},
|
||||
track: {
|
||||
'&::before': {
|
||||
backgroundColor: 'var(--playerbar-slider-track-bg)',
|
||||
right: 'calc(0.1rem * -1)',
|
||||
},
|
||||
},
|
||||
classNames={{
|
||||
bar: styles.bar,
|
||||
label: styles.label,
|
||||
root: styles.root,
|
||||
thumb: styles.thumb,
|
||||
track: styles.track,
|
||||
}}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
40
src/renderer/features/player/components/playerbar.module.css
Normal file
40
src/renderer/features/player/components/playerbar.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.container {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
border-top: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.controls-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
|
||||
@media (width < 768px) {
|
||||
grid-template-columns: minmax(0, 0.5fr) minmax(0, 1fr) minmax(0, 0.5fr);
|
||||
}
|
||||
}
|
||||
|
||||
.right-grid-item {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-grid-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.center-grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { MouseEvent, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import styles from './playerbar.module.css';
|
||||
|
||||
import { AudioPlayer } from '/@/renderer/components';
|
||||
import { CenterControls } from '/@/renderer/features/player/components/center-controls';
|
||||
|
|
@ -25,47 +26,6 @@ import {
|
|||
} from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
|
||||
const PlayerbarContainer = styled.div`
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
border-top: var(--playerbar-border-top);
|
||||
`;
|
||||
|
||||
const PlayerbarControlsGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: minmax(0, 0.5fr) minmax(0, 1fr) minmax(0, 0.5fr);
|
||||
}
|
||||
`;
|
||||
|
||||
const RightGridItem = styled.div`
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const LeftGridItem = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const CenterGridItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const Playerbar = () => {
|
||||
const playersRef = PlayersRef;
|
||||
const settings = useSettingsStore((state) => state.playback);
|
||||
|
|
@ -92,20 +52,21 @@ export const Playerbar = () => {
|
|||
}, [autoNext]);
|
||||
|
||||
return (
|
||||
<PlayerbarContainer
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={playerbarOpenDrawer ? handleToggleFullScreenPlayer : undefined}
|
||||
>
|
||||
<PlayerbarControlsGrid>
|
||||
<LeftGridItem>
|
||||
<div className={styles.controlsGrid}>
|
||||
<div className={styles.leftGridItem}>
|
||||
<LeftControls />
|
||||
</LeftGridItem>
|
||||
<CenterGridItem>
|
||||
</div>
|
||||
<div className={styles.centerGridItem}>
|
||||
<CenterControls playersRef={playersRef} />
|
||||
</CenterGridItem>
|
||||
<RightGridItem>
|
||||
</div>
|
||||
<div className={styles.rightGridItem}>
|
||||
<RightControls />
|
||||
</RightGridItem>
|
||||
</PlayerbarControlsGrid>
|
||||
</div>
|
||||
</div>
|
||||
{playbackType === PlaybackType.WEB && (
|
||||
<AudioPlayer
|
||||
autoNext={autoNextFn}
|
||||
|
|
@ -122,6 +83,6 @@ export const Playerbar = () => {
|
|||
volume={(volume / 100) ** 2}
|
||||
/>
|
||||
)}
|
||||
</PlayerbarContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,8 @@
|
|||
import { Flex, Group } from '@mantine/core';
|
||||
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HiOutlineQueueList } from 'react-icons/hi2';
|
||||
import {
|
||||
RiHeartFill,
|
||||
RiHeartLine,
|
||||
RiVolumeDownFill,
|
||||
RiVolumeMuteFill,
|
||||
RiVolumeUpFill,
|
||||
} from 'react-icons/ri';
|
||||
|
||||
import { DropdownMenu, Rating } from '/@/renderer/components';
|
||||
import { Slider } from '/@/renderer/components/slider';
|
||||
import { PlayerButton } from '/@/renderer/features/player/components/player-button';
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
import { useRightControls } from '/@/renderer/features/player/hooks/use-right-controls';
|
||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||
|
|
@ -30,6 +18,12 @@ import {
|
|||
useSpeed,
|
||||
useVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
|
@ -210,15 +204,15 @@ export const RightControls = () => {
|
|||
{showRating && (
|
||||
<Rating
|
||||
onChange={handleUpdateRating}
|
||||
size="sm"
|
||||
size="xs"
|
||||
value={currentSong?.userRating || 0}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Group
|
||||
align="center"
|
||||
noWrap
|
||||
spacing="xs"
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<DropdownMenu
|
||||
arrowOffset={12}
|
||||
|
|
@ -228,13 +222,17 @@ export const RightControls = () => {
|
|||
withArrow
|
||||
>
|
||||
<DropdownMenu.Target>
|
||||
<PlayerButton
|
||||
icon={<>{speed} x</>}
|
||||
<ActionIcon
|
||||
icon="mediaSpeed"
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
}}
|
||||
size="sm"
|
||||
tooltip={{
|
||||
label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 500,
|
||||
openDelay: 0,
|
||||
}}
|
||||
variant="secondary"
|
||||
variant="transparent"
|
||||
/>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
|
|
@ -264,78 +262,61 @@ export const RightControls = () => {
|
|||
/>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<PlayerButton
|
||||
icon={
|
||||
currentSong?.userFavorite ? (
|
||||
<RiHeartFill
|
||||
color="var(--primary-color)"
|
||||
size="1.1rem"
|
||||
/>
|
||||
) : (
|
||||
<RiHeartLine size="1.1rem" />
|
||||
)
|
||||
}
|
||||
onClick={() => handleToggleFavorite(currentSong)}
|
||||
sx={{
|
||||
svg: {
|
||||
fill: !currentSong?.userFavorite
|
||||
? undefined
|
||||
: 'var(--primary-color) !important',
|
||||
},
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: currentSong?.userFavorite ? 'primary' : undefined,
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={() => handleToggleFavorite(currentSong)}
|
||||
size="sm"
|
||||
tooltip={{
|
||||
label: currentSong?.userFavorite
|
||||
? t('player.unfavorite', { postProcess: 'titleCase' })
|
||||
: t('player.favorite', { postProcess: 'titleCase' }),
|
||||
openDelay: 500,
|
||||
openDelay: 0,
|
||||
}}
|
||||
variant="secondary"
|
||||
variant="transparent"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={isQueueExpanded ? 'panelRightClose' : 'panelRightOpen'}
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={handleToggleQueue}
|
||||
size="sm"
|
||||
tooltip={{
|
||||
label: t('player.viewQueue', { postProcess: 'titleCase' }),
|
||||
openDelay: 0,
|
||||
}}
|
||||
variant="transparent"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={muted ? 'volumeMute' : volume > 50 ? 'volumeMax' : 'volumeNormal'}
|
||||
iconProps={{
|
||||
color: muted ? 'muted' : undefined,
|
||||
size: 'xl',
|
||||
}}
|
||||
onClick={handleMute}
|
||||
onWheel={handleVolumeWheel}
|
||||
size="sm"
|
||||
tooltip={{
|
||||
label: muted ? t('player.muted', { postProcess: 'titleCase' }) : volume,
|
||||
openDelay: 0,
|
||||
}}
|
||||
variant="transparent"
|
||||
/>
|
||||
{!isMinWidth ? (
|
||||
<PlayerButton
|
||||
icon={<HiOutlineQueueList size="1.1rem" />}
|
||||
onClick={handleToggleQueue}
|
||||
tooltip={{
|
||||
label: t('player.viewQueue', { postProcess: 'titleCase' }),
|
||||
openDelay: 500,
|
||||
}}
|
||||
variant="secondary"
|
||||
<PlayerbarSlider
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={handleVolumeSlider}
|
||||
onWheel={handleVolumeWheel}
|
||||
size={6}
|
||||
value={volume}
|
||||
w={volumeWidth}
|
||||
/>
|
||||
) : null}
|
||||
<Group
|
||||
noWrap
|
||||
spacing="xs"
|
||||
>
|
||||
<PlayerButton
|
||||
icon={
|
||||
muted ? (
|
||||
<RiVolumeMuteFill size="1.2rem" />
|
||||
) : volume > 50 ? (
|
||||
<RiVolumeUpFill size="1.2rem" />
|
||||
) : (
|
||||
<RiVolumeDownFill size="1.2rem" />
|
||||
)
|
||||
}
|
||||
onClick={handleMute}
|
||||
onWheel={handleVolumeWheel}
|
||||
tooltip={{
|
||||
label: muted ? t('player.muted', { postProcess: 'titleCase' }) : volume,
|
||||
openDelay: 500,
|
||||
}}
|
||||
variant="secondary"
|
||||
/>
|
||||
{!isMinWidth ? (
|
||||
<PlayerbarSlider
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={handleVolumeSlider}
|
||||
onWheel={handleVolumeWheel}
|
||||
size={6}
|
||||
value={volume}
|
||||
w={volumeWidth}
|
||||
/>
|
||||
) : null}
|
||||
</Group>
|
||||
</Group>
|
||||
<Group h="calc(100% / 3)" />
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import { Divider, Group, SelectItem, Stack } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import merge from 'lodash/merge';
|
||||
import { useMemo } from 'react';
|
||||
import { RiAddBoxFill, RiAddCircleFill, RiPlayFill } from 'react-icons/ri';
|
||||
import { create } from 'zustand';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, Checkbox, NumberInput, Select } from '/@/renderer/components';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import {
|
||||
GenreListResponse,
|
||||
GenreListSort,
|
||||
|
|
@ -33,7 +39,7 @@ interface ShuffleAllSlice extends RandomSongListQuery {
|
|||
enableMinYear: boolean;
|
||||
}
|
||||
|
||||
const useShuffleAllStore = create<ShuffleAllSlice>()(
|
||||
const useShuffleAllStore = createWithEqualityFn<ShuffleAllSlice>()(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
actions: {
|
||||
|
|
@ -58,7 +64,7 @@ const useShuffleAllStore = create<ShuffleAllSlice>()(
|
|||
),
|
||||
);
|
||||
|
||||
const PLAYED_DATA: SelectItem[] = [
|
||||
const PLAYED_DATA: { label: string; value: Played }[] = [
|
||||
{ label: 'all tracks', value: Played.All },
|
||||
{ label: 'only unplayed tracks', value: Played.Never },
|
||||
{ label: 'only played tracks', value: Played.Played },
|
||||
|
|
@ -81,6 +87,7 @@ export const ShuffleAllModal = ({
|
|||
queryClient,
|
||||
server,
|
||||
}: ShuffleAllModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { enableMaxYear, enableMinYear, genre, limit, maxYear, minYear, musicFolderId, played } =
|
||||
useShuffleAllStore();
|
||||
const { setStore } = useShuffleAllStoreActions();
|
||||
|
|
@ -139,7 +146,7 @@ export const ShuffleAllModal = ({
|
|||
}, [musicFolders]);
|
||||
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<Stack gap="md">
|
||||
<NumberInput
|
||||
label="How many tracks?"
|
||||
max={500}
|
||||
|
|
@ -157,8 +164,8 @@ export const ShuffleAllModal = ({
|
|||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMinYear}
|
||||
mr="0.5rem"
|
||||
onChange={(e) => setStore({ enableMinYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={minYear}
|
||||
|
|
@ -172,8 +179,8 @@ export const ShuffleAllModal = ({
|
|||
rightSection={
|
||||
<Checkbox
|
||||
checked={enableMaxYear}
|
||||
mr="0.5rem"
|
||||
onChange={(e) => setStore({ enableMaxYear: e.currentTarget.checked })}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
}
|
||||
value={maxYear}
|
||||
|
|
@ -210,31 +217,31 @@ export const ShuffleAllModal = ({
|
|||
<Group grow>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftIcon={<RiAddBoxFill size="1rem" />}
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
Add
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftIcon={<RiAddCircleFill size="1rem" />}
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
Add next
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
<Button
|
||||
disabled={!limit}
|
||||
leftIcon={<RiPlayFill size="1rem" />}
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
Play
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
.container {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,11 @@
|
|||
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
||||
import { createRef, useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import styles from './visualizer.module.css';
|
||||
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Visualizer = () => {
|
||||
const { webAudio } = useWebAudio();
|
||||
const canvasRef = createRef<HTMLDivElement>();
|
||||
|
|
@ -67,7 +58,8 @@ export const Visualizer = () => {
|
|||
}, [resize]);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={canvasRef}
|
||||
style={{ height: length, width: length }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import debounce from 'lodash/debounce';
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import {
|
||||
|
|
@ -18,6 +17,7 @@ import {
|
|||
} from '/@/renderer/store';
|
||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||
import { setAutoNext, setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useCallback, useRef } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import {
|
||||
|
|
@ -20,6 +19,7 @@ import {
|
|||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||
import { useGeneralSettings, usePlaybackType } from '/@/renderer/store/settings.store';
|
||||
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
instanceOfCancellationError,
|
||||
LibraryItem,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue