Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7c15dfe
feat(gallery): browse sparse pages across webv2
JPPhoto Oct 3, 2026
b2382ef
chore: merge origin/main into sparse page cache
JPPhoto Oct 4, 2026
c7e9b73
fix(gallery): harden sparse page caching
JPPhoto Oct 4, 2026
3af7947
fix(gallery): keep selection star state in widget owner
JPPhoto Oct 4, 2026
7065f78
fix(gallery): fence preview navigation by account epoch
JPPhoto Oct 4, 2026
4ed2fde
Merge branch 'invoke-ai:main' into webv2-sparse-page-cache
JPPhoto Oct 4, 2026
1bd2e7b
fix(gallery): expose numbered page count retry
JPPhoto Oct 4, 2026
e56ac53
Merge remote-tracking branch 'JPP/webv2-sparse-page-cache' into webv2…
JPPhoto Oct 4, 2026
99b654e
Merge remote-tracking branch 'origin/main' into webv2-sparse-page-cache
JPPhoto Oct 5, 2026
4f0c094
fix(gallery): address sparse browsing review feedback
JPPhoto Oct 5, 2026
8cde6cb
Merge branch 'main' into webv2-sparse-page-cache
JPPhoto Oct 5, 2026
1334f93
Merge branch 'main' into webv2-sparse-page-cache
JPPhoto Oct 6, 2026
3d54e7e
fix(gallery): preserve sparse selection position
JPPhoto Oct 6, 2026
97eec7a
fix(gallery): resolve sparse pagination review findings
JPPhoto Oct 8, 2026
44d6258
Merge origin/main into sparse page cache
JPPhoto Oct 8, 2026
4c0eafd
fix(webv2): resolve relocated preview predecessors
JPPhoto Oct 8, 2026
4bfa9de
merge: integrate origin/main into sparse gallery branch
JPPhoto Oct 8, 2026
3cd816d
fix(gallery): preserve sparse navigation recovery
JPPhoto Oct 8, 2026
45c8fdd
Merge branch 'main' into webv2-sparse-page-cache
JPPhoto Oct 9, 2026
51b2367
Merge branch 'main' into webv2-sparse-page-cache
JPPhoto Oct 9, 2026
de22797
fix(gallery): resolve preview successors, star flags, and range queries
joshistoast Oct 9, 2026
a6d8fe6
fix(gallery): stamp toggled selections with their sparse page
joshistoast Oct 9, 2026
650bce6
fix(gallery): load an indexed reveal past a stale count
joshistoast Oct 9, 2026
e17a69e
Merge branch 'main' into webv2-sparse-page-cache
JPPhoto Oct 9, 2026
b9407fa
fix(gallery): wait for loading pages and bound sparse navigation entries
joshistoast Oct 9, 2026
242a144
fix(gallery): keep the rest of a selection when toggling off a primar…
joshistoast Oct 9, 2026
20fa8c4
fix(gallery): keep star flags across selection changes and picker scr…
joshistoast Oct 9, 2026
ca9dbe6
fix(gallery): keep paginated totals stable through removals and stale…
joshistoast Oct 9, 2026
6d0be70
fix(gallery): keep keyboard steps and star state whole across evicted…
joshistoast Oct 9, 2026
623707b
fix(gallery): keep Preview and picker navigation anchored to the whol…
joshistoast Oct 9, 2026
666d06a
fix(gallery): address review findings on sparse navigation fixes
joshistoast Oct 9, 2026
4aa0b52
test(perf): re-record route resources for sparse gallery paging
joshistoast Oct 9, 2026
d564de3
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Oct 9, 2026
4645a2f
fix(gallery): restore rejected deletions, stamp palette picks with th…
joshistoast Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/content/docs/Users Guide/06.Gallery/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ To leave semantic search, click the **✕** in the search field, click the spark

The sort menu orders media by creation time, **Newest** first (the default) or **Oldest** first. The star button next to it (**Starred only**) shows only starred items. Images and videos are always shown together.

### Browsing large boards

The media grid continues through the full listing as you scroll, including older items beyond the first few screens. You can also drag the scrollbar to jump to a distant part of the list; the Gallery loads items near that position without first loading every item in between. Scroll back up to browse earlier positions.

While a part of the list loads, its tile positions stay in place. If one page cannot be loaded, the Gallery marks its first empty tile with an error and **Retry**, while keeping nearby loaded items available.

## Starring favorites

Star an item to keep it at hand. Hover over a thumbnail and click the star in its corner, choose **Star image** (or **Star video**) from its menu, or select items and press **.** (period). Starred thumbnails keep a yellow star.
Expand Down
64 changes: 63 additions & 1 deletion invokeai/app/api/routers/gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.images import _assert_board_read_access
from invokeai.app.services.gallery.gallery_common import GalleryItem, GalleryItemNames, GalleryItemNamesResult
from invokeai.app.services.gallery.gallery_common import (
GalleryItem,
GalleryItemKind,
GalleryItemLocation,
GalleryItemNames,
GalleryItemNamesResult,
)
from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
from invokeai.app.services.shared.pagination import MAX_PAGE_SIZE, OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
Expand Down Expand Up @@ -72,6 +78,62 @@ def list_gallery_items(
)


@gallery_router.get(
"/items/location",
operation_id="get_gallery_item_location",
response_model=GalleryItemLocation,
)
def get_gallery_item_location(
current_user: CurrentUserOrDefault,
kind: GalleryItemKind = Query(description="Whether the target is an image or video."),
name: str = Query(description="The target image or video name."),
origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of items to list."),
categories: Optional[list[ImageCategory]] = Query(
default=None,
description="The categories to include. Shared between images and videos.",
),
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate items."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find items without a board.",
),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred: Optional[bool] = Query(
default=None,
description="Filter by starred state: true for starred items only, false for unstarred only. Omit to include both.",
),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
created_from: Optional[date] = Query(
default=None, description="Inclusive start date (YYYY-MM-DD) to filter by created_at."
),
created_to: Optional[date] = Query(
default=None, description="Inclusive end date (YYYY-MM-DD) to filter by created_at."
),
) -> GalleryItemLocation:
"""Returns exact item's position in an ordinary filtered gallery listing."""
if board_id is not None and board_id != "none":
_assert_board_read_access(board_id, current_user)

location = ApiDependencies.invoker.services.gallery.get_item_location(
kind=kind,
name=name,
order_dir=order_dir,
origin=origin,
categories=categories,
is_intermediate=is_intermediate,
board_id=board_id,
search_term=search_term,
user_id=current_user.user_id,
is_admin=current_user.is_admin,
created_from=created_from.isoformat() if created_from else None,
created_to=created_to.isoformat() if created_to else None,
starred=starred,
)
if location is None:
raise HTTPException(status_code=404, detail="Gallery item not found")
return location


@gallery_router.get(
"/item_names",
operation_id="list_gallery_item_names",
Expand Down
22 changes: 22 additions & 0 deletions invokeai/app/services/gallery/gallery_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from invokeai.app.services.gallery.gallery_common import (
BoardMediaSummary,
GalleryItem,
GalleryItemKind,
GalleryItemLocation,
GalleryItemNames,
GalleryItemNamesResult,
)
Expand Down Expand Up @@ -37,6 +39,26 @@ def list_items(
"""Lists a paginated, time-sorted stream of image + video items."""
pass

@abstractmethod
def get_item_location(
self,
kind: GalleryItemKind,
name: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
origin: Optional[ResourceOrigin] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
user_id: Optional[str] = None,
is_admin: bool = False,
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
) -> Optional[GalleryItemLocation]:
"""Finds an item's exact zero-based position in an ordinary item listing."""
pass

@abstractmethod
def list_item_names(
self,
Expand Down
9 changes: 9 additions & 0 deletions invokeai/app/services/gallery/gallery_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ class GalleryItemRef(BaseModel):
name: str = Field(description="The unique name of the image or video.")


class GalleryItemLocation(BaseModel):
"""A gallery item's position in a filtered, ordered listing."""

kind: GalleryItemKind = Field(description="Whether the item is an image or video.")
name: str = Field(description="The unique name of the image or video.")
index: int = Field(description="The item's zero-based index in the listing.")
total: int = Field(description="Number of items matching the listing filters.")


class GalleryItem(BaseModelExcludeNull):
"""A gallery item — either an image or a video, with shared fields and a discriminator.
Expand Down
81 changes: 81 additions & 0 deletions invokeai/app/services/gallery/gallery_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
BoardMediaSummary,
GalleryItem,
GalleryItemKind,
GalleryItemLocation,
GalleryItemNames,
GalleryItemNamesResult,
GalleryItemRef,
Expand Down Expand Up @@ -124,6 +125,86 @@ def list_items(
total=image_count + video_count,
)

def get_item_location(
self,
kind: GalleryItemKind,
name: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
origin: Optional[ResourceOrigin] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
user_id: Optional[str] = None,
is_admin: bool = False,
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
) -> Optional[GalleryItemLocation]:
"""Finds item's exact rank without building or returning the full name list."""
image_half, image_params, _ = self._build_half(
kind="image",
origin=origin,
categories=categories,
is_intermediate=is_intermediate,
board_id=board_id,
search_term=search_term,
user_id=user_id,
is_admin=is_admin,
names_only=True,
created_from=created_from,
created_to=created_to,
starred=starred,
)
video_half, video_params, _ = self._build_half(
kind="video",
origin=origin,
categories=categories,
is_intermediate=is_intermediate,
board_id=board_id,
search_term=search_term,
user_id=user_id,
is_admin=is_admin,
names_only=True,
created_from=created_from,
created_to=created_to,
starred=starred,
)

# Keep ranking expressions identical to paged listing order. Window rank avoids
# materializing every matching name in Python while still computing exact index and total.
order_clause = self._build_order_clause(starred_first=False, order_dir=order_dir)
query = f"""--sql
SELECT kind, name, item_index, total
FROM (
SELECT
kind,
name,
ROW_NUMBER() OVER ({order_clause}) - 1 AS item_index,
COUNT(*) OVER () AS total
FROM (
{image_half}
UNION ALL
{video_half}
)
)
WHERE kind = ? AND name = ?
LIMIT 1;
"""

with self._db.transaction() as cursor:
cursor.execute(query, image_params + video_params + [kind.value, name])
row = cursor.fetchone()

if row is None:
return None
return GalleryItemLocation(
kind=GalleryItemKind(row["kind"]),
name=row["name"],
index=row["item_index"],
total=row["total"],
)

def _query_name_rows(
self,
starred_first: bool,
Expand Down
Loading
Loading