Improve media indexing and processing flow

This commit is contained in:
2026-04-05 18:58:10 +01:00
parent c299c7d452
commit c4036140e6
18 changed files with 1663 additions and 472 deletions
+58 -8
View File
@@ -28,6 +28,11 @@ export interface ImageRecord {
modified_at: string | null;
mime_type: string;
media_kind: MediaKind;
duration_ms: number | null;
video_codec: string | null;
audio_codec: string | null;
metadata_updated_at: string | null;
metadata_error: string | null;
favorite: boolean;
rating: number;
embedding_status: string;
@@ -44,6 +49,16 @@ export interface IndexProgress {
done: boolean;
}
export interface FolderJobProgress {
folder_id: number;
thumbnail_pending: number;
metadata_pending: number;
}
export interface MediaJobProgressEvent {
progress: FolderJobProgress[];
}
export interface IndexedImagesBatch {
folder_id: number;
images: ImageRecord[];
@@ -75,6 +90,7 @@ interface GalleryState {
zoomPreset: ZoomPreset;
selectedImage: ImageRecord | null;
indexingProgress: Record<number, IndexProgress>;
mediaJobProgress: Record<number, FolderJobProgress>;
cacheDir: string;
loadFolders: () => Promise<void>;
@@ -98,6 +114,16 @@ interface GalleryState {
const PAGE_SIZE = 200;
function mergeIntoVisibleWindow(
currentImages: ImageRecord[],
newImages: ImageRecord[],
sort: SortOrder,
windowSize: number,
): ImageRecord[] {
const merged = mergeImages(currentImages, newImages, sort);
return merged.slice(0, Math.max(windowSize, 0));
}
function matchesSearch(image: ImageRecord, search: string): boolean {
if (!search) return true;
return image.filename.toLowerCase().includes(search.toLowerCase());
@@ -175,6 +201,16 @@ function replaceImage(images: ImageRecord[], updatedImage: ImageRecord, sort: So
return mergeImages(images, [updatedImage], sort);
}
function replaceExistingImages(
currentImages: ImageRecord[],
updatedImages: ImageRecord[],
sort: SortOrder,
): ImageRecord[] {
const updatesByPath = new Map(updatedImages.map((image) => [image.path, image]));
const nextImages = currentImages.map((image) => updatesByPath.get(image.path) ?? image);
return nextImages.sort((a, b) => compareImages(a, b, sort));
}
export function tileSizeForZoom(zoomPreset: ZoomPreset): number {
switch (zoomPreset) {
case "compact":
@@ -200,6 +236,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
zoomPreset: "comfortable",
selectedImage: null,
indexingProgress: {},
mediaJobProgress: {},
cacheDir: "",
setCacheDir: (cacheDir) => set({ cacheDir }),
@@ -210,8 +247,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
},
addFolder: async (path) => {
const { cacheDir, loadFolders } = get();
await invoke("add_folder", { path, cacheDir });
const { loadFolders } = get();
await invoke("add_folder", { path });
await loadFolders();
},
@@ -226,8 +263,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
},
reindexFolder: async (folderId) => {
const { cacheDir, loadFolders } = get();
await invoke("reindex_folder", { folderId, cacheDir });
const { loadFolders } = get();
await invoke("reindex_folder", { folderId });
await loadFolders();
},
@@ -329,6 +366,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
if (progress.done) {
void get().loadFolders();
void get().loadImages(true);
setTimeout(() => {
set((state) => {
@@ -340,6 +378,16 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}
});
const unlistenMediaJobs = await listen<MediaJobProgressEvent>("media-job-progress", (event) => {
set((state) => {
const next = { ...state.mediaJobProgress };
for (const progress of event.payload.progress) {
next[progress.folder_id] = progress;
}
return { mediaJobProgress: next };
});
});
const unlistenImages = await listen<IndexedImagesBatch>("indexed-images", (event) => {
const batch = event.payload;
@@ -359,16 +407,17 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}
const newVisibleCount = countNewImages(state.images, visibleImages);
const images = mergeImages(state.images, visibleImages, state.sort);
const visibleWindow = Math.max(state.loadedCount, PAGE_SIZE);
const images = mergeIntoVisibleWindow(state.images, visibleImages, state.sort, visibleWindow);
return {
images,
loadedCount: images.length,
loadedCount: Math.max(state.loadedCount, Math.min(images.length, visibleWindow)),
totalImages: Math.max(state.totalImages + newVisibleCount, images.length),
};
});
});
const unlistenThumbnails = await listen<ThumbnailBatch>("thumbnail-updated", (event) => {
const unlistenThumbnails = await listen<ThumbnailBatch>("media-updated", (event) => {
const batch = event.payload;
set((state) => {
@@ -392,7 +441,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}
return {
images: mergeImages(state.images, visibleImages, state.sort),
images: replaceExistingImages(state.images, visibleImages, state.sort),
selectedImage,
};
});
@@ -400,6 +449,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
return () => {
unlistenProgress();
unlistenMediaJobs();
unlistenImages();
unlistenThumbnails();
};