feat: expand media discovery and AI workflows #8
+1
-1
@@ -1523,7 +1523,7 @@ pub fn get_explore_tags(
|
|||||||
JOIN images i ON i.id = t.image_id
|
JOIN images i ON i.id = t.image_id
|
||||||
WHERE (?1 IS NULL OR i.folder_id = ?1)
|
WHERE (?1 IS NULL OR i.folder_id = ?1)
|
||||||
GROUP BY t.tag
|
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
|
ORDER BY tag_count DESC, t.tag ASC
|
||||||
LIMIT ?2",
|
LIMIT ?2",
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { useGalleryStore, Folder, IndexProgress } from "../store";
|
import { useGalleryStore, Folder, IndexProgress } from "../store";
|
||||||
|
|
||||||
@@ -12,6 +13,22 @@ function FolderItem({
|
|||||||
}) {
|
}) {
|
||||||
const { selectFolder, removeFolder, reindexFolder } = useGalleryStore();
|
const { selectFolder, removeFolder, reindexFolder } = useGalleryStore();
|
||||||
const isIndexing = progress && !progress.done;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -46,10 +63,32 @@ function FolderItem({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<button
|
||||||
className="p-1 rounded-md hover:bg-white/10 text-gray-500 hover:text-gray-200"
|
className="p-1 rounded-md hover:bg-white/10 text-gray-500 hover:text-gray-200"
|
||||||
title="Re-index"
|
title="Re-index"
|
||||||
|
aria-label={`Re-index ${folder.name}`}
|
||||||
onClick={(e) => { e.stopPropagation(); reindexFolder(folder.id); }}
|
onClick={(e) => { e.stopPropagation(); reindexFolder(folder.id); }}
|
||||||
>
|
>
|
||||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -60,13 +99,18 @@ function FolderItem({
|
|||||||
<button
|
<button
|
||||||
className="p-1 rounded-md hover:bg-red-500/15 text-gray-500 hover:text-red-400"
|
className="p-1 rounded-md hover:bg-red-500/15 text-gray-500 hover:text-red-400"
|
||||||
title="Remove folder"
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1360,15 +1360,20 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addUserTag: async (imageId, tag) => {
|
addUserTag: async (imageId, tag) => {
|
||||||
return invoke<ImageTag>("add_user_tag", {
|
const result = await invoke<ImageTag>("add_user_tag", {
|
||||||
params: { image_id: imageId, tag },
|
params: { image_id: imageId, tag },
|
||||||
});
|
});
|
||||||
|
// Invalidate explore tags cache so new tag appears immediately
|
||||||
|
set({ exploreTagsFolderId: undefined });
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
removeTag: async (tagId) => {
|
removeTag: async (tagId) => {
|
||||||
await invoke<void>("remove_tag", {
|
await invoke<void>("remove_tag", {
|
||||||
params: { tag_id: tagId },
|
params: { tag_id: tagId },
|
||||||
});
|
});
|
||||||
|
// Invalidate explore tags cache so removed tag disappears immediately
|
||||||
|
set({ exploreTagsFolderId: undefined });
|
||||||
},
|
},
|
||||||
|
|
||||||
loadDuplicateScanCache: async (folderId = null) => {
|
loadDuplicateScanCache: async (folderId = null) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user