feat: search filter ui

This commit is contained in:
Alex Tran
2025-11-29 20:19:48 +00:00
parent 08f320c801
commit e4f1c4ee64
9 changed files with 115 additions and 41 deletions

View File

@@ -81,9 +81,7 @@
</script>
<div id="camera-selection">
<p class="uppercase immich-form-label">{$t('camera')}</p>
<div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="grid grid-auto-fit-40 gap-5">
<div class="w-full">
<Combobox
label={$t('make')}

View File

@@ -20,8 +20,7 @@
<div id="display-options-selection">
<fieldset>
<legend class="uppercase immich-form-label">{$t('display_options')}</legend>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
<div class="flex flex-wrap gap-x-5 gap-y-2">
<div class="flex items-center gap-2">
<Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} />
<Label label={$t('not_in_any_album')} for="not-in-album-checkbox" />

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import { Icon } from '@immich/ui';
import { mdiChevronDown } from '@mdi/js';
import type { Snippet } from 'svelte';
import { slide } from 'svelte/transition';
interface Props {
title: string;
icon: string;
expanded?: boolean;
children?: Snippet;
isFirst?: boolean;
isLast?: boolean;
}
let { title, icon, expanded = $bindable(false), children, isFirst = false, isLast = false }: Props = $props();
</script>
<div
class="border border-gray-200 dark:border-gray-700 overflow-hidden transition-all duration-200"
class:rounded-t-2xl={isFirst}
class:rounded-b-2xl={isLast}
>
<button
type="button"
aria-expanded={expanded}
onclick={() => (expanded = !expanded)}
class="flex w-full items-center justify-between gap-3 px-4 py-3 text-start bg-light-50 hover:bg-primary-50 dark:hover:bg-gray-800 transition-colors"
class:bg-light-200={expanded}
>
<div class="flex items-center gap-3">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg bg-immich-primary/10 dark:bg-immich-dark-primary/20"
>
<Icon {icon} size="18" class="text-immich-primary dark:text-immich-dark-primary" />
</div>
<span class="font-medium text-gray-900 dark:text-gray-100">{title}</span>
</div>
<div
class="flex items-center justify-center w-7 h-7 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<Icon
icon={mdiChevronDown}
size="20"
class="text-gray-500 dark:text-gray-400 transition-transform duration-200 {expanded ? 'rotate-180' : ''}"
/>
</div>
</button>
{#if expanded}
<div transition:slide={{ duration: 150 }} class="px-4 py-4">
{@render children?.()}
</div>
{/if}
</div>

View File

@@ -74,9 +74,7 @@
</script>
<div id="location-selection">
<p class="uppercase immich-form-label">{$t('place')}</p>
<div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="grid grid-auto-fit-40 gap-5">
<div class="w-full">
<Combobox
label={$t('country')}

View File

@@ -12,8 +12,7 @@
<div id="media-type-selection">
<fieldset>
<legend class="uppercase immich-form-label">{$t('media_type')}</legend>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
<div class="flex flex-wrap gap-x-5 gap-y-2">
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} />
<RadioButton
name="media-type"

View File

@@ -62,8 +62,7 @@
: filterPeople(people, name).slice(0, numberOfPeople)}
<div id="people-selection" class="max-h-60 -mb-4 overflow-y-auto immich-scrollbar">
<div class="flex items-center w-full justify-between gap-6">
<p class="uppercase immich-form-label py-3">{$t('people')}</p>
<div class="flex items-center w-full justify-end gap-6">
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
</div>

View File

@@ -19,15 +19,13 @@
</script>
<div class="grid grid-auto-fit-40 gap-5">
<label class="immich-form-label" for="start-date">
<div class="[&_label]:uppercase">
<Combobox
label={$t('rating')}
placeholder={$t('search_rating')}
{options}
selectedOption={rating === undefined ? undefined : options[rating]}
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
/>
</div>
</label>
<div class="[&_label]:uppercase">
<Combobox
label=""
placeholder={$t('search_rating')}
{options}
selectedOption={rating === undefined ? undefined : options[rating]}
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
/>
</div>
</div>

View File

@@ -41,14 +41,14 @@
</script>
{#if $preferences?.tags?.enabled}
<div id="location-selection">
<div id="tags-selection">
<form autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<div class="flex flex-col gap-2">
<div class="[&_label]:uppercase">
<Combobox
label=""
disabled={selectedTags === null}
onSelect={handleSelect}
label={$t('tags')}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
bind:selectedOption

View File

@@ -25,6 +25,7 @@
} from '$lib/components/shared-components/search-bar/search-camera-section.svelte';
import SearchDateSection from '$lib/components/shared-components/search-bar/search-date-section.svelte';
import SearchDisplaySection from '$lib/components/shared-components/search-bar/search-display-section.svelte';
import SearchFilterSection from '$lib/components/shared-components/search-bar/search-filter-section.svelte';
import SearchLocationSection from '$lib/components/shared-components/search-bar/search-location-section.svelte';
import SearchMediaSection from '$lib/components/shared-components/search-bar/search-media-section.svelte';
import SearchPeopleSection from '$lib/components/shared-components/search-bar/search-people-section.svelte';
@@ -36,7 +37,18 @@
import { generateId } from '$lib/utils/generate-id';
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiTune } from '@mdi/js';
import {
mdiAccountMultiple,
mdiCalendarRange,
mdiCamera,
mdiCog,
mdiImageMultiple,
mdiMagnify,
mdiMapMarker,
mdiStar,
mdiTag,
mdiTune,
} from '@mdi/js';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
@@ -187,37 +199,52 @@
<Modal icon={mdiTune} size="giant" title={$t('search_options')} {onClose}>
<ModalBody>
<form id={formId} autocomplete="off" {onsubmit} {onreset}>
<div class="flex flex-col gap-4 pb-10" tabindex="-1">
<!-- PEOPLE -->
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
<div class="flex flex-col gap-0" tabindex="-1">
<!-- TEXT -->
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />
<SearchFilterSection title={$t('search_type')} icon={mdiMagnify} isFirst expanded>
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />
</SearchFilterSection>
<!-- PEOPLE -->
<SearchFilterSection title={$t('people')} icon={mdiAccountMultiple}>
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
</SearchFilterSection>
<!-- TAGS -->
<SearchTagsSection bind:selectedTags={filter.tagIds} />
<SearchFilterSection title={$t('tags')} icon={mdiTag}>
<SearchTagsSection bind:selectedTags={filter.tagIds} />
</SearchFilterSection>
<!-- LOCATION -->
<SearchLocationSection bind:filters={filter.location} />
<SearchFilterSection title={$t('place')} icon={mdiMapMarker}>
<SearchLocationSection bind:filters={filter.location} />
</SearchFilterSection>
<!-- CAMERA MODEL -->
<SearchCameraSection bind:filters={filter.camera} />
<SearchFilterSection title={$t('camera')} icon={mdiCamera}>
<SearchCameraSection bind:filters={filter.camera} />
</SearchFilterSection>
<!-- DATE RANGE -->
<SearchDateSection bind:filters={filter.date} />
<SearchFilterSection title={$t('date_range')} icon={mdiCalendarRange}>
<SearchDateSection bind:filters={filter.date} />
</SearchFilterSection>
<!-- RATING -->
{#if $preferences?.ratings.enabled}
<SearchRatingsSection bind:rating={filter.rating} />
<SearchFilterSection title={$t('rating')} icon={mdiStar}>
<SearchRatingsSection bind:rating={filter.rating} />
</SearchFilterSection>
{/if}
<div class="grid md:grid-cols-2 gap-x-5 gap-y-10">
<!-- MEDIA TYPE -->
<!-- MEDIA TYPE & DISPLAY OPTIONS -->
<SearchFilterSection title={$t('media_type')} icon={mdiImageMultiple}>
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
</SearchFilterSection>
<!-- DISPLAY OPTIONS -->
<SearchFilterSection title={$t('display_options')} icon={mdiCog} isLast>
<SearchDisplaySection bind:filters={filter.display} />
</div>
</SearchFilterSection>
</div>
</form>
</ModalBody>