feat: add color search and reusable tooltips

Add dominant-color palette extraction, storage, filtering, and startup backfill so the gallery and Timeline can be filtered from the toolbar color picker.

Introduce a reusable tooltip component and migrate the color filter, update indicator, and gallery filename hover affordances to it.
This commit is contained in:
2026-06-28 10:25:52 +01:00
parent e3fde46e91
commit 90dec3b212
13 changed files with 682 additions and 46 deletions
+20 -1
View File
@@ -354,6 +354,8 @@ interface GalleryState {
minimumRating: number;
failedEmbeddingsOnly: boolean;
failedTaggingOnly: boolean;
colorFilter: [number, number, number] | null; // [r,g,b] dominant-color filter
colorBackfill: { processed: number; total: number; done: boolean } | null;
zoomPreset: ZoomPreset;
selectedImage: ImageRecord | null;
collectionTitle: string | null;
@@ -469,6 +471,7 @@ interface GalleryState {
setMinimumRating: (minimumRating: number) => void;
setFailedEmbeddingsOnly: (failedEmbeddingsOnly: boolean) => void;
setFailedTaggingOnly: (failedTaggingOnly: boolean) => void;
setColorFilter: (color: [number, number, number] | null) => void;
showFailedTagging: (folderId: number) => void;
setZoomPreset: (zoomPreset: ZoomPreset) => void;
openImage: (image: ImageRecord) => void;
@@ -834,6 +837,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
minimumRating: 0,
failedEmbeddingsOnly: false,
failedTaggingOnly: false,
colorFilter: null,
colorBackfill: null,
zoomPreset: "comfortable",
selectedImage: null,
collectionTitle: null,
@@ -1062,7 +1067,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
},
loadImages: async (reset = false) => {
const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly, minimumRating, failedEmbeddingsOnly, failedTaggingOnly, activeView } = get();
const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly, minimumRating, failedEmbeddingsOnly, failedTaggingOnly, colorFilter, activeView } = get();
const parsedSearch = parseSearchValue(search);
const requestToken = ++galleryRequestToken;
// Any fresh collection load invalidates a selection that referenced the
@@ -1176,6 +1181,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
rating_min: minimumRating > 0 ? minimumRating : null,
embedding_failed_only: failedEmbeddingsOnly,
tagging_failed_only: failedTaggingOnly,
color: colorFilter,
sort,
offset,
limit: activeView === "timeline" ? TIMELINE_PAGE_SIZE : PAGE_SIZE,
@@ -1317,6 +1323,11 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
void get().loadImages(true);
},
setColorFilter: (colorFilter) => {
set({ colorFilter, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, imageLoadError: null });
void get().loadImages(true);
},
showFailedTagging: (folderId) => {
set({
selectedFolderId: folderId,
@@ -2939,6 +2950,13 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
}
});
const unlistenColorBackfill = await listen<{ processed: number; total: number; done: boolean }>(
"color-backfill-progress",
(event) => {
set({ colorBackfill: event.payload.done ? null : event.payload });
},
);
return () => {
unlistenProgress();
unlistenMediaJobs();
@@ -2949,6 +2967,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
unlistenWatcherDeleted();
unlistenFolderCounts();
unlistenFfmpegProgress();
unlistenColorBackfill();
};
},
}));