feat(discovery): surface all tags and guard folder removal

Include tags used by a single image in Explore and invalidate the tag cache after edits so changes appear immediately. Add an accessible, time-limited confirmation step before removing indexed folders from the sidebar.
This commit is contained in:
2026-06-06 19:52:05 +01:00
parent 6824dcffb3
commit 2cdab000fb
3 changed files with 71 additions and 22 deletions
+1 -1
View File
@@ -1523,7 +1523,7 @@ pub fn get_explore_tags(
JOIN images i ON i.id = t.image_id
WHERE (?1 IS NULL OR i.folder_id = ?1)
GROUP BY t.tag
HAVING COUNT(DISTINCT t.image_id) >= 2
HAVING COUNT(DISTINCT t.image_id) >= 1
ORDER BY tag_count DESC, t.tag ASC
LIMIT ?2",
)?;
+46 -2
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import { useGalleryStore, Folder, IndexProgress } from "../store";
@@ -12,6 +13,22 @@ function FolderItem({
}) {
const { selectFolder, removeFolder, reindexFolder } = useGalleryStore();
const isIndexing = progress && !progress.done;
const [confirmingRemoval, setConfirmingRemoval] = useState(false);
useEffect(() => {
if (!confirmingRemoval) return;
const timeout = window.setTimeout(() => {
setConfirmingRemoval(false);
}, 4000);
return () => window.clearTimeout(timeout);
}, [confirmingRemoval]);
const handleRemove = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
await removeFolder(folder.id);
};
return (
<div
@@ -46,10 +63,32 @@ function FolderItem({
)}
</div>
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
{confirmingRemoval ? (
<div
className="flex items-center gap-1 shrink-0"
role="group"
aria-label={`Confirm removal of ${folder.name}`}
onClick={(event) => event.stopPropagation()}
>
<button
className="px-1.5 py-1 rounded-md text-[10px] font-medium text-red-400 bg-red-500/10 hover:bg-red-500/20 hover:text-red-300"
onClick={handleRemove}
>
Remove
</button>
<button
className="px-1.5 py-1 rounded-md text-[10px] font-medium text-gray-400 hover:text-gray-100 hover:bg-white/10"
onClick={() => setConfirmingRemoval(false)}
>
Cancel
</button>
</div>
) : (
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity shrink-0">
<button
className="p-1 rounded-md hover:bg-white/10 text-gray-500 hover:text-gray-200"
title="Re-index"
aria-label={`Re-index ${folder.name}`}
onClick={(e) => { e.stopPropagation(); reindexFolder(folder.id); }}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -60,13 +99,18 @@ function FolderItem({
<button
className="p-1 rounded-md hover:bg-red-500/15 text-gray-500 hover:text-red-400"
title="Remove folder"
onClick={(e) => { e.stopPropagation(); removeFolder(folder.id); }}
aria-label={`Remove ${folder.name}`}
onClick={(event) => {
event.stopPropagation();
setConfirmingRemoval(true);
}}
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
);
}
+6 -1
View File
@@ -1360,15 +1360,20 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
},
addUserTag: async (imageId, tag) => {
return invoke<ImageTag>("add_user_tag", {
const result = await invoke<ImageTag>("add_user_tag", {
params: { image_id: imageId, tag },
});
// Invalidate explore tags cache so new tag appears immediately
set({ exploreTagsFolderId: undefined });
return result;
},
removeTag: async (tagId) => {
await invoke<void>("remove_tag", {
params: { tag_id: tagId },
});
// Invalidate explore tags cache so removed tag disappears immediately
set({ exploreTagsFolderId: undefined });
},
loadDuplicateScanCache: async (folderId = null) => {