feat(ui): update UI for sorting

This commit is contained in:
psychedelicious 2024-06-28 16:27:52 +10:00
parent abb8d34b56
commit 2b744480d6
10 changed files with 86 additions and 155 deletions

View File

@ -352,14 +352,11 @@
}, },
"gallery": { "gallery": {
"alwaysShowImageSizeBadge": "Always Show Image Size Badge", "alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"ascending": "Ascending",
"assets": "Assets", "assets": "Assets",
"autoAssignBoardOnClick": "Auto-Assign Board on Click", "autoAssignBoardOnClick": "Auto-Assign Board on Click",
"autoSwitchNewImages": "Auto-Switch to New Images", "autoSwitchNewImages": "Auto-Switch to New Images",
"copy": "Copy", "copy": "Copy",
"createdDate": "Created Date",
"currentlyInUse": "This image is currently in use in the following features:", "currentlyInUse": "This image is currently in use in the following features:",
"descending": "Descending",
"drop": "Drop", "drop": "Drop",
"dropOrUpload": "$t(gallery.drop) or Upload", "dropOrUpload": "$t(gallery.drop) or Upload",
"dropToUpload": "$t(gallery.drop) to Upload", "dropToUpload": "$t(gallery.drop) to Upload",
@ -375,14 +372,13 @@
"loading": "Loading", "loading": "Loading",
"loadMore": "Load More", "loadMore": "Load More",
"newestFirst": "Newest First", "newestFirst": "Newest First",
"oldestFirst": "Oldest First",
"sortDirection": "Sort Direction",
"showStarredImagesFirst": "Show Starred Images First",
"noImageSelected": "No Image Selected", "noImageSelected": "No Image Selected",
"noImagesInGallery": "No Images to Display", "noImagesInGallery": "No Images to Display",
"oldestFirst": "Oldest First",
"setCurrentImage": "Set as Current Image", "setCurrentImage": "Set as Current Image",
"starImage": "Star Image", "starImage": "Star Image",
"starred": "Starred",
"starredFirst": "Starred First",
"starredLast": "Starred Last",
"unstarImage": "Unstar Image", "unstarImage": "Unstar Image",
"unableToLoad": "Unable to load Gallery", "unableToLoad": "Unable to load Gallery",
"deleteSelection": "Delete Selection", "deleteSelection": "Delete Selection",
@ -403,10 +399,6 @@
"selectAnImageToCompare": "Select an Image to Compare", "selectAnImageToCompare": "Select an Image to Compare",
"slider": "Slider", "slider": "Slider",
"sideBySide": "Side-by-Side", "sideBySide": "Side-by-Side",
"sortAscending": "Ascending",
"sortBy": "Sort By",
"sortingBy": "Sorting by",
"sortDescending": "Descending",
"hover": "Hover", "hover": "Hover",
"swapImages": "Swap Images", "swapImages": "Swap Images",
"compareOptions": "Comparison Options", "compareOptions": "Comparison Options",

View File

@ -1,13 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import { GalleryBulkSelect } from './GalleryBulkSelect';
import { GallerySort } from './GallerySort';
export const GalleryMenu = () => {
return (
<Flex alignItems="center" justifyContent="space-between">
<GalleryBulkSelect />
<GallerySort />
</Flex>
);
};

View File

@ -1,109 +0,0 @@
import type { ComboboxOption } from '@invoke-ai/ui-library';
import {
Button,
ButtonGroup,
Combobox,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { SingleValue } from 'chakra-react-select';
import { orderByChanged, orderDirChanged } from 'features/gallery/store/gallerySlice';
import type { OrderBy, OrderDir } from 'features/gallery/store/types';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
import { PiSortAscending, PiSortDescending } from 'react-icons/pi';
const OPTIONS = [
{ value: 'created_at', label: t('gallery.createdDate') },
{ value: 'starred', label: t('gallery.starred') },
];
export const GallerySort = () => {
const { orderBy, orderDir } = useAppSelector((s) => s.gallery);
const dispatch = useAppDispatch();
const handleChangeOrderDir = useCallback(
(dir: OrderDir) => {
dispatch(orderDirChanged(dir));
},
[dispatch]
);
const handleChangeOrderBy = useCallback(
(v: SingleValue<ComboboxOption>) => {
if (v) {
dispatch(orderByChanged(v.value as OrderBy));
}
},
[dispatch]
);
const orderByValue = useMemo(() => {
return OPTIONS.find((opt) => opt.value === orderBy);
}, [orderBy]);
const ascendingText = useMemo(() => {
return orderBy === 'created_at' ? t('gallery.oldestFirst') : t('gallery.starredLast');
}, [orderBy]);
const descendingText = useMemo(() => {
return orderBy === 'created_at' ? t('gallery.newestFirst') : t('gallery.starredFirst');
}, [orderBy]);
const sortTooltip = useMemo(() => {
if (orderDir === 'ASC') {
return `${t('gallery.sortingBy')}: ${ascendingText}`;
} else {
return `${t('gallery.sortingBy')}: ${descendingText}`;
}
}, [orderDir, ascendingText, descendingText]);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
tooltip={sortTooltip}
variant="outline"
size="sm"
icon={orderDir === 'ASC' ? <PiSortAscending /> : <PiSortDescending />}
aria-label="Sort"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<Flex direction="column" gap={4}>
<ButtonGroup>
<Button
size="sm"
flexShrink={0}
onClick={handleChangeOrderDir.bind(null, 'DESC')}
colorScheme={orderDir === 'DESC' ? 'invokeBlue' : 'base'}
>
{descendingText}
</Button>
<Button
size="sm"
flexShrink={0}
onClick={handleChangeOrderDir.bind(null, 'ASC')}
colorScheme={orderDir === 'ASC' ? 'invokeBlue' : 'base'}
>
{ascendingText}
</Button>
</ButtonGroup>
<FormControl>
<FormLabel>{t('gallery.sortBy')}</FormLabel>
<Combobox value={orderByValue} options={OPTIONS} onChange={handleChangeOrderBy} isSearchable={false} />
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};

View File

@ -1,7 +1,9 @@
import type { FormLabelProps } from '@invoke-ai/ui-library'; import type { ComboboxOption, FormLabelProps } from '@invoke-ai/ui-library';
import { import {
Checkbox, Checkbox,
Combobox,
CompositeSlider, CompositeSlider,
Divider,
Flex, Flex,
FormControl, FormControl,
FormControlGroup, FormControlGroup,
@ -14,17 +16,21 @@ import {
Switch, Switch,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { SingleValue } from 'chakra-react-select';
import { import {
alwaysShowImageSizeBadgeChanged, alwaysShowImageSizeBadgeChanged,
autoAssignBoardOnClickChanged, autoAssignBoardOnClickChanged,
orderDirChanged,
setGalleryImageMinimumWidth, setGalleryImageMinimumWidth,
shouldAutoSwitchChanged, shouldAutoSwitchChanged,
shouldShowArchivedBoardsChanged, shouldShowArchivedBoardsChanged,
starredFirstChanged,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiSettings4Fill } from 'react-icons/ri'; import { RiSettings4Fill } from 'react-icons/ri';
import { assert } from 'tsafe';
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect'; import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
@ -40,6 +46,8 @@ const GallerySettingsPopover = () => {
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge); const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
const shouldShowArchivedBoards = useAppSelector((s) => s.gallery.shouldShowArchivedBoards); const shouldShowArchivedBoards = useAppSelector((s) => s.gallery.shouldShowArchivedBoards);
const orderDir = useAppSelector((s) => s.gallery.orderDir);
const starredFirst = useAppSelector((s) => s.gallery.starredFirst);
const handleChangeGalleryImageMinimumWidth = useCallback( const handleChangeGalleryImageMinimumWidth = useCallback(
(v: number) => { (v: number) => {
@ -72,15 +80,37 @@ const GallerySettingsPopover = () => {
[dispatch] [dispatch]
); );
const onChangeStarredFirst = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(starredFirstChanged(e.target.checked));
},
[dispatch]
);
const orderDirOptions = useMemo<ComboboxOption[]>(
() => [
{ value: 'DESC', label: t('gallery.newestFirst') },
{ value: 'ASC', label: t('gallery.oldestFirst') },
],
[t]
);
const onChangeOrderDir = useCallback(
(v: SingleValue<ComboboxOption>) => {
assert(v?.value === 'ASC' || v?.value === 'DESC');
dispatch(orderDirChanged(v.value));
},
[dispatch]
);
const orderDirValue = useMemo(() => {
return orderDirOptions.find((opt) => opt.value === orderDir);
}, [orderDir, orderDirOptions]);
return ( return (
<Popover isLazy> <Popover isLazy>
<PopoverTrigger> <PopoverTrigger>
<IconButton <IconButton aria-label={t('gallery.gallerySettings')} size="sm" icon={<RiSettings4Fill />} />
tooltip={t('gallery.gallerySettings')}
aria-label={t('gallery.gallerySettings')}
size="sm"
icon={<RiSettings4Fill />}
/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<PopoverBody> <PopoverBody>
@ -98,7 +128,7 @@ const GallerySettingsPopover = () => {
<FormControlGroup formLabelProps={formLabelProps}> <FormControlGroup formLabelProps={formLabelProps}>
<FormControl> <FormControl>
<FormLabel>{t('gallery.autoSwitchNewImages')}</FormLabel> <FormLabel>{t('gallery.autoSwitchNewImages')}</FormLabel>
<Switch isChecked={shouldAutoSwitch} onChange={handleChangeAutoSwitch} /> <Checkbox isChecked={shouldAutoSwitch} onChange={handleChangeAutoSwitch} />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel>{t('gallery.autoAssignBoardOnClick')}</FormLabel> <FormLabel>{t('gallery.autoAssignBoardOnClick')}</FormLabel>
@ -114,6 +144,24 @@ const GallerySettingsPopover = () => {
</FormControl> </FormControl>
</FormControlGroup> </FormControlGroup>
<BoardAutoAddSelect /> <BoardAutoAddSelect />
<Divider />
<FormControl w="full">
<FormLabel flexGrow={1} m={0}>
{t('gallery.showStarredImagesFirst')}
</FormLabel>
<Switch size="sm" isChecked={starredFirst} onChange={onChangeStarredFirst} />
</FormControl>
<FormControl>
<FormLabel flexGrow={1} m={0}>
{t('gallery.sortDirection')}
</FormLabel>
<Combobox
isSearchable={false}
value={orderDirValue}
options={orderDirOptions}
onChange={onChangeOrderDir}
/>
</FormControl>
</Flex> </Flex>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>

View File

@ -10,7 +10,6 @@ import { RiServerLine } from 'react-icons/ri';
import BoardsList from './Boards/BoardsList/BoardsList'; import BoardsList from './Boards/BoardsList/BoardsList';
import GalleryBoardName from './GalleryBoardName'; import GalleryBoardName from './GalleryBoardName';
import { GalleryMenu } from './GalleryMenu/GalleryMenu';
import GallerySettingsPopover from './GallerySettingsPopover'; import GallerySettingsPopover from './GallerySettingsPopover';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid'; import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination'; import { GalleryPagination } from './ImageGrid/GalleryPagination';
@ -81,7 +80,7 @@ const ImageGalleryContent = () => {
</TabList> </TabList>
</Tabs> </Tabs>
</Flex> </Flex>
<GalleryMenu />
<GalleryImageGrid /> <GalleryImageGrid />
<GalleryPagination /> <GalleryPagination />
</Flex> </Flex>

View File

@ -1,4 +1,4 @@
import { Spacer, Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library'; import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { selectionChanged } from 'features/gallery/store/gallerySlice';
@ -23,11 +23,23 @@ export const GalleryBulkSelect = () => {
useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true }, [onSelectPage]); useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true }, [onSelectPage]);
if (selection.length <= 1) { if (selection.length <= 1) {
return <Spacer />; return null;
} }
return ( return (
<Tag py={1} px={3} userSelect="none" border={1} borderStyle="solid" borderColor="whiteAlpha.300"> <Tag
position="absolute"
bg="invokeBlue.800"
color="base.50"
py={1}
px={3}
userSelect="none"
shadow="dark-lg"
fontWeight="semibold"
border={1}
borderStyle="solid"
borderColor="whiteAlpha.300"
>
<TagLabel> <TagLabel>
{selection.length} {t('common.selected')} {selection.length} {t('common.selected')}
</TagLabel> </TagLabel>

View File

@ -2,6 +2,7 @@ import { Box, Flex, Grid } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants'; import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { GalleryBulkSelect } from 'features/gallery/components/ImageGrid/GalleryBulkSelect';
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys'; import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { limitChanged } from 'features/gallery/store/gallerySlice'; import { limitChanged } from 'features/gallery/store/gallerySlice';
@ -144,6 +145,7 @@ const Content = () => {
))} ))}
</Grid> </Grid>
</Box> </Box>
<GalleryBulkSelect />
</Box> </Box>
); );
}; };

View File

@ -20,7 +20,7 @@ export const selectListImagesQueryArgs = createMemoizedSelector(
offset: gallery.offset, offset: gallery.offset,
limit: gallery.limit, limit: gallery.limit,
is_intermediate: false, is_intermediate: false,
order_by: gallery.orderBy, starred_first: gallery.starredFirst,
order_dir: gallery.orderDir, order_dir: gallery.orderDir,
} }
: skipToken : skipToken

View File

@ -4,7 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store';
import { uniqBy } from 'lodash-es'; import { uniqBy } from 'lodash-es';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import type { BoardId, ComparisonMode, GalleryState, GalleryView, OrderBy, OrderDir } from './types'; import type { BoardId, ComparisonMode, GalleryState, GalleryView, OrderDir } from './types';
import { IMAGE_LIMIT } from './types'; import { IMAGE_LIMIT } from './types';
const initialGalleryState: GalleryState = { const initialGalleryState: GalleryState = {
@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = {
boardSearchText: '', boardSearchText: '',
limit: 20, limit: 20,
offset: 0, offset: 0,
orderBy: 'starred', starredFirst: true,
orderDir: 'ASC', orderDir: 'ASC',
isImageViewerOpen: true, isImageViewerOpen: true,
imageToCompare: null, imageToCompare: null,
@ -114,8 +114,8 @@ export const gallerySlice = createSlice({
shouldShowArchivedBoardsChanged: (state, action: PayloadAction<boolean>) => { shouldShowArchivedBoardsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowArchivedBoards = action.payload; state.shouldShowArchivedBoards = action.payload;
}, },
orderByChanged: (state, action: PayloadAction<OrderBy>) => { starredFirstChanged: (state, action: PayloadAction<boolean>) => {
state.orderBy = action.payload; state.starredFirst = action.payload;
}, },
orderDirChanged: (state, action: PayloadAction<OrderDir>) => { orderDirChanged: (state, action: PayloadAction<OrderDir>) => {
state.orderDir = action.payload; state.orderDir = action.payload;
@ -142,8 +142,9 @@ export const {
comparisonModeCycled, comparisonModeCycled,
offsetChanged, offsetChanged,
limitChanged, limitChanged,
orderByChanged,
orderDirChanged, orderDirChanged,
starredFirstChanged,
shouldShowArchivedBoardsChanged,
} = gallerySlice.actions; } = gallerySlice.actions;
export const selectGallerySlice = (state: RootState) => state.gallery; export const selectGallerySlice = (state: RootState) => state.gallery;

View File

@ -8,7 +8,6 @@ export type GalleryView = 'images' | 'assets';
export type BoardId = 'none' | (string & Record<never, never>); export type BoardId = 'none' | (string & Record<never, never>);
export type ComparisonMode = 'slider' | 'side-by-side' | 'hover'; export type ComparisonMode = 'slider' | 'side-by-side' | 'hover';
export type ComparisonFit = 'contain' | 'fill'; export type ComparisonFit = 'contain' | 'fill';
export type OrderBy = 'created_at' | 'starred';
export type OrderDir = 'ASC' | 'DESC'; export type OrderDir = 'ASC' | 'DESC';
export type GalleryState = { export type GalleryState = {
@ -22,7 +21,7 @@ export type GalleryState = {
boardSearchText: string; boardSearchText: string;
offset: number; offset: number;
limit: number; limit: number;
orderBy: OrderBy; starredFirst: boolean;
orderDir: OrderDir; orderDir: OrderDir;
alwaysShowImageSizeBadge: boolean; alwaysShowImageSizeBadge: boolean;
imageToCompare: ImageDTO | null; imageToCompare: ImageDTO | null;