feat(tags): open the tag manager from Settings

Add an "Open tag manager" button under a new Tag library group in Settings →
AI Workspace. It closes Settings and jumps to Explore's tag Manage mode.

To make manage mode reachable from outside the Explore view, lift its flag out
of TagCloud's local state into the store (tagManagerOpen / setTagManagerOpen)
behind an openTagManager() action. Manage mode is reset whenever Explore is
entered normally or the visual-cluster view is selected, so openTagManager()
stays the only path that opens it programmatically.
This commit is contained in:
2026-06-29 20:26:49 +01:00
parent a9a8f8422e
commit 79ce458fd5
4 changed files with 44 additions and 8 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
- **Tag management** — Explore → Tag Cloud gains a Manage mode with a flat tag - **Tag management** — Explore → Tag Cloud gains a Manage mode with a flat tag
list where you can rename a tag, merge it into another (rename it to an list where you can rename a tag, merge it into another (rename it to an
existing tag's name), or delete it from every image. Changes apply across the existing tag's name), or delete it from every image. Changes apply across the
whole library. whole library. Settings → AI Workspace also has an "Open tag manager" button
that jumps straight into it.
- **Camera info in the lightbox** — the image info panel now shows EXIF details - **Camera info in the lightbox** — the image info panel now shows EXIF details
(camera, lens, aperture, shutter, ISO, focal length) and, when a photo is (camera, lens, aperture, shutter, ISO, focal length) and, when a photo is
geotagged, its GPS coordinates as a link that opens the location in your geotagged, its GPS coordinates as a link that opens the location in your
+12
View File
@@ -247,6 +247,7 @@ export function SettingsModal() {
const openOnboarding = useGalleryStore((state) => state.openOnboarding); const openOnboarding = useGalleryStore((state) => state.openOnboarding);
const theme = useGalleryStore((state) => state.theme); const theme = useGalleryStore((state) => state.theme);
const setTheme = useGalleryStore((state) => state.setTheme); const setTheme = useGalleryStore((state) => state.setTheme);
const openTagManager = useGalleryStore((state) => state.openTagManager);
const lightboxAutoplay = useGalleryStore((state) => state.lightboxAutoplay); const lightboxAutoplay = useGalleryStore((state) => state.lightboxAutoplay);
const setLightboxAutoplay = useGalleryStore((state) => state.setLightboxAutoplay); const setLightboxAutoplay = useGalleryStore((state) => state.setLightboxAutoplay);
const lightboxAutoMute = useGalleryStore((state) => state.lightboxAutoMute); const lightboxAutoMute = useGalleryStore((state) => state.lightboxAutoMute);
@@ -686,6 +687,17 @@ export function SettingsModal() {
{taggerQueueStatus ? <p className="pt-3 text-xs text-gray-500">{taggerQueueStatus}</p> : null} {taggerQueueStatus ? <p className="pt-3 text-xs text-gray-500">{taggerQueueStatus}</p> : null}
</SettingsGroup> </SettingsGroup>
<SettingsGroup title="Tag library" description="Review and clean up the tags across your library.">
<SettingsItem label="Manage tags" description="Open the tag manager in Explore to search, rename, and delete tags.">
<button
className="rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800 light-theme:hover:text-white"
onClick={openTagManager}
>
Open tag manager
</button>
</SettingsItem>
</SettingsGroup>
</div> </div>
) : ( ) : (
<div className="mt-8 space-y-9"> <div className="mt-8 space-y-9">
+4 -2
View File
@@ -983,7 +983,9 @@ export function TagCloud() {
const renameTag = useGalleryStore((state) => state.renameTag); const renameTag = useGalleryStore((state) => state.renameTag);
const deleteTag = useGalleryStore((state) => state.deleteTag); const deleteTag = useGalleryStore((state) => state.deleteTag);
const selectedFolderId = useGalleryStore((state) => state.selectedFolderId); const selectedFolderId = useGalleryStore((state) => state.selectedFolderId);
const [manageTags, setManageTags] = useState(false); // Manage mode lives in the store so it can be opened from elsewhere (Settings).
const manageTags = useGalleryStore((state) => state.tagManagerOpen);
const setManageTags = useGalleryStore((state) => state.setTagManagerOpen);
const handleDeleteTag = async (tag: string) => { await deleteTag(tag); }; const handleDeleteTag = async (tag: string) => { await deleteTag(tag); };
useEffect(() => { useEffect(() => {
@@ -1028,7 +1030,7 @@ export function TagCloud() {
? "border-white/15 bg-white/10 text-white" ? "border-white/15 bg-white/10 text-white"
: "border-white/8 bg-white/[0.03] text-gray-500 hover:text-gray-300" : "border-white/8 bg-white/[0.03] text-gray-500 hover:text-gray-300"
}`} }`}
onClick={() => setManageTags((v) => !v)} onClick={() => setManageTags(!manageTags)}
> >
{manageTags ? "Done" : "Manage"} {manageTags ? "Done" : "Manage"}
</button> </button>
+26 -5
View File
@@ -375,20 +375,21 @@ interface GalleryState {
galleryScrollResetKey: number; galleryScrollResetKey: number;
activeView: ActiveView; activeView: ActiveView;
exploreMode: ExploreMode; exploreMode: ExploreMode;
tagManagerOpen: boolean;
tagCloudEntries: TagCloudEntry[]; tagCloudEntries: TagCloudEntry[];
tagCloudLoading: boolean; tagCloudLoading: boolean;
tagCloudFolderId: number | null | undefined; // undefined = never loaded tagCloudFolderId: number | null | undefined; // undefined = never loaded
exploreTagEntries: ExploreTagEntry[]; exploreTagEntries: ExploreTagEntry[];
exploreTagLoading: boolean; exploreTagLoading: boolean;
exploreTagsFolderId: number | null | undefined;
// Cache-freshness key: the folder the loaded tags belong to. Set to undefined // Cache-freshness key: the folder the loaded tags belong to. Set to undefined
// by content mutations (tag add/remove/rename/delete) to mark the cache dirty // by content mutations (tag add/remove/rename/delete) to mark the cache dirty
// and force the next load to refetch. // and force the next load to refetch.
relatedTagsByKey: Record<string, RelatedTagEntry[]>; exploreTagsFolderId: number | null | undefined;
// The folder whose tags are actually on screen. Kept separate from the dirty // The folder whose tags are actually on screen. Kept separate from the dirty
// marker above so a same-folder invalidation isn't mistaken for a folder switch // marker above so a same-folder invalidation isn't mistaken for a folder switch
// (which would wipe the visible list and remount manager UI mid-refresh). // (which would wipe the visible list and remount manager UI mid-refresh).
exploreTagsShownFolderId: number | null | undefined; exploreTagsShownFolderId: number | null | undefined;
relatedTagsByKey: Record<string, RelatedTagEntry[]>;
indexingProgress: Record<number, IndexProgress>; indexingProgress: Record<number, IndexProgress>;
mediaJobProgress: Record<number, FolderJobProgress>; mediaJobProgress: Record<number, FolderJobProgress>;
cacheDir: string; cacheDir: string;
@@ -494,6 +495,8 @@ interface GalleryState {
closeImage: () => void; closeImage: () => void;
setView: (view: ActiveView) => void; setView: (view: ActiveView) => void;
setExploreMode: (mode: ExploreMode) => void; setExploreMode: (mode: ExploreMode) => void;
setTagManagerOpen: (open: boolean) => void;
openTagManager: () => void;
loadTagCloud: (options?: { force?: boolean }) => Promise<void>; loadTagCloud: (options?: { force?: boolean }) => Promise<void>;
loadExploreTags: (options?: { force?: boolean }) => Promise<void>; loadExploreTags: (options?: { force?: boolean }) => Promise<void>;
loadRelatedTags: (tag: string) => Promise<RelatedTagEntry[]>; loadRelatedTags: (tag: string) => Promise<RelatedTagEntry[]>;
@@ -874,17 +877,18 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
galleryScrollResetKey: 0, galleryScrollResetKey: 0,
activeView: "gallery", activeView: "gallery",
exploreMode: "visual", exploreMode: "visual",
tagManagerOpen: false,
tagCloudEntries: [], tagCloudEntries: [],
tagCloudLoading: false, tagCloudLoading: false,
tagCloudFolderId: undefined, tagCloudFolderId: undefined,
exploreTagEntries: [], exploreTagEntries: [],
exploreTagLoading: false, exploreTagLoading: false,
exploreTagsFolderId: undefined, exploreTagsFolderId: undefined,
exploreTagsShownFolderId: undefined,
relatedTagsByKey: {}, relatedTagsByKey: {},
indexingProgress: {}, indexingProgress: {},
mediaJobProgress: {}, mediaJobProgress: {},
cacheDir: "", cacheDir: "",
exploreTagsShownFolderId: undefined,
captionModelStatus: null, captionModelStatus: null,
captionModelPreparing: false, captionModelPreparing: false,
captionModelError: null, captionModelError: null,
@@ -1399,10 +1403,27 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
return; return;
} }
} }
set({ activeView, similarSourceAlbumId: null, similarScope: similarScopeReset }); // Entering Explore normally always starts in browse mode; openTagManager() is
// the only path that re-opens manage mode (it runs this then sets the flag).
set({
activeView,
similarSourceAlbumId: null,
similarScope: similarScopeReset,
...(activeView === "explore" ? { tagManagerOpen: false } : {}),
});
}, },
setExploreMode: (exploreMode) => set({ exploreMode }), setExploreMode: (exploreMode) =>
// Manage mode only exists for the tag view; drop it when switching to visual
// clusters so re-entering the tag view starts in the normal browse state.
set(exploreMode === "visual" ? { exploreMode, tagManagerOpen: false } : { exploreMode }),
setTagManagerOpen: (tagManagerOpen) => set({ tagManagerOpen }),
// Jump straight to the tag manager from anywhere (e.g. the Settings panel):
// switch to Explore, select the tag view, open manage mode, and close Settings.
openTagManager: () => {
get().setView("explore");
set({ exploreMode: "tags", tagManagerOpen: true, settingsOpen: false });
},
loadTagCloud: async (options) => { loadTagCloud: async (options) => {
const { selectedFolderId, tagCloudFolderId, tagCloudLoading } = get(); const { selectedFolderId, tagCloudFolderId, tagCloudLoading } = get();