feat: expand media discovery and AI workflows #8
@@ -1153,9 +1153,9 @@ pub async fn invalidate_duplicate_scan_cache(
|
|||||||
pub async fn delete_images_from_disk(
|
pub async fn delete_images_from_disk(
|
||||||
db: State<'_, DbState>,
|
db: State<'_, DbState>,
|
||||||
params: DeleteImagesFromDiskParams,
|
params: DeleteImagesFromDiskParams,
|
||||||
) -> Result<usize, String> {
|
) -> Result<Vec<i64>, String> {
|
||||||
if params.image_ids.is_empty() {
|
if params.image_ids.is_empty() {
|
||||||
return Ok(0);
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let conn = db.get().map_err(|e| e.to_string())?;
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
let records = db::get_all_image_paths(&conn, None).map_err(|e| e.to_string())?;
|
let records = db::get_all_image_paths(&conn, None).map_err(|e| e.to_string())?;
|
||||||
@@ -1170,11 +1170,11 @@ pub async fn delete_images_from_disk(
|
|||||||
succeeded_ids.push(r.id);
|
succeeded_ids.push(r.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let deleted = succeeded_ids.len();
|
|
||||||
if !succeeded_ids.is_empty() {
|
if !succeeded_ids.is_empty() {
|
||||||
db::delete_images_by_ids(&conn, &succeeded_ids).map_err(|e| e.to_string())?;
|
db::delete_images_by_ids(&conn, &succeeded_ids).map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
Ok(deleted)
|
// Return the IDs that were actually removed so the caller can update state precisely.
|
||||||
|
Ok(succeeded_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -175,9 +175,13 @@ export function Lightbox() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedImage) return;
|
if (!selectedImage) return;
|
||||||
|
// Capture the ID so a stale response for image A cannot overwrite B's tags
|
||||||
|
// when the user navigates before the request resolves.
|
||||||
|
let cancelled = false;
|
||||||
void getImageTags(selectedImage.id)
|
void getImageTags(selectedImage.id)
|
||||||
.then(setImageTags)
|
.then((tags) => { if (!cancelled) setImageTags(tags); })
|
||||||
.catch(() => setImageTags([]));
|
.catch(() => { if (!cancelled) setImageTags([]); });
|
||||||
|
return () => { cancelled = true; };
|
||||||
}, [selectedImage?.id, selectedImage?.ai_tagged_at, getImageTags]);
|
}, [selectedImage?.id, selectedImage?.ai_tagged_at, getImageTags]);
|
||||||
|
|
||||||
// Reset the queued state once the worker finishes so the button is usable again
|
// Reset the queued state once the worker finishes so the button is usable again
|
||||||
|
|||||||
+20
-7
@@ -1513,20 +1513,33 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
clearDuplicateSelection: () => set({ duplicateSelectedIds: new Set() }),
|
clearDuplicateSelection: () => set({ duplicateSelectedIds: new Set() }),
|
||||||
|
|
||||||
deleteSelectedDuplicates: async () => {
|
deleteSelectedDuplicates: async () => {
|
||||||
const { duplicateSelectedIds, duplicateScanFolderId } = get();
|
const { duplicateSelectedIds, duplicateGroups } = get();
|
||||||
const ids = Array.from(duplicateSelectedIds);
|
const ids = Array.from(duplicateSelectedIds);
|
||||||
if (ids.length === 0) return 0;
|
if (ids.length === 0) return 0;
|
||||||
const deleted = await invoke<number>("delete_images_from_disk", { params: { image_ids: ids } });
|
// Backend returns only the IDs that were actually removed from disk.
|
||||||
// Remove deleted images from groups and drop now-trivial groups
|
const succeededIds = await invoke<number[]>("delete_images_from_disk", { params: { image_ids: ids } });
|
||||||
|
const succeededSet = new Set(succeededIds);
|
||||||
|
// Only remove images confirmed deleted — failed files remain visible so the user can retry.
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
duplicateSelectedIds: new Set(),
|
duplicateSelectedIds: new Set(),
|
||||||
duplicateGroups: state.duplicateGroups
|
duplicateGroups: state.duplicateGroups
|
||||||
.map((g) => ({ ...g, images: g.images.filter((img) => !duplicateSelectedIds.has(img.id)) }))
|
.map((g) => ({ ...g, images: g.images.filter((img) => !succeededSet.has(img.id)) }))
|
||||||
.filter((g) => g.images.length > 1),
|
.filter((g) => g.images.length > 1),
|
||||||
}));
|
}));
|
||||||
// Invalidate the persisted cache so a restart doesn't reload stale entries
|
// Invalidate the persisted cache for every affected scope:
|
||||||
await invoke("invalidate_duplicate_scan_cache", { folderId: duplicateScanFolderId ?? null });
|
// - global "all" cache (always, since a folder-scoped deletion still makes the global result stale)
|
||||||
return deleted;
|
// - each folder that contained a deleted image (so a folder-scoped scan is also evicted)
|
||||||
|
const affectedFolderIds = new Set<number>(
|
||||||
|
duplicateGroups
|
||||||
|
.flatMap((g) => g.images)
|
||||||
|
.filter((img) => succeededSet.has(img.id))
|
||||||
|
.map((img) => img.folder_id),
|
||||||
|
);
|
||||||
|
await invoke("invalidate_duplicate_scan_cache", { folderId: null }); // global
|
||||||
|
for (const folderId of affectedFolderIds) {
|
||||||
|
await invoke("invalidate_duplicate_scan_cache", { folderId });
|
||||||
|
}
|
||||||
|
return succeededIds.length;
|
||||||
},
|
},
|
||||||
|
|
||||||
retryFailedEmbeddings: async (folderId) => {
|
retryFailedEmbeddings: async (folderId) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user