mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Update base components
This commit is contained in:
+8
-10
@@ -7,12 +7,10 @@ import {
|
||||
} from 'react';
|
||||
import ReactPlayer, { ReactPlayerProps } from 'react-player';
|
||||
import {
|
||||
CrossfadeStyle,
|
||||
PlaybackStyle,
|
||||
PlayerStatus,
|
||||
Song,
|
||||
} from '../../../types';
|
||||
import { crossfadeHandler, gaplessHandler } from './utils/listenHandlers';
|
||||
crossfadeHandler,
|
||||
gaplessHandler,
|
||||
} from '@/renderer/components/audio-player/utils/list-handlers';
|
||||
import { CrossfadeStyle, PlaybackStyle, PlayerStatus, Song } from '@/types';
|
||||
|
||||
interface AudioPlayerProps extends ReactPlayerProps {
|
||||
crossfadeDuration: number;
|
||||
@@ -125,13 +123,13 @@ export const AudioPlayer = forwardRef(
|
||||
return gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player1Ref),
|
||||
isFlac: player1?.suffix === 'flac',
|
||||
isFlac: player1?.container === 'flac',
|
||||
isTransitioning,
|
||||
nextPlayerRef: player2Ref,
|
||||
setIsTransitioning,
|
||||
});
|
||||
},
|
||||
[isTransitioning, player1?.suffix]
|
||||
[isTransitioning, player1?.container]
|
||||
);
|
||||
|
||||
const handleGapless2 = useCallback(
|
||||
@@ -139,13 +137,13 @@ export const AudioPlayer = forwardRef(
|
||||
return gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(player2Ref),
|
||||
isFlac: player2?.suffix === 'flac',
|
||||
isFlac: player2?.container === 'flac',
|
||||
isTransitioning,
|
||||
nextPlayerRef: player1Ref,
|
||||
setIsTransitioning,
|
||||
});
|
||||
},
|
||||
[isTransitioning, player2?.suffix]
|
||||
[isTransitioning, player2?.container]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -1,10 +0,0 @@
|
||||
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,114 @@
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
Button as MantineButton,
|
||||
ButtonProps as MantineButtonProps,
|
||||
createPolymorphicComponent,
|
||||
TooltipProps,
|
||||
} from '@mantine/core';
|
||||
import { Tooltip } from '@/renderer/components/tooltip';
|
||||
|
||||
interface ButtonProps extends MantineButtonProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
tooltip?: Omit<TooltipProps, 'children'>;
|
||||
}
|
||||
|
||||
interface StyledButtonProps extends ButtonProps {
|
||||
ref: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const StyledButton = styled(MantineButton)<StyledButtonProps>`
|
||||
color: ${(props) => {
|
||||
switch (props.variant) {
|
||||
case 'default':
|
||||
return 'var(--btn-default-fg)';
|
||||
case 'filled':
|
||||
return 'var(--btn-primary-fg)';
|
||||
case 'subtle':
|
||||
return 'var(--btn-subtle-fg)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}};
|
||||
background-color: ${(props) => {
|
||||
switch (props.variant) {
|
||||
case 'default':
|
||||
return 'var(--btn-default-bg)';
|
||||
case 'filled':
|
||||
return 'var(--btn-primary-bg)';
|
||||
case 'subtle':
|
||||
return 'var(--btn-subtle-bg)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => {
|
||||
switch (props.variant) {
|
||||
case 'default':
|
||||
return 'var(--btn-default-fg-hover)';
|
||||
case 'filled':
|
||||
return 'var(--btn-primary-fg-hover)';
|
||||
case 'subtle':
|
||||
return 'var(--btn-subtle-fg-hover)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}};
|
||||
background-color: ${(props) => {
|
||||
switch (props.variant) {
|
||||
case 'default':
|
||||
return 'var(--btn-default-bg-hover)';
|
||||
case 'filled':
|
||||
return 'var(--btn-primary-bg-hover)';
|
||||
case 'subtle':
|
||||
return 'var(--btn-subtle-bg-hover)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 1px var(--primary-color) solid;
|
||||
}
|
||||
|
||||
&.mantine-Button-root &:focus-visible {
|
||||
outline: --var-primary;
|
||||
}
|
||||
`;
|
||||
|
||||
const pButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, tooltip, ...props }: ButtonProps, ref) => {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip {...tooltip}>
|
||||
<StyledButton ref={ref} {...props}>
|
||||
{children}
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton ref={ref} {...props}>
|
||||
{children}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const Button = createPolymorphicComponent<'button', ButtonProps>(
|
||||
pButton
|
||||
);
|
||||
|
||||
pButton.defaultProps = {
|
||||
onClick: undefined,
|
||||
tooltip: undefined,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
|
||||
export const Draggable = ({ element, id, children, key }: any) => {
|
||||
const Element = element || 'div';
|
||||
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Element key={key} ref={setNodeRef} {...listeners} {...attributes}>
|
||||
{children}
|
||||
</Element>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
Menu as MantineMenu,
|
||||
MenuProps as MantineMenuProps,
|
||||
MenuItemProps as MantineMenuItemProps,
|
||||
MenuLabelProps as MantineMenuLabelProps,
|
||||
MenuDividerProps as MantineMenuDividerProps,
|
||||
MenuDropdownProps as MantineMenuDropdownProps,
|
||||
createPolymorphicComponent,
|
||||
} from '@mantine/core';
|
||||
|
||||
type MenuProps = MantineMenuProps;
|
||||
type MenuLabelProps = MantineMenuLabelProps;
|
||||
type MenuItemProps = MantineMenuItemProps;
|
||||
type MenuDividerProps = MantineMenuDividerProps;
|
||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
||||
|
||||
const StyledMenu = styled(MantineMenu)<MenuProps>``;
|
||||
|
||||
const StyledMenuLabel = styled(MantineMenu.Label)`
|
||||
font-family: var(--label-font-family);
|
||||
`;
|
||||
|
||||
const StyledMenuItem = styled(MantineMenu.Item)`
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9em;
|
||||
font-family: var(--label-font-family);
|
||||
background-color: var(--dropdown-menu-bg);
|
||||
|
||||
& .mantine-Menu-itemIcon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMenuDropdown = styled(MantineMenu.Dropdown)`
|
||||
background: var(--dropdown-menu-bg);
|
||||
`;
|
||||
|
||||
const StyledMenuDivider = styled(MantineMenu.Divider)`
|
||||
margin: 0.3rem 0;
|
||||
`;
|
||||
|
||||
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
||||
return <StyledMenu {...props}>{children}</StyledMenu>;
|
||||
};
|
||||
|
||||
const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
||||
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
|
||||
};
|
||||
|
||||
const pMenuItem = ({ children, ...props }: MenuItemProps) => {
|
||||
return <StyledMenuItem {...props}>{children}</StyledMenuItem>;
|
||||
};
|
||||
|
||||
const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => {
|
||||
return <StyledMenuDropdown {...props}>{children}</StyledMenuDropdown>;
|
||||
};
|
||||
|
||||
const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem);
|
||||
|
||||
const MenuDivider = ({ ...props }: MenuDividerProps) => {
|
||||
return <StyledMenuDivider {...props} />;
|
||||
};
|
||||
|
||||
DropdownMenu.Label = MenuLabel;
|
||||
DropdownMenu.Item = MenuItem;
|
||||
DropdownMenu.Target = MantineMenu.Target;
|
||||
DropdownMenu.Dropdown = MenuDropdown;
|
||||
DropdownMenu.Divider = MenuDivider;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import { ActionIcon, ActionIconProps, TooltipProps } from '@mantine/core';
|
||||
import { Tooltip } from '../tooltip/Tooltip';
|
||||
|
||||
type MantineIconButtonProps = ActionIconProps &
|
||||
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,
|
||||
};
|
||||
@@ -1,4 +1,10 @@
|
||||
export * from './tooltip/Tooltip';
|
||||
export * from './audio-player/AudioPlayer';
|
||||
export * from './icon-button/IconButton';
|
||||
export * from './text/Text';
|
||||
export * from './tooltip';
|
||||
export * from './audio-player';
|
||||
export * from './text';
|
||||
export * from './button';
|
||||
export * from './virtual-grid';
|
||||
export * from './modal';
|
||||
export * from './input';
|
||||
export * from './segmented-control';
|
||||
export * from './dropdown-menu';
|
||||
export * from './toast';
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
TextInput as MantineTextInput,
|
||||
TextInputProps as MantineTextInputProps,
|
||||
PasswordInput as MantinePasswordInput,
|
||||
PasswordInputProps as MantinePasswordInputProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
interface TextInputProps extends MantineTextInputProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface PasswordInputProps extends MantinePasswordInputProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
& .mantine-TextInput-wrapper {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
& .mantine-TextInput-input {
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
& .mantine-TextInput-required {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
& .mantine-TextInput-label {
|
||||
font-family: var(--label-font-faimly);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPasswordInput = styled(MantinePasswordInput)<PasswordInputProps>`
|
||||
& .mantine-PasswordInput-input {
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
& .mantine-PasswordInput-required {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
& .mantine-PasswordInput-label {
|
||||
font-family: var(--label-font-faimly);
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
({ children, ...props }: TextInputProps, ref) => {
|
||||
return (
|
||||
<StyledTextInput ref={ref} spellCheck={false} {...props}>
|
||||
{children}
|
||||
</StyledTextInput>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
({ children, ...props }: PasswordInputProps, ref) => {
|
||||
return (
|
||||
<StyledPasswordInput ref={ref} spellCheck={false} {...props}>
|
||||
{children}
|
||||
</StyledPasswordInput>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal as MantineModal,
|
||||
ModalProps as MantineModalProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
|
||||
children?: React.ReactNode;
|
||||
handlers: {
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const Modal = ({ children, handlers, ...rest }: ModalProps) => {
|
||||
return (
|
||||
<MantineModal
|
||||
overlayBlur={2}
|
||||
overlayOpacity={0.2}
|
||||
{...rest}
|
||||
onClose={handlers.close}
|
||||
>
|
||||
{children}
|
||||
</MantineModal>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.defaultProps = {
|
||||
children: undefined,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { forwardRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
SegmentedControl as MantineSegmentedControl,
|
||||
SegmentedControlProps as MantineSegmentedControlProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
type SegmentedControlProps = MantineSegmentedControlProps;
|
||||
|
||||
const StyledSegmentedControl = styled(
|
||||
MantineSegmentedControl
|
||||
)<MantineSegmentedControlProps>`
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&:active {
|
||||
outline: 1px var(--primary-color) solid;
|
||||
}
|
||||
|
||||
& .mantine-SegmentedControl-label {
|
||||
font-family: var(--label-font-family);
|
||||
}
|
||||
`;
|
||||
|
||||
export const SegmentedControl = forwardRef<
|
||||
HTMLDivElement,
|
||||
SegmentedControlProps
|
||||
>(({ ...props }: SegmentedControlProps, ref) => {
|
||||
return <StyledSegmentedControl ref={ref} {...props} />;
|
||||
});
|
||||
@@ -3,14 +3,13 @@ import {
|
||||
ComponentPropsWithRef,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
Text as MantineText,
|
||||
TextProps as MantineTextProps,
|
||||
} from '@mantine/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { Font } from '../../styles';
|
||||
import { textEllipsis } from '../../styles/mixins';
|
||||
import { Font, textEllipsis } from '@/renderer/styles';
|
||||
|
||||
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
|
||||
type MantineTextLinkProps = MantineTextProps & ComponentPropsWithRef<'link'>;
|
||||
@@ -42,7 +41,7 @@ const BaseText = styled(MantineText)<any>`
|
||||
? 'var(--playerbar-text-secondary-color)'
|
||||
: 'var(--playerbar-text-primary-color)'};
|
||||
font-family: ${(props) => props.font || Font.GOTHAM};
|
||||
cursor: ${(props) => (props.link ? 'cursor' : 'default')};
|
||||
/* cursor: ${(props) => (props.link ? 'cursor' : 'default')}; */
|
||||
user-select: ${(props) => (props.$noSelect ? 'none' : 'auto')};
|
||||
${(props) => props.overflow === 'hidden' && textEllipsis}
|
||||
`;
|
||||
@@ -63,7 +62,7 @@ export const Text = ({
|
||||
}: TextProps) => {
|
||||
if (link) {
|
||||
return (
|
||||
<StyledLinkText<typeof Link>
|
||||
<StyledLinkText
|
||||
$noSelect={noSelect}
|
||||
$secondary={secondary}
|
||||
component={Link}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
showNotification,
|
||||
NotificationProps as MantineNotificationProps,
|
||||
updateNotification,
|
||||
hideNotification,
|
||||
cleanNotifications,
|
||||
cleanNotificationsQueue,
|
||||
} from '@mantine/notifications';
|
||||
|
||||
interface NotificationProps extends MantineNotificationProps {
|
||||
type?: 'success' | 'error' | 'warning' | 'info';
|
||||
}
|
||||
|
||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
||||
const color =
|
||||
type === 'success'
|
||||
? 'var(--success-color)'
|
||||
: type === 'warning'
|
||||
? 'var(--warning-color)'
|
||||
: type === 'error'
|
||||
? 'var(--danger-color)'
|
||||
: 'var(--primary-color)';
|
||||
|
||||
const defaultTitle =
|
||||
type === 'success'
|
||||
? 'Success'
|
||||
: type === 'warning'
|
||||
? 'Warning'
|
||||
: type === 'error'
|
||||
? 'Error'
|
||||
: 'Info';
|
||||
|
||||
const defaultDuration = type === 'error' ? 3500 : 2000;
|
||||
|
||||
return showNotification({
|
||||
autoClose: defaultDuration,
|
||||
disallowClose: true,
|
||||
styles: () => ({
|
||||
closeButton: {},
|
||||
description: {
|
||||
color: 'var(--toast-description-fg)',
|
||||
fontSize: '.9em',
|
||||
},
|
||||
loader: {
|
||||
margin: '1rem',
|
||||
},
|
||||
root: {
|
||||
'&::before': { backgroundColor: color },
|
||||
background: 'var(--toast-bg)',
|
||||
},
|
||||
title: {
|
||||
color: 'var(--toast-title-fg)',
|
||||
fontSize: '1em',
|
||||
},
|
||||
}),
|
||||
title: defaultTitle,
|
||||
...props,
|
||||
});
|
||||
};
|
||||
|
||||
export const toast = {
|
||||
clean: cleanNotifications,
|
||||
cleanQueue: cleanNotificationsQueue,
|
||||
hide: hideNotification,
|
||||
show: showToast,
|
||||
update: updateNotification,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
.body {
|
||||
padding: 5px;
|
||||
color: var(--tooltip-text-color);
|
||||
background: var(--tooltip-bg);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
background: var(--tooltip-bg);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Tooltip as MantineTooltip, TooltipProps } from '@mantine/core';
|
||||
|
||||
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
||||
return (
|
||||
<MantineTooltip
|
||||
radius="xs"
|
||||
styles={{
|
||||
arrow: {
|
||||
background: 'var(--tooltip-bg)',
|
||||
},
|
||||
root: {
|
||||
background: 'var(--tooltip-bg)',
|
||||
color: 'var(--tooltip-text-color)',
|
||||
padding: '5px',
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</MantineTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
Tooltip.defaultProps = {
|
||||
openDelay: 0,
|
||||
position: 'top',
|
||||
transition: 'fade',
|
||||
transitionDuration: 250,
|
||||
withArrow: true,
|
||||
withinPortal: true,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Tooltip as MantineTooltip, TooltipProps } from '@mantine/core';
|
||||
|
||||
const StyledTooltip = styled(MantineTooltip)`
|
||||
& .mantine-Tooltip-arrow {
|
||||
background: var(--tooltip-bg);
|
||||
}
|
||||
|
||||
& .mantine-Tooltip-tooltip {
|
||||
margin: 20px;
|
||||
color: var(--tooltip-text-color);
|
||||
background: var(--tooltip-bg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tooltip = ({ children, ...rest }: TooltipProps) => {
|
||||
return (
|
||||
<StyledTooltip pl={10} pr={10} py={5} radius="xs" {...rest}>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
Tooltip.defaultProps = {
|
||||
openDelay: 0,
|
||||
position: 'top',
|
||||
transition: 'fade',
|
||||
transitionDuration: 250,
|
||||
withArrow: true,
|
||||
withinPortal: true,
|
||||
};
|
||||
@@ -1,171 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DragOverlay, useDndMonitor } from '@dnd-kit/core';
|
||||
import { snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { Card, Skeleton } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
import { CardRow } from '../../types';
|
||||
import { Draggable } from '../drag-drop/Draggable';
|
||||
import { Text } from '../text/Text';
|
||||
import { GridCardControls } from './GridCardControls';
|
||||
|
||||
const CardWrapper = styled(motion.div)<{
|
||||
itemGap: number;
|
||||
itemHeight: number;
|
||||
itemWidth: number;
|
||||
}>`
|
||||
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%;
|
||||
`;
|
||||
|
||||
interface ImageProps {
|
||||
height: number;
|
||||
src: string;
|
||||
}
|
||||
|
||||
const Image = styled(motion.div).attrs((props: ImageProps) => ({
|
||||
style: {
|
||||
background: `url(${props.src})`,
|
||||
},
|
||||
}))<ImageProps>`
|
||||
height: ${({ height }) => `${height}px`};
|
||||
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 }: any) => {
|
||||
const {
|
||||
itemHeight,
|
||||
itemWidth,
|
||||
columnCount,
|
||||
itemGap,
|
||||
itemCount,
|
||||
cardControls,
|
||||
handlePlayQueueAdd,
|
||||
cardRows,
|
||||
itemData,
|
||||
} = data;
|
||||
|
||||
const startIndex = index * columnCount;
|
||||
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
||||
const cards = [];
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useDndMonitor({
|
||||
onDragCancel: () => setIsDragging(false),
|
||||
onDragEnd: () => setIsDragging(false),
|
||||
onDragStart: () => setIsDragging(true),
|
||||
});
|
||||
|
||||
for (let i = startIndex; i <= stopIndex; i += 1) {
|
||||
cards.push(
|
||||
<React.Fragment key={`card-${i}-${index}`}>
|
||||
<Draggable id={`${i}-${index}`}>
|
||||
<CardWrapper
|
||||
itemGap={itemGap}
|
||||
itemHeight={itemHeight}
|
||||
itemWidth={itemWidth}
|
||||
>
|
||||
<Skeleton visible={!itemData[i]}>
|
||||
<StyledCard>
|
||||
<ImageSection>
|
||||
<Image height={itemWidth} src={itemData[i]?.image}>
|
||||
<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>
|
||||
</Skeleton>
|
||||
</CardWrapper>
|
||||
</Draggable>
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={null} modifiers={[snapCenterToCursor]}>
|
||||
{isDragging ? <div>OVERLAY</div> : null}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
{cards}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Button, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { Play } from '../../../types';
|
||||
|
||||
type PlayButtonType = UnstyledButtonProps &
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Skeleton } from '@mantine/core';
|
||||
import { motion } from 'framer-motion';
|
||||
import { CardRow } from '../../types';
|
||||
import { Text } from '../text';
|
||||
import { GridCardControls } from './grid-card-controls';
|
||||
|
||||
const CardWrapper = styled(motion.div)<{
|
||||
itemGap: number;
|
||||
itemHeight: number;
|
||||
itemWidth: number;
|
||||
}>`
|
||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
||||
height: ${({ itemHeight }) => `${itemHeight}px`};
|
||||
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
||||
filter: drop-shadow(0 4px 4px #000);
|
||||
transition: border 0.2s ease-in-out;
|
||||
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.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
background: var(--grid-card-bg);
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const ImageSection = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
interface ImageProps {
|
||||
height: number;
|
||||
src: string;
|
||||
}
|
||||
|
||||
// const Image = styled(motion.div).attrs((props: ImageProps) => ({
|
||||
// style: {
|
||||
// background: `url(${props.src})`,
|
||||
// backgroundPosition: 'center',
|
||||
// backgroundSize: 'cover',
|
||||
// },
|
||||
// }))<ImageProps>`
|
||||
// height: ${({ height }) => `${height}px`};
|
||||
// background-position: center;
|
||||
// background-size: cover;
|
||||
// border: 0;
|
||||
// `;
|
||||
|
||||
const Image = styled(motion.div)<ImageProps>`
|
||||
height: ${({ height }) => `${height}px`};
|
||||
background: ${({ src }) => `url(${src})`};
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const ControlsContainer = styled.div`
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
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 }: any) => {
|
||||
const {
|
||||
itemHeight,
|
||||
itemWidth,
|
||||
columnCount,
|
||||
itemGap,
|
||||
itemCount,
|
||||
cardControls,
|
||||
handlePlayQueueAdd,
|
||||
cardRows,
|
||||
itemData,
|
||||
} = 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(
|
||||
<React.Fragment key={`card-${i}-${index}`}>
|
||||
<CardWrapper
|
||||
itemGap={itemGap}
|
||||
itemHeight={itemHeight}
|
||||
itemWidth={itemWidth}
|
||||
>
|
||||
<Skeleton visible={!itemData[i]}>
|
||||
<StyledCard>
|
||||
<ImageSection>
|
||||
<Image height={itemWidth} src={itemData[i]?.imageUrl}>
|
||||
<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>
|
||||
</Skeleton>
|
||||
</CardWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
>
|
||||
{cards}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './virtual-grid-wrapper';
|
||||
export * from './virtual-infinite-grid';
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { Ref, useMemo } from 'react';
|
||||
import { FixedSizeList, FixedSizeListProps } from 'react-window';
|
||||
import { usePlayQueueHandler } from '../../features/player/hooks/usePlayQueueHandler';
|
||||
import { CardRow } from '../../types';
|
||||
import { GridCard } from './GridCard';
|
||||
import { GridCard } from '@/renderer/components/virtual-grid/grid-card';
|
||||
import { usePlayQueueHandler } from '@/renderer/features/player/hooks/usePlayQueueHandler';
|
||||
import { CardRow } from '@/renderer/types';
|
||||
|
||||
export const VirtualGridWrapper = ({
|
||||
refInstance,
|
||||
+14
-9
@@ -2,8 +2,8 @@ import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { FixedSizeListProps } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import { CardRow } from '../../types';
|
||||
import { VirtualGridWrapper } from './VirtualGridWrapper';
|
||||
import { VirtualGridWrapper } from '@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||
import { CardRow } from '@/renderer/types';
|
||||
|
||||
interface VirtualGridProps
|
||||
extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
||||
@@ -12,8 +12,9 @@ interface VirtualGridProps
|
||||
itemGap?: number;
|
||||
itemSize: number;
|
||||
minimumBatchSize?: number;
|
||||
query: (props: any) => Promise<any>;
|
||||
query: ({ serverId }: { serverId: string }, props: any) => Promise<any>;
|
||||
queryParams?: Record<string, any>;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const VirtualInfiniteGrid = ({
|
||||
@@ -26,6 +27,7 @@ export const VirtualInfiniteGrid = ({
|
||||
query,
|
||||
queryParams,
|
||||
height,
|
||||
serverId,
|
||||
width,
|
||||
}: VirtualGridProps) => {
|
||||
const [itemData, setItemData] = useState<any[]>([]);
|
||||
@@ -58,11 +60,14 @@ export const VirtualInfiniteGrid = ({
|
||||
const start = startIndex * columnCount;
|
||||
const end = stopIndex * columnCount + columnCount;
|
||||
|
||||
const t = await query({
|
||||
skip: start,
|
||||
take: end - start,
|
||||
...queryParams,
|
||||
});
|
||||
const t = await query(
|
||||
{ serverId },
|
||||
{
|
||||
skip: start,
|
||||
take: end - start,
|
||||
...queryParams,
|
||||
}
|
||||
);
|
||||
|
||||
const newData: any[] = [...itemData];
|
||||
|
||||
@@ -109,7 +114,7 @@ export const VirtualInfiniteGrid = ({
|
||||
itemCount={itemCount || 0}
|
||||
itemData={itemData}
|
||||
itemGap={itemGap!}
|
||||
itemHeight={itemHeight!}
|
||||
itemHeight={itemHeight! + itemGap! / 2}
|
||||
itemWidth={itemSize}
|
||||
refInstance={(list) => {
|
||||
infiniteLoaderRef(list);
|
||||
Reference in New Issue
Block a user