chore(web): search ui tweaks and refactoring

This commit is contained in:
Ben Beckford 2026-08-04 23:54:28 -07:00
parent c54722d666
commit b3a0283fcf
11 changed files with 110 additions and 166 deletions

View file

@ -2111,7 +2111,6 @@
"tags": "Tags",
"tap_to_run_job": "Tap to run job",
"template": "Template",
"text_in_images": "Text in images",
"text_recognition": "Text recognition",
"theme": "Theme",
"theme_selection": "Theme selection",

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Button } from '@immich/ui';
import { mdiCheck } from '@mdi/js';
let {
active = $bindable(),
children,
class: className = '',
leadingIcon = undefined,
checked = false,
...rest
} = $props();
</script>
<Button
size="small"
shape="round"
color={active ? 'primary' : 'secondary'}
variant="outline"
class="{active ? '' : 'bg-transparent'} {className}"
leadingIcon={checked && active ? mdiCheck : leadingIcon}
{...rest}>{@render children()}</Button
>

View file

@ -1,10 +1,10 @@
<script lang="ts">
import { getSearchDatePreset, SearchDatePreset } from './search-bar-utils';
import { Button, DatePicker, Text } from '@immich/ui';
import { mdiCheck } from '@mdi/js';
import { DatePicker, Text } from '@immich/ui';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import { searchManager } from '$lib/managers/search-manager.svelte';
import SearchButton from './SearchButton.svelte';
let filters = $derived(searchManager.filter.date);
let invalid = $derived(filters.takenAfter && filters.takenBefore && filters.takenAfter > filters.takenBefore);
@ -48,42 +48,26 @@
<div>
<Text class="pb-5">{$t('search_filter_date_description')}</Text>
<div class="flex flex-wrap gap-2">
<Button
shape="round"
color={currentPreset === SearchDatePreset.ThisYear ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setPreset(SearchDatePreset.ThisYear)}
class={currentPreset === SearchDatePreset.ThisYear ? undefined : 'bg-transparent'}
leadingIcon={currentPreset === SearchDatePreset.ThisYear ? mdiCheck : undefined}
>{$t('search_filter_date_this_year')}
</Button>
<Button
shape="round"
color={currentPreset === SearchDatePreset.LastYear ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setPreset(SearchDatePreset.LastYear)}
class={currentPreset === SearchDatePreset.LastYear ? undefined : 'bg-transparent'}
leadingIcon={currentPreset === SearchDatePreset.LastYear ? mdiCheck : undefined}
>{$t('search_filter_date_last_year')}
</Button>
<Button
shape="round"
color={currentPreset === SearchDatePreset.Last30Days ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setPreset(SearchDatePreset.Last30Days)}
class={currentPreset === SearchDatePreset.Last30Days ? undefined : 'bg-transparent'}
leadingIcon={currentPreset === SearchDatePreset.Last30Days ? mdiCheck : undefined}
>{$t('search_filter_date_last_30_days')}
</Button>
<Button
shape="round"
color={currentPreset === SearchDatePreset.Custom ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setPreset(SearchDatePreset.Custom)}
class={currentPreset === SearchDatePreset.Custom ? undefined : 'bg-transparent'}
leadingIcon={currentPreset === SearchDatePreset.Custom ? mdiCheck : undefined}
>{$t('search_filter_date_custom')}
</Button>
<SearchButton
checked
active={currentPreset === SearchDatePreset.ThisYear}
onclick={() => setPreset(SearchDatePreset.ThisYear)}>{$t('search_filter_date_this_year')}</SearchButton
>
<SearchButton
checked
active={currentPreset === SearchDatePreset.LastYear}
onclick={() => setPreset(SearchDatePreset.LastYear)}>{$t('search_filter_date_last_year')}</SearchButton
>
<SearchButton
checked
active={currentPreset === SearchDatePreset.Last30Days}
onclick={() => setPreset(SearchDatePreset.Last30Days)}>{$t('search_filter_date_last_30_days')}</SearchButton
>
<SearchButton
checked
active={currentPreset === SearchDatePreset.Custom}
onclick={() => setPreset(SearchDatePreset.Custom)}>{$t('search_filter_date_custom')}</SearchButton
>
</div>
{#if currentPreset === SearchDatePreset.Custom}
<div id="date-range-selection" class="grid grid-auto-fit-40 gap-5 py-5">

View file

@ -1,7 +1,7 @@
<script lang="ts">
import SearchButton from './SearchButton.svelte';
import { searchManager } from '$lib/managers/search-manager.svelte';
import { Button, Text } from '@immich/ui';
import { mdiCheck } from '@mdi/js';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n';
let filters = $derived(searchManager.filter.display);
@ -10,35 +10,16 @@
<div id="display-options-selection">
<fieldset>
<Text class="py-5" fontWeight="medium">{$t('library')}</Text>
<div class="flex flex-wrap gap-2">
<Button
shape="round"
color={filters.isFavorite ? 'primary' : 'secondary'}
variant="outline"
onclick={() => (filters.isFavorite = !filters.isFavorite)}
class={filters.isFavorite ? undefined : 'bg-transparent'}
leadingIcon={filters.isFavorite ? mdiCheck : undefined}
>{$t('favorites')}
</Button>
<Button
shape="round"
color={filters.isArchive ? 'primary' : 'secondary'}
variant="outline"
onclick={() => (filters.isArchive = !filters.isArchive)}
class={filters.isArchive ? undefined : 'bg-transparent'}
leadingIcon={filters.isArchive ? mdiCheck : undefined}
>{$t('archive')}
</Button>
<Button
shape="round"
color={filters.isNotInAlbum ? 'primary' : 'secondary'}
variant="outline"
onclick={() => (filters.isNotInAlbum = !filters.isNotInAlbum)}
class={filters.isNotInAlbum ? undefined : 'bg-transparent'}
leadingIcon={filters.isNotInAlbum ? mdiCheck : undefined}
>{$t('search_filter_display_option_not_in_album')}
</Button>
<SearchButton checked active={filters.isFavorite} onclick={() => (filters.isFavorite = !filters.isFavorite)}
>{$t('favorites')}</SearchButton
>
<SearchButton checked active={filters.isArchive} onclick={() => (filters.isArchive = !filters.isArchive)}
>{$t('archive')}</SearchButton
>
<SearchButton checked active={filters.isNotInAlbum} onclick={() => (filters.isNotInAlbum = !filters.isNotInAlbum)}
>{$t('search_filter_display_option_not_in_album')}</SearchButton
>
</div>
</fieldset>
</div>

View file

@ -37,6 +37,7 @@
} from './search-bar-utils';
import { onMount } from 'svelte';
import { searchManager } from '$lib/managers/search-manager.svelte';
import SearchButton from './SearchButton.svelte';
interface Props {
id: string;
@ -196,16 +197,14 @@
<Text class="py-5" fontWeight="medium" aria-hidden={true}>{$t('filter_by')}</Text>
<div class="flex flex-wrap gap-2">
{#each filters as item (item.name)}
<Button
shape="round"
color={activeFilter === item.name || item.activeTitle() ? 'primary' : 'secondary'}
variant="outline"
<SearchButton
active={activeFilter === item.name || item.activeTitle()}
leadingIcon={item.icon}
class={activeFilter === item.name ? 'border-2' : undefined}
onclick={() => (activeFilter = item.name)}
class="{activeFilter === item.name || item.activeTitle() ? undefined : 'bg-transparent'}
{activeFilter === item.name ? 'border-2' : undefined}"
>{item.activeTitle() ?? item.title}
</Button>
>
{item.activeTitle() ?? item.title}
</SearchButton>
{/each}
</div>
</div>
@ -238,6 +237,7 @@
<div class="my-5 h-px w-full bg-light-200 dark:bg-dark-600"></div>
<div class="flex gap-2 px-5 pb-5">
<Button
size="small"
variant={advancedFiltersSet ? 'outline' : 'ghost'}
leadingIcon={mdiTune}
trailingIcon={activeFilter === 'advanced' ? mdiChevronUp : mdiChevronDown}
@ -245,10 +245,15 @@
>{$t('advanced_filters')}</Button
>
<div class="flex-1"></div>
<Button shape="round" variant="outline" color="secondary" class="bg-transparent" onclick={() => clear()}
>{$t('clear_all')}</Button
<Button
size="small"
shape="round"
variant="outline"
color="secondary"
class="bg-transparent"
onclick={() => clear()}>{$t('clear_all')}</Button
>
<Button shape="round" onclick={() => onSearch()}>{$t('search')}</Button>
<Button size="small" shape="round" onclick={() => onSearch()}>{$t('search')}</Button>
</div>
</div>
{/if}

View file

@ -1,10 +1,10 @@
<script lang="ts">
import { searchMediaTitle } from './search-bar-utils';
import { MediaType } from '$lib/constants';
import { Button, Text } from '@immich/ui';
import { mdiCheck } from '@mdi/js';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n';
import { searchManager } from '$lib/managers/search-manager.svelte';
import SearchButton from './SearchButton.svelte';
interface Props {
title: string | undefined;
@ -25,33 +25,15 @@
<Text class="pb-5">{$t('media_type_description')}</Text>
<div class="flex flex-wrap gap-2">
<Button
shape="round"
color={filteredMedia === MediaType.All ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setMedia(MediaType.All)}
class={filteredMedia === MediaType.All ? undefined : 'bg-transparent'}
leadingIcon={filteredMedia === MediaType.All ? mdiCheck : undefined}
>{$t('all')}
</Button>
<Button
shape="round"
color={filteredMedia === MediaType.Image ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setMedia(MediaType.Image)}
class={filteredMedia === MediaType.Image ? undefined : 'bg-transparent'}
leadingIcon={filteredMedia === MediaType.Image ? mdiCheck : undefined}
>{$t('image')}
</Button>
<Button
shape="round"
color={filteredMedia === MediaType.Video ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setMedia(MediaType.Video)}
class={filteredMedia === MediaType.Video ? undefined : 'bg-transparent'}
leadingIcon={filteredMedia === MediaType.Video ? mdiCheck : undefined}
>{$t('video')}
</Button>
<SearchButton checked active={filteredMedia === MediaType.All} onclick={() => setMedia(MediaType.All)}>
{$t('all')}
</SearchButton>
<SearchButton checked active={filteredMedia === MediaType.Image} onclick={() => setMedia(MediaType.Image)}>
{$t('image')}
</SearchButton>
<SearchButton checked active={filteredMedia === MediaType.Video} onclick={() => setMedia(MediaType.Video)}>
{$t('video')}
</SearchButton>
</div>
</fieldset>
</div>

View file

@ -88,6 +88,7 @@
{#if showAllPeople || people.length > peopleList.length}
<div class="mt-2 flex justify-center">
<Button
size="small"
color="primary"
variant="ghost"
shape="round"

View file

@ -1,7 +1,8 @@
<script lang="ts">
import { searchManager } from '$lib/managers/search-manager.svelte';
import { Button, Text } from '@immich/ui';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n';
import SearchButton from './SearchButton.svelte';
let rating = $derived(searchManager.filter.rating);
@ -18,14 +19,12 @@
<Text class="my-5" fontWeight="medium">{$t('rating')}</Text>
<div class="flex flex-wrap gap-2">
{#each options as option (option.value)}
<Button
shape="round"
color={rating === option.value ? 'primary' : 'secondary'}
variant="outline"
<SearchButton
active={rating === option.value}
onclick={() => (searchManager.filter.rating = rating === option.value ? undefined : option.value)}
class={rating === option.value ? undefined : 'bg-transparent'}
>{option.label}
</Button>
>
{option.label}
</SearchButton>
{/each}
</div>
</div>

View file

@ -72,6 +72,7 @@
{@const tag = tagMap[tagId]}
{#if tag}
<Button
size="small"
shape="round"
color="primary"
variant="outline"

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { searchTypeTitle } from '$lib/components/shared-components/search-bar/search-bar-utils';
import { searchTypeTitle } from './search-bar-utils';
import SearchButton from './SearchButton.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { searchManager } from '$lib/managers/search-manager.svelte';
import { Button, Text } from '@immich/ui';
import { mdiCheck } from '@mdi/js';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n';
let queryType = $derived(searchManager.filter.queryType);
@ -18,53 +18,23 @@
<Text class="mb-5">{$t('search_type_description')}</Text>
<div class="flex flex-wrap gap-2">
{#if featureFlagsManager.value.smartSearch}
<Button
shape="round"
color={queryType === 'smart' ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setType('smart')}
class={queryType === 'smart' ? undefined : 'bg-transparent'}
leadingIcon={queryType === 'smart' ? mdiCheck : undefined}
>{$t('context')}
</Button>
<SearchButton checked active={queryType === 'smart'} onclick={() => setType('smart')}>
{$t('context')}
</SearchButton>
{/if}
<Button
shape="round"
color={queryType === 'metadata' ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setType('metadata')}
class={queryType === 'metadata' ? undefined : 'bg-transparent'}
leadingIcon={queryType === 'metadata' ? mdiCheck : undefined}
>{searchTypeTitle('metadata')}
</Button>
<Button
shape="round"
color={queryType === 'description' ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setType('description')}
class={queryType === 'description' ? undefined : 'bg-transparent'}
leadingIcon={queryType === 'description' ? mdiCheck : undefined}
>{searchTypeTitle('description')}
</Button>
<Button
shape="round"
color={queryType === 'fullPath' ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setType('fullPath')}
class={queryType === 'fullPath' ? undefined : 'bg-transparent'}
leadingIcon={queryType === 'fullPath' ? mdiCheck : undefined}
>{searchTypeTitle('fullPath')}
</Button>
<SearchButton checked active={queryType === 'metadata'} onclick={() => setType('metadata')}>
{searchTypeTitle('metadata')}
</SearchButton>
<SearchButton checked active={queryType === 'description'} onclick={() => setType('description')}>
{searchTypeTitle('description')}
</SearchButton>
<SearchButton checked active={queryType === 'fullPath'} onclick={() => setType('fullPath')}>
{searchTypeTitle('fullPath')}
</SearchButton>
{#if featureFlagsManager.value.ocr}
<Button
shape="round"
color={queryType === 'ocr' ? 'primary' : 'secondary'}
variant="outline"
onclick={() => setType('ocr')}
class={queryType === 'ocr' ? undefined : 'bg-transparent'}
leadingIcon={queryType === 'ocr' ? mdiCheck : undefined}
>{searchTypeTitle('ocr')}
</Button>
<SearchButton checked active={queryType === 'ocr'} onclick={() => setType('ocr')}>
{searchTypeTitle('ocr')}
</SearchButton>
{/if}
</div>
</fieldset>

View file

@ -84,7 +84,7 @@ export const searchTypeTitle = (type: string) => {
return $t('full_path_or_folder');
}
case 'ocr': {
return $t('text_in_images');
return $t('ocr');
}
default: {
return undefined;