diff --git a/i18n/en.json b/i18n/en.json
index d0c8d3cbe71cdf..b00d36cb09da3d 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -492,6 +492,7 @@
"admin_password": "Admin Password",
"administration": "Administration",
"advanced": "Advanced",
+ "advanced_filters": "Advanced filters",
"advanced_settings_clear_image_cache": "Clear Image Cache",
"advanced_settings_clear_image_cache_error": "Failed to clear image cache",
"advanced_settings_clear_image_cache_success": "Successfully cleared {size}",
@@ -1133,6 +1134,7 @@
"filename": "Filename",
"filetype": "Filetype",
"filter": "Filter",
+ "filter_by": "Filter by",
"filter_people": "Filter people",
"filter_places": "Filter places",
"filter_tags": "Filter tags",
@@ -1396,6 +1398,7 @@
"marked_all_as_read": "Marked all as read",
"matches": "Matches",
"matching_assets": "Matching Assets",
+ "media": "Media",
"media_chrome": {
"auto": "Auto",
"captions": "Captions",
@@ -1429,6 +1432,7 @@
"volume": "volume"
},
"media_type": "Media type",
+ "media_type_description": "Show only photos or videos from your library.",
"memories": "Memories",
"memories_all_caught_up": "All caught up",
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
@@ -1471,6 +1475,7 @@
"my_immich_title": "My Immich link",
"name": "Name",
"name_or_nickname": "Name or nickname",
+ "name_plus_more_people": "{name} + {count, plural, one {# person} other {# people}}",
"name_required": "Name is required",
"navigate": "Navigate",
"navigate_to_time": "Navigate to Time",
@@ -1610,8 +1615,10 @@
"paused": "Paused",
"pending": "Pending",
"people": "People",
+ "people_count": "{count, plural, one {# person} other {# people}}",
"people_edits_count": "Edited {count, plural, one {# person} other {# people}}",
"people_feature_description": "Browsing photos and videos grouped by people",
+ "people_search_description": "Select one or more people to find photos they appear in.",
"permanent_deletion_warning": "Permanent deletion warning",
"permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets",
"permanently_delete": "Permanently delete",
@@ -1835,17 +1842,24 @@
"search_filter_apply": "Apply filter",
"search_filter_camera_title": "Select camera type",
"search_filter_date": "Date",
+ "search_filter_date_custom": "Custom",
+ "search_filter_date_description": "Filter by when your photos were taken using a preset range or custom date window.",
"search_filter_date_interval": "{start} to {end}",
+ "search_filter_date_last_30_days": "Last 30 days",
+ "search_filter_date_last_year": "Last year",
+ "search_filter_date_this_year": "This year",
"search_filter_date_title": "Select a date range",
"search_filter_display_option_not_in_album": "Not in album",
"search_filter_display_options": "Display Options",
"search_filter_filename": "Search by file name",
"search_filter_location": "Location",
+ "search_filter_location_description": "Search for photos taken in a specific city, country, or location.",
"search_filter_location_title": "Select location",
"search_filter_media_type": "Media Type",
"search_filter_media_type_title": "Select media type",
"search_filter_people_title": "Select people",
"search_filter_star_rating": "Star Rating",
+ "search_filter_tags_description": "Filter by tags you've added to organize your library.",
"search_filter_tags_title": "Select tags",
"search_for": "Search for",
"search_for_existing_person": "Search for existing person",
@@ -1864,6 +1878,7 @@
"search_tags": "Search tags...",
"search_timezone": "Search timezone...",
"search_type": "Search type",
+ "search_type_description": "Choose how Immich interprets your search: by visual content, file details, or embedded text.",
"search_your_photos": "Search your photos",
"searching_locales": "Searching locales...",
"second": "Second",
@@ -2095,6 +2110,7 @@
"tag_face": "Tag face",
"tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
"tag_people": "Tag People",
+ "tag_plus_more_tags": "{tag} + {count, plural, one {# tag} other {# tags}}",
"tag_updated": "Updated tag: {tag}",
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
"tags": "Tags",
diff --git a/web/src/lib/components/shared-components/Combobox.svelte b/web/src/lib/components/shared-components/Combobox.svelte
index 388317a4ef9584..83d56a550ed6f6 100644
--- a/web/src/lib/components/shared-components/Combobox.svelte
+++ b/web/src/lib/components/shared-components/Combobox.svelte
@@ -268,7 +268,9 @@
-
+{#if !hideLabel}
+
+{/if}
- import { goto } from '$app/navigation';
import { focusOutside } from '$lib/actions/focus-outside';
import { shortcuts } from '$lib/actions/shortcut';
- import SearchFilterModal from '$lib/modals/SearchFilterModal.svelte';
import { Route } from '$lib/route';
import { searchStore } from '$lib/stores/search.svelte';
import { handlePromiseError } from '$lib/utils';
import { generateId } from '$lib/utils/generate-id';
- import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
- import { Button, IconButton, modalManager } from '@immich/ui';
- import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
- import { onDestroy, onMount, tick } from 'svelte';
+ import { IconButton } from '@immich/ui';
+ import { mdiClose, mdiMagnify } from '@mdi/js';
+ import { onDestroy, tick } from 'svelte';
import { t } from 'svelte-i18n';
- import SearchHistoryBox from './SearchHistoryBox.svelte';
+ import SearchFilters from './SearchFilters.svelte';
+ import { searchManager } from '$lib/managers/search-manager.svelte';
type Props = {
- value?: string;
grayTheme: boolean;
- searchQuery?: MetadataSearchDto | SmartSearchDto;
};
- let { value = $bindable(''), grayTheme, searchQuery = {} }: Props = $props();
+ let { grayTheme }: Props = $props();
- let showClearIcon = $derived(value.length > 0);
+ let showClearIcon = $derived(searchManager.filter.query.length > 0);
let input = $state
();
- let searchHistoryBox = $state>();
+ let searchFilters = $state>();
let showSuggestions = $state(false);
- let isSearchSuggestions = $state(false);
let selectedId: string | undefined = $state();
- let close: (() => Promise) | undefined;
- let showSearchTypeDropdown = $state(false);
- let currentSearchType = $state('smart');
const listboxId = generateId();
const searchTypeId = generateId();
@@ -40,10 +32,10 @@
searchStore.isSearchEnabled = false;
});
- const handleSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
+ const handleSearch = async () => {
closeDropdown();
searchStore.isSearchEnabled = false;
- await goto(Route.search(payload));
+ await searchManager.submit();
};
const clearSearchTerm = (searchTerm: string) => {
@@ -69,90 +61,35 @@
const onFocusIn = () => {
searchStore.isSearchEnabled = true;
- getSearchType();
};
const onFocusOut = () => {
searchStore.isSearchEnabled = false;
};
- const buildSearchPayload = (term: string): SmartSearchDto | MetadataSearchDto => {
- const searchType = getSearchType();
- switch (searchType) {
- case 'smart': {
- return { query: term };
- }
- case 'metadata': {
- return { originalFileName: term };
- }
- case 'description': {
- return { description: term };
- }
- case 'fullPath': {
- const normalizedTerm = term.trim();
- return normalizedTerm ? { originalPath: normalizedTerm } : {};
- }
- case 'ocr': {
- return { ocr: term };
- }
- default: {
- return { query: term };
- }
- }
- };
-
const onHistoryTermClick = async (searchTerm: string) => {
- value = searchTerm;
- await handleSearch(buildSearchPayload(searchTerm));
- };
-
- const onFilterClick = async () => {
- value = '';
-
- if (close) {
- await close();
- close = undefined;
- searchStore.isSearchEnabled = false;
- return;
- }
-
- const result = modalManager.open(SearchFilterModal, { searchQuery });
- close = () => result.close();
- closeDropdown();
-
- const searchResult = await result.onClose;
- close = undefined;
- searchStore.isSearchEnabled = false;
-
- // Refresh search type after modal closes
- getSearchType();
-
- if (!searchResult) {
- return;
- }
-
- await handleSearch(searchResult);
+ searchManager.filter.query = searchTerm;
+ await handleSearch();
};
const onSubmit = () => {
- handlePromiseError(handleSearch(buildSearchPayload(value)));
- saveSearchTerm(value);
+ handlePromiseError(handleSearch());
+ saveSearchTerm(searchManager.filter.query);
};
const onClear = () => {
- value = '';
+ searchManager.filter.query = '';
input?.focus();
};
const onEscape = () => {
closeDropdown();
- closeSearchTypeDropdown();
};
const onArrow = async (direction: 1 | -1) => {
openDropdown();
await tick();
- searchHistoryBox?.moveSelection(direction);
+ searchFilters?.moveSelection(direction);
};
const onEnter = (event: KeyboardEvent) => {
@@ -161,12 +98,12 @@
}
event.preventDefault();
- searchHistoryBox?.selectActiveOption();
+ searchFilters?.selectActiveOption();
};
const onInput = () => {
openDropdown();
- searchHistoryBox?.clearSelection();
+ searchFilters?.clearSelection();
};
const openDropdown = () => {
@@ -175,89 +112,16 @@
const closeDropdown = () => {
showSuggestions = false;
- searchHistoryBox?.clearSelection();
- };
-
- const toggleSearchTypeDropdown = () => {
- showSearchTypeDropdown = !showSearchTypeDropdown;
- };
-
- const closeSearchTypeDropdown = () => {
- showSearchTypeDropdown = false;
- };
-
- const selectSearchType = (type: string) => {
- localStorage.setItem('searchQueryType', type);
- currentSearchType = type;
- showSearchTypeDropdown = false;
- input?.focus();
+ searchFilters?.clearSelection();
};
const onsubmit = (event: Event) => {
event.preventDefault();
onSubmit();
};
-
- function getSearchType() {
- const searchType = localStorage.getItem('searchQueryType');
- switch (searchType) {
- case 'smart':
- case 'metadata':
- case 'description':
- case 'fullPath':
- case 'ocr': {
- currentSearchType = searchType;
- return searchType;
- }
- default: {
- currentSearchType = 'smart';
- return 'smart';
- }
- }
- }
-
- function getSearchTypeText(): string {
- switch (currentSearchType) {
- case 'smart': {
- return $t('context');
- }
- case 'metadata': {
- return $t('filename');
- }
- case 'description': {
- return $t('description');
- }
- case 'fullPath': {
- return $t('full_path_or_folder');
- }
- case 'ocr': {
- return $t('ocr');
- }
- default: {
- return $t('context');
- }
- }
- }
-
- onMount(() => {
- getSearchType();
- });
-
- const searchTypes = [
- { value: 'smart', label: () => $t('context') },
- { value: 'metadata', label: () => $t('filename') },
- { value: 'description', label: () => $t('description') },
- { value: 'fullPath', label: () => $t('full_path_or_folder') },
- { value: 'ocr', label: () => $t('ocr') },
- ] as const;
- input?.select() },
- { shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
- ]}
-/>
+ input?.select() }]} />
- 0}
- >
-
-
-
- {#if showSearchTypeDropdown}
-
- {#each searchTypes as searchType (searchType.value)}
-
- {/each}
-
- {/if}
-
-
-
{#if showClearIcon}
-
-
-
-
diff --git a/web/src/lib/components/shared-components/search-bar/SearchButton.svelte b/web/src/lib/components/shared-components/search-bar/SearchButton.svelte
new file mode 100644
index 00000000000000..820af91f4b0d50
--- /dev/null
+++ b/web/src/lib/components/shared-components/search-bar/SearchButton.svelte
@@ -0,0 +1,31 @@
+
+
+
diff --git a/web/src/lib/components/shared-components/search-bar/SearchCameraSection.svelte b/web/src/lib/components/shared-components/search-bar/SearchCameraSection.svelte
index b1d93210cfa468..3fe6d0d088d256 100644
--- a/web/src/lib/components/shared-components/search-bar/SearchCameraSection.svelte
+++ b/web/src/lib/components/shared-components/search-bar/SearchCameraSection.svelte
@@ -1,16 +1,12 @@
-
{$t('camera')}
-
+
{$t('camera')}
+
- import type { SearchDateFilter } from '$lib/types';
+ import { getSearchDatePreset, SearchDatePreset } from './search-bar-utils';
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';
- type Props = {
- filters: SearchDateFilter;
- };
+ let filters = $derived(searchManager.filter.date);
+ let invalid = $derived(filters.takenAfter && filters.takenBefore && filters.takenAfter > filters.takenBefore);
- let { filters = $bindable() }: Props = $props();
+ let currentPreset = $derived(getSearchDatePreset(filters.takenAfter, filters.takenBefore));
- let invalid = $derived(filters.takenAfter && filters.takenBefore && filters.takenAfter > filters.takenBefore);
-
+ const setPreset = (preset: SearchDatePreset) => {
+ if (currentPreset === preset) {
+ currentPreset = filters.takenAfter = filters.takenBefore = undefined;
+ return;
+ }
-
-
-
- {$t('start_date')}
-
-
+ switch (preset) {
+ case SearchDatePreset.ThisYear: {
+ filters.takenAfter = DateTime.utc().startOf('year');
+ filters.takenBefore = DateTime.utc().endOf('year');
+ break;
+ }
+ case SearchDatePreset.LastYear: {
+ filters.takenAfter = DateTime.utc().minus({ years: 1 }).startOf('year');
+ filters.takenBefore = DateTime.utc().minus({ years: 1 }).endOf('year');
+ break;
+ }
+ case SearchDatePreset.Last30Days: {
+ filters.takenAfter = DateTime.utc().minus({ days: 30 }).startOf('day');
+ filters.takenBefore = DateTime.utc().endOf('day');
+ break;
+ }
+ case SearchDatePreset.Custom: {
+ filters.takenAfter = filters.takenBefore = undefined;
+ break;
+ }
+ }
-
- {$t('end_date')}
-
-
+ currentPreset = preset;
+ };
+
+
+
+
{$t('search_filter_date_description')}
+
+ setPreset(SearchDatePreset.ThisYear)}>{$t('search_filter_date_this_year')}
+ setPreset(SearchDatePreset.LastYear)}>{$t('search_filter_date_last_year')}
+ setPreset(SearchDatePreset.Last30Days)}>{$t('search_filter_date_last_30_days')}
+ setPreset(SearchDatePreset.Custom)}>{$t('search_filter_date_custom')}
+ {#if currentPreset === SearchDatePreset.Custom}
+
+
+ {$t('start_date')}
+
+
+
+ {$t('end_date')}
+
+
+
+ {/if}
{#if invalid}
{$t('start_date_before_end_date')}
{/if}
diff --git a/web/src/lib/components/shared-components/search-bar/SearchDisplaySection.svelte b/web/src/lib/components/shared-components/search-bar/SearchDisplaySection.svelte
index c011d6a97523cf..db1a087282b3c4 100644
--- a/web/src/lib/components/shared-components/search-bar/SearchDisplaySection.svelte
+++ b/web/src/lib/components/shared-components/search-bar/SearchDisplaySection.svelte
@@ -1,32 +1,25 @@