From 43ec49e3bc68db60914d35f1274ca3ef1ba2d07f Mon Sep 17 00:00:00 2001 From: Robert White <159860618+Realystic1@users.noreply.github.com> Date: Fri, 6 Feb 2026 07:07:12 -0800 Subject: [PATCH] Add slideshow tab settings for boards --- invokeai/frontend/web/public/locales/en.json | 3 ++ .../gallery/components/GalleryPanel.tsx | 21 +++++++++- .../Slideshow/GallerySlideshowPanel.tsx | 40 +++++++++++++++++++ .../gallery/store/gallerySelectors.ts | 10 +++-- .../features/gallery/store/gallerySlice.ts | 5 +++ .../web/src/features/gallery/store/types.ts | 3 +- 6 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/Slideshow/GallerySlideshowPanel.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ddd5e535098..599423074ec 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -358,6 +358,7 @@ "gallery": "Gallery", "images": "Images", "assets": "Assets", + "slideshow": "Slideshow", "alwaysShowImageSizeBadge": "Always Show Image Size Badge", "assetsTab": "Files you've uploaded for use in your projects.", "autoAssignBoardOnClick": "Auto-Assign Board on Click", @@ -387,6 +388,8 @@ "loading": "Loading", "newestFirst": "Newest First", "oldestFirst": "Oldest First", + "slideshowDuration": "Duration (seconds)", + "slideshowTab": "Play images from this board as a slideshow.", "sortDirection": "Sort Direction", "showStarredImagesFirst": "Show Starred Images First", "usePagedGalleryView": "Use Paged Gallery View", diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx index 7c57aa08497..262fe30b938 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryPanel.tsx @@ -20,6 +20,7 @@ import { GalleryImageGridPaged } from './GalleryImageGridPaged'; import { GallerySettingsPopover } from './GallerySettingsPopover/GallerySettingsPopover'; import { GalleryUploadButton } from './GalleryUploadButton'; import { GallerySearch } from './ImageGrid/GallerySearch'; +import { GallerySlideshowPanel } from './Slideshow/GallerySlideshowPanel'; const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '100%' }; @@ -45,6 +46,10 @@ export const GalleryPanel = memo(() => { dispatch(galleryViewChanged('assets')); }, [dispatch]); + const handleClickSlideshow = useCallback(() => { + dispatch(galleryViewChanged('slideshow')); + }, [dispatch]); + const handleClickSearch = useCallback(() => { onResetSearchTerm(); if (!searchDisclosure.isOpen && galleryPanel.$isCollapsed.get()) { @@ -87,6 +92,14 @@ export const GalleryPanel = memo(() => { > {t('gallery.assets')} + @@ -113,7 +126,13 @@ export const GalleryPanel = memo(() => { - {shouldUsePagedGalleryView ? : } + {galleryView === 'slideshow' ? ( + + ) : shouldUsePagedGalleryView ? ( + + ) : ( + + )} ); diff --git a/invokeai/frontend/web/src/features/gallery/components/Slideshow/GallerySlideshowPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/Slideshow/GallerySlideshowPanel.tsx new file mode 100644 index 00000000000..2383a617c93 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/Slideshow/GallerySlideshowPanel.tsx @@ -0,0 +1,40 @@ +import { Flex, FormControl, FormLabel, NumberInput, NumberInputField } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectGallerySlice, slideshowDurationSecondsChanged } from 'features/gallery/store/gallerySlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selectSlideshowDurationSeconds = createSelector( + selectGallerySlice, + (gallery) => gallery.slideshowDurationSeconds +); + +export const GallerySlideshowPanel = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const durationSeconds = useAppSelector(selectSlideshowDurationSeconds); + + const handleDurationChange = useCallback( + (_valueAsString: string, valueAsNumber: number) => { + if (Number.isNaN(valueAsNumber)) { + return; + } + dispatch(slideshowDurationSecondsChanged(valueAsNumber)); + }, + [dispatch] + ); + + return ( + + + {t('gallery.slideshowDuration')} + + + + + + ); +}); + +GallerySlideshowPanel.displayName = 'GallerySlideshowPanel'; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index aad849fdb59..1552e88d2ef 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -21,10 +21,10 @@ export const selectAutoSwitch = createSelector(selectGallerySlice, (gallery) => export const selectSelectedBoardId = createSelector(selectGallerySlice, (gallery) => gallery.selectedBoardId); export const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView); const selectGalleryQueryCategories = createSelector(selectGalleryView, (galleryView) => { - if (galleryView === 'images') { - return IMAGE_CATEGORIES; + if (galleryView === 'assets') { + return ASSETS_CATEGORIES; } - return ASSETS_CATEGORIES; + return IMAGE_CATEGORIES; }); const selectGallerySearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm); const selectGalleryOrderDir = createSelector(selectGallerySlice, (gallery) => gallery.orderDir); @@ -56,6 +56,10 @@ export const selectBoardSearchText = createSelector(selectGallerySlice, (gallery export const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm); export const selectBoardsListOrderBy = createSelector(selectGallerySlice, (gallery) => gallery.boardsListOrderBy); export const selectBoardsListOrderDir = createSelector(selectGallerySlice, (gallery) => gallery.boardsListOrderDir); +export const selectSlideshowDurationSeconds = createSelector( + selectGallerySlice, + (gallery) => gallery.slideshowDurationSeconds +); export const selectSelectionCount = createSelector(selectGallerySlice, (gallery) => gallery.selection.length); export const selectSelection = createSelector(selectGallerySlice, (gallery) => gallery.selection); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index d66feefa2c9..8d8d37c3a3e 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -34,6 +34,7 @@ const getInitialState = (): GalleryState => ({ shouldShowArchivedBoards: false, boardsListOrderBy: 'created_at', boardsListOrderDir: 'DESC', + slideshowDurationSeconds: 5, }); const slice = createSlice({ @@ -141,6 +142,9 @@ const slice = createSlice({ boardsListOrderDirChanged: (state, action: PayloadAction) => { state.boardsListOrderDir = action.payload; }, + slideshowDurationSecondsChanged: (state, action: PayloadAction) => { + state.slideshowDurationSeconds = action.payload; + }, }, }); @@ -166,6 +170,7 @@ export const { searchTermChanged, boardsListOrderByChanged, boardsListOrderDirChanged, + slideshowDurationSecondsChanged, } = slice.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 addeefe870f..5ad2a5ed4c2 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -1,7 +1,7 @@ import type { ImageCategory } from 'services/api/types'; import z from 'zod'; -const zGalleryView = z.enum(['images', 'assets']); +const zGalleryView = z.enum(['images', 'assets', 'slideshow']); export type GalleryView = z.infer; const zBoardId = z.string(); // TS hack to get autocomplete for "none" but accept any string @@ -37,6 +37,7 @@ export const zGalleryState = z.object({ shouldShowArchivedBoards: z.boolean(), boardsListOrderBy: zBoardRecordOrderBy, boardsListOrderDir: zOrderDir, + slideshowDurationSeconds: z.number().min(1).default(5), }); export type GalleryState = z.infer;