diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 6c4a5b9988..6d3829bc71 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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", diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryMenu.tsx deleted file mode 100644 index b347980612..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryMenu.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; - -import { GalleryBulkSelect } from './GalleryBulkSelect'; -import { GallerySort } from './GallerySort'; - -export const GalleryMenu = () => { - return ( - - - - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GallerySort.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GallerySort.tsx deleted file mode 100644 index 1fb17ab050..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GallerySort.tsx +++ /dev/null @@ -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) => { - 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 ( - - - : } - aria-label="Sort" - /> - - - - - - - - - - {t('gallery.sortBy')} - - - - - - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx index 3cc01f0b28..8d50f4a309 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GallerySettingsPopover.tsx @@ -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) => { + dispatch(starredFirstChanged(e.target.checked)); + }, + [dispatch] + ); + + const orderDirOptions = useMemo( + () => [ + { value: 'DESC', label: t('gallery.newestFirst') }, + { value: 'ASC', label: t('gallery.oldestFirst') }, + ], + [t] + ); + + const onChangeOrderDir = useCallback( + (v: SingleValue) => { + 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 ( - } - /> + } /> @@ -98,7 +128,7 @@ const GallerySettingsPopover = () => { {t('gallery.autoSwitchNewImages')} - + {t('gallery.autoAssignBoardOnClick')} @@ -114,6 +144,24 @@ const GallerySettingsPopover = () => { + + + + {t('gallery.showStarredImagesFirst')} + + + + + + {t('gallery.sortDirection')} + + + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 105016539d..1ce64753df 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -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 = () => { - + diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryBulkSelect.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryBulkSelect.tsx similarity index 75% rename from invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryBulkSelect.tsx rename to invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryBulkSelect.tsx index fba5677769..b0c1eaa52c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryMenu/GalleryBulkSelect.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryBulkSelect.tsx @@ -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 ; + return null; } return ( - + {selection.length} {t('common.selected')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx index b16c13a783..d9e9216621 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageGrid.tsx @@ -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 = () => { ))} + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index f8e47c204b..832da474ca 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -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 diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 30e36c9def..24796cb329 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -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) => { state.shouldShowArchivedBoards = action.payload; }, - orderByChanged: (state, action: PayloadAction) => { - state.orderBy = action.payload; + starredFirstChanged: (state, action: PayloadAction) => { + state.starredFirst = action.payload; }, orderDirChanged: (state, action: PayloadAction) => { 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; diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index a2f62849f5..751fd5cb8e 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -8,7 +8,6 @@ export type GalleryView = 'images' | 'assets'; export type BoardId = 'none' | (string & Record); 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;