Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 30 additions & 9 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,10 @@ def apply_filter(self, filter_string: str, filter_flags: list):

if self._is_grid_view_active and self._thumbnail_model:
self._grid_refreshes += 1
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)
self._path_resolver.update_from_model(self._thumbnail_model)
self._grid_model_dirty = False
else:
Expand Down Expand Up @@ -683,7 +686,10 @@ def clear_filter(self):

if self._is_grid_view_active and self._thumbnail_model:
self._grid_refreshes += 1
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)
self._path_resolver.update_from_model(self._thumbnail_model)
self._grid_model_dirty = False
else:
Expand Down Expand Up @@ -790,7 +796,10 @@ def set_sort_mode(self, mode: str):

if self._is_grid_view_active and self._thumbnail_model:
self._grid_refreshes += 1
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)
self._path_resolver.update_from_model(self._thumbnail_model)
self._grid_model_dirty = False
else:
Expand Down Expand Up @@ -1139,7 +1148,10 @@ def load(self, skip_thumbnail_refresh: bool = False):
and self._thumbnail_model.rowCount() == 0
):
self._grid_refreshes += 1
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)
self._path_resolver.update_from_model(self._thumbnail_model)
self._grid_model_dirty = False

Expand Down Expand Up @@ -1218,7 +1230,10 @@ def refresh_image_list(self):
# Refresh thumbnail model if it exists (for external file changes or startup)
if self._thumbnail_model and self._is_grid_view_active:
self._grid_refreshes += 1
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)
self._path_resolver.update_from_model(self._thumbnail_model)
self._grid_model_dirty = False

Expand Down Expand Up @@ -2351,7 +2366,10 @@ def _set_grid_view_active(self, active: bool):
self._grid_refreshes += 1

# Always use controller's list, even if empty.
self._thumbnail_model.refresh_from_controller(self.image_files)
self._thumbnail_model.refresh_from_controller(
self.image_files,
metadata_map_fn=self._get_bulk_metadata_map,
)

# Update path resolver for the current directory
self._path_resolver.update_from_model(self._thumbnail_model)
Expand Down Expand Up @@ -2588,11 +2606,14 @@ def _get_metadata_dict(self, image_path: Path | str) -> dict:
"todo": False,
}

def _get_bulk_metadata_map(self) -> Dict[str, dict]:
"""Get flattened metadata map for all images (for efficient grid refresh)."""
def _get_bulk_metadata_map(self, images=None) -> Dict[str, dict]:
"""Get flattened metadata map for the given images (defaults to self.image_files).

Used to avoid per-image sidecar lookups on the UI thread during grid refresh.
"""
bulk_map = {}
try:
for img in self.image_files:
for img in (images if images is not None else self.image_files):
key = self.sidecar.metadata_key_for_path(img.path)
meta = self.sidecar.get_metadata(img.path, create=False)
if meta is None:
Expand Down
14 changes: 13 additions & 1 deletion faststack/thumbnail_view/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,12 +528,19 @@ def remove_rows_by_path(self, paths: List[Path]) -> None:
)

def refresh_from_controller(
self, images: List, metadata_map: Optional[Dict[str, dict]] = None
self,
images: List,
metadata_map: Optional[Dict[str, dict]] = None,
metadata_map_fn: Optional[Callable[[List], Dict[str, dict]]] = None,
):
"""Refresh images from a pre-loaded list without scanning disk.

Folders are still scanned, but image entries are built from the
provided objects.

If ``metadata_map_fn`` is provided and ``metadata_map`` is not,
the map is built lazily after filename filtering so we only
fetch sidecar metadata for images that will actually be shown.
"""
self._next_source_reason = "refresh"
cur, own = QThread.currentThread(), self.thread()
Expand All @@ -556,6 +563,11 @@ def refresh_from_controller(
needle = self._active_filter.lower()
images = [img for img in images if needle in img.path.stem.lower()]

# Build metadata map lazily, after filename filter, so we
# don't do sidecar lookups for images that got filtered out.
if metadata_map is None and metadata_map_fn is not None:
metadata_map = metadata_map_fn(images)

# Apply active flag filters (AND logic)
if self._active_filter_flags:
flags = self._active_filter_flags
Expand Down
Loading