mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-05 04:01:39 +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
|
|
@ -1,9 +1,9 @@
|
|||
.animated-page {
|
||||
container-type: inline-size;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import type { ReactNode, Ref } from 'react';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { motion } from 'motion/react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import styles from './animated-page.module.scss';
|
||||
import styles from './animated-page.module.css';
|
||||
|
||||
interface AnimatedPageProps {
|
||||
children: ReactNode;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
.filter-bar {
|
||||
z-index: 1;
|
||||
padding: var(--theme-spacing-md) var(--theme-spacing-sm);
|
||||
box-shadow: 0 5px 15px rgb(0 0 0 / 65%);
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import { PaperProps } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
import styles from './filter-bar.module.css';
|
||||
|
||||
import { Paper } from '/@/renderer/components';
|
||||
|
||||
const StyledFilterBar = styled(Paper)`
|
||||
z-index: 1;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 5px 15px rgb(0 0 0 / 65%);
|
||||
`;
|
||||
|
||||
export const FilterBar = ({ children, ...props }: PaperProps) => {
|
||||
return <StyledFilterBar {...props}>{children}</StyledFilterBar>;
|
||||
export const FilterBar = ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.filterBar}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
29
src/renderer/features/shared/components/filter-button.tsx
Normal file
29
src/renderer/features/shared/components/filter-button.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
interface FilterButtonProps extends ActionIconProps {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export const FilterButton = ({ isActive, onClick, ...props }: FilterButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon="filter"
|
||||
iconProps={{
|
||||
fill: isActive ? 'primary' : undefined,
|
||||
size: 'lg',
|
||||
...props.iconProps,
|
||||
}}
|
||||
onClick={onClick}
|
||||
tooltip={{
|
||||
label: t('common.filters', { count: 2, postProcess: 'sentenceCase' }),
|
||||
...props.tooltip,
|
||||
}}
|
||||
variant="subtle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
28
src/renderer/features/shared/components/folder-button.tsx
Normal file
28
src/renderer/features/shared/components/folder-button.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
interface FolderButtonProps extends ActionIconProps {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export const FolderButton = ({ isActive, ...props }: FolderButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon="folder"
|
||||
iconProps={{
|
||||
fill: isActive ? 'primary' : undefined,
|
||||
size: 'lg',
|
||||
...props.iconProps,
|
||||
}}
|
||||
tooltip={{
|
||||
label: t('entity.folder', { postProcess: 'sentenceCase' }),
|
||||
...props.tooltip,
|
||||
}}
|
||||
variant="subtle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--placeholder-bg);
|
||||
border-radius: var(--card-default-radius);
|
||||
|
||||
svg {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
color: var(--placeholder-fg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { Center } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
||||
|
||||
import styles from './item-image-placeholder.module.css';
|
||||
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
interface ItemImagePlaceholderProps {
|
||||
itemType?: LibraryItem;
|
||||
}
|
||||
|
||||
const Image = memo(function Image(props: ItemImagePlaceholderProps) {
|
||||
switch (props.itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return <RiAlbumFill />;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return <RiUserVoiceFill />;
|
||||
case LibraryItem.ARTIST:
|
||||
return <RiUserVoiceFill />;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return <RiPlayListFill />;
|
||||
default:
|
||||
return <RiAlbumFill />;
|
||||
}
|
||||
});
|
||||
|
||||
export const ItemImagePlaceholder = ({ itemType }: ItemImagePlaceholderProps) => {
|
||||
return (
|
||||
<Center className={clsx(styles.imagePlaceholder, 'item-image-placeholder')}>
|
||||
<Image itemType={itemType} />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.root {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 20vh;
|
||||
min-height: 200px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background-image: var(--theme-overlay-subheader);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import styled from 'styled-components';
|
||||
import styles from './library-background-overlay.module.css';
|
||||
|
||||
export const LibraryBackgroundOverlay = styled.div<{ $backgroundColor?: string }>`
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 20vh;
|
||||
min-height: 200px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
background: ${(props) => props.$backgroundColor};
|
||||
background-image: var(--bg-subheader-overlay);
|
||||
opacity: 0.3;
|
||||
`;
|
||||
interface LibraryBackgroundOverlayProps {
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export const LibraryBackgroundOverlay = ({ backgroundColor }: LibraryBackgroundOverlayProps) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
style={{ backgroundColor }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -1,67 +1,48 @@
|
|||
import { Box } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Paper, PaperProps, SpinnerIcon, TextTitle } from '/@/renderer/components';
|
||||
import { PlayButton as PlayBtn } from '/@/renderer/features/shared/components/play-button';
|
||||
import styles from './library-header-bar.module.css';
|
||||
|
||||
import { PlayButton, PlayButtonProps } from '/@/renderer/features/shared/components/play-button';
|
||||
import { Badge, BadgeProps } from '/@/shared/components/badge/badge';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
|
||||
interface LibraryHeaderBarProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
`;
|
||||
|
||||
export const LibraryHeaderBar = ({ children }: LibraryHeaderBarProps) => {
|
||||
return <HeaderContainer>{children}</HeaderContainer>;
|
||||
return <div className={styles.headerContainer}>{children}</div>;
|
||||
};
|
||||
|
||||
interface TitleProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const HeaderPlayButton = ({ className, ...props }: PlayButtonProps) => {
|
||||
return (
|
||||
<div className={styles.playButtonContainer}>
|
||||
<PlayButton
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Title = ({ children }: TitleProps) => {
|
||||
return (
|
||||
<TextTitle
|
||||
fw={700}
|
||||
order={1}
|
||||
overflow="hidden"
|
||||
weight={700}
|
||||
>
|
||||
{children}
|
||||
</TextTitle>
|
||||
);
|
||||
};
|
||||
|
||||
interface PlayButtonProps {
|
||||
onClick: (args: any) => void;
|
||||
}
|
||||
|
||||
const PlayButton = ({ onClick }: PlayButtonProps) => {
|
||||
return (
|
||||
<Box>
|
||||
<PlayBtn
|
||||
h="45px"
|
||||
onClick={onClick}
|
||||
w="45px"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Badge = styled(Paper)`
|
||||
padding: 0.3rem 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.3rem;
|
||||
`;
|
||||
|
||||
interface HeaderBadgeProps extends PaperProps {
|
||||
interface HeaderBadgeProps extends BadgeProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -70,5 +51,5 @@ const HeaderBadge = ({ children, isLoading, ...props }: HeaderBadgeProps) => {
|
|||
};
|
||||
|
||||
LibraryHeaderBar.Title = Title;
|
||||
LibraryHeaderBar.PlayButton = PlayButton;
|
||||
LibraryHeaderBar.PlayButton = HeaderPlayButton;
|
||||
LibraryHeaderBar.Badge = HeaderBadge;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
grid-template-rows: 100%;
|
||||
grid-template-columns: 175px minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 30vh;
|
||||
|
|
@ -79,7 +80,8 @@
|
|||
@container (min-width: 1200px) {
|
||||
grid-template-columns: 250px minmax(0, 1fr);
|
||||
|
||||
.image {
|
||||
.image,
|
||||
.image-section {
|
||||
width: 250px !important;
|
||||
height: 250px;
|
||||
}
|
||||
|
|
@ -98,12 +100,11 @@
|
|||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 0 8px rgb(0, 0, 0, 50%));
|
||||
filter: drop-shadow(0 0 8px rgb(0 0 0 / 50%));
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
z-index: 15;
|
||||
font-size: 1.15rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-area: info;
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
}
|
||||
|
||||
.image {
|
||||
object-fit: var(--image-fit);
|
||||
object-fit: var(--theme-image-fit);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
|
@ -122,9 +123,9 @@
|
|||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.9;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
background-size: cover !important;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
|
|
@ -134,7 +135,7 @@
|
|||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-header-overlay);
|
||||
background: var(--theme-overlay-header);
|
||||
}
|
||||
|
||||
.opaque-overlay {
|
||||
|
|
@ -142,12 +143,14 @@
|
|||
}
|
||||
|
||||
.title {
|
||||
display: -webkit-box;
|
||||
display: flex;
|
||||
align-items: center !important;
|
||||
margin: var(--theme-spacing-sm) 0;
|
||||
overflow: hidden;
|
||||
color: var(--main-fg);
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
color: var(--theme-colors-foreground);
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Center, Group } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { AutoTextSize } from 'auto-text-size';
|
||||
import clsx from 'clsx';
|
||||
|
|
@ -8,9 +7,10 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import styles from './library-header.module.css';
|
||||
|
||||
import { Text } from '/@/renderer/components';
|
||||
import { ItemImagePlaceholder } from '/@/renderer/features/shared/components/item-image-placeholder';
|
||||
import { useGeneralSettings } from '/@/renderer/store';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
|
|
@ -107,37 +107,34 @@ export const LibraryHeader = forwardRef(
|
|||
style={{ cursor: 'pointer' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{!loading &&
|
||||
(imageUrl && !isImageError ? (
|
||||
<img
|
||||
alt="cover"
|
||||
className={styles.image}
|
||||
onError={onImageError}
|
||||
// placeholder={imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
||||
src={imageUrl}
|
||||
style={{ height: '' }}
|
||||
/>
|
||||
) : (
|
||||
<ItemImagePlaceholder itemType={item.type} />
|
||||
))}
|
||||
{!loading && imageUrl && !isImageError && (
|
||||
<Image
|
||||
alt="cover"
|
||||
className={styles.image}
|
||||
onError={onImageError}
|
||||
src={imageUrl || ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{title && (
|
||||
<div className={styles.metadataSection}>
|
||||
<Group>
|
||||
<h2>
|
||||
<Text
|
||||
$link
|
||||
component={Link}
|
||||
to={item.route}
|
||||
tt="uppercase"
|
||||
weight={600}
|
||||
>
|
||||
{itemTypeString()}
|
||||
</Text>
|
||||
</h2>
|
||||
</Group>
|
||||
<Text
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
size="md"
|
||||
to={item.route}
|
||||
tt="uppercase"
|
||||
>
|
||||
{itemTypeString()}
|
||||
</Text>
|
||||
<h1 className={styles.title}>
|
||||
<AutoTextSize mode="box">{title}</AutoTextSize>
|
||||
<AutoTextSize
|
||||
maxFontSizePx={80}
|
||||
mode="box"
|
||||
>
|
||||
{title}
|
||||
</AutoTextSize>
|
||||
</h1>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
271
src/renderer/features/shared/components/list-config-menu.tsx
Normal file
271
src/renderer/features/shared/components/list-config-menu.tsx
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { SettingsButton } from '/@/renderer/features/shared/components/settings-button';
|
||||
import { CheckboxSelect } from '/@/shared/components/checkbox-select/checkbox-select';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Table } from '/@/shared/components/table/table';
|
||||
import { ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const DISPLAY_TYPES = [
|
||||
{
|
||||
label: (
|
||||
<Stack
|
||||
align="center"
|
||||
p="sm"
|
||||
>
|
||||
<Icon
|
||||
icon="layoutTable"
|
||||
size="lg"
|
||||
/>
|
||||
{i18n.t('table.config.view.table', { postProcess: 'sentenceCase' }) as string}
|
||||
</Stack>
|
||||
),
|
||||
value: ListDisplayType.TABLE,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack
|
||||
align="center"
|
||||
p="sm"
|
||||
>
|
||||
<Icon
|
||||
icon="layoutGrid"
|
||||
size="lg"
|
||||
/>
|
||||
{i18n.t('table.config.view.card', { postProcess: 'sentenceCase' }) as string}
|
||||
</Stack>
|
||||
),
|
||||
value: ListDisplayType.GRID,
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
label: (
|
||||
<Stack
|
||||
align="center"
|
||||
p="sm"
|
||||
>
|
||||
<Icon
|
||||
icon="layoutList"
|
||||
size="lg"
|
||||
/>
|
||||
{i18n.t('table.config.view.list', { postProcess: 'sentenceCase' }) as string}
|
||||
</Stack>
|
||||
),
|
||||
value: ListDisplayType.LIST,
|
||||
},
|
||||
];
|
||||
|
||||
interface ListConfigMenuProps {
|
||||
autoFitColumns?: boolean;
|
||||
disabledViewTypes?: ListDisplayType[];
|
||||
displayType: ListDisplayType;
|
||||
itemGap?: number;
|
||||
itemSize?: number;
|
||||
onChangeAutoFitColumns?: (autoFitColumns: boolean) => void;
|
||||
onChangeDisplayType?: (displayType: ListDisplayType) => void;
|
||||
onChangeItemGap?: (itemGap: number) => void;
|
||||
onChangeItemSize?: (itemSize: number) => void;
|
||||
onChangeTableColumns?: (tableColumns: string[]) => void;
|
||||
tableColumns?: string[];
|
||||
tableColumnsData?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
export const ListConfigMenu = (props: ListConfigMenuProps) => {
|
||||
return (
|
||||
<Popover
|
||||
position="bottom-end"
|
||||
width={300}
|
||||
>
|
||||
<Popover.Target>
|
||||
<SettingsButton />
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p="md">
|
||||
<Stack>
|
||||
<SegmentedControl
|
||||
data={DISPLAY_TYPES.map((type) => ({
|
||||
...type,
|
||||
disabled: props.disabledViewTypes?.includes(type.value),
|
||||
}))}
|
||||
onChange={(value) => props.onChangeDisplayType?.(value as ListDisplayType)}
|
||||
value={props.displayType}
|
||||
w="100%"
|
||||
withItemsBorders={false}
|
||||
/>
|
||||
<Config {...props} />
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const Config = (props: ListConfigMenuProps) => {
|
||||
switch (props.displayType) {
|
||||
case ListDisplayType.GRID:
|
||||
return <GridConfig {...props} />;
|
||||
|
||||
case ListDisplayType.TABLE:
|
||||
return <TableConfig {...props} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
type TableConfigProps = Pick<
|
||||
ListConfigMenuProps,
|
||||
| 'autoFitColumns'
|
||||
| 'itemSize'
|
||||
| 'onChangeAutoFitColumns'
|
||||
| 'onChangeItemSize'
|
||||
| 'onChangeTableColumns'
|
||||
| 'tableColumns'
|
||||
| 'tableColumnsData'
|
||||
>;
|
||||
|
||||
const TableConfig = ({
|
||||
autoFitColumns,
|
||||
itemSize,
|
||||
onChangeAutoFitColumns,
|
||||
onChangeItemSize,
|
||||
onChangeTableColumns,
|
||||
tableColumns,
|
||||
tableColumnsData,
|
||||
}: TableConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (
|
||||
!tableColumnsData ||
|
||||
!onChangeTableColumns ||
|
||||
!tableColumns ||
|
||||
!onChangeItemSize ||
|
||||
autoFitColumns === undefined ||
|
||||
!onChangeAutoFitColumns ||
|
||||
itemSize === undefined
|
||||
) {
|
||||
console.error('TableConfig: Missing required props', {
|
||||
itemSize,
|
||||
onChangeItemSize,
|
||||
onChangeTableColumns,
|
||||
tableColumns,
|
||||
tableColumnsData,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
variant="vertical"
|
||||
withColumnBorders
|
||||
withRowBorders
|
||||
withTableBorder
|
||||
>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Th>
|
||||
{t('table.config.general.size', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Table.Th>
|
||||
<Table.Td>
|
||||
<Slider
|
||||
defaultValue={itemSize}
|
||||
max={100}
|
||||
min={30}
|
||||
onChangeEnd={onChangeItemSize}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th w="50%">
|
||||
{t('table.config.general.autoFitColumns', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Table.Th>
|
||||
<Table.Td style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Switch
|
||||
defaultChecked={autoFitColumns}
|
||||
onChange={(e) => onChangeAutoFitColumns?.(e.target.checked)}
|
||||
size="xs"
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<ScrollArea
|
||||
allowDragScroll
|
||||
style={{ maxHeight: '200px' }}
|
||||
>
|
||||
<CheckboxSelect
|
||||
data={tableColumnsData}
|
||||
onChange={onChangeTableColumns}
|
||||
value={tableColumns}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type GridConfigProps = Pick<
|
||||
ListConfigMenuProps,
|
||||
'itemGap' | 'itemSize' | 'onChangeItemGap' | 'onChangeItemSize'
|
||||
>;
|
||||
|
||||
const GridConfig = ({ itemSize, onChangeItemGap, onChangeItemSize }: GridConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!onChangeItemGap || !onChangeItemSize || !itemSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
variant="vertical"
|
||||
withColumnBorders
|
||||
withRowBorders
|
||||
withTableBorder
|
||||
>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Th w="50%">
|
||||
{t('table.config.general.gap', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Table.Th>
|
||||
<Table.Td>
|
||||
<Slider
|
||||
defaultValue={itemSize}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={onChangeItemGap}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th w="50%">
|
||||
{t('table.config.general.size', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Table.Th>
|
||||
<Table.Td>
|
||||
<Slider
|
||||
defaultValue={itemSize}
|
||||
max={300}
|
||||
min={135}
|
||||
onChangeEnd={onChangeItemSize}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
src/renderer/features/shared/components/more-button.tsx
Normal file
17
src/renderer/features/shared/components/more-button.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
interface MoreButtonProps extends ActionIconProps {}
|
||||
|
||||
export const MoreButton = ({ ...props }: MoreButtonProps) => {
|
||||
return (
|
||||
<ActionIcon
|
||||
icon="ellipsisHorizontal"
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
...props.iconProps,
|
||||
}}
|
||||
variant="subtle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,42 +1,32 @@
|
|||
import { ButtonProps } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiSortAsc, RiSortDesc } from 'react-icons/ri';
|
||||
|
||||
import { Button, Tooltip } from '/@/renderer/components';
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
import { SortOrder } from '/@/shared/types/domain-types';
|
||||
|
||||
interface OrderToggleButtonProps {
|
||||
buttonProps?: Partial<ButtonProps>;
|
||||
buttonProps?: Partial<ActionIconProps>;
|
||||
onToggle: () => void;
|
||||
sortOrder: SortOrder;
|
||||
}
|
||||
|
||||
export const OrderToggleButton = ({ buttonProps, onToggle, sortOrder }: OrderToggleButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
sortOrder === SortOrder.ASC
|
||||
? t('common.ascending', { postProcess: 'sentenceCase' })
|
||||
: t('common.descending', { postProcess: 'sentenceCase' })
|
||||
}
|
||||
>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
onClick={onToggle}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
{...buttonProps}
|
||||
>
|
||||
<>
|
||||
{sortOrder === SortOrder.ASC ? (
|
||||
<RiSortAsc size="1.3rem" />
|
||||
) : (
|
||||
<RiSortDesc size="1.3rem" />
|
||||
)}
|
||||
</>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
icon={sortOrder === SortOrder.ASC ? 'sortAsc' : 'sortDesc'}
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={onToggle}
|
||||
tooltip={{
|
||||
label:
|
||||
sortOrder === SortOrder.ASC
|
||||
? t('common.ascending', { postProcess: 'sentenceCase' })
|
||||
: t('common.descending', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="subtle"
|
||||
{...buttonProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
.button {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +1,24 @@
|
|||
import { UnstyledButton } from '@mantine/core';
|
||||
import { RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const MotionButton = styled(UnstyledButton)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--btn-filled-bg);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
import styles from './play-button.module.css';
|
||||
|
||||
svg {
|
||||
fill: var(--btn-filled-fg);
|
||||
}
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background: var(--btn-filled-bg);
|
||||
transform: scale(1.1);
|
||||
export interface PlayButtonProps extends ActionIconProps {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--btn-filled-fg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
`;
|
||||
|
||||
export const PlayButton = ({ ...props }: any) => {
|
||||
export const PlayButton = ({ className, ...props }: PlayButtonProps) => {
|
||||
return (
|
||||
<MotionButton
|
||||
<ActionIcon
|
||||
className={clsx(styles.button, className)}
|
||||
icon="mediaPlay"
|
||||
iconProps={{
|
||||
fill: 'default',
|
||||
size: 'lg',
|
||||
}}
|
||||
variant="filled"
|
||||
{...props}
|
||||
h="45px"
|
||||
w="45px"
|
||||
>
|
||||
<RiPlayFill size={20} />
|
||||
</MotionButton>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
26
src/renderer/features/shared/components/refresh-button.tsx
Normal file
26
src/renderer/features/shared/components/refresh-button.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
interface RefreshButtonProps extends ActionIconProps {}
|
||||
|
||||
export const RefreshButton = ({ onClick, ...props }: RefreshButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
...props.iconProps,
|
||||
}}
|
||||
onClick={onClick}
|
||||
tooltip={{
|
||||
label: t('common.refresh', { postProcess: 'sentenceCase' }),
|
||||
...props.tooltip,
|
||||
}}
|
||||
variant="subtle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
.handle {
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
background-color: var(--theme-colors-border);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.handle-top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.handle-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.handle-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.handle-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.handle.resizing {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
@ -1,33 +1,25 @@
|
|||
import styled from 'styled-components';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, HTMLAttributes } from 'react';
|
||||
|
||||
export const ResizeHandle = styled.div<{
|
||||
$isResizing: boolean;
|
||||
$placement: 'bottom' | 'left' | 'right' | 'top';
|
||||
}>`
|
||||
position: absolute;
|
||||
top: ${(props) => props.$placement === 'top' && 0};
|
||||
right: ${(props) => props.$placement === 'right' && 0};
|
||||
bottom: ${(props) => props.$placement === 'bottom' && 0};
|
||||
left: ${(props) => props.$placement === 'left' && 0};
|
||||
z-index: 90;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
opacity: ${(props) => (props.$isResizing ? 1 : 0)};
|
||||
import styles from './resize-handle.module.css';
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
interface ResizeHandleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
isResizing: boolean;
|
||||
placement: 'bottom' | 'left' | 'right' | 'top';
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: ${(props) => props.$placement === 'top' && 0};
|
||||
right: ${(props) => props.$placement === 'right' && 0};
|
||||
bottom: ${(props) => props.$placement === 'bottom' && 0};
|
||||
left: ${(props) => props.$placement === 'left' && 0};
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background-color: var(--sidebar-handle-bg);
|
||||
}
|
||||
`;
|
||||
export const ResizeHandle = forwardRef<HTMLDivElement, ResizeHandleProps>(
|
||||
({ isResizing, placement, ...props }: ResizeHandleProps, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
[styles.handle]: true,
|
||||
[styles.resizing]: isResizing,
|
||||
[styles[`handle-${placement}`]]: true,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
58
src/renderer/features/shared/components/search-input.tsx
Normal file
58
src/renderer/features/shared/components/search-input.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { ChangeEvent, KeyboardEvent, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { TextInput, TextInputProps } from '/@/shared/components/text-input/text-input';
|
||||
|
||||
interface SearchInputProps extends TextInputProps {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const SearchInput = ({ onChange, ...props }: SearchInputProps) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
|
||||
|
||||
useHotkeys([[binding.hotkey, () => ref?.current?.select()]]);
|
||||
|
||||
const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.code === 'Escape') {
|
||||
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
||||
if (ref.current) {
|
||||
ref.current.value = '';
|
||||
ref.current.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
if (ref.current) {
|
||||
ref.current.value = '';
|
||||
ref.current.focus();
|
||||
onChange?.({ target: { value: '' } } as ChangeEvent<HTMLInputElement>);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
leftSection={<Icon icon="search" />}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleEscape}
|
||||
ref={ref}
|
||||
size="sm"
|
||||
width={200}
|
||||
{...props}
|
||||
rightSection={
|
||||
ref.current?.value ? (
|
||||
<ActionIcon
|
||||
icon="x"
|
||||
onClick={handleClear}
|
||||
variant="transparent"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
25
src/renderer/features/shared/components/settings-button.tsx
Normal file
25
src/renderer/features/shared/components/settings-button.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
interface SettingsButtonProps extends ActionIconProps {}
|
||||
|
||||
export const SettingsButton = ({ ...props }: SettingsButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon="settings"
|
||||
iconProps={{
|
||||
size: 'lg',
|
||||
...props.iconProps,
|
||||
}}
|
||||
tooltip={{
|
||||
label: t('common.configure', { postProcess: 'sentenceCase' }),
|
||||
...props.tooltip,
|
||||
}}
|
||||
variant="subtle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue