From e3fde46e91a9f33169cf99cd3474b6a2d430f928 Mon Sep 17 00:00:00 2001 From: LyAhn Date: Sun, 28 Jun 2026 01:19:53 +0100 Subject: [PATCH] feat: add album scope for similar image search Similar search scoping: - Add current_album as a similar-scope option and remember the source album when similar or region searches are launched from an album. - Route gallery and lightbox similar actions through scope-aware store helpers so Album/Folder/All choices are applied consistently. - Keep pagination and scope toggles working for both whole-image and region-search result sets. Backend filtering: - Extend find_similar_images and find_similar_by_region params with album_id, giving album scope precedence over folder scope. - Add album_membership filtering for HNSW whole-image search and brute-force crop embedding search. --- CHANGELOG.md | 3 ++ src-tauri/src/commands.rs | 22 ++++++++- src-tauri/src/hnsw_index.rs | 16 +++++-- src-tauri/src/vector.rs | 35 ++++++++++++++ src/components/Gallery.tsx | 12 ++--- src/components/Lightbox.tsx | 13 ++--- src/components/Toolbar.tsx | 12 ++++- src/store.ts | 94 ++++++++++++++++++++++++++++--------- 8 files changed, 162 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b81b5..6dd0a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) whole library. - **Reorderable albums** — drag albums in the sidebar (hover the row for the drag handle) to set their order, which persists across sessions. +- **Album-scoped similar search** — when finding visually similar images or + searching by a selected region from an album, you can now keep results scoped + to that album, switch back to the source folder, or search all media. - **What's New** — after updating, Phokus now greets you with a "What's new" toast that opens an in-app release-notes screen for the new version, with the changes grouped into collapsible Added / Changed / Fixed sections. It's diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9539579..7ec7dce 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -58,6 +58,9 @@ pub struct UpdateImageDetailsParams { pub struct FindSimilarImagesParams { pub image_id: i64, pub folder_id: Option, + /// When set, restrict results to images in this album (takes precedence + /// over `folder_id`). Used by the "Similar: Album" scope. + pub album_id: Option, pub offset: Option, pub limit: Option, pub threshold: Option, @@ -72,6 +75,8 @@ pub struct FindSimilarByRegionParams { pub crop_w: f32, pub crop_h: f32, pub folder_id: Option, + /// Restrict to an album (takes precedence over `folder_id`). + pub album_id: Option, pub offset: Option, pub limit: Option, } @@ -704,6 +709,7 @@ pub async fn find_similar_images( &conn, params.image_id, params.folder_id, + params.album_id, threshold, offset, limit + 1, @@ -749,8 +755,19 @@ pub async fn find_similar_by_region( ) .map_err(|e| e.to_string())?; - // Search for similar images using the crop embedding - let image_ids = match params.folder_id { + // Search for similar images using the crop embedding. Album scope takes + // precedence over folder scope. + let image_ids = if let Some(album_id) = params.album_id { + vector::search_image_ids_by_embedding_in_album( + &conn, + &embedding, + album_id, + Some(params.image_id), + offset + limit + 1, + ) + .map_err(|e| e.to_string())? + } else { + match params.folder_id { Some(folder_id) => vector::search_image_ids_by_embedding_in_folder( &conn, &embedding, @@ -768,6 +785,7 @@ pub async fn find_similar_by_region( ids.retain(|&id| id != params.image_id); ids } + } }; let has_more = image_ids.len() > offset + limit; diff --git a/src-tauri/src/hnsw_index.rs b/src-tauri/src/hnsw_index.rs index 0836a03..bc4f497 100644 --- a/src-tauri/src/hnsw_index.rs +++ b/src-tauri/src/hnsw_index.rs @@ -92,6 +92,7 @@ pub fn find_similar_image_matches( conn: &Connection, image_id: i64, folder_id: Option, + album_id: Option, threshold: f32, offset: usize, limit: usize, @@ -103,10 +104,17 @@ pub fn find_similar_image_matches( None => return Ok(Vec::new()), }; - // Fetch folder image IDs *before* acquiring the read lock so we don't hold + // Build the allowed-id set *before* acquiring the read lock so we don't hold // the lock across a potentially slow SQLite query, which would delay any - // concurrent ensure_index call waiting for a write lock. - let folder_image_ids: Option> = if let Some(folder_id) = folder_id { + // concurrent ensure_index call waiting for a write lock. Album scope takes + // precedence over folder scope; both reuse the HNSW filtered search. + let allowed_image_ids: Option> = if let Some(album_id) = album_id { + let mut stmt = conn.prepare("SELECT image_id FROM album_images WHERE album_id = ?1")?; + let ids = stmt + .query_map([album_id], |row| row.get::<_, i64>(0))? + .collect::>>()?; + Some(ids) + } else if let Some(folder_id) = folder_id { let ids = vector::get_all_image_embeddings_with_ids(conn, Some(folder_id))? .into_iter() .map(|(id, _)| id) @@ -122,7 +130,7 @@ pub fn find_similar_image_matches( }; let knbn = (offset + limit).max(limit).saturating_add(32); - let neighbours: Vec = if let Some(image_ids) = folder_image_ids { + let neighbours: Vec = if let Some(image_ids) = allowed_image_ids { let mut allowed_ids = image_ids .into_iter() .filter_map(|allowed_image_id| { diff --git a/src-tauri/src/vector.rs b/src-tauri/src/vector.rs index f6b5545..7a7b842 100644 --- a/src-tauri/src/vector.rs +++ b/src-tauri/src/vector.rs @@ -372,6 +372,41 @@ pub fn search_image_ids_by_embedding_in_folder( Ok(rows.collect::>>()?) } +/// Brute-force cosine search scoped to a single album (membership via +/// `album_images`), ordered by ascending distance. Mirrors the folder-scoped +/// variant for region-based similarity search. +pub fn search_image_ids_by_embedding_in_album( + conn: &Connection, + embedding: &[f32], + album_id: i64, + exclude_image_id: Option, + limit: usize, +) -> Result> { + if embedding.len() != CLIP_VECTOR_DIM { + return Err(anyhow!( + "expected {}-dimensional embedding, got {}", + CLIP_VECTOR_DIM, + embedding.len() + )); + } + + let packed = pack_f32(embedding); + let exclude_id = exclude_image_id.unwrap_or(-1); + let mut stmt = conn.prepare( + "SELECT v.image_id + FROM image_vec v + JOIN album_images ai ON ai.image_id = v.image_id + WHERE ai.album_id = ?2 + AND v.image_id != ?3 + ORDER BY vec_distance_cosine(v.embedding, vec_f32(?1)) ASC + LIMIT ?4", + )?; + let rows = stmt.query_map((&packed, album_id, exclude_id, limit as i64), |row| { + row.get::<_, i64>(0) + })?; + Ok(rows.collect::>>()?) +} + #[allow(dead_code)] pub fn search_caption_ids_by_embedding( conn: &Connection, diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index ad417a1..d4ff5e4 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -31,8 +31,7 @@ export function ContextMenu({ }) { const openImage = useGalleryStore((state) => state.openImage); const updateImageDetails = useGalleryStore((state) => state.updateImageDetails); - const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); - const similarScope = useGalleryStore((state) => state.similarScope); + const findSimilar = useGalleryStore((state) => state.findSimilar); const canFindSimilar = image.embedding_status === "ready"; return ( @@ -60,9 +59,9 @@ export function ContextMenu({ ? "text-gray-200 hover:bg-white/5 hover:text-white" : "text-gray-600 cursor-not-allowed" }`} - onClick={async () => { + onClick={() => { if (!canFindSimilar) return; - await loadSimilarImages(image.id, similarScope === "current_folder" ? image.folder_id : null, true, image.folder_id); + findSimilar(image.id, image.folder_id); onClose(); }} disabled={!canFindSimilar} @@ -117,8 +116,7 @@ export function ImageTile({ }) { const [loaded, setLoaded] = useState(false); const [errored, setErrored] = useState(false); - const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); - const similarScope = useGalleryStore((state) => state.similarScope); + const findSimilar = useGalleryStore((state) => state.findSimilar); const selected = useGalleryStore((state) => state.gallerySelectedIds.has(image.id)); const selectionActive = useGalleryStore((state) => state.gallerySelectedIds.size > 0); const toggleGallerySelected = useGalleryStore((state) => state.toggleGallerySelected); @@ -274,7 +272,7 @@ export function ImageTile({ onClick={(event) => { event.stopPropagation(); if (!canFindSimilar) return; - void loadSimilarImages(image.id, similarScope === "current_folder" ? image.folder_id : null, true, image.folder_id); + findSimilar(image.id, image.folder_id); }} disabled={!canFindSimilar} > diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index dffa8c5..45c7e46 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -144,9 +144,8 @@ export function Lightbox() { const closeImage = useGalleryStore((state) => state.closeImage); const images = useGalleryStore((state) => state.images); const openImage = useGalleryStore((state) => state.openImage); - const loadSimilarImages = useGalleryStore((state) => state.loadSimilarImages); - const loadSimilarByRegion = useGalleryStore((state) => state.loadSimilarByRegion); - const similarScope = useGalleryStore((state) => state.similarScope); + const findSimilar = useGalleryStore((state) => state.findSimilar); + const findSimilarByRegion = useGalleryStore((state) => state.findSimilarByRegion); const updateImageDetails = useGalleryStore((state) => state.updateImageDetails); const getImageTags = useGalleryStore((state) => state.getImageTags); const addUserTag = useGalleryStore((state) => state.addUserTag); @@ -384,12 +383,10 @@ export function Lightbox() { exitRegionMode(); setRegionSearching(true); - const folderId = - similarScope === "current_folder" ? selectedImage.folder_id : null; - void loadSimilarByRegion(selectedImage.id, crop, folderId, selectedImage.folder_id) + void findSimilarByRegion(selectedImage.id, crop, selectedImage.folder_id) .finally(() => setRegionSearching(false)); }, - [isPanning, isDragging, dragRect, selectedImage, similarScope, loadSimilarByRegion, exitRegionMode], + [isPanning, isDragging, dragRect, selectedImage, findSimilarByRegion, exitRegionMode], ); // Build the CSS rect for the selection overlay (viewport-relative) @@ -543,7 +540,7 @@ export function Lightbox() { }`} onClick={() => { if (!canFindSimilar) return; - void loadSimilarImages(selectedImage.id, similarScope === "current_folder" ? selectedImage.folder_id : null, true, selectedImage.folder_id); + void findSimilar(selectedImage.id, selectedImage.folder_id); }} disabled={!canFindSimilar} > diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 56e021e..73ab370 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -162,6 +162,7 @@ export function Toolbar() { const setFailedTaggingOnly = useGalleryStore((state) => state.setFailedTaggingOnly); const similarScope = useGalleryStore((state) => state.similarScope); const setSimilarScope = useGalleryStore((state) => state.setSimilarScope); + const similarSourceAlbumId = useGalleryStore((state) => state.similarSourceAlbumId); const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress); const zoomPreset = useGalleryStore((state) => state.zoomPreset); const setZoomPreset = useGalleryStore((state) => state.setZoomPreset); @@ -187,6 +188,12 @@ export function Toolbar() { const hasActiveSearch = search.trim().length > 0; const parsedSearch = parseSearchValue(composeSearchValue(searchCommand, searchQuery)); const isSimilarResults = collectionTitle === "Similar Images"; + // Album scope is offered while browsing an album (where it's the default) and + // while viewing similar/region results that were launched from an album. + const showAlbumScope = + activeView === "album" || + (similarSourceAlbumId !== null && + (collectionTitle === "Similar Images" || collectionTitle === "Region Search Results")); // If current sort is video-only but we switched away from video filter, reset to date_desc useEffect(() => { @@ -450,6 +457,9 @@ export function Toolbar() { { setFavoritesOnly(!favoritesOnly); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} /> { setMinimumRating(minimumRating === 1 ? 0 : 1); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} /> { setMinimumRating(minimumRating === 4 ? 0 : 4); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} /> + {showAlbumScope ? ( + setSimilarScope("current_album")} /> + ) : null} setSimilarScope("current_folder")} /> setSimilarScope("all_media")} /> {hasAnyFailedEmbeddings ? ( @@ -468,7 +478,7 @@ export function Toolbar() { onClick={() => setFailedTaggingOnly(!failedTaggingOnly)} /> ) : null} - {isSimilarResults ? Current similar scope: {similarScope === "current_folder" ? "current folder" : "all media"} : null} + {isSimilarResults ? Current similar scope: {similarScope === "current_album" ? "this album" : similarScope === "current_folder" ? "current folder" : "all media"} : null} ); diff --git a/src/store.ts b/src/store.ts index e5fa0c6..3dbcbd7 100644 --- a/src/store.ts +++ b/src/store.ts @@ -53,7 +53,7 @@ export type CaptionDetail = "short" | "detailed" | "paragraph"; export type TaggerAcceleration = "auto" | "cpu" | "directml"; export type AiRating = "general" | "sensitive" | "questionable" | "explicit"; export type TaggingQueueScope = "all" | "selected"; -export type SimilarScope = "all_media" | "current_folder"; +export type SimilarScope = "all_media" | "current_folder" | "current_album"; export type ExploreMode = "visual" | "tags"; export type AppTheme = "phokus" | "subtle-light" | "conventional-dark"; @@ -359,6 +359,7 @@ interface GalleryState { collectionTitle: string | null; similarSourceImageId: number | null; similarSourceFolderId: number | null; + similarSourceAlbumId: number | null; // album a similar search was launched from (enables "Similar: Album") similarHasMore: boolean; similarScope: SimilarScope; similarFolderId: number | null; @@ -478,8 +479,11 @@ interface GalleryState { loadExploreTags: () => Promise; showVisualCluster: (imageIds: number[]) => Promise; searchForTag: (tag: string) => void; - loadSimilarImages: (imageId: number, folderId?: number | null, reset?: boolean, sourceFolderId?: number | null) => Promise; - loadSimilarByRegion: (imageId: number, crop: { x: number; y: number; w: number; h: number }, folderId?: number | null, sourceFolderId?: number | null) => Promise; + loadSimilarImages: (imageId: number, folderId?: number | null, reset?: boolean, sourceFolderId?: number | null, albumId?: number | null) => Promise; + loadSimilarByRegion: (imageId: number, crop: { x: number; y: number; w: number; h: number }, folderId?: number | null, sourceFolderId?: number | null, albumId?: number | null) => Promise; + // Entry points that decide scope (album when launched from an album, else folder/all per similarScope). + findSimilar: (imageId: number, sourceFolderId: number | null) => Promise; + findSimilarByRegion: (imageId: number, crop: { x: number; y: number; w: number; h: number }, sourceFolderId: number | null) => Promise; setSimilarScope: (scope: SimilarScope) => void; suggestImageTags: (imageId: number) => Promise; loadCaptionModelStatus: () => Promise; @@ -835,6 +839,7 @@ export const useGalleryStore = create((set, get) => ({ collectionTitle: null, similarSourceImageId: null, similarSourceFolderId: null, + similarSourceAlbumId: null, similarHasMore: false, similarScope: "all_media", similarFolderId: null, @@ -1021,7 +1026,10 @@ export const useGalleryStore = create((set, get) => ({ }, selectFolder: (folderId) => { - set({ selectedFolderId: folderId, selectedAlbumId: null, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, activeView: "gallery", failedEmbeddingsOnly: false, failedTaggingOnly: false, imageLoadError: null }); + // Leaving any album: drop the album-origin scope so the Folder/All pills + // highlight correctly again. + const similarScope = get().similarScope === "current_album" ? "all_media" : get().similarScope; + set({ selectedFolderId: folderId, selectedAlbumId: null, similarSourceAlbumId: null, similarScope, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, activeView: "gallery", failedEmbeddingsOnly: false, failedTaggingOnly: false, imageLoadError: null }); void get().loadImages(true); }, @@ -1218,9 +1226,10 @@ export const useGalleryStore = create((set, get) => ({ } return; } + const pageAlbumId = get().similarScope === "current_album" ? get().similarSourceAlbumId : null; if (collectionTitle === "Similar Images" && similarSourceImageId !== null) { if (!similarHasMore) return; - await get().loadSimilarImages(similarSourceImageId, similarFolderId, false, get().similarSourceFolderId ?? null); + await get().loadSimilarImages(similarSourceImageId, similarFolderId, false, get().similarSourceFolderId ?? null, pageAlbumId); return; } if (collectionTitle === "Region Search Results" && similarSourceImageId !== null && similarCrop !== null) { @@ -1235,7 +1244,8 @@ export const useGalleryStore = create((set, get) => ({ crop_y: similarCrop.y, crop_w: similarCrop.w, crop_h: similarCrop.h, - folder_id: similarFolderId, + folder_id: pageAlbumId !== null ? null : similarFolderId, + album_id: pageAlbumId, offset: loadedCount, limit: PAGE_SIZE, }, @@ -1336,8 +1346,10 @@ export const useGalleryStore = create((set, get) => ({ closeImage: () => set({ selectedImage: null }), setView: (activeView) => { + // Leaving an album view drops the album-origin similar scope. + const similarScopeReset = get().similarScope === "current_album" ? "all_media" : get().similarScope; if (activeView === "timeline") { - set({ activeView, sort: "taken_asc", images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarSourceFolderId: null, similarFolderId: null, similarHasMore: false, similarCrop: null, imageLoadError: null }); + set({ activeView, sort: "taken_asc", images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarSourceFolderId: null, similarSourceAlbumId: null, similarScope: similarScopeReset, similarFolderId: null, similarHasMore: false, similarCrop: null, imageLoadError: null }); void get().loadImages(true); return; } @@ -1355,7 +1367,7 @@ export const useGalleryStore = create((set, get) => ({ return; } } - set({ activeView }); + set({ activeView, similarSourceAlbumId: null, similarScope: similarScopeReset }); }, setExploreMode: (exploreMode) => set({ exploreMode }), @@ -1452,10 +1464,12 @@ export const useGalleryStore = create((set, get) => ({ void get().loadImages(true); }, - loadSimilarImages: async (imageId, folderId = get().selectedFolderId, reset = true, sourceFolderId = folderId ?? null) => { + loadSimilarImages: async (imageId, folderId = get().selectedFolderId, reset = true, sourceFolderId = folderId ?? null, albumId = null) => { const requestToken = ++galleryRequestToken; const offset = reset ? 0 : get().loadedCount; - const similarScope = folderId === null ? "all_media" : "current_folder"; + const similarScope: SimilarScope = albumId !== null ? "current_album" : folderId === null ? "all_media" : "current_folder"; + // Album scope drives results off album membership, so the folder query is null. + const queryFolderId = albumId !== null ? null : folderId ?? null; set((state) => ({ images: reset ? [] : state.images, loadedCount: reset ? 0 : state.loadedCount, @@ -1464,7 +1478,7 @@ export const useGalleryStore = create((set, get) => ({ imageLoadError: null, similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, similarScope, // Force the gallery grid so results (and the bulk bar) render regardless // of which view the search was launched from. @@ -1478,7 +1492,8 @@ export const useGalleryStore = create((set, get) => ({ const result = await invoke("find_similar_images", { params: { image_id: imageId, - folder_id: folderId ?? null, + folder_id: queryFolderId, + album_id: albumId, offset, limit: PAGE_SIZE, threshold: SIMILAR_DISTANCE_THRESHOLD, @@ -1500,7 +1515,7 @@ export const useGalleryStore = create((set, get) => ({ similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, similarHasMore: result.has_more, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, similarScope, selectedImage: reset ? null : state.selectedImage, }; @@ -1518,16 +1533,17 @@ export const useGalleryStore = create((set, get) => ({ similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, similarHasMore: false, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, similarScope, selectedImage: null, }); } }, - loadSimilarByRegion: async (imageId, crop, folderId = get().selectedFolderId, sourceFolderId = folderId ?? null) => { + loadSimilarByRegion: async (imageId, crop, folderId = get().selectedFolderId, sourceFolderId = folderId ?? null, albumId = null) => { const requestToken = ++galleryRequestToken; - const similarScope = folderId === null ? "all_media" : "current_folder"; + const similarScope: SimilarScope = albumId !== null ? "current_album" : folderId === null ? "all_media" : "current_folder"; + const queryFolderId = albumId !== null ? null : folderId ?? null; set((state) => ({ images: [], loadedCount: 0, @@ -1536,7 +1552,7 @@ export const useGalleryStore = create((set, get) => ({ imageLoadError: null, similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, similarCrop: crop, similarScope, // Force the gallery grid so results (and the bulk bar) render regardless @@ -1556,7 +1572,8 @@ export const useGalleryStore = create((set, get) => ({ crop_y: crop.y, crop_w: crop.w, crop_h: crop.h, - folder_id: folderId ?? null, + folder_id: queryFolderId, + album_id: albumId, offset: 0, limit: PAGE_SIZE, }, @@ -1574,7 +1591,7 @@ export const useGalleryStore = create((set, get) => ({ similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, similarHasMore: result.has_more, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, similarCrop: crop, similarScope, }); @@ -1591,22 +1608,51 @@ export const useGalleryStore = create((set, get) => ({ similarSourceImageId: imageId, similarSourceFolderId: sourceFolderId, similarHasMore: false, - similarFolderId: folderId ?? null, + similarFolderId: queryFolderId, + similarCrop: crop, similarScope, selectedImage: null, }); } }, + // Decide the scope at launch: album when triggered from an album, else the + // current folder/all preference. Sets similarSourceAlbumId so the "Similar: + // Album" pill and scope toggle work afterward. + findSimilar: (imageId, sourceFolderId) => { + const { activeView, selectedAlbumId, similarScope } = get(); + const albumOrigin = activeView === "album" ? selectedAlbumId : null; + set({ similarSourceAlbumId: albumOrigin }); + // Respect the chosen scope; album is the default in an album view but the + // user can override to Folder/All before searching. + if (similarScope === "current_album" && albumOrigin !== null) { + return get().loadSimilarImages(imageId, null, true, sourceFolderId, albumOrigin); + } + const folderId = similarScope === "current_folder" ? sourceFolderId : null; + return get().loadSimilarImages(imageId, folderId, true, sourceFolderId, null); + }, + + findSimilarByRegion: (imageId, crop, sourceFolderId) => { + const { activeView, selectedAlbumId, similarScope } = get(); + const albumOrigin = activeView === "album" ? selectedAlbumId : null; + set({ similarSourceAlbumId: albumOrigin }); + if (similarScope === "current_album" && albumOrigin !== null) { + return get().loadSimilarByRegion(imageId, crop, null, sourceFolderId, albumOrigin); + } + const folderId = similarScope === "current_folder" ? sourceFolderId : null; + return get().loadSimilarByRegion(imageId, crop, folderId, sourceFolderId, null); + }, + setSimilarScope: (similarScope) => { set({ similarScope }); - const { similarSourceImageId, similarSourceFolderId, selectedFolderId, collectionTitle, similarCrop } = get(); + const { similarSourceImageId, similarSourceFolderId, similarSourceAlbumId, selectedFolderId, collectionTitle, similarCrop } = get(); if (similarSourceImageId === null) return; + const albumId = similarScope === "current_album" ? similarSourceAlbumId : null; const folderId = similarScope === "current_folder" ? (similarSourceFolderId ?? selectedFolderId) : null; if (collectionTitle === "Region Search Results" && similarCrop !== null) { - void get().loadSimilarByRegion(similarSourceImageId, similarCrop, folderId, similarSourceFolderId); + void get().loadSimilarByRegion(similarSourceImageId, similarCrop, folderId, similarSourceFolderId, albumId); } else { - void get().loadSimilarImages(similarSourceImageId, folderId, true, similarSourceFolderId); + void get().loadSimilarImages(similarSourceImageId, folderId, true, similarSourceFolderId, albumId); } }, @@ -2470,6 +2516,8 @@ export const useGalleryStore = create((set, get) => ({ imageLoadError: null, similarSourceImageId: null, similarSourceFolderId: null, + similarSourceAlbumId: albumId, + similarScope: "current_album", similarHasMore: false, similarFolderId: null, similarCrop: null,