diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efd20c..6cc4c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index c9a2a41..92e43e1 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -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 ?

{taggerQueueStatus}

: null} + + + + + + ) : (
diff --git a/src/components/TagCloud.tsx b/src/components/TagCloud.tsx index c38026a..380ae5d 100644 --- a/src/components/TagCloud.tsx +++ b/src/components/TagCloud.tsx @@ -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"} diff --git a/src/store.ts b/src/store.ts index f8e0d52..6a2fa65 100644 --- a/src/store.ts +++ b/src/store.ts @@ -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; + 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; indexingProgress: Record; mediaJobProgress: Record; 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; loadExploreTags: (options?: { force?: boolean }) => Promise; loadRelatedTags: (tag: string) => Promise; @@ -874,17 +877,18 @@ export const useGalleryStore = create((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((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();