mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): update UI for sorting
This commit is contained in:
parent
abb8d34b56
commit
2b744480d6
@ -352,14 +352,11 @@
|
||||
},
|
||||
"gallery": {
|
||||
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
|
||||
"ascending": "Ascending",
|
||||
"assets": "Assets",
|
||||
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
|
||||
"autoSwitchNewImages": "Auto-Switch to New Images",
|
||||
"copy": "Copy",
|
||||
"createdDate": "Created Date",
|
||||
"currentlyInUse": "This image is currently in use in the following features:",
|
||||
"descending": "Descending",
|
||||
"drop": "Drop",
|
||||
"dropOrUpload": "$t(gallery.drop) or Upload",
|
||||
"dropToUpload": "$t(gallery.drop) to Upload",
|
||||
@ -375,14 +372,13 @@
|
||||
"loading": "Loading",
|
||||
"loadMore": "Load More",
|
||||
"newestFirst": "Newest First",
|
||||
"oldestFirst": "Oldest First",
|
||||
"sortDirection": "Sort Direction",
|
||||
"showStarredImagesFirst": "Show Starred Images First",
|
||||
"noImageSelected": "No Image Selected",
|
||||
"noImagesInGallery": "No Images to Display",
|
||||
"oldestFirst": "Oldest First",
|
||||
"setCurrentImage": "Set as Current Image",
|
||||
"starImage": "Star Image",
|
||||
"starred": "Starred",
|
||||
"starredFirst": "Starred First",
|
||||
"starredLast": "Starred Last",
|
||||
"unstarImage": "Unstar Image",
|
||||
"unableToLoad": "Unable to load Gallery",
|
||||
"deleteSelection": "Delete Selection",
|
||||
@ -403,10 +399,6 @@
|
||||
"selectAnImageToCompare": "Select an Image to Compare",
|
||||
"slider": "Slider",
|
||||
"sideBySide": "Side-by-Side",
|
||||
"sortAscending": "Ascending",
|
||||
"sortBy": "Sort By",
|
||||
"sortingBy": "Sorting by",
|
||||
"sortDescending": "Descending",
|
||||
"hover": "Hover",
|
||||
"swapImages": "Swap Images",
|
||||
"compareOptions": "Comparison Options",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
import type { FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import type { ComboboxOption, FormLabelProps } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Checkbox,
|
||||
Combobox,
|
||||
CompositeSlider,
|
||||
Divider,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormControlGroup,
|
||||
@ -14,17 +16,21 @@ import {
|
||||
Switch,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { SingleValue } from 'chakra-react-select';
|
||||
import {
|
||||
alwaysShowImageSizeBadgeChanged,
|
||||
autoAssignBoardOnClickChanged,
|
||||
orderDirChanged,
|
||||
setGalleryImageMinimumWidth,
|
||||
shouldAutoSwitchChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
starredFirstChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiSettings4Fill } from 'react-icons/ri';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
|
||||
|
||||
@ -40,6 +46,8 @@ const GallerySettingsPopover = () => {
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
||||
const shouldShowArchivedBoards = useAppSelector((s) => s.gallery.shouldShowArchivedBoards);
|
||||
const orderDir = useAppSelector((s) => s.gallery.orderDir);
|
||||
const starredFirst = useAppSelector((s) => s.gallery.starredFirst);
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = useCallback(
|
||||
(v: number) => {
|
||||
@ -72,15 +80,37 @@ const GallerySettingsPopover = () => {
|
||||
[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 (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
tooltip={t('gallery.gallerySettings')}
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
size="sm"
|
||||
icon={<RiSettings4Fill />}
|
||||
/>
|
||||
<IconButton aria-label={t('gallery.gallerySettings')} size="sm" icon={<RiSettings4Fill />} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
@ -98,7 +128,7 @@ const GallerySettingsPopover = () => {
|
||||
<FormControlGroup formLabelProps={formLabelProps}>
|
||||
<FormControl>
|
||||
<FormLabel>{t('gallery.autoSwitchNewImages')}</FormLabel>
|
||||
<Switch isChecked={shouldAutoSwitch} onChange={handleChangeAutoSwitch} />
|
||||
<Checkbox isChecked={shouldAutoSwitch} onChange={handleChangeAutoSwitch} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>{t('gallery.autoAssignBoardOnClick')}</FormLabel>
|
||||
@ -114,6 +144,24 @@ const GallerySettingsPopover = () => {
|
||||
</FormControl>
|
||||
</FormControlGroup>
|
||||
<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>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
@ -10,7 +10,6 @@ import { RiServerLine } from 'react-icons/ri';
|
||||
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import GalleryBoardName from './GalleryBoardName';
|
||||
import { GalleryMenu } from './GalleryMenu/GalleryMenu';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
@ -81,7 +80,7 @@ const ImageGalleryContent = () => {
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
<GalleryMenu />
|
||||
|
||||
<GalleryImageGrid />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
|
@ -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 { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
@ -23,11 +23,23 @@ export const GalleryBulkSelect = () => {
|
||||
useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true }, [onSelectPage]);
|
||||
|
||||
if (selection.length <= 1) {
|
||||
return <Spacer />;
|
||||
return null;
|
||||
}
|
||||
|
||||
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>
|
||||
{selection.length} {t('common.selected')}
|
||||
</TagLabel>
|
@ -2,6 +2,7 @@ import { Box, Flex, Grid } from '@invoke-ai/ui-library';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { GalleryBulkSelect } from 'features/gallery/components/ImageGrid/GalleryBulkSelect';
|
||||
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { limitChanged } from 'features/gallery/store/gallerySlice';
|
||||
@ -144,6 +145,7 @@ const Content = () => {
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
<GalleryBulkSelect />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export const selectListImagesQueryArgs = createMemoizedSelector(
|
||||
offset: gallery.offset,
|
||||
limit: gallery.limit,
|
||||
is_intermediate: false,
|
||||
order_by: gallery.orderBy,
|
||||
starred_first: gallery.starredFirst,
|
||||
order_dir: gallery.orderDir,
|
||||
}
|
||||
: skipToken
|
||||
|
@ -4,7 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
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';
|
||||
|
||||
const initialGalleryState: GalleryState = {
|
||||
@ -19,7 +19,7 @@ const initialGalleryState: GalleryState = {
|
||||
boardSearchText: '',
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
orderBy: 'starred',
|
||||
starredFirst: true,
|
||||
orderDir: 'ASC',
|
||||
isImageViewerOpen: true,
|
||||
imageToCompare: null,
|
||||
@ -114,8 +114,8 @@ export const gallerySlice = createSlice({
|
||||
shouldShowArchivedBoardsChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowArchivedBoards = action.payload;
|
||||
},
|
||||
orderByChanged: (state, action: PayloadAction<OrderBy>) => {
|
||||
state.orderBy = action.payload;
|
||||
starredFirstChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.starredFirst = action.payload;
|
||||
},
|
||||
orderDirChanged: (state, action: PayloadAction<OrderDir>) => {
|
||||
state.orderDir = action.payload;
|
||||
@ -142,8 +142,9 @@ export const {
|
||||
comparisonModeCycled,
|
||||
offsetChanged,
|
||||
limitChanged,
|
||||
orderByChanged,
|
||||
orderDirChanged,
|
||||
starredFirstChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export const selectGallerySlice = (state: RootState) => state.gallery;
|
||||
|
@ -8,7 +8,6 @@ export type GalleryView = 'images' | 'assets';
|
||||
export type BoardId = 'none' | (string & Record<never, never>);
|
||||
export type ComparisonMode = 'slider' | 'side-by-side' | 'hover';
|
||||
export type ComparisonFit = 'contain' | 'fill';
|
||||
export type OrderBy = 'created_at' | 'starred';
|
||||
export type OrderDir = 'ASC' | 'DESC';
|
||||
|
||||
export type GalleryState = {
|
||||
@ -22,7 +21,7 @@ export type GalleryState = {
|
||||
boardSearchText: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
orderBy: OrderBy;
|
||||
starredFirst: boolean;
|
||||
orderDir: OrderDir;
|
||||
alwaysShowImageSizeBadge: boolean;
|
||||
imageToCompare: ImageDTO | null;
|
||||
|
Loading…
Reference in New Issue
Block a user