mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(web): move search options into a modal (#12438)
* feat(web): move search options into a modal * chore: revert adding focus ring * minor styling --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f2f6713a53
commit
02047a0104
6 changed files with 46 additions and 40 deletions
|
@ -22,7 +22,7 @@
|
||||||
* - `narrow`: 28rem
|
* - `narrow`: 28rem
|
||||||
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
|
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
|
||||||
*/
|
*/
|
||||||
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
|
export let width: 'extra-wide' | 'wide' | 'narrow' | 'auto' = 'narrow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for the modal.
|
* Unique identifier for the modal.
|
||||||
|
@ -34,12 +34,25 @@
|
||||||
|
|
||||||
let modalWidth: string;
|
let modalWidth: string;
|
||||||
$: {
|
$: {
|
||||||
if (width === 'wide') {
|
switch (width) {
|
||||||
modalWidth = 'w-[48rem]';
|
case 'extra-wide': {
|
||||||
} else if (width === 'narrow') {
|
modalWidth = 'w-[56rem]';
|
||||||
modalWidth = 'w-[28rem]';
|
break;
|
||||||
} else {
|
}
|
||||||
modalWidth = 'sm:max-w-4xl';
|
|
||||||
|
case 'wide': {
|
||||||
|
modalWidth = 'w-[48rem]';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'narrow': {
|
||||||
|
modalWidth = 'w-[28rem]';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
modalWidth = 'sm:max-w-4xl';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -62,7 +75,7 @@
|
||||||
aria-labelledby={titleId}
|
aria-labelledby={titleId}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="immich-scrollbar overflow-y-auto max-h-[min(92dvh,56rem)]"
|
class="immich-scrollbar overflow-y-auto max-h-[min(92dvh,64rem)]"
|
||||||
class:scroll-pb-40={isStickyBottom}
|
class:scroll-pb-40={isStickyBottom}
|
||||||
class:sm:scroll-p-24={isStickyBottom}
|
class:sm:scroll-p-24={isStickyBottom}
|
||||||
>
|
>
|
||||||
|
@ -72,7 +85,7 @@
|
||||||
</div>
|
</div>
|
||||||
{#if isStickyBottom}
|
{#if isStickyBottom}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky bottom-0 pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow"
|
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky bottom-0 pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow z-[9999]"
|
||||||
>
|
>
|
||||||
<slot name="sticky-bottom" />
|
<slot name="sticky-bottom" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{#if showLogo}
|
{#if showLogo}
|
||||||
<ImmichLogo noText={true} width={32} />
|
<ImmichLogo noText={true} width={32} />
|
||||||
{:else if icon}
|
{:else if icon}
|
||||||
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
<Icon path={icon} size={24} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
<h1 {id}>
|
<h1 {id}>
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
|
import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
|
||||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||||
import SearchHistoryBox from './search-history-box.svelte';
|
import SearchHistoryBox from './search-history-box.svelte';
|
||||||
import SearchFilterBox from './search-filter-box.svelte';
|
import SearchFilterModal from './search-filter-modal.svelte';
|
||||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
@ -160,8 +160,8 @@
|
||||||
id="main-search-bar"
|
id="main-search-bar"
|
||||||
class="w-full transition-all border-2 px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg
|
class="w-full transition-all border-2 px-14 py-4 text-immich-fg/75 dark:text-immich-dark-fg
|
||||||
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
||||||
{(showSuggestions && isSearchSuggestions) || showFilter ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
||||||
{$isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
{$isSearchEnabled && !showFilter ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
||||||
placeholder={$t('search_your_photos')}
|
placeholder={$t('search_your_photos')}
|
||||||
required
|
required
|
||||||
pattern="^(?!m:$).*$"
|
pattern="^(?!m:$).*$"
|
||||||
|
@ -215,6 +215,6 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
<SearchFilterBox {searchQuery} on:search={({ detail }) => onSearch(detail)} />
|
<SearchFilterModal {searchQuery} onSearch={(payload) => onSearch(payload)} onClose={() => (showFilter = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
|
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
import { fly } from 'svelte/transition';
|
|
||||||
import SearchPeopleSection from './search-people-section.svelte';
|
import SearchPeopleSection from './search-people-section.svelte';
|
||||||
import SearchLocationSection from './search-location-section.svelte';
|
import SearchLocationSection from './search-location-section.svelte';
|
||||||
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
|
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
|
||||||
|
@ -35,12 +33,17 @@
|
||||||
import SearchDisplaySection from './search-display-section.svelte';
|
import SearchDisplaySection from './search-display-section.svelte';
|
||||||
import SearchTextSection from './search-text-section.svelte';
|
import SearchTextSection from './search-text-section.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import { mdiTune } from '@mdi/js';
|
||||||
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
|
|
||||||
export let searchQuery: MetadataSearchDto | SmartSearchDto;
|
export let searchQuery: MetadataSearchDto | SmartSearchDto;
|
||||||
|
export let onClose: () => void;
|
||||||
|
export let onSearch: (search: SmartSearchDto | MetadataSearchDto) => void;
|
||||||
|
|
||||||
const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
|
const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
|
||||||
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
|
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
|
||||||
const dispatch = createEventDispatcher<{ search: SmartSearchDto | MetadataSearchDto }>();
|
const formId = generateId();
|
||||||
|
|
||||||
// combobox and all the search components have terrible support for value | null so we use empty string instead.
|
// combobox and all the search components have terrible support for value | null so we use empty string instead.
|
||||||
function withNullAsUndefined<T>(value: T | null) {
|
function withNullAsUndefined<T>(value: T | null) {
|
||||||
|
@ -117,21 +120,13 @@
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch('search', payload);
|
onSearch(payload);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<FullScreenModal icon={mdiTune} width="extra-wide" title={$t('search_options')} {onClose}>
|
||||||
transition:fly={{ y: 25, duration: 250 }}
|
<form id={formId} autocomplete="off" on:submit|preventDefault={search} on:reset|preventDefault={resetForm}>
|
||||||
class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300"
|
<div class="space-y-10 pb-10" tabindex="-1">
|
||||||
>
|
|
||||||
<form
|
|
||||||
id="search-filter-form"
|
|
||||||
autocomplete="off"
|
|
||||||
on:submit|preventDefault={search}
|
|
||||||
on:reset|preventDefault={resetForm}
|
|
||||||
>
|
|
||||||
<div class="px-4 sm:px-6 py-4 space-y-10 max-h-[calc(100dvh-12rem)] overflow-y-auto immich-scrollbar" tabindex="-1">
|
|
||||||
<!-- PEOPLE -->
|
<!-- PEOPLE -->
|
||||||
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
||||||
|
|
||||||
|
@ -147,7 +142,7 @@
|
||||||
<!-- DATE RANGE -->
|
<!-- DATE RANGE -->
|
||||||
<SearchDateSection bind:filters={filter.date} />
|
<SearchDateSection bind:filters={filter.date} />
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-x-5 gap-y-8">
|
<div class="grid md:grid-cols-2 gap-x-5 gap-y-10">
|
||||||
<!-- MEDIA TYPE -->
|
<!-- MEDIA TYPE -->
|
||||||
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
||||||
|
|
||||||
|
@ -155,13 +150,10 @@
|
||||||
<SearchDisplaySection bind:filters={filter.display} />
|
<SearchDisplaySection bind:filters={filter.display} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
id="button-row"
|
|
||||||
class="flex justify-end gap-4 border-t dark:border-gray-800 dark:bg-immich-dark-gray px-4 sm:py-6 py-4 mt-2 rounded-b-3xl"
|
|
||||||
>
|
|
||||||
<Button type="reset" color="gray">{$t('clear_all')}</Button>
|
|
||||||
<Button type="submit">{$t('search')}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
<svelte:fragment slot="sticky-bottom">
|
||||||
|
<Button type="reset" color="gray" fullwidth form={formId}>{$t('clear_all')}</Button>
|
||||||
|
<Button type="submit" fullwidth form={formId}>{$t('search')}</Button>
|
||||||
|
</svelte:fragment>
|
||||||
|
</FullScreenModal>
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
import RadioButton from '$lib/components/elements/radio-button.svelte';
|
||||||
import { MediaType } from './search-filter-box.svelte';
|
import { MediaType } from './search-filter-modal.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let filteredMedia: MediaType;
|
export let filteredMedia: MediaType;
|
||||||
|
|
|
@ -1068,6 +1068,7 @@
|
||||||
"search_for_existing_person": "Search for existing person",
|
"search_for_existing_person": "Search for existing person",
|
||||||
"search_no_people": "No people",
|
"search_no_people": "No people",
|
||||||
"search_no_people_named": "No people named \"{name}\"",
|
"search_no_people_named": "No people named \"{name}\"",
|
||||||
|
"search_options": "Search options",
|
||||||
"search_people": "Search people",
|
"search_people": "Search people",
|
||||||
"search_places": "Search places",
|
"search_places": "Search places",
|
||||||
"search_state": "Search state...",
|
"search_state": "Search state...",
|
||||||
|
|
Loading…
Reference in a new issue