mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 17:24:56 +03:00
feat(server, web): smart search filtering and pagination (#6525)
* initial pagination impl * use limit + offset instead of take + skip * wip web pagination * working infinite scroll * update api * formatting * fix rebase * search refactor * re-add runtime config for vector search * fix rebase * fixes * useless omitBy * unnecessary handling * add sql decorator for `searchAssets` * fixed search builder * fixed sql * remove mock method * linting * fixed pagination * fixed unit tests * formatting * fix e2e tests * re-flatten search builder * refactor endpoints * clean up dto * refinements * don't break everything just yet * update openapi spec & sql * update api * linting * update sql * fixes * optimize web code * fix typing * add page limit * make limit based on asset count * increase limit * simpler import
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
export let right = 0;
|
||||
export let root: HTMLElement | null = null;
|
||||
|
||||
let intersecting = false;
|
||||
export let intersecting = false;
|
||||
let container: HTMLDivElement;
|
||||
const dispatch = createEventDispatcher<{
|
||||
hidden: HTMLDivElement;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
export let readonly = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let showStackedIcon = true;
|
||||
export let intersecting = false;
|
||||
|
||||
let className = '';
|
||||
export { className as class };
|
||||
@@ -85,7 +86,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<IntersectionObserver once={false} let:intersecting>
|
||||
<IntersectionObserver once={false} on:intersected bind:intersecting>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
style:width="{width}px"
|
||||
@@ -95,8 +96,8 @@
|
||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:hover:cursor-pointer={!disabled}
|
||||
on:mouseenter={() => onMouseEnter()}
|
||||
on:mouseleave={() => onMouseLeave()}
|
||||
on:mouseenter={onMouseEnter}
|
||||
on:mouseleave={onMouseLeave}
|
||||
on:click={thumbnailClickedHandler}
|
||||
on:keydown={thumbnailKeyDownHandler}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { BucketPosition } from '$lib/stores/assets.store';
|
||||
|
||||
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
@@ -18,7 +22,6 @@
|
||||
|
||||
let selectedAsset: AssetResponseDto;
|
||||
let currentViewAssetIndex = 0;
|
||||
|
||||
let viewWidth: number;
|
||||
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
|
||||
|
||||
@@ -88,7 +91,7 @@
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
|
||||
{#each assets as asset (asset.id)}
|
||||
{#each assets as asset, i (asset.id)}
|
||||
<div animate:flip={{ duration: 500 }}>
|
||||
<Thumbnail
|
||||
{asset}
|
||||
@@ -97,6 +100,8 @@
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
on:intersected={(event) =>
|
||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||
selected={selectedAssets.has(asset)}
|
||||
{showArchiveIcon}
|
||||
/>
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
const parameters = new URLSearchParams({
|
||||
q: searchValue,
|
||||
smart: smartSearch,
|
||||
take: '100',
|
||||
});
|
||||
|
||||
showHistory = false;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import type { AssetResponseDto } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
@@ -27,15 +26,20 @@
|
||||
import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||
import type { AssetResponseDto, SearchResponseDto } from '@immich/sdk';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { api } from '@api';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const MAX_ASSET_COUNT = 5000;
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
// The GalleryViewer pushes it's own history state, which causes weird
|
||||
// behavior for history.back(). To prevent that we store the previous page
|
||||
// manually and navigate back to that.
|
||||
let previousRoute = AppRoute.EXPLORE as string;
|
||||
$: curPage = data.results?.assets.nextPage;
|
||||
$: albums = data.results?.albums.items;
|
||||
|
||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||
@@ -107,6 +111,33 @@
|
||||
const handleSelectAll = () => {
|
||||
selectedAssets = new Set(searchResultAssets);
|
||||
};
|
||||
|
||||
export const loadNextPage = async () => {
|
||||
if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await authenticate();
|
||||
let results: SearchResponseDto | null = null;
|
||||
$page.url.searchParams.set('page', curPage.toString());
|
||||
const res = await api.searchApi.search({}, { params: $page.url.searchParams });
|
||||
if (searchResultAssets) {
|
||||
searchResultAssets.push(...res.data.assets.items);
|
||||
} else {
|
||||
searchResultAssets = res.data.assets.items;
|
||||
}
|
||||
|
||||
const assets = {
|
||||
...res.data.assets,
|
||||
items: searchResultAssets,
|
||||
};
|
||||
results = {
|
||||
assets,
|
||||
albums: res.data.albums,
|
||||
};
|
||||
|
||||
data.results = results;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -164,7 +195,12 @@
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if searchResultAssets && searchResultAssets.length > 0}
|
||||
<div class="pl-4">
|
||||
<GalleryViewer assets={searchResultAssets} bind:selectedAssets showArchiveIcon={true} />
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
bind:selectedAssets
|
||||
on:intersected={loadNextPage}
|
||||
showArchiveIcon={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { type SearchResponseDto, api } from '@api';
|
||||
import { type AssetResponseDto, type SearchResponseDto, api } from '@api';
|
||||
import type { PageLoad } from './$types';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
|
||||
@@ -10,8 +10,18 @@ export const load = (async (data) => {
|
||||
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
|
||||
let results: SearchResponseDto | null = null;
|
||||
if (term) {
|
||||
const { data } = await api.searchApi.search({}, { params: url.searchParams });
|
||||
results = data;
|
||||
const res = await api.searchApi.search({}, { params: data.url.searchParams });
|
||||
let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items;
|
||||
if (items) {
|
||||
items.push(...res.data.assets.items);
|
||||
} else {
|
||||
items = res.data.assets.items;
|
||||
}
|
||||
const assets = { ...res.data.assets, items };
|
||||
results = {
|
||||
assets,
|
||||
albums: res.data.albums,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user