feat(ui): improved model list styling

This commit is contained in:
psychedelicious 2024-03-07 15:14:38 +11:00
parent b0add805c5
commit 2f0a653a7f
6 changed files with 131 additions and 58 deletions

View File

@ -0,0 +1,18 @@
import { Badge } from '@invoke-ai/ui-library';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
import { memo } from 'react';
import type { BaseModelType } from 'services/api/types';
type Props = {
base: BaseModelType;
};
const ModelBaseBadge = ({ base }: Props) => {
return (
<Badge flexGrow={0} colorScheme="invokeBlue" variant="subtle">
{MODEL_TYPE_SHORT_MAP[base]}
</Badge>
);
};
export default memo(ModelBaseBadge);

View File

@ -0,0 +1,26 @@
import { Badge } from '@invoke-ai/ui-library';
import { memo } from 'react';
import type { AnyModelConfig } from 'services/api/types';
type Props = {
format: AnyModelConfig['format'];
};
const MODEL_FORMAT_NAME_MAP = {
diffusers: 'diffusers',
lycoris: 'lycoris',
checkpoint: 'checkpoint',
invokeai: 'internal',
embedding_file: 'embedding',
embedding_folder: 'embedding',
};
const ModelFormatBadge = ({ format }: Props) => {
return (
<Badge flexGrow={0} colorScheme="base" variant="subtle">
{MODEL_FORMAT_NAME_MAP[format]}
</Badge>
);
};
export default memo(ModelFormatBadge);

View File

@ -1,13 +1,28 @@
import { Box, Image } from '@invoke-ai/ui-library';
import { Flex, Icon, Image } from '@invoke-ai/ui-library';
import { typedMemo } from 'common/util/typedMemo';
import { PiImage } from 'react-icons/pi';
type Props = {
image_url?: string;
image_url?: string | null;
};
export const MODEL_IMAGE_THUMBNAIL_SIZE = '40px';
const FALLBACK_ICON_SIZE = '24px';
const ModelImage = ({ image_url }: Props) => {
if (!image_url) {
return <Box height="50px" minWidth="50px" />;
return (
<Flex
height={MODEL_IMAGE_THUMBNAIL_SIZE}
minWidth={MODEL_IMAGE_THUMBNAIL_SIZE}
bg="base.650"
borderRadius="base"
alignItems="center"
justifyContent="center"
>
<Icon color="base.500" as={PiImage} boxSize={FALLBACK_ICON_SIZE} />
</Flex>
);
}
return (
@ -15,10 +30,10 @@ const ModelImage = ({ image_url }: Props) => {
src={image_url}
objectFit="cover"
objectPosition="50% 50%"
height="50px"
width="50px"
minHeight="50px"
minWidth="50px"
height={MODEL_IMAGE_THUMBNAIL_SIZE}
width={MODEL_IMAGE_THUMBNAIL_SIZE}
minHeight={MODEL_IMAGE_THUMBNAIL_SIZE}
minWidth={MODEL_IMAGE_THUMBNAIL_SIZE}
borderRadius="base"
/>
);

View File

@ -1,33 +1,29 @@
import {
Badge,
Box,
Button,
ConfirmationAlertDialog,
Flex,
Icon,
IconButton,
Text,
Tooltip,
useDisclosure,
} from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { IoWarning } from 'react-icons/io5';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteModelsMutation } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
import ModelImage from './ModelImage';
import ModelImage, { MODEL_IMAGE_THUMBNAIL_SIZE } from './ModelImage';
type ModelListItemProps = {
model: AnyModelConfig;
};
const sx: SystemStyleObject = {
_hover: { bg: 'base.700' },
"&[aria-selected='true']": { bg: 'base.700' },
};
const ModelListItem = (props: ModelListItemProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@ -41,6 +37,14 @@ const ModelListItem = (props: ModelListItemProps) => {
dispatch(setSelectedModelKey(model.key));
}, [model.key, dispatch]);
const onClickDeleteButton = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onOpen();
},
[onOpen]
);
const isSelected = useMemo(() => {
return selectedModelKey === model.key;
}, [selectedModelKey, model.key]);
@ -74,41 +78,47 @@ const ModelListItem = (props: ModelListItemProps) => {
}, [deleteModel, model, dispatch, t]);
return (
<Flex gap={2} alignItems="center" w="full">
<ModelImage image_url={model.cover_image || ''} />
<Flex
as={Button}
isChecked={isSelected}
variant={isSelected ? 'solid' : 'ghost'}
justifyContent="start"
p={2}
borderRadius="base"
w="full"
alignItems="center"
onClick={handleSelectModel}
>
<Flex gap={4} alignItems="center">
<Badge minWidth={14} p={0.5} fontSize="sm" variant="solid">
{MODEL_TYPE_SHORT_MAP[model.base as keyof typeof MODEL_TYPE_SHORT_MAP]}
</Badge>
<Tooltip label={model.description} placement="bottom">
<Text>{model.name}</Text>
</Tooltip>
{model.format === 'checkpoint' && (
<Tooltip label="Checkpoint">
<Box>
<Icon as={IoWarning} />
</Box>
</Tooltip>
)}
<Flex
sx={sx}
aria-selected={isSelected}
justifyContent="flex-start"
p={2}
borderRadius="base"
w="full"
alignItems="center"
gap={2}
cursor="pointer"
onClick={handleSelectModel}
>
<Flex gap={2} w="full" h="full">
<ModelImage image_url={model.cover_image} />
<Flex gap={1} alignItems="flex-start" flexDir="column" w="full">
<Flex gap={2} w="full" alignItems="flex-start">
<Text fontWeight="semibold">{model.name}</Text>
<Spacer />
</Flex>
<Text variant="subtext" noOfLines={1}>
{model.description || 'No Description'}
</Text>
</Flex>
<Flex
h={MODEL_IMAGE_THUMBNAIL_SIZE}
flexDir="column"
alignItems="flex-end"
justifyContent="space-between"
gap={2}
>
<ModelBaseBadge base={model.base} />
<ModelFormatBadge format={model.format} />
</Flex>
</Flex>
<IconButton
onClick={onOpen}
icon={<PiTrashSimpleBold />}
onClick={onClickDeleteButton}
icon={<PiTrashSimpleBold size={16} />}
aria-label={t('modelManager.deleteConfig')}
colorScheme="error"
h={MODEL_IMAGE_THUMBNAIL_SIZE}
w={MODEL_IMAGE_THUMBNAIL_SIZE}
/>
<ConfirmationAlertDialog
isOpen={isOpen}

View File

@ -11,7 +11,7 @@ type ModelListWrapperProps = {
export const ModelListWrapper = (props: ModelListWrapperProps) => {
const { title, modelList } = props;
return (
<StickyScrollable title={title}>
<StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}>
{modelList.map((model) => (
<ModelListItem key={model.key} model={model} />
))}

View File

@ -1,14 +1,16 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Heading } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type StickyScrollableHeadingProps = {
title: string;
sx?: SystemStyleObject;
};
const StickyScrollableHeading = memo((props: StickyScrollableHeadingProps) => {
return (
<Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800">
<Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800" sx={props.sx}>
<Heading size="sm">{props.title}</Heading>
</Flex>
);
@ -16,11 +18,11 @@ const StickyScrollableHeading = memo((props: StickyScrollableHeadingProps) => {
StickyScrollableHeading.displayName = 'StickyScrollableHeading';
type StickyScrollableContentProps = PropsWithChildren;
type StickyScrollableContentProps = PropsWithChildren<{ sx?: SystemStyleObject }>;
const StickyScrollableContent = memo((props: StickyScrollableContentProps) => {
return (
<Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4}>
<Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4} sx={props.sx}>
{props.children}
</Flex>
);
@ -30,13 +32,15 @@ StickyScrollableContent.displayName = 'StickyScrollableContent';
type StickyScrollableProps = PropsWithChildren<{
title: string;
headingSx?: SystemStyleObject;
contentSx?: SystemStyleObject;
}>;
export const StickyScrollable = memo((props: StickyScrollableProps) => {
return (
<Flex key={props.title} flexDir="column">
<StickyScrollableHeading title={props.title} />
<StickyScrollableContent>{props.children}</StickyScrollableContent>
<StickyScrollableHeading title={props.title} sx={props.headingSx} />
<StickyScrollableContent sx={props.contentSx}>{props.children}</StickyScrollableContent>
</Flex>
);
});