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 && (
-
-
setExploreMode("visual")}
- >
- Clusters
-
-
setExploreMode("tags")}
- >
- Tag Cloud
-
+
+
+
+ setExploreMode("visual")}
+ >
+ Clusters
+
+ setExploreMode("tags")}
+ >
+ Tag Cloud
+
+
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);