diff --git a/faststack/app.py b/faststack/app.py index 375dc78..fda5ad9 100644 --- a/faststack/app.py +++ b/faststack/app.py @@ -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: @@ -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: @@ -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: @@ -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 @@ -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 @@ -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) @@ -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: diff --git a/faststack/thumbnail_view/model.py b/faststack/thumbnail_view/model.py index 926a2d0..6096d71 100644 --- a/faststack/thumbnail_view/model.py +++ b/faststack/thumbnail_view/model.py @@ -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() @@ -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