add initial files

This commit is contained in:
jeffvli
2022-07-25 19:40:16 -07:00
commit e8b612c974
283 changed files with 62820 additions and 0 deletions
@@ -0,0 +1,184 @@
import {
useImperativeHandle,
forwardRef,
useRef,
useState,
useCallback,
} from 'react';
import ReactPlayer, { ReactPlayerProps } from 'react-player';
import {
CrossfadeStyle,
PlaybackStyle,
PlayerStatus,
Song,
} from '../../../types';
import { crossfadeHandler, gaplessHandler } from './utils/listenHandlers';
interface AudioPlayerProps extends ReactPlayerProps {
crossfadeDuration: number;
crossfadeStyle: CrossfadeStyle;
currentPlayer: 1 | 2;
player1: Song;
player2: Song;
status: PlayerStatus;
style: PlaybackStyle;
volume: number;
}
export type AudioPlayerProgress = {
loaded: number;
loadedSeconds: number;
played: number;
playedSeconds: number;
};
const getDuration = (ref: any) => {
return ref.current?.player?.player?.player?.duration;
};
export const AudioPlayer = forwardRef(
(
{
status,
style,
crossfadeStyle,
crossfadeDuration,
currentPlayer,
autoNext,
player1,
player2,
muted,
volume,
}: AudioPlayerProps,
ref: any
) => {
const player1Ref = useRef<any>(null);
const player2Ref = useRef<any>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
useImperativeHandle(ref, () => ({
get player1() {
return player1Ref?.current;
},
get player2() {
return player2Ref?.current;
},
}));
const handleOnEnded = () => {
autoNext();
setIsTransitioning(false);
};
const handleCrossfade1 = useCallback(
(e: AudioPlayerProgress) => {
return crossfadeHandler({
currentPlayer,
currentPlayerRef: player1Ref,
currentTime: e.playedSeconds,
duration: getDuration(player1Ref),
fadeDuration: crossfadeDuration,
fadeType: crossfadeStyle,
isTransitioning,
nextPlayerRef: player2Ref,
player: 1,
setIsTransitioning,
volume,
});
},
[
crossfadeDuration,
crossfadeStyle,
currentPlayer,
isTransitioning,
volume,
]
);
const handleCrossfade2 = useCallback(
(e: AudioPlayerProgress) => {
return crossfadeHandler({
currentPlayer,
currentPlayerRef: player2Ref,
currentTime: e.playedSeconds,
duration: getDuration(player2Ref),
fadeDuration: crossfadeDuration,
fadeType: crossfadeStyle,
isTransitioning,
nextPlayerRef: player1Ref,
player: 2,
setIsTransitioning,
volume,
});
},
[
crossfadeDuration,
crossfadeStyle,
currentPlayer,
isTransitioning,
volume,
]
);
const handleGapless1 = useCallback(
(e: AudioPlayerProgress) => {
return gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(player1Ref),
isFlac: player1?.suffix === 'flac',
isTransitioning,
nextPlayerRef: player2Ref,
setIsTransitioning,
});
},
[isTransitioning, player1?.suffix]
);
const handleGapless2 = useCallback(
(e: AudioPlayerProgress) => {
return gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(player2Ref),
isFlac: player2?.suffix === 'flac',
isTransitioning,
nextPlayerRef: player1Ref,
setIsTransitioning,
});
},
[isTransitioning, player2?.suffix]
);
return (
<>
<ReactPlayer
ref={player1Ref}
height={0}
muted={muted}
playing={currentPlayer === 1 && status === PlayerStatus.Playing}
progressInterval={isTransitioning ? 10 : 250}
url={player1?.streamUrl}
volume={volume}
width={0}
onEnded={handleOnEnded}
onProgress={
style === PlaybackStyle.Gapless ? handleGapless1 : handleCrossfade1
}
/>
<ReactPlayer
ref={player2Ref}
height={0}
muted={muted}
playing={currentPlayer === 2 && status === PlayerStatus.Playing}
progressInterval={isTransitioning ? 10 : 250}
url={player2?.streamUrl}
volume={volume}
width={0}
onEnded={handleOnEnded}
onProgress={
style === PlaybackStyle.Gapless ? handleGapless2 : handleCrossfade2
}
/>
</>
);
}
);
@@ -0,0 +1,147 @@
/* eslint-disable no-nested-ternary */
import { Dispatch } from 'react';
import { CrossfadeStyle } from '../../../../types';
export const gaplessHandler = (args: {
currentTime: number;
duration: number;
isFlac: boolean;
isTransitioning: boolean;
nextPlayerRef: any;
setIsTransitioning: Dispatch<boolean>;
}) => {
const {
nextPlayerRef,
currentTime,
duration,
isTransitioning,
setIsTransitioning,
isFlac,
} = args;
if (!isTransitioning) {
if (currentTime > duration - 2) {
return setIsTransitioning(true);
}
return null;
}
const durationPadding = isFlac ? 0.065 : 0.116;
if (currentTime + durationPadding >= duration) {
return nextPlayerRef.current.getInternalPlayer().play();
}
return null;
};
export const crossfadeHandler = (args: {
currentPlayer: 1 | 2;
currentPlayerRef: any;
currentTime: number;
duration: number;
fadeDuration: number;
fadeType: CrossfadeStyle;
isTransitioning: boolean;
nextPlayerRef: any;
player: 1 | 2;
setIsTransitioning: Dispatch<boolean>;
volume: number;
}) => {
const {
currentTime,
player,
currentPlayer,
currentPlayerRef,
nextPlayerRef,
fadeDuration,
fadeType,
duration,
volume,
isTransitioning,
setIsTransitioning,
} = args;
if (!isTransitioning || currentPlayer !== player) {
const shouldBeginTransition = currentTime >= duration - fadeDuration;
if (shouldBeginTransition) {
setIsTransitioning(true);
return nextPlayerRef.current.getInternalPlayer().play();
}
return null;
}
const timeLeft = duration - currentTime;
let currentPlayerVolumeCalculation;
let nextPlayerVolumeCalculation;
let percentageOfFadeLeft;
let n;
switch (fadeType) {
case 'equalPower':
// https://dsp.stackexchange.com/a/14755
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
currentPlayerVolumeCalculation =
Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
nextPlayerVolumeCalculation =
Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
break;
case 'linear':
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
nextPlayerVolumeCalculation =
((fadeDuration - timeLeft) / fadeDuration) * volume;
break;
case 'dipped':
// https://math.stackexchange.com/a/4622
percentageOfFadeLeft = timeLeft / fadeDuration;
currentPlayerVolumeCalculation = percentageOfFadeLeft ** 2 * volume;
nextPlayerVolumeCalculation = (percentageOfFadeLeft - 1) ** 2 * volume;
break;
case fadeType.match(/constantPower.*/)?.input:
// https://math.stackexchange.com/a/26159
n =
fadeType === 'constantPower'
? 0
: fadeType === 'constantPowerSlowFade'
? 1
: fadeType === 'constantPowerSlowCut'
? 3
: 10;
percentageOfFadeLeft = timeLeft / fadeDuration;
currentPlayerVolumeCalculation =
Math.cos(
(Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)
) * volume;
nextPlayerVolumeCalculation =
Math.cos(
(Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)
) * volume;
break;
default:
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
nextPlayerVolumeCalculation =
((fadeDuration - timeLeft) / fadeDuration) * volume;
break;
}
const currentPlayerVolume =
currentPlayerVolumeCalculation >= 0 ? currentPlayerVolumeCalculation : 0;
const nextPlayerVolume =
nextPlayerVolumeCalculation <= volume
? nextPlayerVolumeCalculation
: volume;
if (currentPlayer === 1) {
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
} else {
currentPlayerRef.current.getInternalPlayer().volume = currentPlayerVolume;
nextPlayerRef.current.getInternalPlayer().volume = nextPlayerVolume;
}
// }
return null;
};
+10
View File
@@ -0,0 +1,10 @@
import { ReactNode } from 'react';
import { Button as MantineButton } from '@mantine/core';
interface ButtonProps {
icon?: ReactNode;
}
export const Button = ({ icon }: ButtonProps) => {
return <MantineButton>Button</MantineButton>;
};
@@ -0,0 +1,34 @@
import React, { ReactNode } from 'react';
import { ActionIcon, ActionIconProps, TooltipProps } from '@mantine/core';
import { Tooltip } from '../tooltip/Tooltip';
type MantineIconButtonProps = ActionIconProps &
React.ComponentPropsWithoutRef<'button'>;
interface IconButtonProps extends MantineIconButtonProps {
active?: boolean;
icon: ReactNode;
tooltip?: Omit<TooltipProps, 'children'>;
}
export const IconButton = ({
active,
tooltip,
icon,
...rest
}: IconButtonProps) => {
if (tooltip) {
return (
<Tooltip {...tooltip}>
<ActionIcon {...rest}>{icon}</ActionIcon>
</Tooltip>
);
}
return <ActionIcon {...rest}>{icon}</ActionIcon>;
};
IconButton.defaultProps = {
active: false,
tooltip: undefined,
};
+4
View File
@@ -0,0 +1,4 @@
export * from './tooltip/Tooltip';
export * from './audio-player/AudioPlayer';
export * from './icon-button/IconButton';
export * from './text/Text';
+27
View File
@@ -0,0 +1,27 @@
import {
Modal as MantineModal,
ModalProps as MantineModalProps,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
interface ModalProps extends MantineModalProps {
condition: boolean;
}
export const Modal = ({ condition, children, ...rest }: ModalProps) => {
const [opened, handlers] = useDisclosure(false);
return (
<>
{condition && (
<MantineModal
{...rest}
opened={opened}
onClose={() => handlers.close()}
>
{children}
</MantineModal>
)}
</>
);
};
+95
View File
@@ -0,0 +1,95 @@
import { ReactNode } from 'react';
import {
Text as MantineText,
TextProps as MantineTextProps,
} from '@mantine/core';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Font } from 'renderer/styles';
import { textEllipsis } from 'renderer/styles/mixins';
interface TextProps extends MantineTextProps<'div'> {
children: ReactNode;
font?: Font;
link?: boolean;
noSelect?: boolean;
overflow?: 'hidden' | 'visible';
secondary?: boolean;
to?: string;
weight?: number;
}
interface LinkTextProps extends MantineTextProps<'Link'> {
children: ReactNode;
font?: Font;
link?: boolean;
overflow?: 'hidden' | 'visible';
secondary?: boolean;
to: string;
weight?: number;
}
const BaseText = styled(MantineText)<any>`
color: ${(props) =>
props.$secondary
? 'var(--playerbar-text-secondary-color)'
: 'var(--playerbar-text-primary-color)'};
font-family: ${(props) => props.font || Font.GOTHAM};
cursor: ${(props) => (props.link ? 'cursor' : 'default')};
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
${(props) => props.overflow === 'hidden' && textEllipsis}
`;
const StyledText = styled(BaseText)<TextProps>``;
const StyledLinkText = styled(BaseText)<LinkTextProps>``;
export const Text = ({
children,
link,
secondary,
overflow,
font,
to,
noSelect,
...rest
}: TextProps) => {
if (link) {
return (
<StyledLinkText<typeof Link>
$noSelect={noSelect}
$secondary={secondary}
component={Link}
font={font}
link="true"
overflow={overflow}
to={to || ''}
{...rest}
>
{children}
</StyledLinkText>
);
}
return (
<StyledText
$noSelect={noSelect}
$secondary={secondary}
font={font}
overflow={overflow}
{...rest}
>
{children}
</StyledText>
);
};
Text.defaultProps = {
font: Font.GOTHAM,
link: false,
noSelect: false,
overflow: 'visible',
secondary: false,
to: '',
weight: 500,
};
@@ -0,0 +1,9 @@
.body {
padding: 5px;
color: var(--tooltip-text-color);
background: var(--tooltip-bg);
}
.arrow {
background: var(--tooltip-bg);
}
@@ -0,0 +1,32 @@
import { Tooltip as MantineTooltip, TooltipProps } from '@mantine/core';
import styles from './Tooltip.module.scss';
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
return (
<MantineTooltip
classNames={{ arrow: styles.arrow, body: styles.body }}
radius="xs"
styles={{
arrow: {
background: 'var(--tooltip-bg)',
},
body: {
background: 'var(--tooltip-bg)',
color: 'var(--tooltip-text-color)',
padding: '5px',
},
}}
{...rest}
>
{children}
</MantineTooltip>
);
};
Tooltip.defaultProps = {
openDelay: 0,
placement: 'center',
transition: 'fade',
transitionDuration: 250,
withArrow: true,
};
@@ -0,0 +1,156 @@
import { Card } from '@mantine/core';
import { motion } from 'framer-motion';
import styled from 'styled-components';
import { CardRow } from 'renderer/types';
import { Text } from '../text/Text';
import { GridCardControls } from './GridCardControls';
const CardWrapper = styled(motion.div)<{
itemGap: number;
itemHeight: number;
itemWidth: number;
}>`
display: flex;
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
width: ${({ itemWidth }) => `${itemWidth}px`};
height: ${({ itemHeight }) => `${itemHeight}px`};
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
border-radius: 3px;
filter: drop-shadow(0 4px 4px #000);
user-select: none;
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
&:focus-visible {
outline: 1px solid #fff;
}
`;
const StyledCard = styled(Card)`
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
height: 100%;
padding: 0;
background-color: rgb(50, 50, 50, 50%);
border-radius: 3px;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: rgb(50, 50, 50, 60%);
}
`;
const ImageSection = styled.div`
width: 100%;
height: 100%;
`;
const Image = styled(motion.div)<{ height: number; src: string }>`
height: ${({ height }) => `${height}px`};
background: ${({ src }) => `url(${src})`};
background-position: center;
background-size: cover;
border: 0;
`;
const ControlsContainer = styled.div`
display: none;
width: 100%;
height: 100%;
${StyledCard}:hover & {
display: block;
}
`;
const DetailSection = styled.div`
display: flex;
flex-direction: column;
`;
const Row = styled.div`
height: 25px;
padding: 0 0.2rem;
`;
export const GridCard = ({ data, index, style, isScrolling }: any) => {
const {
itemHeight,
itemWidth,
columnCount,
itemGap,
itemCount,
cardControls,
cardRows,
itemData,
handlePlayQueueAdd,
} = data;
const startIndex = index * columnCount;
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
const cards = [];
for (let i = startIndex; i <= stopIndex; i += 1) {
cards.push(
<CardWrapper
key={`card-${i}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
tabIndex={0}
>
<StyledCard>
<ImageSection>
<Image
animate={{
opacity: 1,
}}
height={itemWidth}
initial={{
opacity: 0,
}}
src={itemData[i]?.image}
transition={{
duration: 0.5,
ease: 'anticipate',
}}
>
{!isScrolling && (
<ControlsContainer>
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={itemData[i]}
/>
</ControlsContainer>
)}
</Image>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow) => (
<Row key={`row-${row.prop}`}>
<Text overflow="hidden" weight={500}>
{itemData[i] && itemData[i][row.prop]}
</Text>
</Row>
))}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
return (
<div
style={{
...style,
alignItems: 'center',
display: 'flex',
justifyContent: 'start',
}}
>
{cards}
</div>
);
};
@@ -0,0 +1,90 @@
import React from 'react';
import { UnstyledButton, UnstyledButtonProps } from '@mantine/core';
import { motion } from 'framer-motion';
import styled from 'styled-components';
import { PlayerPlay } from 'tabler-icons-react';
type PlayButtonType = UnstyledButtonProps &
React.ComponentPropsWithoutRef<'button'>;
const PlayButton = styled(UnstyledButton)<PlayButtonType>`
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background-color: var(--primary-color);
border-radius: 50%;
cursor: default;
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
&:hover {
opacity: 1;
}
svg {
fill: #000;
stroke: #000;
}
`;
const GridCardControlsContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`;
const ControlsRow = styled(motion.div)`
width: 100%;
height: calc(100% / 3);
`;
const TopControls = styled(ControlsRow)`
display: flex;
align-items: flex-start;
justify-content: space-between;
`;
const CenterControls = styled(ControlsRow)`
display: flex;
align-items: center;
justify-content: center;
`;
const BottomControls = styled(ControlsRow)`
display: flex;
align-items: flex-end;
justify-content: space-between;
`;
export const GridCardControls = ({
itemData,
handlePlayQueueAdd,
cardControls,
}: any) => {
return (
<GridCardControlsContainer>
<TopControls />
<CenterControls animate={{ opacity: 1 }} initial={{ opacity: 0 }}>
<PlayButton
onClick={() => {
handlePlayQueueAdd({
byItemType: {
endpoint: cardControls.endpoint,
id: itemData[cardControls.idProperty],
type: cardControls.type,
},
});
}}
>
<PlayerPlay />
</PlayButton>
</CenterControls>
<BottomControls />
</GridCardControlsContainer>
);
};
@@ -0,0 +1,69 @@
import { Ref, useMemo } from 'react';
import { FixedSizeList, FixedSizeListProps } from 'react-window';
import { usePlayQueueHandler } from 'renderer/features/player/hooks/usePlayQueueHandler';
import { CardRow } from 'renderer/types';
import { GridCard } from './GridCard';
export const VirtualGridWrapper = ({
refInstance,
cardControls,
cardRows,
itemGap,
itemWidth,
itemHeight,
itemCount,
columnCount,
rowCount,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
cardControls: any;
cardRows: CardRow[];
columnCount: number;
itemGap: number;
itemHeight: number;
itemWidth: number;
refInstance: Ref<any>;
rowCount: number;
}) => {
const { handlePlayQueueAdd } = usePlayQueueHandler();
const itemData = useMemo(
() => ({
cardControls,
cardRows,
columnCount,
handlePlayQueueAdd,
itemCount,
itemData: rest.itemData,
itemGap,
itemHeight,
itemWidth,
}),
[
cardRows,
cardControls,
columnCount,
itemCount,
rest.itemData,
itemGap,
itemHeight,
itemWidth,
handlePlayQueueAdd,
]
);
return (
<FixedSizeList
style={{ scrollBehavior: 'smooth' }}
{...rest}
ref={refInstance}
initialScrollOffset={0}
itemCount={rowCount}
itemData={itemData}
itemSize={itemHeight + itemGap}
overscanCount={10}
>
{GridCard}
</FixedSizeList>
);
};
@@ -0,0 +1,141 @@
import { forwardRef, Ref, useState } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeListProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { CardRow } from 'renderer/types';
import { VirtualGridWrapper } from './VirtualGridWrapper';
interface VirtualGridProps
extends Omit<
FixedSizeListProps,
'children' | 'itemSize' | 'height' | 'width'
> {
cardControls: any;
cardRows: CardRow[];
itemGap?: number;
itemSize: number;
minimumBatchSize?: number;
query: (props: any) => Promise<any>;
queryParams?: Record<string, any>;
}
export const VirtualInfiniteGrid = forwardRef(
(
{
itemCount,
itemGap,
itemSize,
cardControls,
cardRows,
minimumBatchSize,
query,
queryParams,
}: VirtualGridProps,
ref: Ref<InfiniteLoader>
) => {
const [itemData, setItemData] = useState<any[]>([]);
const isItemLoaded = (index: number, columnCount: number) => {
const itemIndex = index * columnCount;
return (
itemIndex < itemData.length * columnCount &&
itemData[itemIndex] !== undefined
);
};
const loadMoreItems = async (
startIndex: number,
stopIndex: number,
limit: number,
columnCount: number
) => {
const currentPage = Math.ceil(startIndex / minimumBatchSize!);
const t = await query({
limit,
page: currentPage,
...queryParams,
});
// Need to multiply by columnCount due to the grid layout
const start = startIndex * columnCount;
const end = (stopIndex + 1) * columnCount;
return new Promise<void>((resolve) => {
const newData: any[] = [...itemData];
let itemIndex = 0;
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
newData[rowIndex] = t?.data[itemIndex];
itemIndex += 1;
}
setItemData(newData);
resolve();
});
};
const debouncedLoadMoreItems = debounce(loadMoreItems, 300);
return (
<AutoSizer>
{({ height, width }) => {
const itemHeight = itemSize! + cardRows.length * 25;
const columnCount = Math.floor(
(Number(width) - itemGap! + 3) / (itemSize! + itemGap! + 2)
);
const rowCount = Math.ceil(itemCount / columnCount);
const pageItemLimit = columnCount * minimumBatchSize!;
return (
<InfiniteLoader
ref={ref}
isItemLoaded={(index) => isItemLoaded(index, columnCount)}
itemCount={itemCount || 0}
loadMoreItems={(startIndex, stopIndex) =>
debouncedLoadMoreItems(
startIndex,
stopIndex,
pageItemLimit,
columnCount
)
}
minimumBatchSize={minimumBatchSize}
threshold={10}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<VirtualGridWrapper
useIsScrolling
cardControls={cardControls}
cardRows={cardRows}
columnCount={columnCount}
height={height}
itemCount={itemCount || 0}
itemData={itemData}
itemGap={itemGap!}
itemHeight={itemHeight!}
itemWidth={itemSize}
refInstance={infiniteLoaderRef}
rowCount={rowCount}
width={width}
onItemsRendered={onItemsRendered}
/>
)}
</InfiniteLoader>
);
}}
</AutoSizer>
);
}
);
VirtualInfiniteGrid.defaultProps = {
itemGap: 10,
minimumBatchSize: 20,
queryParams: {},
};