From 406b8c69d976100cc640be799f74fd570c3d5077 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 25 Feb 2026 03:57:33 -0700 Subject: [PATCH 1/4] feat(stage): scaffold t2i stage tab --- invokeai/frontend/web/public/locales/en.json | 3 +- .../src/common/components/Picker/Picker.tsx | 7 +- .../frontend/web/src/common/hooks/focus.ts | 1 + .../StageViewer/BoardsSwitcherDropdown.tsx | 193 ++++++++++++++++++ .../components/StageViewer/StageViewer.tsx | 33 +++ .../StageViewer/StageViewerGridView.tsx | 7 + .../StageViewer/StageViewerLinearView.tsx | 7 + .../StageViewer/StageViewerPanel.tsx | 9 + .../StageViewer/StageViewerToolbar.tsx | 21 ++ .../StageViewerToolbarToggleView.tsx | 41 ++++ .../gallery/components/StageViewer/common.ts | 3 + .../ui/layouts/DockViewTabBoardExhibit.tsx | 3 + .../ui/layouts/generate-tab-auto-layout.tsx | 45 +++- .../src/features/ui/layouts/navigation-api.ts | 49 +++++ .../web/src/features/ui/layouts/shared.ts | 13 +- .../web/src/features/ui/store/uiSelectors.ts | 2 + .../web/src/features/ui/store/uiSlice.ts | 5 + .../web/src/features/ui/store/uiTypes.ts | 3 + 18 files changed, 441 insertions(+), 4 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewer.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerPanel.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts create mode 100644 invokeai/frontend/web/src/features/ui/layouts/DockViewTabBoardExhibit.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 99685323030..aabcabfa1ba 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2886,7 +2886,8 @@ "launchpad": "Launchpad", "workflowEditor": "Workflow Editor", "imageViewer": "Viewer", - "canvas": "Canvas" + "canvas": "Canvas", + "stageViewer": "Stage" }, "launchpad": { "workflowsTitle": "Go deep with Workflows.", diff --git a/invokeai/frontend/web/src/common/components/Picker/Picker.tsx b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx index ffd0b30242a..f6ac6dd68bd 100644 --- a/invokeai/frontend/web/src/common/components/Picker/Picker.tsx +++ b/invokeai/frontend/web/src/common/components/Picker/Picker.tsx @@ -206,6 +206,10 @@ type PickerProps = { * Whether the picker should be searchable. If true, renders a search input. */ searchable?: boolean; + /** + * An initial search term to populate the search input with. + */ + searchTerm?: string; /** * Initial state for group toggles. If provided, groups will start with these states instead of all being disabled. */ @@ -535,6 +539,7 @@ export const Picker = typedMemo((props: PickerProps) => { OptionComponent = DefaultOptionComponent, NextToSearchBar, searchable, + searchTerm = '', initialGroupStates, } = props; const rootRef = useRef(null); @@ -553,7 +558,7 @@ export const Picker = typedMemo((props: PickerProps) => { const $filteredOptionsCount = useComputed([$flattenedFilteredOptions], (options) => options.length); const $hasFilteredOptions = useComputed([$filteredOptionsCount], (count) => count > 0); const $selectedItem = useAtom(undefined); - const $searchTerm = useAtom(''); + const $searchTerm = useAtom(searchTerm); const $selectedItemId = useComputed([$selectedItem], (item) => (item ? getOptionId(item) : undefined)); const selectIsCompactView = useMemo(() => buildSelectIsCompactView(pickerId), [pickerId]); diff --git a/invokeai/frontend/web/src/common/hooks/focus.ts b/invokeai/frontend/web/src/common/hooks/focus.ts index 4e093c5c631..cca0ee143c8 100644 --- a/invokeai/frontend/web/src/common/hooks/focus.ts +++ b/invokeai/frontend/web/src/common/hooks/focus.ts @@ -30,6 +30,7 @@ const log = logger('system'); const REGION_NAMES = [ 'launchpad', 'viewer', + 'stage', 'gallery', 'boards', 'layers', diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx new file mode 100644 index 00000000000..83edaac7c9b --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx @@ -0,0 +1,193 @@ +import { + type BoxProps, + Button, + Flex, + Image, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + Portal, + Spacer, + type SystemStyleObject, + Text, +} from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { getRegex, Picker, usePickerContext } from 'common/components/Picker/Picker'; +import { useDisclosure } from 'common/hooks/useBoolean'; +import { + selectAutoAddBoardId, + selectAutoAssignBoardOnClick, + selectBoardSearchText, + selectListBoardsQueryArgs, + selectSelectedBoardId, +} from 'features/gallery/store/gallerySelectors'; +import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice'; +import { memo, useCallback, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; +import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; +import { useGetImageDTOQuery } from 'services/api/endpoints/images'; +import type { BoardDTO } from 'services/api/types'; + +const isMatch = (board: BoardDTO, searchTerm: string) => { + const regex = getRegex(searchTerm); + const testString = `${board.board_name}`.toLowerCase(); + + if (testString.includes(searchTerm) || regex.test(testString)) { + return true; + } + + return false; +}; + +export const BoardsDropdown = memo(() => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const popover = useDisclosure(false); + const pickerRef = useRef(null); + + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); + const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick); + + const boardSearchText = useAppSelector(selectBoardSearchText); + const selectedBoardId = useAppSelector(selectSelectedBoardId); + const queryArgs = useAppSelector(selectListBoardsQueryArgs); + + const { data: boards } = useListAllBoardsQuery(queryArgs); + + const selectedBoardData = boards?.find((board) => board.board_id === selectedBoardId); + + const onClose = useCallback(() => { + popover.close(); + }, [popover]); + + const getOptionId = useCallback((option: BoardDTO) => option.board_id, []); + + const onOptionSelect = useCallback( + (option: BoardDTO) => { + if (selectedBoardId !== option.board_id) { + dispatch(boardIdSelected({ boardId: option.board_id })); + } + if (autoAssignBoardOnClick && autoAddBoardId !== option.board_id) { + dispatch(autoAddBoardIdChanged(option.board_id)); + } + }, + [selectedBoardId, autoAssignBoardOnClick, autoAddBoardId, dispatch] + ); + + return ( + + + + + + + + + + + + + + ); +}); + +BoardsDropdown.displayName = 'BoardsDropdown'; + +const optionSx: SystemStyleObject = { + p: 1, + gap: 2, + alignItems: 'center', + cursor: 'pointer', + borderRadius: 'base', + '&[data-selected="true"]': { + bg: 'invokeBlue.300', + color: 'base.900', + '.extra-info': { + color: 'base.700', + }, + '.picker-option': { + fontWeight: 'bold', + '&[data-is-compact="true"]': { + fontWeight: 'semibold', + }, + }, + '&[data-active="true"]': { + bg: 'invokeBlue.250', + }, + }, + '&[data-active="true"]': { + bg: 'base.750', + }, + '&[data-disabled="true"]': { + cursor: 'not-allowed', + opacity: 0.5, + }, + '&[data-is-compact="true"]': { + px: 1, + py: 0.5, + + '& img': { + w: 8, + h: 8, + }, + }, + scrollMarginTop: '24px', // magic number, this is the height of the header +}; + +const BoardsSwitcherOptionComponent = memo( + ({ + option, + ...rest + }: { + option: BoardDTO; + } & BoxProps) => { + const { board_name, cover_image_name } = option; + const { currentData: coverImage } = useGetImageDTOQuery(cover_image_name ?? skipToken); + const { isCompactView } = usePickerContext(); + + return ( + + + + {board_name} + + + ); + } +); + +BoardsSwitcherOptionComponent.displayName = 'BoardsSwitcherOptionComponent'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewer.tsx new file mode 100644 index 00000000000..4d1f1778f57 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewer.tsx @@ -0,0 +1,33 @@ +import { Divider, Flex, type SystemStyleObject } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectStageViewerMode } from 'features/ui/store/uiSelectors'; +import { memo } from 'react'; + +import { StageViewerGridView } from './StageViewerGridView'; +import { StageViewerLinearView } from './StageViewerLinearView'; +import { StageViewerToolbar } from './StageViewerToolbar'; + +const StageViewerSx: SystemStyleObject = { + flexDir: 'column', + w: 'full', + h: 'full', + overflow: 'hidden', + gap: 2, + position: 'relative', +}; + +export const StageViewer = memo(() => { + const viewMode = useAppSelector(selectStageViewerMode); + + return ( + + + + + {viewMode === 'grid' ? : viewMode === 'linear' ? : null} + + + ); +}); + +StageViewer.displayName = 'StageViewer'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx new file mode 100644 index 00000000000..3eef593232a --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx @@ -0,0 +1,7 @@ +import { memo } from 'react'; + +export const StageViewerGridView = memo(() => { + return
StageViewerGridView
; +}); + +StageViewerGridView.displayName = 'StageViewerGridView'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx new file mode 100644 index 00000000000..fa89add7cc3 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx @@ -0,0 +1,7 @@ +import { memo } from 'react'; + +export const StageViewerLinearView = memo(() => { + return
StageViewerLinearView
; +}); + +StageViewerLinearView.displayName = 'StageViewerLinearView'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerPanel.tsx new file mode 100644 index 00000000000..5b57a512bed --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerPanel.tsx @@ -0,0 +1,9 @@ +import { memo } from 'react'; + +import { StageViewer } from './StageViewer'; + +export const StageViewerPanel = memo(() => { + return ; +}); + +StageViewerPanel.displayName = 'StageViewerPanel'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx new file mode 100644 index 00000000000..16cc0108374 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx @@ -0,0 +1,21 @@ +import { Flex, Spacer, Text } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { BoardsDropdown } from './BoardsSwitcherDropdown'; +import { StageViewerToggleView } from './StageViewerToolbarToggleView'; + +export const StageViewerToolbar = memo(() => { + const { t } = useTranslation(); + + return ( + + {t('common.board')} + + + + + ); +}); + +StageViewerToolbar.displayName = 'StageViewerToolbar'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx new file mode 100644 index 00000000000..839803f08d1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx @@ -0,0 +1,41 @@ +import { Flex, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectStageViewerMode } from 'features/ui/store/uiSelectors'; +import { stageViewerModeChanged } from 'features/ui/store/uiSlice'; +import { memo, useCallback } from 'react'; +import { PiGridFour, PiListBold } from 'react-icons/pi'; + +export const StageViewerToggleView = memo(() => { + const viewMode = useAppSelector(selectStageViewerMode); + const dispatch = useAppDispatch(); + + const onSelect = useCallback( + (mode: 'grid' | 'linear') => { + dispatch(stageViewerModeChanged(mode)); + }, + [dispatch] + ); + + return ( + + } + /> + } + /> + + ); +}); + +StageViewerToggleView.displayName = 'StageViewerToggleView'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts new file mode 100644 index 00000000000..b362bdd7332 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const zStageViewerMode = z.enum(['grid', 'linear']); diff --git a/invokeai/frontend/web/src/features/ui/layouts/DockViewTabBoardExhibit.tsx b/invokeai/frontend/web/src/features/ui/layouts/DockViewTabBoardExhibit.tsx new file mode 100644 index 00000000000..e7eaad81630 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/DockViewTabBoardExhibit.tsx @@ -0,0 +1,3 @@ +export function DockViewTabBoardExhibit() {} + +DockViewTabBoardExhibit.displayName = 'DockViewTabBoardExhibit'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx index e60c15b5da3..7c0e61b249a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx +++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx @@ -3,6 +3,7 @@ import { DockviewReact, GridviewReact, LayoutPriority, Orientation } from 'dockv import { BoardsPanel } from 'features/gallery/components/BoardsListPanelContent'; import { GalleryPanel } from 'features/gallery/components/GalleryPanel'; import { ImageViewerPanel } from 'features/gallery/components/ImageViewer/ImageViewerPanel'; +import { StageViewerPanel } from 'features/gallery/components/StageViewer/StageViewerPanel'; import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons'; import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons'; import type { @@ -16,7 +17,7 @@ import { AutoLayoutProvider, useAutoLayoutContext, withPanelContainer } from 'fe import type { TabName } from 'features/ui/store/uiTypes'; import { dockviewTheme } from 'features/ui/styles/theme'; import { t } from 'i18next'; -import { memo, useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { DockviewTab } from './DockviewTab'; import { DockviewTabLaunchpad } from './DockviewTabLaunchpad'; @@ -42,6 +43,7 @@ import { RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX, SETTINGS_PANEL_ID, + STAGE_PANEL_ID, VIEWER_PANEL_ID, } from './shared'; @@ -54,10 +56,12 @@ const tabComponents = { const mainPanelComponents: AutoLayoutDockviewComponents = { [LAUNCHPAD_PANEL_ID]: withPanelContainer(GenerateLaunchpadPanel), [VIEWER_PANEL_ID]: withPanelContainer(ImageViewerPanel), + [STAGE_PANEL_ID]: withPanelContainer(StageViewerPanel), }; const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => { navigationApi.registerContainer(tab, 'main', api, () => { + // Launchpad Tab const launchpad = api.addPanel({ id: LAUNCHPAD_PANEL_ID, component: LAUNCHPAD_PANEL_ID, @@ -70,6 +74,7 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => { }, }); + // Image Viewer Tab api.addPanel({ id: VIEWER_PANEL_ID, component: VIEWER_PANEL_ID, @@ -86,16 +91,54 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => { }, }); + // Stage Viewer Tab + api.addPanel({ + id: STAGE_PANEL_ID, + component: STAGE_PANEL_ID, + title: t('ui.panels.stageViewer'), + tabComponent: DOCKVIEW_TAB_PROGRESS_ID, + params: { + tab, + focusRegion: 'stage', + i18nKey: 'ui.panels.stageViewer', + }, + position: { + direction: 'within', + referencePanel: launchpad.id, + }, + }); + launchpad.api.setActive(); }); }; const MainPanel = memo(() => { const { tab } = useAutoLayoutContext(); + const wasRightPanelCollapsed = useRef(false); const onReady = useCallback( ({ api }) => { initializeMainPanelLayout(tab, api); + + // Automatically collapse right panel when switching to Stage Viewer Tab. + // TODO: Hide the right panel completely instead of collapsing it, to avoid duplicating it's functions. + // TODO: Fix window losing focus resulting in 'Right panel not found in active tab "generate"' error. + api.onDidActivePanelChange((panel) => { + if (!panel) { + return; + } + + if (panel.id === STAGE_PANEL_ID) { + wasRightPanelCollapsed.current = navigationApi.isRightPanelCollapsed(); + if (!wasRightPanelCollapsed.current) { + navigationApi.collapseRightPanel(); + } + } else { + if (!wasRightPanelCollapsed.current) { + navigationApi.expandRightPanel(); + } + } + }); }, [tab] ); diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts index 98866a12f94..50b1cae972a 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts @@ -442,6 +442,55 @@ export class NavigationApi { panel.api.setSize({ width: 0 }); }; + /** + * Collapse the right panel in the currently active tab. + */ + collapseRightPanel = (): boolean => { + const activeTab = this._app?.activeTab.get() ?? null; + if (!activeTab) { + log.warn('No active tab found to collapse right panel'); + return false; + } + + const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID); + if (!rightPanel) { + log.warn(`Right panel not found in active tab "${activeTab}"`); + return false; + } + + if (!(rightPanel instanceof GridviewPanel)) { + log.error(`Right panels must be instances of GridviewPanel`); + return false; + } + + this._collapsePanel(rightPanel); + return true; + }; + + /** + * Check if the right panel in the currently active tab is collapsed. + */ + isRightPanelCollapsed = (): boolean => { + const activeTab = this._app?.activeTab.get() ?? null; + if (!activeTab) { + log.warn('No active tab found to check right panel state'); + return false; + } + + const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID); + if (!rightPanel) { + log.warn(`Right panel not found in active tab "${activeTab}"`); + return false; + } + + if (!(rightPanel instanceof GridviewPanel)) { + log.error(`Right panels must be instances of GridviewPanel`); + return false; + } + + return rightPanel.width === 0; + }; + /** * Get a panel by its tab and ID. * diff --git a/invokeai/frontend/web/src/features/ui/layouts/shared.ts b/invokeai/frontend/web/src/features/ui/layouts/shared.ts index efb17037ee8..0dc7d96999c 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/shared.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/shared.ts @@ -1,20 +1,31 @@ +// Panel IDs export const LEFT_PANEL_ID = 'left'; export const MAIN_PANEL_ID = 'main'; export const RIGHT_PANEL_ID = 'right'; +// Generic Tab Panels export const LAUNCHPAD_PANEL_ID = 'launchpad'; -export const WORKSPACE_PANEL_ID = 'workspace'; export const VIEWER_PANEL_ID = 'viewer'; +// Generate Tab Panels +export const STAGE_PANEL_ID = 'stage'; + +// Workspace Tab Panels +export const WORKSPACE_PANEL_ID = 'workspace'; + +// Canvas Tab Panels export const BOARDS_PANEL_ID = 'boards'; export const GALLERY_PANEL_ID = 'gallery'; export const LAYERS_PANEL_ID = 'layers'; +// Settings Tab Panels export const SETTINGS_PANEL_ID = 'settings'; +// Model Manager Tab Panels export const MODELS_PANEL_ID = 'models'; export const QUEUE_PANEL_ID = 'queue'; +// Tab IDs export const DOCKVIEW_TAB_ID = 'tab-default'; export const DOCKVIEW_TAB_PROGRESS_ID = 'tab-progress'; export const DOCKVIEW_TAB_LAUNCHPAD_ID = 'tab-launchpad'; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts index 4287a8a08b3..fe5942652eb 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts @@ -5,4 +5,6 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa export const selectShouldShowItemDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowItemDetails); export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer); export const selectShouldUsePagedGalleryView = createSelector(selectUiSlice, (ui) => ui.shouldUsePagedGalleryView); +// TODO: One day maybe this'll be by board or something? +export const selectStageViewerMode = createSelector(selectUiSlice, (ui) => ui.stageViewerMode); export const selectPickerCompactViewStates = createSelector(selectUiSlice, (ui) => ui.pickerCompactViewStates); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 67036928a96..de75b27d18c 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -6,6 +6,7 @@ import { isPlainObject } from 'es-toolkit'; import { assert } from 'tsafe'; import { getInitialUIState, type UIState, zUIState } from './uiTypes'; +import { selectStageViewerMode } from './uiSelectors'; const slice = createSlice({ name: 'ui', @@ -70,6 +71,9 @@ const slice = createSlice({ shouldShowNotificationChanged: (state, action: PayloadAction) => { state.shouldShowNotificationV2 = action.payload; }, + stageViewerModeChanged: (state, action: PayloadAction) => { + state.stageViewerMode = action.payload; + }, pickerCompactViewStateChanged: (state, action: PayloadAction<{ pickerId: string; isCompact: boolean }>) => { state.pickerCompactViewStates[action.payload.pickerId] = action.payload.isCompact; }, @@ -86,6 +90,7 @@ export const { shouldShowNotificationChanged, textAreaSizesStateChanged, dockviewStorageKeyChanged, + stageViewerModeChanged, pickerCompactViewStateChanged, } = slice.actions; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 4e6e851ed1f..c1bc4143e8e 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,4 +1,5 @@ import { isPlainObject } from 'es-toolkit'; +import { zStageViewerMode } from 'features/gallery/components/StageViewer/common'; import { z } from 'zod'; const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']); @@ -23,6 +24,7 @@ export const zUIState = z.object({ textAreaSizes: z.record(z.string(), zPartialDimensions), panels: z.record(z.string(), zSerializable), shouldShowNotificationV2: z.boolean(), + stageViewerMode: zStageViewerMode, pickerCompactViewStates: z.record(z.string(), z.boolean()), }); export type UIState = z.infer; @@ -37,5 +39,6 @@ export const getInitialUIState = (): UIState => ({ textAreaSizes: {}, panels: {}, shouldShowNotificationV2: true, + stageViewerMode: 'grid', pickerCompactViewStates: {}, }); From 1f0576c126a35252819597f2c92b5ca689bc434b Mon Sep 17 00:00:00 2001 From: joshistoast Date: Thu, 26 Feb 2026 17:53:16 -0700 Subject: [PATCH 2/4] feat(stage): translations and ui tweaks --- invokeai/frontend/web/public/locales/en.json | 4 +++ .../StageViewer/StageViewerToolbar.tsx | 2 +- .../StageViewerToolbarToggleView.tsx | 25 +++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index aabcabfa1ba..65d80a298cf 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -431,6 +431,10 @@ "move": "Move", "useForPromptGeneration": "Use for Prompt Generation" }, + "stageViewer": { + "gridView": "Grid View", + "linearView": "Linear View" + }, "hotkeys": { "hotkeys": "Hotkeys", "searchHotkeys": "Search Hotkeys", diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx index 16cc0108374..29b2caaecc3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbar.tsx @@ -9,7 +9,7 @@ export const StageViewerToolbar = memo(() => { const { t } = useTranslation(); return ( - + {t('common.board')} diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx index 839803f08d1..a55b284c357 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerToolbarToggleView.tsx @@ -1,11 +1,13 @@ -import { Flex, IconButton } from '@invoke-ai/ui-library'; +import { Divider, Flex, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectStageViewerMode } from 'features/ui/store/uiSelectors'; import { stageViewerModeChanged } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { PiGridFour, PiListBold } from 'react-icons/pi'; export const StageViewerToggleView = memo(() => { + const { t } = useTranslation(); const viewMode = useAppSelector(selectStageViewerMode); const dispatch = useAppDispatch(); @@ -17,22 +19,29 @@ export const StageViewerToggleView = memo(() => { ); return ( - + } size="sm" - variant="ghost" + aria-label={t('stageViewer.gridView')} + tooltip={t('stageViewer.gridView')} + variant="link" colorScheme={viewMode === 'grid' ? 'blue' : 'base.500'} - aria-label="grid" onClick={onSelect.bind(null, 'grid')} - icon={} + alignSelf="stretch" /> + + + } size="sm" - variant="ghost" + aria-label={t('stageViewer.linearView')} + tooltip={t('stageViewer.linearView')} + variant="link" colorScheme={viewMode === 'linear' ? 'blue' : 'base.500'} - aria-label="linear" onClick={onSelect.bind(null, 'linear')} - icon={} + alignSelf="stretch" /> ); From c922b41ebf18a095b41a2629de5c95d282aff7af Mon Sep 17 00:00:00 2001 From: joshistoast Date: Thu, 26 Feb 2026 20:52:14 -0700 Subject: [PATCH 3/4] feat(stage): scaffold board image and queue feeds --- .../StageViewer/BoardsSwitcherDropdown.tsx | 1 + .../components/StageViewer/StageViewer.tsx | 6 +- .../StageViewer/StageViewerGridItem.tsx | 5 ++ .../StageViewer/StageViewerGridView.tsx | 33 ++++++++- .../StageViewer/StageViewerLinearItem.tsx | 5 ++ .../StageViewer/StageViewerLinearView.tsx | 25 ++++++- .../gallery/components/StageViewer/common.ts | 26 +++++++ .../gallery/components/StageViewer/styles.ts | 1 + .../features/gallery/hooks/useStageFeed.ts | 68 +++++++++++++++++++ 9 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/StageViewer/styles.ts create mode 100644 invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx index 83edaac7c9b..b042b8ceb6e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/BoardsSwitcherDropdown.tsx @@ -80,6 +80,7 @@ export const BoardsDropdown = memo(() => { ); return ( + // TODO: Add 'Uncategorized' Board Option { const viewMode = useAppSelector(selectStageViewerMode); + const {feed} = useStageFeed(); return ( - - {viewMode === 'grid' ? : viewMode === 'linear' ? : null} + + {viewMode === 'grid' ? : viewMode === 'linear' ? : null} ); diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx new file mode 100644 index 00000000000..3db64bbd3d3 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx @@ -0,0 +1,5 @@ +import { memo } from "react"; + +export const StageViewerGridItem = memo(() => { }); + +StageViewerGridItem.displayName = 'StageViewerGridItem'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx index 3eef593232a..12bc0934c8a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx @@ -1,7 +1,36 @@ +import { Button, Flex, Text } from '@invoke-ai/ui-library'; +import { useStageFeedPagination } from 'features/gallery/hooks/useStageFeed'; import { memo } from 'react'; -export const StageViewerGridView = memo(() => { - return
StageViewerGridView
; +import type { StageViewProps } from './common'; + +/** + * TODO: + * + * - Use Colcade for masonry layout + */ + +export const StageViewerGridView = memo(({ feed }: StageViewProps) => { + const { visibleFeed, loadMore, hasMore, isLoading } = useStageFeedPagination(feed); + + return ( + + + {visibleFeed.map((item, index) => ( + + {item.type} + + ))} + + {hasMore && ( + + + + )} + + ); }); StageViewerGridView.displayName = 'StageViewerGridView'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx new file mode 100644 index 00000000000..ebc8e261a26 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx @@ -0,0 +1,5 @@ +import { memo } from "react"; + +export const StageViewerLinearItem = memo(() => { }); + +StageViewerLinearItem.displayName = 'StageViewerLinearItem'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx index fa89add7cc3..63a14a4040e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx @@ -1,7 +1,28 @@ +import { Button, Flex, Text } from '@invoke-ai/ui-library'; +import { useStageFeedPagination } from 'features/gallery/hooks/useStageFeed'; import { memo } from 'react'; -export const StageViewerLinearView = memo(() => { - return
StageViewerLinearView
; +import type { StageViewProps } from './common'; + +export const StageViewerLinearView = memo(({ feed }: StageViewProps) => { + const { visibleFeed, loadMore, hasMore, isLoading } = useStageFeedPagination(feed); + + return ( + + + {visibleFeed.map((item, index) => ( + + {item.type} + + ))} + + {hasMore && ( + + )} + + ); }); StageViewerLinearView.displayName = 'StageViewerLinearView'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts index b362bdd7332..77c0582bcf0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts @@ -1,3 +1,29 @@ import { z } from 'zod'; export const zStageViewerMode = z.enum(['grid', 'linear']); + +export const STAGE_FEED_QUEUE_ITEM_STATUS = ['pending', 'in_progress', 'failed'] as const; + +export type StageFeedQueueItemStatus = typeof STAGE_FEED_QUEUE_ITEM_STATUS[number]; + +type StageFeedQueueItem = { + type: 'queue_item'; + id: number; + status: StageFeedQueueItemStatus; + createdAt: string; +} + +type StageFeedBoardItem = { + type: 'board_item'; + id: string; // id is the image name +} + +export type StageFeedItem = + | StageFeedQueueItem + | StageFeedBoardItem + +export type StageFeed = StageFeedItem[]; + +export type StageViewProps = { + feed: StageFeed; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/styles.ts b/invokeai/frontend/web/src/features/gallery/components/StageViewer/styles.ts new file mode 100644 index 00000000000..f6de561ece0 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/styles.ts @@ -0,0 +1 @@ +import type { SystemStyleObject } from "@invoke-ai/ui-library"; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts b/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts new file mode 100644 index 00000000000..9968e81d692 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts @@ -0,0 +1,68 @@ +import { STAGE_FEED_QUEUE_ITEM_STATUS, type StageFeed, type StageFeedQueueItemStatus } from 'features/gallery/components/StageViewer/common'; +import { useGalleryImageNames } from 'features/gallery/components/use-gallery-image-names'; +import { useCallback, useMemo, useState } from 'react'; +import { queueApi } from 'services/api/endpoints/queue'; + +const PAGE_SIZE = 20; + +export const useStageFeed = () => { + const { imageNames, isLoading: isGalleryLoading } = useGalleryImageNames(); + const { data: queueItems } = queueApi.endpoints.listAllQueueItems.useQuery({ + destination: 'generate', + }) + + const feed = useMemo(() => { + const activeQueueItems = (queueItems ?? []).filter((item) => STAGE_FEED_QUEUE_ITEM_STATUS.includes(item.status as StageFeedQueueItemStatus)); + + const mappedQueueItems = activeQueueItems.map((item) => ({ + type: 'queue_item' as const, + id: item.item_id, + status: item.status as StageFeedQueueItemStatus, + createdAt: item.created_at, + })); + + + const mappedBoardItems = (imageNames ?? []).map((name) => ({ + type: 'board_item' as const, + id: name, + })); + + return [ + ...mappedQueueItems, + ...mappedBoardItems, + ] + }, [imageNames, queueItems]); + + return { + feed, + isLoading: isGalleryLoading, + }; +} + +export const useStageFeedPagination = (fullFeed: StageFeed) => { + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + const visibleFeed = useMemo(() => { + return fullFeed.slice(0, page * PAGE_SIZE); + }, [fullFeed, page]); + + const hasMore = useMemo(() => { + return visibleFeed.length < fullFeed.length; + }, [visibleFeed, fullFeed]); + + const loadMore = useCallback(() => { + if (hasMore) { + setIsLoading(true); + setPage((p) => p + 1); + } + setIsLoading(false); + }, [hasMore]) + + return { + hasMore, + loadMore, + isLoading, + visibleFeed, + } +} From 9a5ee6a2a383ffae681be737e5ce5de6c0d9a385 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Thu, 26 Feb 2026 23:35:59 -0700 Subject: [PATCH 4/4] feat(stage): basic grid items --- .../StageViewer/StageViewerGridItem.tsx | 59 ++++++++++++++++++- .../StageViewer/StageViewerGridView.tsx | 19 +++--- .../StageViewer/StageViewerLinearItem.tsx | 25 +++++++- .../StageViewer/StageViewerLinearView.tsx | 15 +++-- .../gallery/components/StageViewer/common.ts | 8 ++- .../features/gallery/hooks/useStageFeed.ts | 8 ++- 6 files changed, 110 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx index 3db64bbd3d3..3e862d59e5c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridItem.tsx @@ -1,5 +1,60 @@ +import { Flex, Image, Spinner } from "@invoke-ai/ui-library"; import { memo } from "react"; +import { imagesApi } from "services/api/endpoints/images"; -export const StageViewerGridItem = memo(() => { }); +import type { StageFeedBoardItem, StageFeedQueueItem } from "./common"; -StageViewerGridItem.displayName = 'StageViewerGridItem'; +export const StageViewerGridBoardItem = memo(({ item }: { item: StageFeedBoardItem }) => { + const imageName = item.id; + const { currentData: imageDTO, isUninitialized } = imagesApi.endpoints.getImageDTO.useQueryState(imageName); + imagesApi.endpoints.getImageDTO.useQuerySubscription(imageName, { skip: isUninitialized }); + + const aspectRatio = imageDTO ? imageDTO.width / imageDTO.height : 1; + + return ( + + + {imageDTO && ( + {imageDTO.image_name} + )} + + + + {imageDTO ? imageDTO.image_name : 'Loading...'} + + + ); +}); + +StageViewerGridBoardItem.displayName = 'StageViewerGridBoardItem'; + +export const StageViewerGridQueueItem = memo(({ item }: {item: StageFeedQueueItem}) => { + const queueItemId = item.id; + + return ( + + + + + + Queue Item: {queueItemId} + + + ); +}); + +StageViewerGridQueueItem.displayName = 'StageViewerGridQueueItem'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx index 12bc0934c8a..045f5001d40 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerGridView.tsx @@ -1,8 +1,9 @@ -import { Button, Flex, Text } from '@invoke-ai/ui-library'; +import { Box, Button, Flex } from '@invoke-ai/ui-library'; import { useStageFeedPagination } from 'features/gallery/hooks/useStageFeed'; import { memo } from 'react'; import type { StageViewProps } from './common'; +import { StageViewerGridBoardItem, StageViewerGridQueueItem } from './StageViewerGridItem' /** * TODO: @@ -14,18 +15,16 @@ export const StageViewerGridView = memo(({ feed }: StageViewProps) => { const { visibleFeed, loadMore, hasMore, isLoading } = useStageFeedPagination(feed); return ( - - + + {visibleFeed.map((item, index) => ( - - {item.type} - + item.type === 'board_item' ? : ))} - + + + {/* Pagination */} {hasMore && ( - + )} diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx index ebc8e261a26..d117e6b1828 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearItem.tsx @@ -1,5 +1,26 @@ +import { Flex } from "@invoke-ai/ui-library"; import { memo } from "react"; -export const StageViewerLinearItem = memo(() => { }); +import type { StageFeedBoardItem } from './common'; -StageViewerLinearItem.displayName = 'StageViewerLinearItem'; +export const StageViewerLinearBoardItem = memo(({ item }: { item: StageFeedBoardItem }) => { + return ( + + Board Item: {item.id} + + ); +}); + +StageViewerLinearBoardItem.displayName = 'StageViewerLinearBoardItem'; + +// TODO + +export const StageViewerLinearQueueItem = memo(() => { + return ( + + Loading... + + ) +}); + +StageViewerLinearQueueItem.displayName = 'StageViewerLinearQueueItem'; diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx index 63a14a4040e..260cf7cb56c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/StageViewerLinearView.tsx @@ -1,8 +1,15 @@ -import { Button, Flex, Text } from '@invoke-ai/ui-library'; +import { Button, Flex } from '@invoke-ai/ui-library'; import { useStageFeedPagination } from 'features/gallery/hooks/useStageFeed'; import { memo } from 'react'; import type { StageViewProps } from './common'; +import { StageViewerLinearBoardItem, StageViewerLinearQueueItem } from './StageViewerLinearItem' + +/** + * TODO: + * + * - Refactor this to make parameters the prominent list item, accompanied by images sharing them in their generation details + */ export const StageViewerLinearView = memo(({ feed }: StageViewProps) => { const { visibleFeed, loadMore, hasMore, isLoading } = useStageFeedPagination(feed); @@ -11,11 +18,7 @@ export const StageViewerLinearView = memo(({ feed }: StageViewProps) => { {visibleFeed.map((item, index) => ( - - {item.type} - + item.type === 'board_item' ? : ))} {hasMore && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts index 77c0582bcf0..dfc43d5d953 100644 --- a/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts +++ b/invokeai/frontend/web/src/features/gallery/components/StageViewer/common.ts @@ -6,16 +6,18 @@ export const STAGE_FEED_QUEUE_ITEM_STATUS = ['pending', 'in_progress', 'failed'] export type StageFeedQueueItemStatus = typeof STAGE_FEED_QUEUE_ITEM_STATUS[number]; -type StageFeedQueueItem = { +export type StageFeedQueueItem = { type: 'queue_item'; + /** queue id */ id: number; status: StageFeedQueueItemStatus; createdAt: string; } -type StageFeedBoardItem = { +export type StageFeedBoardItem = { type: 'board_item'; - id: string; // id is the image name + /** image_name */ + id: string; } export type StageFeedItem = diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts b/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts index 9968e81d692..66efda846fb 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useStageFeed.ts @@ -3,7 +3,7 @@ import { useGalleryImageNames } from 'features/gallery/components/use-gallery-im import { useCallback, useMemo, useState } from 'react'; import { queueApi } from 'services/api/endpoints/queue'; -const PAGE_SIZE = 20; +const PAGE_SIZE = 50; export const useStageFeed = () => { const { imageNames, isLoading: isGalleryLoading } = useGalleryImageNames(); @@ -39,6 +39,12 @@ export const useStageFeed = () => { }; } +/** + * TODO: + * + * - Tie the page to global state so that it persists across view mode changes and unmounts + */ + export const useStageFeedPagination = (fullFeed: StageFeed) => { const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(false);