Add additional controls to playerbar
This commit is contained in:
parent
5ddd0872ef
commit
85bf910d65
3 changed files with 238 additions and 49 deletions
|
@ -1,19 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Center } from '@mantine/core';
|
import { Center, Group } from '@mantine/core';
|
||||||
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
|
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||||
import { RiArrowUpSLine, RiDiscLine } from 'react-icons/ri';
|
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, Text } from '/@/renderer/components';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAppStoreActions, useAppStore, useCurrentSong } from '/@/renderer/store';
|
import { useAppStoreActions, useAppStore, useCurrentSong } from '/@/renderer/store';
|
||||||
import { fadeIn } from '/@/renderer/styles';
|
import { fadeIn } from '/@/renderer/styles';
|
||||||
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const LeftControlsContainer = styled.div`
|
const LeftControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-left: 1rem;
|
padding-left: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageWrapper = styled.div`
|
const ImageWrapper = styled.div`
|
||||||
|
@ -77,6 +80,44 @@ export const LeftControls = () => {
|
||||||
const title = currentSong?.name;
|
const title = currentSong?.name;
|
||||||
const artists = currentSong?.artists;
|
const artists = currentSong?.artists;
|
||||||
|
|
||||||
|
const isSongDefined = Boolean(currentSong?.id);
|
||||||
|
|
||||||
|
const openAddToPlaylistModal = () => {
|
||||||
|
openContextModal({
|
||||||
|
innerProps: {
|
||||||
|
songId: [currentSong?.id],
|
||||||
|
},
|
||||||
|
modal: 'addToPlaylist',
|
||||||
|
size: 'md',
|
||||||
|
title: 'Add to playlist',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToFavoritesMutation = useCreateFavorite();
|
||||||
|
const removeFromFavoritesMutation = useDeleteFavorite();
|
||||||
|
|
||||||
|
const handleAddToFavorites = () => {
|
||||||
|
if (!isSongDefined || !currentSong) return;
|
||||||
|
|
||||||
|
addToFavoritesMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id: [currentSong.id],
|
||||||
|
type: LibraryItem.SONG,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = () => {
|
||||||
|
if (!isSongDefined || !currentSong) return;
|
||||||
|
|
||||||
|
removeFromFavoritesMutation.mutate({
|
||||||
|
query: {
|
||||||
|
id: [currentSong.id],
|
||||||
|
type: LibraryItem.SONG,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftControlsContainer>
|
<LeftControlsContainer>
|
||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
|
@ -139,16 +180,45 @@ export const LeftControls = () => {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<MetadataStack layout="position">
|
<MetadataStack layout="position">
|
||||||
<LineItem>
|
<LineItem>
|
||||||
<Text
|
<Group
|
||||||
$link
|
align="flex-start"
|
||||||
component={Link}
|
spacing="xs"
|
||||||
overflow="hidden"
|
|
||||||
size="sm"
|
|
||||||
to={AppRoute.NOW_PLAYING}
|
|
||||||
weight={500}
|
|
||||||
>
|
>
|
||||||
{title || '—'}
|
<Text
|
||||||
</Text>
|
$link
|
||||||
|
component={Link}
|
||||||
|
overflow="hidden"
|
||||||
|
size="md"
|
||||||
|
to={AppRoute.NOW_PLAYING}
|
||||||
|
weight={500}
|
||||||
|
>
|
||||||
|
{title || '—'}
|
||||||
|
</Text>
|
||||||
|
{isSongDefined && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiMore2Fill size="1.2rem" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item onClick={openAddToPlaylistModal}>
|
||||||
|
Add to playlist
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Item onClick={handleAddToFavorites}>
|
||||||
|
Add to favorites
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={handleRemoveFromFavorites}>
|
||||||
|
Remove from favorites
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</LineItem>
|
</LineItem>
|
||||||
<LineItem $secondary>
|
<LineItem $secondary>
|
||||||
{artists?.map((artist, index) => (
|
{artists?.map((artist, index) => (
|
||||||
|
@ -157,7 +227,7 @@ export const LeftControls = () => {
|
||||||
<Text
|
<Text
|
||||||
$link
|
$link
|
||||||
$secondary
|
$secondary
|
||||||
size="xs"
|
size="md"
|
||||||
style={{ display: 'inline-block' }}
|
style={{ display: 'inline-block' }}
|
||||||
>
|
>
|
||||||
,
|
,
|
||||||
|
@ -167,7 +237,7 @@ export const LeftControls = () => {
|
||||||
$link
|
$link
|
||||||
component={Link}
|
component={Link}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="xs"
|
size="md"
|
||||||
to={
|
to={
|
||||||
artist.id
|
artist.id
|
||||||
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
@ -187,7 +257,7 @@ export const LeftControls = () => {
|
||||||
$link
|
$link
|
||||||
component={Link}
|
component={Link}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size="xs"
|
size="md"
|
||||||
to={
|
to={
|
||||||
currentSong?.albumId
|
currentSong?.albumId
|
||||||
? generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
? generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
import { Group } from '@mantine/core';
|
import { Flex, Group } from '@mantine/core';
|
||||||
import { HiOutlineQueueList } from 'react-icons/hi2';
|
import { HiOutlineQueueList } from 'react-icons/hi2';
|
||||||
import { RiVolumeUpFill, RiVolumeDownFill, RiVolumeMuteFill } from 'react-icons/ri';
|
import {
|
||||||
|
RiVolumeUpFill,
|
||||||
|
RiVolumeDownFill,
|
||||||
|
RiVolumeMuteFill,
|
||||||
|
RiHeartLine,
|
||||||
|
RiHeartFill,
|
||||||
|
} from 'react-icons/ri';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useAppStoreActions, useMuted, useSidebarStore, useVolume } from '/@/renderer/store';
|
import {
|
||||||
|
useAppStoreActions,
|
||||||
|
useCurrentServer,
|
||||||
|
useCurrentSong,
|
||||||
|
useMuted,
|
||||||
|
useSetQueueFavorite,
|
||||||
|
useSidebarStore,
|
||||||
|
useVolume,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { useRightControls } from '../hooks/use-right-controls';
|
import { useRightControls } from '../hooks/use-right-controls';
|
||||||
import { PlayerButton } from './player-button';
|
import { PlayerButton } from './player-button';
|
||||||
import { Slider } from './slider';
|
import { Slider } from './slider';
|
||||||
|
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||||
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
|
import { Rating } from '/@/renderer/components';
|
||||||
|
|
||||||
const RightControlsContainer = styled.div`
|
const RightControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -13,7 +30,6 @@ const RightControlsContainer = styled.div`
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-right: 1rem;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const VolumeSliderWrapper = styled.div`
|
const VolumeSliderWrapper = styled.div`
|
||||||
|
@ -35,47 +51,141 @@ const MetadataStack = styled.div`
|
||||||
export const RightControls = () => {
|
export const RightControls = () => {
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
const muted = useMuted();
|
const muted = useMuted();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const currentSong = useCurrentSong();
|
||||||
const { setSidebar } = useAppStoreActions();
|
const { setSidebar } = useAppStoreActions();
|
||||||
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
||||||
const { handleVolumeSlider, handleVolumeSliderState, handleMute } = useRightControls();
|
const { handleVolumeSlider, handleVolumeSliderState, handleMute } = useRightControls();
|
||||||
|
|
||||||
|
const addToFavoritesMutation = useCreateFavorite();
|
||||||
|
const removeFromFavoritesMutation = useDeleteFavorite();
|
||||||
|
const setFavorite = useSetQueueFavorite();
|
||||||
|
|
||||||
|
const handleAddToFavorites = () => {
|
||||||
|
if (!currentSong) return;
|
||||||
|
|
||||||
|
addToFavoritesMutation.mutate(
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
id: [currentSong.id],
|
||||||
|
type: LibraryItem.SONG,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setFavorite([currentSong.id], true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = () => {
|
||||||
|
if (!currentSong) return;
|
||||||
|
|
||||||
|
removeFromFavoritesMutation.mutate(
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
id: [currentSong.id],
|
||||||
|
type: LibraryItem.SONG,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setFavorite([currentSong.id], false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = () => {
|
||||||
|
if (!currentSong) return;
|
||||||
|
|
||||||
|
if (currentSong.userFavorite) {
|
||||||
|
handleRemoveFromFavorites();
|
||||||
|
} else {
|
||||||
|
handleAddToFavorites();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSongDefined = Boolean(currentSong?.id);
|
||||||
|
const showRating = isSongDefined && server?.type === ServerType.NAVIDROME;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightControlsContainer>
|
<Flex
|
||||||
<Group>
|
align="flex-end"
|
||||||
<PlayerButton
|
direction="column"
|
||||||
icon={<HiOutlineQueueList />}
|
h="100%"
|
||||||
tooltip={{ label: 'View queue', openDelay: 500 }}
|
p="1rem"
|
||||||
variant="secondary"
|
>
|
||||||
onClick={() => setSidebar({ rightExpanded: !isQueueExpanded })}
|
{showRating && (
|
||||||
/>
|
<Group>
|
||||||
</Group>
|
<Rating
|
||||||
<MetadataStack>
|
readOnly
|
||||||
<VolumeSliderWrapper>
|
size="sm"
|
||||||
|
value={currentSong?.userRating ?? 0}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
<RightControlsContainer>
|
||||||
|
<Group spacing="xs">
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={
|
icon={
|
||||||
muted ? (
|
currentSong?.userFavorite ? (
|
||||||
<RiVolumeMuteFill size={15} />
|
<RiHeartFill
|
||||||
) : volume > 50 ? (
|
color="var(--primary-color)"
|
||||||
<RiVolumeUpFill size={15} />
|
size="1.3rem"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RiVolumeDownFill size={15} />
|
<RiHeartLine size="1.3rem" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
tooltip={{ label: muted ? 'Muted' : volume, openDelay: 500 }}
|
sx={{
|
||||||
|
svg: {
|
||||||
|
fill: !currentSong?.userFavorite ? undefined : 'var(--primary-color) !important',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
tooltip={{
|
||||||
|
label: currentSong?.userFavorite ? 'Unfavorite' : 'Favorite',
|
||||||
|
openDelay: 500,
|
||||||
|
}}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleMute}
|
onClick={handleToggleFavorite}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<PlayerButton
|
||||||
hasTooltip
|
icon={<HiOutlineQueueList size="1.3rem" />}
|
||||||
height="60%"
|
tooltip={{ label: 'View queue', openDelay: 500 }}
|
||||||
max={100}
|
variant="secondary"
|
||||||
min={0}
|
onClick={() => setSidebar({ rightExpanded: !isQueueExpanded })}
|
||||||
value={volume}
|
|
||||||
onAfterChange={handleVolumeSliderState}
|
|
||||||
onChange={handleVolumeSlider}
|
|
||||||
/>
|
/>
|
||||||
</VolumeSliderWrapper>
|
</Group>
|
||||||
</MetadataStack>
|
<MetadataStack>
|
||||||
</RightControlsContainer>
|
<VolumeSliderWrapper>
|
||||||
|
<PlayerButton
|
||||||
|
icon={
|
||||||
|
muted ? (
|
||||||
|
<RiVolumeMuteFill size="1.2rem" />
|
||||||
|
) : volume > 50 ? (
|
||||||
|
<RiVolumeUpFill size="1.2rem" />
|
||||||
|
) : (
|
||||||
|
<RiVolumeDownFill size="1.2rem" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tooltip={{ label: muted ? 'Muted' : volume, openDelay: 500 }}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleMute}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
hasTooltip
|
||||||
|
height="60%"
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
value={volume}
|
||||||
|
onAfterChange={handleVolumeSliderState}
|
||||||
|
onChange={handleVolumeSlider}
|
||||||
|
/>
|
||||||
|
</VolumeSliderWrapper>
|
||||||
|
</MetadataStack>
|
||||||
|
</RightControlsContainer>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -659,6 +659,15 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentSongId = get().current.song?.id;
|
||||||
|
if (currentSongId && ids.includes(currentSongId)) {
|
||||||
|
set((state) => {
|
||||||
|
if (state.current.song) {
|
||||||
|
state.current.song.userFavorite = favorite;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return foundUniqueIds;
|
return foundUniqueIds;
|
||||||
},
|
},
|
||||||
setMuted: (muted: boolean) => {
|
setMuted: (muted: boolean) => {
|
||||||
|
|
Reference in a new issue