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
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
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, lens, aperture, shutter, ISO, focal length) and, when a photo is
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 theme = useGalleryStore((state) => state.theme);
const setTheme = useGalleryStore((state) => state.setTheme);
const openTagManager = useGalleryStore((state) => state.openTagManager);
const lightboxAutoplay = useGalleryStore((state) => state.lightboxAutoplay);
const setLightboxAutoplay = useGalleryStore((state) => state.setLightboxAutoplay);
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}
</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 className="mt-8 space-y-9">
+4 -2
View File
@@ -983,7 +983,9 @@ export function TagCloud() {
const renameTag = useGalleryStore((state) => state.renameTag);
const deleteTag = useGalleryStore((state) => state.deleteTag);
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); };
useEffect(() => {
@@ -1028,7 +1030,7 @@ export function TagCloud() {
? "border-white/15 bg-white/10 text-white"
: "border-white/8 bg-white/[0.03] text-gray-500 hover:text-gray-300"
}`}
onClick={() => setManageTags((v) => !v)}
onClick={() => setManageTags(!manageTags)}
>
{manageTags ? "Done" : "Manage"}
</button>
+26 -5
View File
@@ -375,20 +375,21 @@ interface GalleryState {
galleryScrollResetKey: number;
activeView: ActiveView;
exploreMode: ExploreMode;
tagManagerOpen: boolean;
tagCloudEntries: TagCloudEntry[];
tagCloudLoading: boolean;
tagCloudFolderId: number | null | undefined; // undefined = never loaded
exploreTagEntries: ExploreTagEntry[];
exploreTagLoading: boolean;
exploreTagsFolderId: number | null | 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
// 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
// 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).
exploreTagsShownFolderId: number | null | undefined;
relatedTagsByKey: Record<string, RelatedTagEntry[]>;
indexingProgress: Record<number, IndexProgress>;
mediaJobProgress: Record<number, FolderJobProgress>;
cacheDir: string;
@@ -494,6 +495,8 @@ interface GalleryState {
closeImage: () => void;
setView: (view: ActiveView) => void;
setExploreMode: (mode: ExploreMode) => void;
setTagManagerOpen: (open: boolean) => void;
openTagManager: () => void;
loadTagCloud: (options?: { force?: boolean }) => Promise<void>;
loadExploreTags: (options?: { force?: boolean }) => Promise<void>;
loadRelatedTags: (tag: string) => Promise<RelatedTagEntry[]>;
@@ -874,17 +877,18 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
galleryScrollResetKey: 0,
activeView: "gallery",
exploreMode: "visual",
tagManagerOpen: false,
tagCloudEntries: [],
tagCloudLoading: false,
tagCloudFolderId: undefined,
exploreTagEntries: [],
exploreTagLoading: false,
exploreTagsFolderId: undefined,
exploreTagsShownFolderId: undefined,
relatedTagsByKey: {},
indexingProgress: {},
mediaJobProgress: {},
cacheDir: "",
exploreTagsShownFolderId: undefined,
captionModelStatus: null,
captionModelPreparing: false,
captionModelError: null,
@@ -1399,10 +1403,27 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
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) => {
const { selectedFolderId, tagCloudFolderId, tagCloudLoading } = get();