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;