From b02bf1da2b8ece580391400b183c0838ad7d06fa Mon Sep 17 00:00:00 2001 From: LyAhn Date: Fri, 12 Jun 2026 12:39:04 +0100 Subject: [PATCH] feat(views): in-view folder scope switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Timeline, Explore, and Duplicates get a folder-scope dropdown in their headers (Timeline via the shared Toolbar), so changing scope no longer means leaving the feature and bouncing through the sidebar. The new setViewFolderScope store action updates selectedFolderId while preserving activeView: Timeline/Gallery reload images, Explore reloads via its existing selectedFolderId effect, and Duplicates loads the cached results for the new scope (fresh scans remain an explicit Rescan). Sidebar behavior is unchanged — clicking a library still opens its gallery. --- src/components/DuplicateFinder.tsx | 2 + src/components/FolderScopeDropdown.tsx | 98 ++++++++++++++++++++++++++ src/components/TagCloud.tsx | 38 +++++----- src/components/Toolbar.tsx | 4 ++ src/store.ts | 29 ++++++++ 5 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 src/components/FolderScopeDropdown.tsx diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index 67b361b..ce9c344 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { convertFileSrc } from "@tauri-apps/api/core"; import { DuplicateGroup, useGalleryStore } from "../store"; +import { FolderScopeDropdown } from "./FolderScopeDropdown"; function formatBytes(bytes: number): string { if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`; @@ -188,6 +189,7 @@ export function DuplicateFinder() { )}
+ {/* Batch select — only shown when there are groups and nothing is selected yet */} {hasResults && selectedCount === 0 && !deleting && ( + {open ? ( +
+ + {folders.map((folder) => { + const active = selectedFolderId === folder.id; + return ( + + ); + })} +
+ ) : null} +
+ ); +} diff --git a/src/components/TagCloud.tsx b/src/components/TagCloud.tsx index 43d2fcc..fffae2f 100644 --- a/src/components/TagCloud.tsx +++ b/src/components/TagCloud.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { motion } from "framer-motion"; import { convertFileSrc } from "@tauri-apps/api/core"; import { ExploreTagEntry, TagCloudEntry, useGalleryStore } from "../store"; +import { FolderScopeDropdown } from "./FolderScopeDropdown"; const ACCENTS = [ "#60a5fa", @@ -317,23 +318,26 @@ export function TagCloud() { : "No tags — run the AI tagger or add tags manually"}

-
- - +
+ +
+ + +
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 02f4041..c276410 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { tileSizeForZoom, useGalleryStore, SortOrder, MediaFilter, SearchCommand, parseSearchValue, searchModeLabel, ExploreTagEntry } from "../store"; +import { FolderScopeDropdown } from "./FolderScopeDropdown"; const BASE_SORT_OPTIONS: { value: SortOrder; label: string }[] = [ { value: "date_desc", label: "Newest first" }, @@ -162,6 +163,7 @@ export function Toolbar() { const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress); const zoomPreset = useGalleryStore((state) => state.zoomPreset); const setZoomPreset = useGalleryStore((state) => state.setZoomPreset); + const activeView = useGalleryStore((state) => state.activeView); const hasAnyFailedEmbeddings = Object.values(mediaJobProgress).some((p) => p.embedding_failed > 0); @@ -269,6 +271,8 @@ export function Toolbar() { )} + {activeView === "timeline" ? : null} +
{/* Search */} diff --git a/src/store.ts b/src/store.ts index 1fd5eb6..f5364b9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -337,6 +337,7 @@ interface GalleryState { renameFolder: (folderId: number, newName: string) => Promise; updateFolderPath: (folderId: number, newPath: string) => Promise; selectFolder: (folderId: number | null) => void; + setViewFolderScope: (folderId: number | null) => void; loadImages: (reset?: boolean) => Promise; loadMoreImages: () => Promise; setSearch: (search: string) => void; @@ -755,6 +756,34 @@ export const useGalleryStore = create((set, get) => ({ void get().loadImages(true); }, + // Change folder scope from inside a feature view (Timeline/Explore/Duplicates) + // without leaving it — unlike selectFolder, activeView is preserved. + setViewFolderScope: (folderId) => { + const { activeView, selectedFolderId } = get(); + if (folderId === selectedFolderId) return; + + set({ selectedFolderId: folderId, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, imageLoadError: null }); + + if (activeView === "duplicates") { + const { duplicateScanFolderId } = get(); + if (duplicateScanFolderId !== folderId) { + set({ + duplicateGroups: [], + duplicateLastScanned: null, + duplicateScanFolderId: undefined, + duplicateScanWarning: null, + }); + void get().loadDuplicateScanCache(folderId); + } + return; + } + + // Explore reloads itself via TagCloud's useEffect on selectedFolderId. + if (activeView === "explore") return; + + void get().loadImages(true); + }, + loadImages: async (reset = false) => { const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly, minimumRating, failedEmbeddingsOnly } = get(); const parsedSearch = parseSearchValue(search);