diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ac2c291..f000579 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1161,7 +1161,7 @@ pub async fn reset_generated_captions( } #[derive(Serialize, Deserialize)] -pub struct TagCloudEntry { +pub struct VisualClusterEntry { pub count: usize, pub representative_image_id: i64, pub thumbnail_path: Option, @@ -1174,10 +1174,10 @@ pub struct TagCloudEntry { /// Results are cached in SQLite keyed by a hash of the embedded image IDs, so repeated /// calls (including across app restarts) return instantly when the library hasn't changed. #[tauri::command] -pub async fn get_tag_cloud( +pub async fn get_visual_clusters( db: State<'_, DbState>, folder_id: Option, -) -> Result, String> { +) -> Result, String> { let folder_scope = match folder_id { Some(id) => format!("folder_{id}"), None => "all".to_string(), @@ -1214,10 +1214,10 @@ pub async fn get_tag_cloud( // Try to return a valid SQLite cache before loading any embeddings. { let conn = db.get().map_err(|e| e.to_string())?; - if let Some(json) = db::get_tag_cloud_cache(&conn, &folder_scope, current_hash) + if let Some(json) = db::get_visual_cluster_cache(&conn, &folder_scope, current_hash) .map_err(|e| e.to_string())? { - if let Ok(entries) = serde_json::from_str::>(&json) { + if let Ok(entries) = serde_json::from_str::>(&json) { // Reject cache entries written before image_ids were tracked — they all // have empty image_ids which causes "No media found" when a cluster is opened. if entries.iter().all(|e| !e.image_ids.is_empty()) { @@ -1247,12 +1247,12 @@ pub async fn get_tag_cloud( let kmeans_start = std::time::Instant::now(); let (centroids, cluster_counts, assignments) = kmeans_cosine(&points, k, 40); log::debug!( - "tag-cloud clustering: {n} embeddings, k={k}, sampled={} → {:.2?}", + "visual-cluster clustering: {n} embeddings, k={k}, sampled={} → {:.2?}", n > MAX_KMEANS_SAMPLE, kmeans_start.elapsed(), ); - let mut entries: Vec = Vec::new(); + let mut entries: Vec = Vec::new(); let mut order: Vec = (0..k).collect(); order.sort_unstable_by(|&a, &b| cluster_counts[b].cmp(&cluster_counts[a])); @@ -1285,7 +1285,7 @@ pub async fn get_tag_cloud( .ok() .and_then(|img| img.thumbnail_path); - entries.push(TagCloudEntry { + entries.push(VisualClusterEntry { count, representative_image_id: best_id, thumbnail_path, @@ -1296,8 +1296,8 @@ pub async fn get_tag_cloud( // Persist to SQLite. The cache is best-effort, but a silent write failure here // would defeat the whole optimization (every visit would recompute), so log it. if let Ok(json) = serde_json::to_string(&entries) { - if let Err(e) = db::set_tag_cloud_cache(&conn, &folder_scope, current_hash, &json) { - log::warn!("failed to persist tag-cloud cache for {folder_scope}: {e}"); + if let Err(e) = db::set_visual_cluster_cache(&conn, &folder_scope, current_hash, &json) { + log::warn!("failed to persist visual-cluster cache for {folder_scope}: {e}"); } } @@ -1861,7 +1861,7 @@ fn normalize(v: &mut [f32]) { /// large libraries while the full assignment still places every image. const MAX_KMEANS_SAMPLE: usize = 3000; -/// Bumped whenever the clustering algorithm changes so persisted tag-cloud caches +/// Bumped whenever the clustering algorithm changes so persisted visual-cluster caches /// computed by an older algorithm are invalidated and recomputed on next view. const CLUSTER_CACHE_VERSION: &str = "sampled-v2"; diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 6e51895..29ba254 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -267,12 +267,16 @@ pub fn migrate(conn: &Connection) -> Result<()> { UNIQUE(image_id, tag) ); - CREATE TABLE IF NOT EXISTS tag_cloud_cache ( + CREATE TABLE IF NOT EXISTS visual_cluster_cache ( folder_scope TEXT PRIMARY KEY, image_ids_hash INTEGER NOT NULL, entries_json TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + -- Renamed from the misnamed tag_cloud_cache (it caches visual clusters, + -- not tags). The cache is disposable and was already invalidated by a + -- cluster-algorithm version bump, so the old table is simply dropped. + DROP TABLE IF EXISTS tag_cloud_cache; CREATE TABLE IF NOT EXISTS duplicate_scan_cache ( folder_scope TEXT PRIMARY KEY, @@ -407,7 +411,7 @@ fn remove_filtered_ai_tags(conn: &Connection) -> Result<()> { delete_stmt.execute([id])?; } } - tx.execute("DELETE FROM tag_cloud_cache", [])?; + tx.execute("DELETE FROM visual_cluster_cache", [])?; tx.commit()?; log::info!( @@ -2799,9 +2803,9 @@ pub fn rename_tag(conn: &Connection, from: &str, to: &str) -> Result<()> { )?; // …then drop the now-duplicate leftovers still under the old name. tx.execute("DELETE FROM image_tags WHERE tag = ?1", params![from])?; - // The tag-cloud cache keys on image-id hashes, not tag text, so a rename + // The visual-cluster cache keys on image-id hashes, not tag text, so a rename // wouldn't invalidate it automatically — clear it. - tx.execute("DELETE FROM tag_cloud_cache", [])?; + tx.execute("DELETE FROM visual_cluster_cache", [])?; tx.commit()?; Ok(()) } @@ -2810,7 +2814,7 @@ pub fn rename_tag(conn: &Connection, from: &str, to: &str) -> Result<()> { pub fn delete_tag(conn: &Connection, name: &str) -> Result { let tx = conn.unchecked_transaction()?; let removed = tx.execute("DELETE FROM image_tags WHERE tag = ?1", params![name])? as i64; - tx.execute("DELETE FROM tag_cloud_cache", [])?; + tx.execute("DELETE FROM visual_cluster_cache", [])?; tx.commit()?; Ok(removed) } @@ -2983,14 +2987,14 @@ fn map_image_row(row: &Row<'_>) -> rusqlite::Result { }) } -/// Returns cached tag-cloud entries if the stored hash matches `current_hash`. -pub fn get_tag_cloud_cache( +/// Returns cached visual-cluster entries if the stored hash matches `current_hash`. +pub fn get_visual_cluster_cache( conn: &Connection, folder_scope: &str, current_hash: u64, ) -> Result> { let result: rusqlite::Result<(i64, String)> = conn.query_row( - "SELECT image_ids_hash, entries_json FROM tag_cloud_cache WHERE folder_scope = ?1", + "SELECT image_ids_hash, entries_json FROM visual_cluster_cache WHERE folder_scope = ?1", params![folder_scope], |row| Ok((row.get(0)?, row.get(1)?)), ); @@ -3002,15 +3006,15 @@ pub fn get_tag_cloud_cache( } } -/// Upserts the tag-cloud cache for the given scope. -pub fn set_tag_cloud_cache( +/// Upserts the visual-cluster cache for the given scope. +pub fn set_visual_cluster_cache( conn: &Connection, folder_scope: &str, image_ids_hash: u64, entries_json: &str, ) -> Result<()> { conn.execute( - "INSERT INTO tag_cloud_cache (folder_scope, image_ids_hash, entries_json, created_at) + "INSERT INTO visual_cluster_cache (folder_scope, image_ids_hash, entries_json, created_at) VALUES (?1, ?2, ?3, datetime('now')) ON CONFLICT(folder_scope) DO UPDATE SET image_ids_hash = excluded.image_ids_hash, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6c51442..5489d92 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -198,7 +198,7 @@ pub fn run() { commands::get_worker_states, commands::get_worker_pauses_persist, commands::set_worker_pauses_persist, - commands::get_tag_cloud, + commands::get_visual_clusters, commands::get_explore_tags, commands::get_related_tags, commands::get_images_by_ids, diff --git a/src-tauri/src/vector.rs b/src-tauri/src/vector.rs index cc2ea24..6130f51 100644 --- a/src-tauri/src/vector.rs +++ b/src-tauri/src/vector.rs @@ -268,7 +268,7 @@ pub fn get_embedding_revision(conn: &Connection) -> Result { /// exact set of IDs, so it is membership-sensitive: adding, removing, or moving an /// image between folders changes it even when the count happens to stay the same. /// Used (together with the embedding revision, which catches an image being -/// re-embedded in place) as the cheap tag-cloud cache key so a cache hit doesn't +/// re-embedded in place) as the cheap visual-cluster cache key so a cache hit doesn't /// have to read and unpack hundreds of MB of embeddings just to validate freshness. pub fn embedding_ids_signature(conn: &Connection, folder_id: Option) -> Result<(i64, u64)> { use xxhash_rust::xxh3::Xxh3; diff --git a/src/App.tsx b/src/App.tsx index 9a66c3f..61945f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { BackgroundTasks } from "./components/BackgroundTasks"; import { Toolbar } from "./components/Toolbar"; import { Gallery } from "./components/Gallery"; import { Lightbox } from "./components/Lightbox"; -import { TagCloud } from "./components/TagCloud"; +import { ExploreView } from "./components/ExploreView"; import { DuplicateFinder } from "./components/DuplicateFinder"; import { Timeline } from "./components/Timeline"; import { TitleBar } from "./components/TitleBar"; @@ -88,7 +88,7 @@ export default function App() { ) : activeView === "explore" ? ( <> - + ) : activeView === "duplicates" ? ( <> diff --git a/src/components/TagCloud.tsx b/src/components/ExploreView.tsx similarity index 97% rename from src/components/TagCloud.tsx rename to src/components/ExploreView.tsx index 380ae5d..d7c51e3 100644 --- a/src/components/TagCloud.tsx +++ b/src/components/ExploreView.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { motion, useReducedMotion } from "framer-motion"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { ExploreMode, ExploreTagEntry, RelatedTagEntry, TagCloudEntry, useGalleryStore } from "../store"; +import { ExploreMode, ExploreTagEntry, RelatedTagEntry, VisualClusterEntry, useGalleryStore } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; import { ThemedDropdown } from "./ThemedDropdown"; import { Tooltip } from "./Tooltip"; @@ -43,7 +43,7 @@ function seeded(n: number): number { } interface PlacedNode { - entry: TagCloudEntry; + entry: VisualClusterEntry; index: number; x: number; y: number; @@ -57,7 +57,7 @@ interface PlacedNode { rotateSeed: number; } -function buildCloud(entries: TagCloudEntry[], containerW: number, containerH: number): PlacedNode[] { +function buildCloud(entries: VisualClusterEntry[], containerW: number, containerH: number): PlacedNode[] { if (!entries.length || containerW <= 0 || containerH <= 0) return []; const maxCount = Math.max(...entries.map((e) => e.count)); @@ -622,13 +622,13 @@ function ExploreLoadingPanel({ mode }: { mode: ExploreMode }) { } // Separate component so its useLayoutEffect fires when the canvas is actually -// mounted — not at TagCloud mount time when the container may still be hidden +// mounted — not at ExploreView mount time when the container may still be hidden // behind a loading state. function ClusterCloud({ entries, onOpen, }: { - entries: TagCloudEntry[]; + entries: VisualClusterEntry[]; onOpen: (imageIds: number[]) => void; }) { const reducedMotion = useReducedMotion(); @@ -968,12 +968,12 @@ function TagManageList({ ); } -export function TagCloud() { +export function ExploreView() { const exploreMode = useGalleryStore((state) => state.exploreMode); const setExploreMode = useGalleryStore((state) => state.setExploreMode); - const tagCloudEntries = useGalleryStore((state) => state.tagCloudEntries); - const tagCloudLoading = useGalleryStore((state) => state.tagCloudLoading); - const loadTagCloud = useGalleryStore((state) => state.loadTagCloud); + const visualClusterEntries = useGalleryStore((state) => state.visualClusterEntries); + const visualClusterLoading = useGalleryStore((state) => state.visualClusterLoading); + const loadVisualClusters = useGalleryStore((state) => state.loadVisualClusters); const exploreTagEntries = useGalleryStore((state) => state.exploreTagEntries); const exploreTagLoading = useGalleryStore((state) => state.exploreTagLoading); const loadExploreTags = useGalleryStore((state) => state.loadExploreTags); @@ -989,13 +989,13 @@ export function TagCloud() { const handleDeleteTag = async (tag: string) => { await deleteTag(tag); }; useEffect(() => { - if (exploreMode === "visual") void loadTagCloud(); + if (exploreMode === "visual") void loadVisualClusters(); else void loadExploreTags(); - }, [exploreMode, selectedFolderId, loadTagCloud, loadExploreTags]); + }, [exploreMode, selectedFolderId, loadVisualClusters, loadExploreTags]); - const loading = exploreMode === "visual" ? tagCloudLoading : exploreTagLoading; - const hasEntries = exploreMode === "visual" ? tagCloudEntries.length > 0 : exploreTagEntries.length > 0; - const entryCount = exploreMode === "visual" ? tagCloudEntries.length : exploreTagEntries.length; + const loading = exploreMode === "visual" ? visualClusterLoading : exploreTagLoading; + const hasEntries = exploreMode === "visual" ? visualClusterEntries.length > 0 : exploreTagEntries.length > 0; + const entryCount = exploreMode === "visual" ? visualClusterEntries.length : exploreTagEntries.length; const visibleTagCount = Math.min(exploreTagEntries.length, TAG_ATLAS_MAX_VISIBLE); return ( @@ -1070,7 +1070,7 @@ export function TagCloud() { ) : exploreMode === "visual" ? (
- +
) : manageTags ? (
diff --git a/src/dev/mockBackend.ts b/src/dev/mockBackend.ts index ef0c7eb..58f86bd 100644 --- a/src/dev/mockBackend.ts +++ b/src/dev/mockBackend.ts @@ -250,8 +250,8 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise case "find_similar_images": case "find_similar_by_region": return similarImages(payload); - case "get_tag_cloud": - return db.scenario === "empty" ? [] : db.tagCloud; + case "get_visual_clusters": + return db.scenario === "empty" ? [] : db.visualClusters; case "get_explore_tags": return db.scenario === "empty" ? [] : db.exploreTags.slice(0, Number(p.limit ?? 180)); case "get_related_tags": { diff --git a/src/dev/mockFixtures.ts b/src/dev/mockFixtures.ts index 8cd5e8e..25d284e 100644 --- a/src/dev/mockFixtures.ts +++ b/src/dev/mockFixtures.ts @@ -9,7 +9,7 @@ import type { ImageRecord, ImageTag, SortOrder, - TagCloudEntry, + VisualClusterEntry, } from "../store"; import type { MockScenario } from "./mockScenarios"; import { fixtureMediaPath } from "./mockMedia"; @@ -21,7 +21,7 @@ export interface MockDb { albums: Album[]; albumImageIds: Record; tagsByImageId: Record; - tagCloud: TagCloudEntry[]; + visualClusters: VisualClusterEntry[]; exploreTags: ExploreTagEntry[]; backgroundJobs: FolderJobProgress[]; duplicateGroups: DuplicateGroup[]; @@ -252,7 +252,7 @@ function makeProgress(folders: Folder[], scenario: MockScenario): FolderJobProgr })); } -function makeTagCloud(images: ImageRecord[]): TagCloudEntry[] { +function makeVisualClusters(images: ImageRecord[]): VisualClusterEntry[] { return tags.slice(0, 10).map((_, index) => { const group = images.filter((image) => image.id % (index + 2) === 0).slice(0, 28); const representative = group[0] ?? images[index]; @@ -305,7 +305,7 @@ export function createMockDb(scenario: MockScenario): MockDb { albums, albumImageIds, tagsByImageId: makeTags(images, scenario), - tagCloud: makeTagCloud(images), + visualClusters: makeVisualClusters(images), exploreTags: makeExploreTags(images, scenario), backgroundJobs: makeProgress(folders, scenario), duplicateGroups: makeDuplicateGroups(images, scenario), diff --git a/src/store.ts b/src/store.ts index 6a2fa65..f044e57 100644 --- a/src/store.ts +++ b/src/store.ts @@ -203,7 +203,7 @@ export interface ImageExif { gps_lon: number | null; } -export interface TagCloudEntry { +export interface VisualClusterEntry { count: number; representative_image_id: number; thumbnail_path: string | null; @@ -376,9 +376,9 @@ interface GalleryState { activeView: ActiveView; exploreMode: ExploreMode; tagManagerOpen: boolean; - tagCloudEntries: TagCloudEntry[]; - tagCloudLoading: boolean; - tagCloudFolderId: number | null | undefined; // undefined = never loaded + visualClusterEntries: VisualClusterEntry[]; + visualClusterLoading: boolean; + visualClusterFolderId: number | null | undefined; // undefined = never loaded exploreTagEntries: ExploreTagEntry[]; exploreTagLoading: boolean; // Cache-freshness key: the folder the loaded tags belong to. Set to undefined @@ -497,7 +497,7 @@ interface GalleryState { setExploreMode: (mode: ExploreMode) => void; setTagManagerOpen: (open: boolean) => void; openTagManager: () => void; - loadTagCloud: (options?: { force?: boolean }) => Promise; + loadVisualClusters: (options?: { force?: boolean }) => Promise; loadExploreTags: (options?: { force?: boolean }) => Promise; loadRelatedTags: (tag: string) => Promise; showVisualCluster: (imageIds: number[]) => Promise; @@ -633,7 +633,7 @@ const SIMILAR_DISTANCE_THRESHOLD = 0.24; // similarity, region search). Any new request increments it so a stale response // from a previous collection type cannot overwrite newer results. let galleryRequestToken = 0; -let tagCloudRequestToken = 0; +let visualClusterRequestToken = 0; let exploreTagRequestToken = 0; let exploreTagRefreshTimer: ReturnType | null = null; @@ -878,9 +878,9 @@ export const useGalleryStore = create((set, get) => ({ activeView: "gallery", exploreMode: "visual", tagManagerOpen: false, - tagCloudEntries: [], - tagCloudLoading: false, - tagCloudFolderId: undefined, + visualClusterEntries: [], + visualClusterLoading: false, + visualClusterFolderId: undefined, exploreTagEntries: [], exploreTagLoading: false, exploreTagsFolderId: undefined, @@ -1010,7 +1010,7 @@ export const useGalleryStore = create((set, get) => ({ await loadFolders(); await loadBackgroundJobProgress(); // Invalidate tag cloud and explore-tags cache since library content changed. - set({ tagCloudFolderId: undefined, tagCloudEntries: [], exploreTagsFolderId: undefined }); + set({ visualClusterFolderId: undefined, visualClusterEntries: [], exploreTagsFolderId: undefined }); // Always refresh the gallery: the removed folder's images may be on screen // (e.g. in All Media), not only when that folder was the active selection. await loadImages(true); @@ -1021,7 +1021,7 @@ export const useGalleryStore = create((set, get) => ({ await invoke("reindex_folder", { folderId }); await loadFolders(); // Invalidate tag cloud cache since embeddings will be regenerated - set({ tagCloudFolderId: undefined, tagCloudEntries: [] }); + set({ visualClusterFolderId: undefined, visualClusterEntries: [] }); await loadBackgroundJobProgress(); }, @@ -1085,7 +1085,7 @@ export const useGalleryStore = create((set, get) => ({ return; } - // Explore reloads itself via TagCloud's useEffect on selectedFolderId. + // Explore reloads itself via ExploreView's useEffect on selectedFolderId. if (activeView === "explore") return; void get().loadImages(true); @@ -1425,33 +1425,33 @@ export const useGalleryStore = create((set, get) => ({ set({ exploreMode: "tags", tagManagerOpen: true, settingsOpen: false }); }, - loadTagCloud: async (options) => { - const { selectedFolderId, tagCloudFolderId, tagCloudLoading } = get(); + loadVisualClusters: async (options) => { + const { selectedFolderId, visualClusterFolderId, visualClusterLoading } = get(); const force = options?.force ?? false; // Skip if already loaded for this folder and not currently loading - if (!force && !tagCloudLoading && tagCloudFolderId !== undefined && tagCloudFolderId === selectedFolderId) { + if (!force && !visualClusterLoading && visualClusterFolderId !== undefined && visualClusterFolderId === selectedFolderId) { return; } - const requestToken = ++tagCloudRequestToken; + const requestToken = ++visualClusterRequestToken; // On a real folder switch, drop the previous folder's clusters so the loading // panel shows instead of lingering stale results. A same-folder refresh keeps // them to avoid a flicker when the cache returns instantly. - const isFolderSwitch = tagCloudFolderId !== selectedFolderId; + const isFolderSwitch = visualClusterFolderId !== selectedFolderId; set({ - tagCloudLoading: true, - tagCloudFolderId: selectedFolderId, - ...(isFolderSwitch ? { tagCloudEntries: [] } : {}), + visualClusterLoading: true, + visualClusterFolderId: selectedFolderId, + ...(isFolderSwitch ? { visualClusterEntries: [] } : {}), }); try { - const entries = await invoke("get_tag_cloud", { + const entries = await invoke("get_visual_clusters", { folderId: selectedFolderId, }); - if (requestToken !== tagCloudRequestToken) return; - set({ tagCloudEntries: entries, tagCloudLoading: false }); + if (requestToken !== visualClusterRequestToken) return; + set({ visualClusterEntries: entries, visualClusterLoading: false }); } catch (error) { - if (requestToken !== tagCloudRequestToken) return; + if (requestToken !== visualClusterRequestToken) return; console.error("Failed to load tag cloud:", error); - set({ tagCloudLoading: false }); + set({ visualClusterLoading: false }); } }, @@ -2411,13 +2411,13 @@ export const useGalleryStore = create((set, get) => ({ renameTag: async (from, to) => { await invoke("rename_tag", { params: { from, to } }); - // Tag content changed — invalidate the explore-tags and tag-cloud caches. + // Tag content changed — invalidate the explore-tags and visual-cluster caches. // Keep the current tag list visible while the refresh runs so manager UI // state such as filtering and sorting is not lost to a loading remount. set({ exploreTagsFolderId: undefined, - tagCloudFolderId: undefined, - tagCloudEntries: [], + visualClusterFolderId: undefined, + visualClusterEntries: [], }); const parsed = parseSearchValue(get().search); if (parsed.mode === "tag" && parsed.query === from) { @@ -2435,8 +2435,8 @@ export const useGalleryStore = create((set, get) => ({ // state such as filtering and sorting is not lost to a loading remount. set({ exploreTagsFolderId: undefined, - tagCloudFolderId: undefined, - tagCloudEntries: [], + visualClusterFolderId: undefined, + visualClusterEntries: [], }); const parsed = parseSearchValue(get().search); if (parsed.mode === "tag" && parsed.query === tag) { @@ -2507,14 +2507,14 @@ export const useGalleryStore = create((set, get) => ({ if (ids.length === 0 || cleaned.length === 0) return; await invoke("bulk_add_tags", { params: { image_ids: ids, tags: cleaned } }); // New tags landed — invalidate Explore tag caches. - set({ exploreTagsFolderId: undefined, tagCloudFolderId: undefined, tagCloudEntries: [] }); + set({ exploreTagsFolderId: undefined, visualClusterFolderId: undefined, visualClusterEntries: [] }); }, bulkRemoveTag: async (tag) => { const ids = Array.from(get().gallerySelectedIds); if (ids.length === 0 || !tag.trim()) return; await invoke("bulk_remove_tag", { params: { image_ids: ids, tag: tag.trim() } }); - set({ exploreTagsFolderId: undefined, tagCloudFolderId: undefined, tagCloudEntries: [] }); + set({ exploreTagsFolderId: undefined, visualClusterFolderId: undefined, visualClusterEntries: [] }); }, bulkDeleteSelected: async () => { @@ -2532,8 +2532,8 @@ export const useGalleryStore = create((set, get) => ({ totalImages: Math.max(0, state.totalImages - succeededIds.length), gallerySelectedIds: new Set([...state.gallerySelectedIds].filter((id) => !succeededSet.has(id))), // Deletion changes tag/duplicate/album aggregates. - tagCloudFolderId: undefined, - tagCloudEntries: [], + visualClusterFolderId: undefined, + visualClusterEntries: [], exploreTagsFolderId: undefined, })); // The DB cascade already removed these from album_images; refresh counts/covers.