mirror of
https://github.com/zebrajr/immich.git
synced 2025-12-06 00:20:20 +01:00
feat(web): support searching by EXIF rating (#16208)
* Add rating to search DTO * Add search by EXIF rating in search query builder * Generate OpenAPI spec * Add rating filter on web * Add rating filter to search docs * Format / lint * Hide rating filter if ratings are disabled * chore: component order in form --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f6ba071569
commit
34b88bb47a
|
|
@ -31,6 +31,7 @@ The filters smart search allows you to search by include:
|
|||
- Not in any album
|
||||
- Archived
|
||||
- Favorited
|
||||
- Rating
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Computer" label="Computer" default>
|
||||
|
|
|
|||
|
|
@ -1134,6 +1134,7 @@
|
|||
"search_timezone": "Search timezone...",
|
||||
"search_type": "Search type",
|
||||
"search_your_photos": "Search your photos",
|
||||
"search_rating": "Search by rating...",
|
||||
"searching_locales": "Searching locales...",
|
||||
"second": "Second",
|
||||
"see_all_people": "See all people",
|
||||
|
|
|
|||
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/random_search_dto.dart
generated
BIN
mobile/openapi/lib/model/random_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
Binary file not shown.
|
|
@ -9956,6 +9956,11 @@
|
|||
"previewPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
|
|
@ -10613,6 +10618,11 @@
|
|||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
|
|
@ -11563,6 +11573,11 @@
|
|||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
|
|
|
|||
|
|
@ -811,6 +811,7 @@ export type MetadataSearchDto = {
|
|||
page?: number;
|
||||
personIds?: string[];
|
||||
previewPath?: string;
|
||||
rating?: number;
|
||||
size?: number;
|
||||
state?: string | null;
|
||||
tagIds?: string[];
|
||||
|
|
@ -878,6 +879,7 @@ export type RandomSearchDto = {
|
|||
make?: string;
|
||||
model?: string | null;
|
||||
personIds?: string[];
|
||||
rating?: number;
|
||||
size?: number;
|
||||
state?: string | null;
|
||||
tagIds?: string[];
|
||||
|
|
@ -914,6 +916,7 @@ export type SmartSearchDto = {
|
|||
page?: number;
|
||||
personIds?: string[];
|
||||
query: string;
|
||||
rating?: number;
|
||||
size?: number;
|
||||
state?: string | null;
|
||||
tagIds?: string[];
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ class BaseSearchDto {
|
|||
|
||||
@ValidateUUID({ each: true, optional: true })
|
||||
tagIds?: string[];
|
||||
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
export class RandomSearchDto extends BaseSearchDto {
|
||||
|
|
|
|||
|
|
@ -387,6 +387,11 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.where('exif.lensModel', options.lensModel === null ? 'is' : '=', options.lensModel!),
|
||||
)
|
||||
.$if(options.rating !== undefined, (qb) =>
|
||||
qb
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.where('exif.rating', options.rating === null ? 'is' : '=', options.rating!),
|
||||
)
|
||||
.$if(!!options.checksum, (qb) => qb.where('assets.checksum', '=', options.checksum!))
|
||||
.$if(!!options.deviceAssetId, (qb) => qb.where('assets.deviceAssetId', '=', options.deviceAssetId!))
|
||||
.$if(!!options.deviceId, (qb) => qb.where('assets.deviceId', '=', options.deviceId!))
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export interface SearchExifOptions {
|
|||
model?: string | null;
|
||||
state?: string | null;
|
||||
description?: string | null;
|
||||
rating?: number | null;
|
||||
}
|
||||
|
||||
export interface SearchEmbeddingOptions {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
date: SearchDateFilter;
|
||||
display: SearchDisplayFilters;
|
||||
mediaType: MediaType;
|
||||
rating?: number;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
|
||||
import SearchDateSection from './search-date-section.svelte';
|
||||
import SearchMediaSection from './search-media-section.svelte';
|
||||
import SearchRatingsSection from './search-ratings-section.svelte';
|
||||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import SearchDisplaySection from './search-display-section.svelte';
|
||||
import SearchTextSection from './search-text-section.svelte';
|
||||
|
|
@ -34,6 +36,7 @@
|
|||
import { mdiTune } from '@mdi/js';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
|
||||
interface Props {
|
||||
searchQuery: MetadataSearchDto | SmartSearchDto;
|
||||
|
|
@ -81,6 +84,7 @@
|
|||
: searchQuery.type === AssetTypeEnum.Video
|
||||
? MediaType.Video
|
||||
: MediaType.All,
|
||||
rating: searchQuery.rating,
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
|
|
@ -94,6 +98,7 @@
|
|||
date: {},
|
||||
display: {},
|
||||
mediaType: MediaType.All,
|
||||
rating: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -124,6 +129,7 @@
|
|||
personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined,
|
||||
tagIds: filter.tagIds.size > 0 ? [...filter.tagIds] : undefined,
|
||||
type,
|
||||
rating: filter.rating,
|
||||
};
|
||||
|
||||
onSearch(payload);
|
||||
|
|
@ -161,6 +167,11 @@
|
|||
<!-- DATE RANGE -->
|
||||
<SearchDateSection bind:filters={filter.date} />
|
||||
|
||||
<!-- RATING -->
|
||||
{#if $preferences?.ratings.enabled}
|
||||
<SearchRatingsSection bind:rating={filter.rating} />
|
||||
{/if}
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-x-5 gap-y-10">
|
||||
<!-- MEDIA TYPE -->
|
||||
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import Combobox from '../combobox.svelte';
|
||||
|
||||
interface Props {
|
||||
rating?: number;
|
||||
}
|
||||
|
||||
let { rating = $bindable() }: Props = $props();
|
||||
|
||||
const options = [
|
||||
{ value: '0', label: $t('rating_count', { values: { count: 0 } }) },
|
||||
{ value: '1', label: $t('rating_count', { values: { count: 1 } }) },
|
||||
{ value: '2', label: $t('rating_count', { values: { count: 2 } }) },
|
||||
{ value: '3', label: $t('rating_count', { values: { count: 3 } }) },
|
||||
{ value: '4', label: $t('rating_count', { values: { count: 4 } }) },
|
||||
{ value: '5', label: $t('rating_count', { values: { count: 5 } }) },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="grid grid-auto-fit-40 gap-5">
|
||||
<label class="immich-form-label" for="start-date">
|
||||
<Combobox
|
||||
label={$t('rating').toUpperCase()}
|
||||
placeholder={$t('search_rating')}
|
||||
{options}
|
||||
selectedOption={rating === undefined ? undefined : options[rating]}
|
||||
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user