mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): improved model list styling
This commit is contained in:
parent
b0add805c5
commit
2f0a653a7f
@ -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);
|
@ -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);
|
@ -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"
|
||||
/>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user