feat(ui): style settings modal

This commit is contained in:
psychedelicious 2024-01-04 21:04:29 +11:00 committed by Kent Keirsey
parent fcf2006502
commit bbca053b48
6 changed files with 268 additions and 227 deletions

View File

@ -16,6 +16,7 @@ export const baseStyle = definePartsStyle(() => ({
header: { header: {
fontWeight: 'semibold', fontWeight: 'semibold',
fontSize: 'lg', fontSize: 'lg',
color: 'base.300'
}, },
closeButton: { closeButton: {
opacity: 0.5, opacity: 0.5,

View File

@ -7,7 +7,6 @@ import {
useDisclosure, useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { InvHeading } from 'common/components/InvHeading/wrapper';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton'; import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvInput } from 'common/components/InvInput/InvInput'; import { InvInput } from 'common/components/InvInput/InvInput';
import { import {
@ -22,6 +21,7 @@ import {
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import type { HotkeyGroup } from 'features/system/components/HotkeysModal/useHotkeyData'; import type { HotkeyGroup } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useHotkeyData } from 'features/system/components/HotkeysModal/useHotkeyData'; import { useHotkeyData } from 'features/system/components/HotkeysModal/useHotkeyData';
import { StickyScrollable } from 'features/system/components/StickyScrollable';
import type { ChangeEventHandler, ReactElement } from 'react'; import type { ChangeEventHandler, ReactElement } from 'react';
import { import {
cloneElement, cloneElement,
@ -121,26 +121,9 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
</InputGroup> </InputGroup>
<ScrollableContent> <ScrollableContent>
{filteredHotkeyGroups.map((group) => ( <Flex flexDir="column" gap={4}>
<Flex key={group.title} pb={4} flexDir="column"> {filteredHotkeyGroups.map((group) => (
<Flex <StickyScrollable key={group.title} title={group.title}>
ps={2}
pb={4}
position="sticky"
zIndex={1}
top={0}
bg="base.800"
>
<InvHeading size="sm">{group.title}</InvHeading>
</Flex>
<Flex
key={group.title}
p={4}
borderRadius="base"
bg="base.750"
flexDir="column"
gap={4}
>
{group.hotkeyListItems.map((hotkey, i) => ( {group.hotkeyListItems.map((hotkey, i) => (
<Fragment key={i}> <Fragment key={i}>
<HotkeyListItem <HotkeyListItem
@ -151,15 +134,15 @@ const HotkeysModal = ({ children }: HotkeysModalProps) => {
{i < group.hotkeyListItems.length - 1 && <Divider />} {i < group.hotkeyListItems.length - 1 && <Divider />}
</Fragment> </Fragment>
))} ))}
</Flex> </StickyScrollable>
</Flex> ))}
))} {!filteredHotkeyGroups.length && (
{!filteredHotkeyGroups.length && ( <IAINoContentFallback
<IAINoContentFallback label={t('hotkeys.noHotkeysFound')}
label={t('hotkeys.noHotkeysFound')} icon={null}
icon={null} />
/> )}
)} </Flex>
</ScrollableContent> </ScrollableContent>
</InvModalBody> </InvModalBody>
<InvModalFooter /> <InvModalFooter />

View File

@ -1,93 +0,0 @@
import { Heading } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { InvButton } from 'common/components/InvButton/InvButton';
import { InvText } from 'common/components/InvText/wrapper';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { addToast } from 'features/system/store/systemSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
useClearIntermediatesMutation,
useGetIntermediatesCountQuery,
} from 'services/api/endpoints/images';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import StyledFlex from './StyledFlex';
const SettingsClearIntermediates = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { data: intermediatesCount } = useGetIntermediatesCountQuery(
undefined,
{
refetchOnMountOrArgChange: true,
}
);
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
useClearIntermediatesMutation();
const { data: queueStatus } = useGetQueueStatusQuery();
const hasPendingItems =
queueStatus &&
(queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0);
const handleClickClearIntermediates = useCallback(() => {
if (hasPendingItems) {
return;
}
clearIntermediates()
.unwrap()
.then((clearedCount) => {
dispatch(controlAdaptersReset());
dispatch(resetCanvas());
dispatch(
addToast({
title: t('settings.intermediatesCleared', { count: clearedCount }),
status: 'info',
})
);
})
.catch(() => {
dispatch(
addToast({
title: t('settings.intermediatesClearedFailed'),
status: 'error',
})
);
});
}, [t, clearIntermediates, dispatch, hasPendingItems]);
return (
<StyledFlex>
<Heading size="sm">{t('settings.clearIntermediates')}</Heading>
<InvButton
tooltip={
hasPendingItems ? t('settings.clearIntermediatesDisabled') : undefined
}
colorScheme="warning"
onClick={handleClickClearIntermediates}
isLoading={isLoadingClearIntermediates}
isDisabled={!intermediatesCount || hasPendingItems}
>
{t('settings.clearIntermediatesWithCount', {
count: intermediatesCount ?? 0,
})}
</InvButton>
<InvText fontWeight="bold">
{t('settings.clearIntermediatesDesc1')}
</InvText>
<InvText variant="subtext">
{t('settings.clearIntermediatesDesc2')}
</InvText>
<InvText variant="subtext">
{t('settings.clearIntermediatesDesc3')}
</InvText>
</StyledFlex>
);
};
export default memo(SettingsClearIntermediates);

View File

@ -4,7 +4,6 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvButton } from 'common/components/InvButton/InvButton'; import { InvButton } from 'common/components/InvButton/InvButton';
import { InvControl } from 'common/components/InvControl/InvControl'; import { InvControl } from 'common/components/InvControl/InvControl';
import { InvHeading } from 'common/components/InvHeading/wrapper';
import { import {
InvModal, InvModal,
InvModalBody, InvModalBody,
@ -16,8 +15,11 @@ import {
} from 'common/components/InvModal/wrapper'; } from 'common/components/InvModal/wrapper';
import { InvSwitch } from 'common/components/InvSwitch/wrapper'; import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import { InvText } from 'common/components/InvText/wrapper'; import { InvText } from 'common/components/InvText/wrapper';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useClearStorage } from 'common/hooks/useClearStorage'; import { useClearStorage } from 'common/hooks/useClearStorage';
import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice'; import { shouldUseCpuNoiseChanged } from 'features/parameters/store/generationSlice';
import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates';
import { StickyScrollable } from 'features/system/components/StickyScrollable';
import { import {
setEnableImageDebugging, setEnableImageDebugging,
setShouldConfirmOnDelete, setShouldConfirmOnDelete,
@ -33,10 +35,8 @@ import { cloneElement, memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
import SettingsClearIntermediates from './SettingsClearIntermediates';
import { SettingsLanguageSelect } from './SettingsLanguageSelect'; import { SettingsLanguageSelect } from './SettingsLanguageSelect';
import { SettingsLogLevelSelect } from './SettingsLogLevelSelect'; import { SettingsLogLevelSelect } from './SettingsLogLevelSelect';
import StyledFlex from './StyledFlex';
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[stateSelector], [stateSelector],
@ -110,6 +110,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
}), }),
}); });
const {
clearIntermediates,
hasPendingItems,
intermediatesCount,
isLoading: isLoadingClearIntermediates,
} = useClearIntermediates();
const { const {
isOpen: isSettingsModalOpen, isOpen: isSettingsModalOpen,
onOpen: onSettingsModalOpen, onOpen: onSettingsModalOpen,
@ -218,120 +225,143 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
isCentered isCentered
> >
<InvModalOverlay /> <InvModalOverlay />
<InvModalContent> <InvModalContent maxH="80vh" h="80vh">
<InvModalHeader bg="none">{t('common.settingsLabel')}</InvModalHeader> <InvModalHeader bg="none">{t('common.settingsLabel')}</InvModalHeader>
<InvModalCloseButton /> <InvModalCloseButton />
<InvModalBody> <InvModalBody display="flex" flexDir="column" gap={4}>
<Flex gap={4} flexDir="column"> <ScrollableContent>
<StyledFlex> <Flex flexDir="column" gap={4}>
<InvHeading size="sm">{t('settings.general')}</InvHeading> <StickyScrollable title={t('settings.general')}>
<InvControl label={t('settings.confirmOnDelete')}> <InvControl label={t('settings.confirmOnDelete')}>
<InvSwitch
isChecked={shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</InvControl>
</StyledFlex>
<StyledFlex>
<InvHeading size="sm">{t('settings.generation')}</InvHeading>
<InvControl
label={t('settings.enableNSFWChecker')}
isDisabled={!isNSFWCheckerAvailable}
>
<InvSwitch
isChecked={shouldUseNSFWChecker}
onChange={handleChangeShouldUseNSFWChecker}
/>
</InvControl>
<InvControl
label={t('settings.enableInvisibleWatermark')}
isDisabled={!isWatermarkerAvailable}
>
<InvSwitch
isChecked={shouldUseWatermarker}
onChange={handleChangeShouldUseWatermarker}
/>
</InvControl>
</StyledFlex>
<StyledFlex>
<InvHeading size="sm">{t('settings.ui')}</InvHeading>
<InvControl label={t('settings.showProgressInViewer')}>
<InvSwitch
isChecked={shouldShowProgressInViewer}
onChange={handleChangeShouldShowProgressInViewer}
/>
</InvControl>
<InvControl label={t('settings.antialiasProgressImages')}>
<InvSwitch
isChecked={shouldAntialiasProgressImage}
onChange={handleChangeShouldAntialiasProgressImage}
/>
</InvControl>
<InvControl
label={t('parameters.useCpuNoise')}
feature="noiseUseCPU"
>
<InvSwitch
isChecked={shouldUseCpuNoise}
onChange={handleChangeShouldUseCpuNoise}
/>
</InvControl>
{shouldShowLocalizationToggle && <SettingsLanguageSelect />}
<InvControl label={t('settings.enableInformationalPopovers')}>
<InvSwitch
isChecked={shouldEnableInformationalPopovers}
onChange={handleChangeShouldEnableInformationalPopovers}
/>
</InvControl>
</StyledFlex>
{shouldShowDeveloperSettings && (
<StyledFlex>
<InvHeading size="sm">{t('settings.developer')}</InvHeading>
<InvControl label={t('settings.shouldLogToConsole')}>
<InvSwitch <InvSwitch
isChecked={shouldLogToConsole} isChecked={shouldConfirmOnDelete}
onChange={handleLogToConsoleChanged} onChange={handleChangeShouldConfirmOnDelete}
/> />
</InvControl> </InvControl>
<SettingsLogLevelSelect /> </StickyScrollable>
<InvControl label={t('settings.enableImageDebugging')}>
<StickyScrollable title={t('settings.generation')}>
<InvControl
label={t('settings.enableNSFWChecker')}
isDisabled={!isNSFWCheckerAvailable}
>
<InvSwitch <InvSwitch
isChecked={enableImageDebugging} isChecked={shouldUseNSFWChecker}
onChange={handleChangeEnableImageDebugging} onChange={handleChangeShouldUseNSFWChecker}
/> />
</InvControl> </InvControl>
</StyledFlex> <InvControl
)} label={t('settings.enableInvisibleWatermark')}
isDisabled={!isWatermarkerAvailable}
>
<InvSwitch
isChecked={shouldUseWatermarker}
onChange={handleChangeShouldUseWatermarker}
/>
</InvControl>
</StickyScrollable>
{shouldShowClearIntermediates && <SettingsClearIntermediates />} <StickyScrollable title={t('settings.ui')}>
<InvControl label={t('settings.showProgressInViewer')}>
<InvSwitch
isChecked={shouldShowProgressInViewer}
onChange={handleChangeShouldShowProgressInViewer}
/>
</InvControl>
<InvControl label={t('settings.antialiasProgressImages')}>
<InvSwitch
isChecked={shouldAntialiasProgressImage}
onChange={handleChangeShouldAntialiasProgressImage}
/>
</InvControl>
<InvControl
label={t('parameters.useCpuNoise')}
feature="noiseUseCPU"
>
<InvSwitch
isChecked={shouldUseCpuNoise}
onChange={handleChangeShouldUseCpuNoise}
/>
</InvControl>
{shouldShowLocalizationToggle && <SettingsLanguageSelect />}
<InvControl label={t('settings.enableInformationalPopovers')}>
<InvSwitch
isChecked={shouldEnableInformationalPopovers}
onChange={handleChangeShouldEnableInformationalPopovers}
/>
</InvControl>
</StickyScrollable>
<StyledFlex> {shouldShowDeveloperSettings && (
<InvHeading size="sm">{t('settings.resetWebUI')}</InvHeading> <StickyScrollable title={t('settings.developer')}>
<InvButton colorScheme="error" onClick={handleClickResetWebUI}> <InvControl label={t('settings.shouldLogToConsole')}>
{t('settings.resetWebUI')} <InvSwitch
</InvButton> isChecked={shouldLogToConsole}
{shouldShowResetWebUiText && ( onChange={handleLogToConsoleChanged}
<> />
<InvText variant="subtext"> </InvControl>
{t('settings.resetWebUIDesc1')} <SettingsLogLevelSelect />
</InvText> <InvControl label={t('settings.enableImageDebugging')}>
<InvText variant="subtext"> <InvSwitch
{t('settings.resetWebUIDesc2')} isChecked={enableImageDebugging}
</InvText> onChange={handleChangeEnableImageDebugging}
</> />
</InvControl>
</StickyScrollable>
)} )}
</StyledFlex>
</Flex> {shouldShowClearIntermediates && (
<StickyScrollable title={t('settings.clearIntermediates')}>
<InvButton
tooltip={
hasPendingItems
? t('settings.clearIntermediatesDisabled')
: undefined
}
colorScheme="warning"
onClick={clearIntermediates}
isLoading={isLoadingClearIntermediates}
isDisabled={!intermediatesCount || hasPendingItems}
>
{t('settings.clearIntermediatesWithCount', {
count: intermediatesCount ?? 0,
})}
</InvButton>
<InvText fontWeight="bold">
{t('settings.clearIntermediatesDesc1')}
</InvText>
<InvText variant="subtext">
{t('settings.clearIntermediatesDesc2')}
</InvText>
<InvText variant="subtext">
{t('settings.clearIntermediatesDesc3')}
</InvText>
</StickyScrollable>
)}
<StickyScrollable title={t('settings.resetWebUI')}>
<InvButton
colorScheme="error"
onClick={handleClickResetWebUI}
>
{t('settings.resetWebUI')}
</InvButton>
{shouldShowResetWebUiText && (
<>
<InvText variant="subtext">
{t('settings.resetWebUIDesc1')}
</InvText>
<InvText variant="subtext">
{t('settings.resetWebUIDesc2')}
</InvText>
</>
)}
</StickyScrollable>
</Flex>
</ScrollableContent>
</InvModalBody> </InvModalBody>
<InvModalFooter> <InvModalFooter />
<InvButton onClick={onSettingsModalClose}>
{t('common.close')}
</InvButton>
</InvModalFooter>
</InvModalContent> </InvModalContent>
</InvModal> </InvModal>

View File

@ -0,0 +1,71 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { addToast } from 'features/system/store/systemSlice';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
useClearIntermediatesMutation,
useGetIntermediatesCountQuery,
} from 'services/api/endpoints/images';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export type UseClearIntermediatesReturn = {
intermediatesCount: number | undefined;
clearIntermediates: () => void;
isLoading: boolean;
hasPendingItems: boolean;
};
export const useClearIntermediates = (): UseClearIntermediatesReturn => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { data: intermediatesCount } = useGetIntermediatesCountQuery(
undefined,
{
refetchOnMountOrArgChange: true,
}
);
const [_clearIntermediates, { isLoading }] = useClearIntermediatesMutation();
const { data: queueStatus } = useGetQueueStatusQuery();
const hasPendingItems = useMemo(
() =>
Boolean(
queueStatus &&
(queueStatus.queue.in_progress > 0 || queueStatus.queue.pending > 0)
),
[queueStatus]
);
const clearIntermediates = useCallback(() => {
if (hasPendingItems) {
return;
}
_clearIntermediates()
.unwrap()
.then((clearedCount) => {
dispatch(controlAdaptersReset());
dispatch(resetCanvas());
dispatch(
addToast({
title: t('settings.intermediatesCleared', { count: clearedCount }),
status: 'info',
})
);
})
.catch(() => {
dispatch(
addToast({
title: t('settings.intermediatesClearedFailed'),
status: 'error',
})
);
});
}, [t, _clearIntermediates, dispatch, hasPendingItems]);
return { intermediatesCount, clearIntermediates, isLoading, hasPendingItems };
};

View File

@ -0,0 +1,49 @@
import { Flex } from '@chakra-ui/layout';
import { InvHeading } from 'common/components/InvHeading/wrapper';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
export type StickyScrollableHeadingProps = {
title: string;
};
export const StickyScrollableHeading = memo(
(props: StickyScrollableHeadingProps) => {
return (
<Flex ps={2} pb={4} position="sticky" zIndex={1} top={0} bg="base.800">
<InvHeading size="sm">{props.title}</InvHeading>
</Flex>
);
}
);
StickyScrollableHeading.displayName = 'StickyScrollableHeading';
export type StickyScrollableContentProps = PropsWithChildren;
export const StickyScrollableContent = memo(
(props: StickyScrollableContentProps) => {
return (
<Flex p={4} borderRadius="base" bg="base.750" flexDir="column" gap={4}>
{props.children}
</Flex>
);
}
);
StickyScrollableContent.displayName = 'StickyScrollableContent';
export type StickyScrollableProps = PropsWithChildren<{
title: string;
}>;
export const StickyScrollable = memo((props: StickyScrollableProps) => {
return (
<Flex key={props.title} flexDir="column">
<StickyScrollableHeading title={props.title} />
<StickyScrollableContent>{props.children}</StickyScrollableContent>
</Flex>
);
});
StickyScrollable.displayName = 'StickyScrollable';