mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
feat: enhance search (#7127)
* feat: hybrid search * fixing normal search * building out the query * okla * filters * date * order by date * Remove hybrid search endpoint * remove search hybrid endpoint * faces query * search for person * search and pagination * with exif * with exif * justify gallery viewer * memory view * Fixed userId is null * openapi and styling * searchdto * lint and format * remove term * generate sql * fix test * chips * not showing true * pr feedback * pr feedback * nit name * linting * pr feedback * styling * linting
This commit is contained in:
parent
60ba37b3a7
commit
69983ff83a
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
@ -90,6 +90,7 @@ doc/MapMarkerResponseDto.md
|
|||||||
doc/MapTheme.md
|
doc/MapTheme.md
|
||||||
doc/MemoryLaneResponseDto.md
|
doc/MemoryLaneResponseDto.md
|
||||||
doc/MergePersonDto.md
|
doc/MergePersonDto.md
|
||||||
|
doc/MetadataSearchDto.md
|
||||||
doc/ModelType.md
|
doc/ModelType.md
|
||||||
doc/OAuthApi.md
|
doc/OAuthApi.md
|
||||||
doc/OAuthAuthorizeResponseDto.md
|
doc/OAuthAuthorizeResponseDto.md
|
||||||
@ -137,6 +138,7 @@ doc/SharedLinkResponseDto.md
|
|||||||
doc/SharedLinkType.md
|
doc/SharedLinkType.md
|
||||||
doc/SignUpDto.md
|
doc/SignUpDto.md
|
||||||
doc/SmartInfoResponseDto.md
|
doc/SmartInfoResponseDto.md
|
||||||
|
doc/SmartSearchDto.md
|
||||||
doc/SystemConfigApi.md
|
doc/SystemConfigApi.md
|
||||||
doc/SystemConfigDto.md
|
doc/SystemConfigDto.md
|
||||||
doc/SystemConfigFFmpegDto.md
|
doc/SystemConfigFFmpegDto.md
|
||||||
@ -288,6 +290,7 @@ lib/model/map_marker_response_dto.dart
|
|||||||
lib/model/map_theme.dart
|
lib/model/map_theme.dart
|
||||||
lib/model/memory_lane_response_dto.dart
|
lib/model/memory_lane_response_dto.dart
|
||||||
lib/model/merge_person_dto.dart
|
lib/model/merge_person_dto.dart
|
||||||
|
lib/model/metadata_search_dto.dart
|
||||||
lib/model/model_type.dart
|
lib/model/model_type.dart
|
||||||
lib/model/o_auth_authorize_response_dto.dart
|
lib/model/o_auth_authorize_response_dto.dart
|
||||||
lib/model/o_auth_callback_dto.dart
|
lib/model/o_auth_callback_dto.dart
|
||||||
@ -329,6 +332,7 @@ lib/model/shared_link_response_dto.dart
|
|||||||
lib/model/shared_link_type.dart
|
lib/model/shared_link_type.dart
|
||||||
lib/model/sign_up_dto.dart
|
lib/model/sign_up_dto.dart
|
||||||
lib/model/smart_info_response_dto.dart
|
lib/model/smart_info_response_dto.dart
|
||||||
|
lib/model/smart_search_dto.dart
|
||||||
lib/model/system_config_dto.dart
|
lib/model/system_config_dto.dart
|
||||||
lib/model/system_config_f_fmpeg_dto.dart
|
lib/model/system_config_f_fmpeg_dto.dart
|
||||||
lib/model/system_config_job_dto.dart
|
lib/model/system_config_job_dto.dart
|
||||||
@ -457,6 +461,7 @@ test/map_marker_response_dto_test.dart
|
|||||||
test/map_theme_test.dart
|
test/map_theme_test.dart
|
||||||
test/memory_lane_response_dto_test.dart
|
test/memory_lane_response_dto_test.dart
|
||||||
test/merge_person_dto_test.dart
|
test/merge_person_dto_test.dart
|
||||||
|
test/metadata_search_dto_test.dart
|
||||||
test/model_type_test.dart
|
test/model_type_test.dart
|
||||||
test/o_auth_api_test.dart
|
test/o_auth_api_test.dart
|
||||||
test/o_auth_authorize_response_dto_test.dart
|
test/o_auth_authorize_response_dto_test.dart
|
||||||
@ -504,6 +509,7 @@ test/shared_link_response_dto_test.dart
|
|||||||
test/shared_link_type_test.dart
|
test/shared_link_type_test.dart
|
||||||
test/sign_up_dto_test.dart
|
test/sign_up_dto_test.dart
|
||||||
test/smart_info_response_dto_test.dart
|
test/smart_info_response_dto_test.dart
|
||||||
|
test/smart_search_dto_test.dart
|
||||||
test/system_config_api_test.dart
|
test/system_config_api_test.dart
|
||||||
test/system_config_dto_test.dart
|
test/system_config_dto_test.dart
|
||||||
test/system_config_f_fmpeg_dto_test.dart
|
test/system_config_f_fmpeg_dto_test.dart
|
||||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
Normal file
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/SearchApi.md
generated
BIN
mobile/openapi/doc/SearchApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
Normal file
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/search_api_test.dart
generated
BIN
mobile/openapi/test/search_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
Normal file
Binary file not shown.
@ -2256,6 +2256,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isNotInAlbum",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isOffline",
|
"name": "isOffline",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -2345,6 +2353,17 @@
|
|||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "personIds",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "resizePath",
|
"name": "resizePath",
|
||||||
"required": false,
|
"required": false,
|
||||||
@ -4526,350 +4545,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/search/metadata": {
|
"/search/metadata": {
|
||||||
"get": {
|
"post": {
|
||||||
"operationId": "searchMetadata",
|
"operationId": "searchMetadata",
|
||||||
"parameters": [
|
"parameters": [],
|
||||||
{
|
"requestBody": {
|
||||||
"name": "checksum",
|
"content": {
|
||||||
"required": false,
|
"application/json": {
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"$ref": "#/components/schemas/MetadataSearchDto"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
"required": true
|
||||||
"name": "city",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "country",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "createdAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "createdBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deviceAssetId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deviceId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "encodedVideoPath",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isEncoded",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isExternal",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isMotion",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isOffline",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isReadOnly",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isVisible",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lensModel",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "libraryId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "make",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "model",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "originalFileName",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "originalPath",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "resizePath",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "takenAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "takenBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "trashedAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "trashedBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "type",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updatedAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updatedBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "webpPath",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withDeleted",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withExif",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPeople",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withStacked",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -4949,269 +4639,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/search/smart": {
|
"/search/smart": {
|
||||||
"get": {
|
"post": {
|
||||||
"operationId": "searchSmart",
|
"operationId": "searchSmart",
|
||||||
"parameters": [
|
"parameters": [],
|
||||||
{
|
"requestBody": {
|
||||||
"name": "city",
|
"content": {
|
||||||
"required": false,
|
"application/json": {
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"$ref": "#/components/schemas/SmartSearchDto"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
"required": true
|
||||||
"name": "country",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "createdAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "createdBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deviceId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isEncoded",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isExternal",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isMotion",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isOffline",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isReadOnly",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isVisible",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lensModel",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "libraryId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "make",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "model",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "page",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "query",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "takenAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "takenBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "trashedAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "trashedBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "type",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updatedAfter",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updatedBefore",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withDeleted",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withExif",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -8805,6 +8247,153 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MetadataSearchDto": {
|
||||||
|
"properties": {
|
||||||
|
"checksum": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceAssetId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"encodedVideoPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isEncoded": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isExternal": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isMotion": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isNotInAlbum": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isOffline": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isReadOnly": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"lensModel": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"libraryId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"make": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
},
|
||||||
|
"originalFileName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"originalPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"personIds": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"resizePath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"takenAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"takenBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trashedAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trashedBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
},
|
||||||
|
"updatedAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"webpPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"withArchived": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withDeleted": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withExif": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withPeople": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withStacked": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ModelType": {
|
"ModelType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"facial-recognition",
|
"facial-recognition",
|
||||||
@ -9760,6 +9349,116 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SmartSearchDto": {
|
||||||
|
"properties": {
|
||||||
|
"city": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isArchived": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isEncoded": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isExternal": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isMotion": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isOffline": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isReadOnly": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"lensModel": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"libraryId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"make": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"takenAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"takenBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trashedAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trashedBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
},
|
||||||
|
"updatedAfter": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedBefore": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"withArchived": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withDeleted": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"withExif": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"query"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SystemConfigDto": {
|
"SystemConfigDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ffmpeg": {
|
"ffmpeg": {
|
||||||
|
1500
open-api/typescript-sdk/axios-client/api.ts
generated
1500
open-api/typescript-sdk/axios-client/api.ts
generated
File diff suppressed because it is too large
Load Diff
BIN
open-api/typescript-sdk/fetch-client.ts
generated
BIN
open-api/typescript-sdk/fetch-client.ts
generated
Binary file not shown.
@ -66,13 +66,14 @@ export interface SearchAssetIDOptions {
|
|||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchUserIDOptions {
|
export interface SearchUserIdOptions {
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
libraryId?: string;
|
libraryId?: string;
|
||||||
ownerId?: string;
|
ownerId?: string;
|
||||||
|
userIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions;
|
export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
|
||||||
|
|
||||||
export interface SearchStatusOptions {
|
export interface SearchStatusOptions {
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
@ -83,6 +84,7 @@ export interface SearchStatusOptions {
|
|||||||
isOffline?: boolean;
|
isOffline?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
isNotInAlbum?: boolean;
|
||||||
type?: AssetType;
|
type?: AssetType;
|
||||||
withArchived?: boolean;
|
withArchived?: boolean;
|
||||||
withDeleted?: boolean;
|
withDeleted?: boolean;
|
||||||
@ -132,6 +134,10 @@ export interface SearchEmbeddingOptions {
|
|||||||
userIds: string[];
|
userIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchPeopleOptions {
|
||||||
|
personIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SearchOrderOptions {
|
export interface SearchOrderOptions {
|
||||||
orderDirection?: 'ASC' | 'DESC';
|
orderDirection?: 'ASC' | 'DESC';
|
||||||
}
|
}
|
||||||
@ -142,12 +148,14 @@ export interface SearchPaginationOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AssetSearchOptions = SearchDateOptions &
|
export type AssetSearchOptions = SearchDateOptions &
|
||||||
SearchIDOptions &
|
SearchIdOptions &
|
||||||
SearchExifOptions &
|
SearchExifOptions &
|
||||||
SearchOrderOptions &
|
SearchOrderOptions &
|
||||||
SearchPathOptions &
|
SearchPathOptions &
|
||||||
SearchRelationOptions &
|
SearchRelationOptions &
|
||||||
SearchStatusOptions;
|
SearchStatusOptions &
|
||||||
|
SearchUserIdOptions &
|
||||||
|
SearchPeopleOptions;
|
||||||
|
|
||||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||||
|
|
||||||
@ -156,7 +164,8 @@ export type SmartSearchOptions = SearchDateOptions &
|
|||||||
SearchExifOptions &
|
SearchExifOptions &
|
||||||
SearchOneToOneRelationOptions &
|
SearchOneToOneRelationOptions &
|
||||||
SearchStatusOptions &
|
SearchStatusOptions &
|
||||||
SearchUserIDOptions;
|
SearchUserIdOptions &
|
||||||
|
SearchPeopleOptions;
|
||||||
|
|
||||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||||
hasPerson?: boolean;
|
hasPerson?: boolean;
|
||||||
|
@ -169,6 +169,12 @@ export class MetadataSearchDto extends BaseSearchDto {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||||
order?: AssetOrder;
|
order?: AssetOrder;
|
||||||
|
|
||||||
|
@QueryBoolean({ optional: true })
|
||||||
|
isNotInAlbum?: boolean;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
personIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SmartSearchDto extends BaseSearchDto {
|
export class SmartSearchDto extends BaseSearchDto {
|
||||||
|
@ -60,6 +60,7 @@ export class SearchService {
|
|||||||
|
|
||||||
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
let checksum: Buffer | undefined;
|
let checksum: Buffer | undefined;
|
||||||
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
|
|
||||||
if (dto.checksum) {
|
if (dto.checksum) {
|
||||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||||
@ -74,7 +75,7 @@ export class SearchService {
|
|||||||
{
|
{
|
||||||
...dto,
|
...dto,
|
||||||
checksum,
|
checksum,
|
||||||
ownerId: auth.user.id,
|
userIds,
|
||||||
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
|
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
SmartSearchDto,
|
SmartSearchDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
|
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { Auth, Authenticated } from '../app.guard';
|
import { Auth, Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
@ -22,13 +22,13 @@ import { UseValidation } from '../app.utils';
|
|||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private service: SearchService) {}
|
constructor(private service: SearchService) {}
|
||||||
|
|
||||||
@Get('metadata')
|
@Post('metadata')
|
||||||
searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.searchMetadata(auth, dto);
|
return this.service.searchMetadata(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('smart')
|
@Post('smart')
|
||||||
searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.searchSmart(auth, dto);
|
return this.service.searchSmart(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,14 +139,27 @@ export function searchAssetBuilder(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined);
|
const exifInfo = _.omitBy(_.pick(options, ['city', 'country', 'lensModel', 'make', 'model', 'state']), _.isUndefined);
|
||||||
if (Object.keys(exifInfo).length > 0) {
|
const hasExifQuery = Object.keys(exifInfo).length > 0;
|
||||||
builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
|
|
||||||
|
if (options.withExif && !hasExifQuery) {
|
||||||
|
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasExifQuery) {
|
||||||
|
options.withExif
|
||||||
|
? builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo')
|
||||||
|
: builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||||
|
|
||||||
builder.andWhere({ exifInfo });
|
builder.andWhere({ exifInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']);
|
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']);
|
||||||
builder.andWhere(_.omitBy(id, _.isUndefined));
|
builder.andWhere(_.omitBy(id, _.isUndefined));
|
||||||
|
|
||||||
|
if (options.userIds) {
|
||||||
|
builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds });
|
||||||
|
}
|
||||||
|
|
||||||
const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']);
|
const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']);
|
||||||
builder.andWhere(_.omitBy(path, _.isUndefined));
|
builder.andWhere(_.omitBy(path, _.isUndefined));
|
||||||
|
|
||||||
@ -164,8 +177,8 @@ export function searchAssetBuilder(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.withExif) {
|
if (options.isNotInAlbum) {
|
||||||
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
|
builder.leftJoin(`${builder.alias}.albums`, 'albums').andWhere('albums.id IS NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.withFaces || options.withPeople) {
|
if (options.withFaces || options.withPeople) {
|
||||||
@ -180,6 +193,18 @@ export function searchAssetBuilder(
|
|||||||
builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo');
|
builder.leftJoinAndSelect(`${builder.alias}.smartInfo`, 'smartInfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.personIds && options.personIds.length > 0) {
|
||||||
|
builder
|
||||||
|
.leftJoin(`${builder.alias}.faces`, 'faces')
|
||||||
|
.andWhere('faces.personId IN (:...personIds)', { personIds: options.personIds })
|
||||||
|
.addGroupBy(`${builder.alias}.id`)
|
||||||
|
.having('COUNT(faces.id) = :personCount', { personCount: options.personIds.length });
|
||||||
|
|
||||||
|
if (options.withExif) {
|
||||||
|
builder.addGroupBy('exifInfo.assetId');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.withStacked) {
|
if (options.withStacked) {
|
||||||
builder
|
builder
|
||||||
.leftJoinAndSelect(`${builder.alias}.stack`, 'stack')
|
.leftJoinAndSelect(`${builder.alias}.stack`, 'stack')
|
||||||
|
@ -58,6 +58,7 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
ownerId: DummyValue.UUID,
|
ownerId: DummyValue.UUID,
|
||||||
withStacked: true,
|
withStacked: true,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
|
ownerIds: [DummyValue.UUID],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@ -66,7 +67,6 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
builder = searchAssetBuilder(builder, options);
|
builder = searchAssetBuilder(builder, options);
|
||||||
|
|
||||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||||
|
|
||||||
return paginatedBuilder<AssetEntity>(builder, {
|
return paginatedBuilder<AssetEntity>(builder, {
|
||||||
mode: PaginationMode.SKIP_TAKE,
|
mode: PaginationMode.SKIP_TAKE,
|
||||||
skip: (pagination.page - 1) * pagination.size,
|
skip: (pagination.page - 1) * pagination.size,
|
||||||
|
@ -77,9 +77,9 @@ FROM
|
|||||||
(
|
(
|
||||||
"asset"."fileCreatedAt" >= $1
|
"asset"."fileCreatedAt" >= $1
|
||||||
AND "exifInfo"."lensModel" = $2
|
AND "exifInfo"."lensModel" = $2
|
||||||
AND "asset"."ownerId" = $3
|
|
||||||
AND 1 = 1
|
AND 1 = 1
|
||||||
AND "asset"."isFavorite" = $4
|
AND 1 = 1
|
||||||
|
AND "asset"."isFavorite" = $3
|
||||||
AND (
|
AND (
|
||||||
"stack"."primaryAssetId" = "asset"."id"
|
"stack"."primaryAssetId" = "asset"."id"
|
||||||
OR "asset"."stackId" IS NULL
|
OR "asset"."stackId" IS NULL
|
||||||
|
@ -38,6 +38,7 @@ module.exports = {
|
|||||||
'unicorn/prevent-abbreviations': 'off',
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
'unicorn/no-nested-ternary': 'off',
|
'unicorn/no-nested-ternary': 'off',
|
||||||
'unicorn/consistent-function-scoping': 'off',
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
|
'unicorn/prefer-top-level-await': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
import { memoryStore } from '$lib/stores/memory.store';
|
import { memoryStore } from '$lib/stores/memory.store';
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||||
@ -34,6 +35,7 @@
|
|||||||
$: canGoForward = !!(nextMemory || nextAsset);
|
$: canGoForward = !!(nextMemory || nextAsset);
|
||||||
$: canGoBack = !!(previousMemory || previousAsset);
|
$: canGoBack = !!(previousMemory || previousAsset);
|
||||||
|
|
||||||
|
const viewport: Viewport = { width: 0, height: 0 };
|
||||||
const toNextMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex + 1}`);
|
const toNextMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex + 1}`);
|
||||||
const toPreviousMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex - 1}`);
|
const toPreviousMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex - 1}`);
|
||||||
|
|
||||||
@ -251,7 +253,7 @@
|
|||||||
|
|
||||||
<!-- GALERY VIEWER -->
|
<!-- GALERY VIEWER -->
|
||||||
|
|
||||||
<section class="bg-immich-dark-gray pl-4">
|
<section class="bg-immich-dark-gray m-4">
|
||||||
<div
|
<div
|
||||||
class="sticky mb-10 mt-4 flex place-content-center place-items-center transition-all"
|
class="sticky mb-10 mt-4 flex place-content-center place-items-center transition-all"
|
||||||
class:opacity-0={galleryInView}
|
class:opacity-0={galleryInView}
|
||||||
@ -268,8 +270,13 @@
|
|||||||
on:hidden={() => (galleryInView = false)}
|
on:hidden={() => (galleryInView = false)}
|
||||||
bottom={-200}
|
bottom={-200}
|
||||||
>
|
>
|
||||||
<div id="gallery-memory" bind:this={memoryGallery}>
|
<div
|
||||||
<GalleryViewer assets={currentMemory.assets} />
|
id="gallery-memory"
|
||||||
|
bind:this={memoryGallery}
|
||||||
|
bind:clientHeight={viewport.height}
|
||||||
|
bind:clientWidth={viewport.width}
|
||||||
|
>
|
||||||
|
<GalleryViewer assets={currentMemory.assets} {viewport} />
|
||||||
</div>
|
</div>
|
||||||
</IntersectionObserver>
|
</IntersectionObserver>
|
||||||
</section>
|
</section>
|
||||||
|
@ -5,7 +5,12 @@
|
|||||||
import type { AssetStore, Viewport } from '$lib/stores/assets.store';
|
import type { AssetStore, Viewport } from '$lib/stores/assets.store';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
import { formatGroupTitle, fromLocalDateTime, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
import {
|
||||||
|
calculateWidth,
|
||||||
|
formatGroupTitle,
|
||||||
|
fromLocalDateTime,
|
||||||
|
splitBucketIntoDateGroups,
|
||||||
|
} from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
import type { AssetResponseDto } from '@immich/sdk';
|
||||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
import justifiedLayout from 'justified-layout';
|
import justifiedLayout from 'justified-layout';
|
||||||
@ -36,12 +41,6 @@
|
|||||||
let actualBucketHeight: number;
|
let actualBucketHeight: number;
|
||||||
let hoveredDateGroup = '';
|
let hoveredDateGroup = '';
|
||||||
|
|
||||||
interface LayoutBox {
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
width: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
$: assetsGroupByDate = splitBucketIntoDateGroups(assets, $locale);
|
$: assetsGroupByDate = splitBucketIntoDateGroups(assets, $locale);
|
||||||
|
|
||||||
$: geometry = (() => {
|
$: geometry = (() => {
|
||||||
@ -80,17 +79,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateWidth = (boxes: LayoutBox[]): number => {
|
|
||||||
let width = 0;
|
|
||||||
for (const box of boxes) {
|
|
||||||
if (box.top < 100) {
|
|
||||||
width = box.left + box.width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
const assetClickHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
|
const assetClickHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
|
||||||
if (isSelectionMode || $isMultiSelectState) {
|
if (isSelectionMode || $isMultiSelectState) {
|
||||||
assetSelectHandler(asset, assetsInDateGroup, groupTitle);
|
assetSelectHandler(asset, assetsInDateGroup, groupTitle);
|
||||||
|
@ -16,10 +16,12 @@
|
|||||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
export let isOwned: boolean;
|
export let isOwned: boolean;
|
||||||
|
|
||||||
|
const viewport: Viewport = { width: 0, height: 0 };
|
||||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||||
|
|
||||||
$: assets = sharedLink.assets;
|
$: assets = sharedLink.assets;
|
||||||
@ -97,7 +99,7 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
{/if}
|
{/if}
|
||||||
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
|
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
|
||||||
<GalleryViewer {assets} bind:selectedAssets />
|
<GalleryViewer {assets} bind:selectedAssets {viewport} />
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
|
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
|
||||||
<div
|
<div
|
||||||
id="asset-selection-app-bar"
|
id="asset-selection-app-bar"
|
||||||
class={`grid grid-cols-[10%_80%_10%] justify-between md:grid-cols-[20%_60%_20%] lg:grid-cols-3 ${appBarBorder} mx-2 mt-2 place-items-center rounded-lg p-2 transition-all ${tailwindClasses} dark:bg-immich-dark-gray ${
|
class={`grid grid-cols-[10%_80%_10%] justify-between md:grid-cols-[15%_70%_15%] lg:grid-cols-[25%_50%_25%] ${appBarBorder} mx-2 mt-2 place-items-center rounded-lg p-2 transition-all ${tailwindClasses} dark:bg-immich-dark-gray ${
|
||||||
forceDark && 'bg-immich-dark-gray text-white'
|
forceDark && 'bg-immich-dark-gray text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import type { BucketPosition } from '$lib/stores/assets.store';
|
import type { BucketPosition, Viewport } from '$lib/stores/assets.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
import { type AssetResponseDto } from '@immich/sdk';
|
||||||
import { ThumbnailFormat, type AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||||
import { flip } from 'svelte/animate';
|
|
||||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||||
|
import justifiedLayout from 'justified-layout';
|
||||||
|
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||||
|
import { calculateWidth } from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
|
const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>();
|
||||||
|
|
||||||
@ -16,14 +17,12 @@
|
|||||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||||
export let disableAssetSelect = false;
|
export let disableAssetSelect = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
|
export let viewport: Viewport;
|
||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
let selectedAsset: AssetResponseDto;
|
let selectedAsset: AssetResponseDto;
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
let viewWidth: number;
|
|
||||||
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
|
|
||||||
|
|
||||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||||
|
|
||||||
const viewAssetHandler = (event: CustomEvent) => {
|
const viewAssetHandler = (event: CustomEvent) => {
|
||||||
@ -86,23 +85,45 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$showAssetViewer = false;
|
$showAssetViewer = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: geometry = (() => {
|
||||||
|
const justifiedLayoutResult = justifiedLayout(
|
||||||
|
assets.map((asset) => getAssetRatio(asset)),
|
||||||
|
{
|
||||||
|
boxSpacing: 2,
|
||||||
|
containerWidth: Math.floor(viewport.width),
|
||||||
|
containerPadding: 0,
|
||||||
|
targetRowHeightTolerance: 0.15,
|
||||||
|
targetRowHeight: 235,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...justifiedLayoutResult,
|
||||||
|
containerWidth: calculateWidth(justifiedLayoutResult.boxes),
|
||||||
|
};
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if assets.length > 0}
|
{#if assets.length > 0}
|
||||||
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
|
<div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
|
||||||
{#each assets as asset, i (asset.id)}
|
{#each assets as asset, i (i)}
|
||||||
<div animate:flip={{ duration: 500 }}>
|
<div
|
||||||
|
class="absolute"
|
||||||
|
style="width: {geometry.boxes[i].width}px; height: {geometry.boxes[i].height}px; top: {geometry.boxes[i]
|
||||||
|
.top}px; left: {geometry.boxes[i].left}px"
|
||||||
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
{asset}
|
{asset}
|
||||||
{thumbnailSize}
|
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
|
||||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||||
on:select={selectAssetHandler}
|
on:select={selectAssetHandler}
|
||||||
on:intersected={(event) =>
|
on:intersected={(event) =>
|
||||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||||
selected={selectedAssets.has(asset)}
|
selected={selectedAssets.has(asset)}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
|
thumbnailWidth={geometry.boxes[i].width}
|
||||||
|
thumbnailHeight={geometry.boxes[i].height}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -2,12 +2,18 @@
|
|||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { isSearchEnabled, preventRaceConditionSearchBar, savedSearchTerms } from '$lib/stores/search.store';
|
import {
|
||||||
|
isSearchEnabled,
|
||||||
|
preventRaceConditionSearchBar,
|
||||||
|
savedSearchTerms,
|
||||||
|
searchQuery,
|
||||||
|
} from '$lib/stores/search.store';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||||
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
|
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
|
||||||
import SearchHistoryBox from './search-history-box.svelte';
|
import SearchHistoryBox from './search-history-box.svelte';
|
||||||
import SearchFilterBox from './search-filter-box.svelte';
|
import SearchFilterBox from './search-filter-box.svelte';
|
||||||
|
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let grayTheme: boolean;
|
export let grayTheme: boolean;
|
||||||
|
|
||||||
@ -17,28 +23,17 @@
|
|||||||
let showFilter = false;
|
let showFilter = false;
|
||||||
$: showClearIcon = value.length > 0;
|
$: showClearIcon = value.length > 0;
|
||||||
|
|
||||||
function onSearch() {
|
const onSearch = (payload: SmartSearchDto | MetadataSearchDto) => {
|
||||||
let smartSearch = 'true';
|
|
||||||
let searchValue = value;
|
|
||||||
|
|
||||||
if (value.slice(0, 2) == 'm:') {
|
|
||||||
smartSearch = 'false';
|
|
||||||
searchValue = value.slice(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
$savedSearchTerms = $savedSearchTerms.filter((item) => item !== value);
|
|
||||||
saveSearchTerm(value);
|
|
||||||
|
|
||||||
const parameters = new URLSearchParams({
|
const parameters = new URLSearchParams({
|
||||||
q: searchValue,
|
query: JSON.stringify(payload),
|
||||||
smart: smartSearch,
|
|
||||||
take: '100',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
showHistory = false;
|
showHistory = false;
|
||||||
|
showFilter = false;
|
||||||
$isSearchEnabled = false;
|
$isSearchEnabled = false;
|
||||||
|
$searchQuery = payload;
|
||||||
goto(`${AppRoute.SEARCH}?${parameters}`, { invalidateAll: true });
|
goto(`${AppRoute.SEARCH}?${parameters}`, { invalidateAll: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearSearchTerm = (searchTerm: string) => {
|
const clearSearchTerm = (searchTerm: string) => {
|
||||||
input.focus();
|
input.focus();
|
||||||
@ -70,6 +65,26 @@
|
|||||||
|
|
||||||
showHistory = false;
|
showHistory = false;
|
||||||
$isSearchEnabled = false;
|
$isSearchEnabled = false;
|
||||||
|
showFilter = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHistoryTermClick = (searchTerm: string) => {
|
||||||
|
const searchPayload = { query: searchTerm };
|
||||||
|
onSearch(searchPayload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterClick = () => {
|
||||||
|
showFilter = !showFilter;
|
||||||
|
value = '';
|
||||||
|
|
||||||
|
if (showFilter) {
|
||||||
|
showHistory = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
onSearch({ query: value });
|
||||||
|
saveSearchTerm(value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -80,7 +95,7 @@
|
|||||||
class="relative select-text text-sm"
|
class="relative select-text text-sm"
|
||||||
action={AppRoute.SEARCH}
|
action={AppRoute.SEARCH}
|
||||||
on:reset={() => (value = '')}
|
on:reset={() => (value = '')}
|
||||||
on:submit|preventDefault={() => onSearch()}
|
on:submit|preventDefault={onSubmit}
|
||||||
>
|
>
|
||||||
<label>
|
<label>
|
||||||
<div class="absolute inset-y-0 left-0 flex items-center pl-6">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-6">
|
||||||
@ -107,9 +122,9 @@
|
|||||||
disabled={showFilter}
|
disabled={showFilter}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="absolute inset-y-0 right-5 flex items-center pl-6">
|
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-5'} flex items-center pl-6 transition-all">
|
||||||
<div class="dark:text-immich-dark-fg/75">
|
<div class="dark:text-immich-dark-fg/75">
|
||||||
<IconButton on:click={() => (showFilter = !showFilter)} title="Show search options">
|
<IconButton on:click={onFilterClick} title="Show search options">
|
||||||
<Icon path={mdiTune} size="1.5em" />
|
<Icon path={mdiTune} size="1.5em" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
@ -131,15 +146,12 @@
|
|||||||
<SearchHistoryBox
|
<SearchHistoryBox
|
||||||
on:clearAllSearchTerms={clearAllSearchTerms}
|
on:clearAllSearchTerms={clearAllSearchTerms}
|
||||||
on:clearSearchTerm={({ detail: searchTerm }) => clearSearchTerm(searchTerm)}
|
on:clearSearchTerm={({ detail: searchTerm }) => clearSearchTerm(searchTerm)}
|
||||||
on:selectSearchTerm={({ detail: searchTerm }) => {
|
on:selectSearchTerm={({ detail: searchTerm }) => onHistoryTermClick(searchTerm)}
|
||||||
value = searchTerm;
|
|
||||||
onSearch();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
<SearchFilterBox />
|
<SearchFilterBox on:search={({ detail }) => onSearch(detail)} />
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,12 +4,20 @@
|
|||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { SearchSuggestionType, type PersonResponseDto } from '@immich/sdk';
|
import {
|
||||||
|
AssetTypeEnum,
|
||||||
|
SearchSuggestionType,
|
||||||
|
type PersonResponseDto,
|
||||||
|
type SmartSearchDto,
|
||||||
|
type MetadataSearchDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
import { getAllPeople, getSearchSuggestions } from '@immich/sdk';
|
import { getAllPeople, getSearchSuggestions } from '@immich/sdk';
|
||||||
import { mdiArrowRight, mdiClose } from '@mdi/js';
|
import { mdiArrowRight, mdiClose } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import Combobox, { type ComboBoxOption } from '../combobox.svelte';
|
import Combobox, { type ComboBoxOption } from '../combobox.svelte';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { searchQuery } from '$lib/stores/search.store';
|
||||||
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
@ -22,8 +30,8 @@
|
|||||||
country: ComboBoxOption[];
|
country: ComboBoxOption[];
|
||||||
state: ComboBoxOption[];
|
state: ComboBoxOption[];
|
||||||
city: ComboBoxOption[];
|
city: ComboBoxOption[];
|
||||||
cameraMake: ComboBoxOption[];
|
make: ComboBoxOption[];
|
||||||
cameraModel: ComboBoxOption[];
|
model: ComboBoxOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
@ -49,14 +57,14 @@
|
|||||||
model?: ComboBoxOption;
|
model?: ComboBoxOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
dateRange: {
|
date: {
|
||||||
startDate?: Date;
|
takenAfter?: string;
|
||||||
endDate?: Date;
|
takenBefore?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
inArchive?: boolean;
|
isArchive?: boolean;
|
||||||
inFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
notInAlbum?: boolean;
|
isNotInAlbum?: boolean;
|
||||||
|
|
||||||
mediaType: MediaType;
|
mediaType: MediaType;
|
||||||
};
|
};
|
||||||
@ -66,8 +74,8 @@
|
|||||||
country: [],
|
country: [],
|
||||||
state: [],
|
state: [],
|
||||||
city: [],
|
city: [],
|
||||||
cameraMake: [],
|
make: [],
|
||||||
cameraModel: [],
|
model: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter: SearchFilter = {
|
let filter: SearchFilter = {
|
||||||
@ -82,21 +90,23 @@
|
|||||||
make: undefined,
|
make: undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
},
|
},
|
||||||
dateRange: {
|
date: {
|
||||||
startDate: undefined,
|
takenAfter: undefined,
|
||||||
endDate: undefined,
|
takenBefore: undefined,
|
||||||
},
|
},
|
||||||
inArchive: undefined,
|
isArchive: undefined,
|
||||||
inFavorite: undefined,
|
isFavorite: undefined,
|
||||||
notInAlbum: undefined,
|
isNotInAlbum: undefined,
|
||||||
mediaType: MediaType.All,
|
mediaType: MediaType.All,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ search: SmartSearchDto | MetadataSearchDto }>();
|
||||||
let showAllPeople = false;
|
let showAllPeople = false;
|
||||||
$: peopleList = showAllPeople ? suggestions.people : suggestions.people.slice(0, 11);
|
$: peopleList = showAllPeople ? suggestions.people : suggestions.people.slice(0, 11);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
getPeople();
|
getPeople();
|
||||||
|
populateExistingFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
const showSelectedPeopleFirst = () => {
|
const showSelectedPeopleFirst = () => {
|
||||||
@ -141,7 +151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === SearchSuggestionType.CameraMake || type === SearchSuggestionType.CameraModel) {
|
if (type === SearchSuggestionType.CameraMake || type === SearchSuggestionType.CameraModel) {
|
||||||
suggestions = { ...suggestions, cameraMake: [], cameraModel: [] };
|
suggestions = { ...suggestions, make: [], model: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -178,14 +188,14 @@
|
|||||||
|
|
||||||
case SearchSuggestionType.CameraMake: {
|
case SearchSuggestionType.CameraMake: {
|
||||||
for (const make of data) {
|
for (const make of data) {
|
||||||
suggestions.cameraMake = [...suggestions.cameraMake, { label: make, value: make }];
|
suggestions.make = [...suggestions.make, { label: make, value: make }];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case SearchSuggestionType.CameraModel: {
|
case SearchSuggestionType.CameraModel: {
|
||||||
for (const model of data) {
|
for (const model of data) {
|
||||||
suggestions.cameraModel = [...suggestions.cameraModel, { label: model, value: model }];
|
suggestions.model = [...suggestions.model, { label: model, value: model }];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -208,18 +218,99 @@
|
|||||||
make: undefined,
|
make: undefined,
|
||||||
model: undefined,
|
model: undefined,
|
||||||
},
|
},
|
||||||
dateRange: {
|
date: {
|
||||||
startDate: undefined,
|
takenAfter: undefined,
|
||||||
endDate: undefined,
|
takenBefore: undefined,
|
||||||
},
|
},
|
||||||
inArchive: undefined,
|
isArchive: undefined,
|
||||||
inFavorite: undefined,
|
isFavorite: undefined,
|
||||||
notInAlbum: undefined,
|
isNotInAlbum: undefined,
|
||||||
mediaType: MediaType.All,
|
mediaType: MediaType.All,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = () => {};
|
const search = async () => {
|
||||||
|
let type: AssetTypeEnum | undefined = undefined;
|
||||||
|
|
||||||
|
if (filter.mediaType === MediaType.Image) {
|
||||||
|
type = AssetTypeEnum.Image;
|
||||||
|
} else if (filter.mediaType === MediaType.Video) {
|
||||||
|
type = AssetTypeEnum.Video;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: SmartSearchDto | MetadataSearchDto = {
|
||||||
|
country: filter.location.country?.value,
|
||||||
|
state: filter.location.state?.value,
|
||||||
|
city: filter.location.city?.value,
|
||||||
|
make: filter.camera.make?.value,
|
||||||
|
model: filter.camera.model?.value,
|
||||||
|
takenAfter: filter.date.takenAfter
|
||||||
|
? DateTime.fromFormat(filter.date.takenAfter, 'yyyy-MM-dd').toUTC().startOf('day').toString()
|
||||||
|
: undefined,
|
||||||
|
takenBefore: filter.date.takenBefore
|
||||||
|
? DateTime.fromFormat(filter.date.takenBefore, 'yyyy-MM-dd').toUTC().endOf('day').toString()
|
||||||
|
: undefined,
|
||||||
|
/* eslint-disable unicorn/prefer-logical-operator-over-ternary */
|
||||||
|
isArchived: filter.isArchive ? filter.isArchive : undefined,
|
||||||
|
isFavorite: filter.isFavorite ? filter.isFavorite : undefined,
|
||||||
|
isNotInAlbum: filter.isNotInAlbum ? filter.isNotInAlbum : undefined,
|
||||||
|
personIds: filter.people && filter.people.length > 0 ? filter.people.map((p) => p.id) : undefined,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filter.context) {
|
||||||
|
if (payload.personIds && payload.personIds.length > 0) {
|
||||||
|
handleError(
|
||||||
|
new Error('Context search does not support people filter'),
|
||||||
|
'Context search does not support people filter',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
...payload,
|
||||||
|
query: filter.context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('search', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
function populateExistingFilters() {
|
||||||
|
if ($searchQuery) {
|
||||||
|
filter = {
|
||||||
|
context: 'query' in $searchQuery ? $searchQuery.query : '',
|
||||||
|
people:
|
||||||
|
'personIds' in $searchQuery ? ($searchQuery.personIds?.map((id) => ({ id })) as PersonResponseDto[]) : [],
|
||||||
|
location: {
|
||||||
|
country: $searchQuery.country ? { label: $searchQuery.country, value: $searchQuery.country } : undefined,
|
||||||
|
state: $searchQuery.state ? { label: $searchQuery.state, value: $searchQuery.state } : undefined,
|
||||||
|
city: $searchQuery.city ? { label: $searchQuery.city, value: $searchQuery.city } : undefined,
|
||||||
|
},
|
||||||
|
camera: {
|
||||||
|
make: $searchQuery.make ? { label: $searchQuery.make, value: $searchQuery.make } : undefined,
|
||||||
|
model: $searchQuery.model ? { label: $searchQuery.model, value: $searchQuery.model } : undefined,
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
takenAfter: $searchQuery.takenAfter
|
||||||
|
? DateTime.fromISO($searchQuery.takenAfter).toUTC().toFormat('yyyy-MM-dd')
|
||||||
|
: undefined,
|
||||||
|
takenBefore: $searchQuery.takenBefore
|
||||||
|
? DateTime.fromISO($searchQuery.takenBefore).toUTC().toFormat('yyyy-MM-dd')
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
isArchive: $searchQuery.isArchived,
|
||||||
|
isFavorite: $searchQuery.isFavorite,
|
||||||
|
isNotInAlbum: 'isNotInAlbum' in $searchQuery ? $searchQuery.isNotInAlbum : undefined,
|
||||||
|
mediaType:
|
||||||
|
$searchQuery.type === AssetTypeEnum.Image
|
||||||
|
? MediaType.Image
|
||||||
|
: $searchQuery.type === AssetTypeEnum.Video
|
||||||
|
? MediaType.Video
|
||||||
|
: MediaType.All,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -347,7 +438,7 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="text-sm text-black dark:text-white">Make</p>
|
<p class="text-sm text-black dark:text-white">Make</p>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={suggestions.cameraMake}
|
options={suggestions.make}
|
||||||
bind:selectedOption={filter.camera.make}
|
bind:selectedOption={filter.camera.make}
|
||||||
placeholder="Search camera make..."
|
placeholder="Search camera make..."
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
@ -358,7 +449,7 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="text-sm text-black dark:text-white">Model</p>
|
<p class="text-sm text-black dark:text-white">Model</p>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={suggestions.cameraModel}
|
options={suggestions.model}
|
||||||
bind:selectedOption={filter.camera.model}
|
bind:selectedOption={filter.camera.model}
|
||||||
placeholder="Search camera model..."
|
placeholder="Search camera model..."
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
@ -379,7 +470,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
id="start-date"
|
id="start-date"
|
||||||
name="start-date"
|
name="start-date"
|
||||||
bind:value={filter.dateRange.startDate}
|
bind:value={filter.date.takenAfter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -391,7 +482,7 @@
|
|||||||
id="end-date"
|
id="end-date"
|
||||||
name="end-date"
|
name="end-date"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
bind:value={filter.dateRange.endDate}
|
bind:value={filter.date.takenBefore}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -450,17 +541,17 @@
|
|||||||
|
|
||||||
<div class="flex gap-5 mt-3">
|
<div class="flex gap-5 mt-3">
|
||||||
<label class="flex items-center mb-2">
|
<label class="flex items-center mb-2">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.notInAlbum} />
|
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.isNotInAlbum} />
|
||||||
<span class="ml-2 text-sm text-black dark:text-white pt-1">Not in any album</span>
|
<span class="ml-2 text-sm text-black dark:text-white pt-1">Not in any album</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="flex items-center mb-2">
|
<label class="flex items-center mb-2">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.inArchive} />
|
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.isArchive} />
|
||||||
<span class="ml-2 text-sm text-black dark:text-white pt-1">Archive</span>
|
<span class="ml-2 text-sm text-black dark:text-white pt-1">Archive</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="flex items-center mb-2">
|
<label class="flex items-center mb-2">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.inFavorite} />
|
<input type="checkbox" class="form-checkbox h-5 w-5 color" bind:checked={filter.isFavorite} />
|
||||||
<span class="ml-2 text-sm text-black dark:text-white pt-1">Favorite</span>
|
<span class="ml-2 text-sm text-black dark:text-white pt-1">Favorite</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,15 +16,6 @@
|
|||||||
transition:fly={{ y: 25, duration: 250 }}
|
transition:fly={{ y: 25, duration: 250 }}
|
||||||
class="absolute w-full rounded-b-3xl border border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300"
|
class="absolute w-full rounded-b-3xl border border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-800 dark:bg-immich-dark-gray dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<div class="flex px-5 pt-5 text-left text-sm">
|
|
||||||
<p>
|
|
||||||
Smart search is enabled by default, to search for metadata use the syntax <span
|
|
||||||
class="rounded-lg bg-gray-100 p-2 font-mono font-semibold leading-7 text-immich-primary dark:bg-gray-900 dark:text-immich-dark-primary"
|
|
||||||
>m:your-search-term</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $savedSearchTerms.length > 0}
|
{#if $savedSearchTerms.length > 0}
|
||||||
<div class="flex items-center justify-between px-5 pt-5 text-xs">
|
<div class="flex items-center justify-between px-5 pt-5 text-xs">
|
||||||
<p>RECENT SEARCHES</p>
|
<p>RECENT SEARCHES</p>
|
||||||
|
@ -71,6 +71,7 @@ export enum QueryParameter {
|
|||||||
SEARCHED_PEOPLE = 'searchedPeople',
|
SEARCHED_PEOPLE = 'searchedPeople',
|
||||||
SEARCH_TERM = 'q',
|
SEARCH_TERM = 'q',
|
||||||
SMART_SEARCH = 'smartSearch',
|
SMART_SEARCH = 'smartSearch',
|
||||||
|
PAGE = 'page',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OpenSettingQueryParameterValue {
|
export enum OpenSettingQueryParameterValue {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||||
import { persisted } from 'svelte-local-storage-store';
|
import { persisted } from 'svelte-local-storage-store';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const savedSearchTerms = persisted<string[]>('search-terms', [], {});
|
export const savedSearchTerms = persisted<string[]>('search-terms', [], {});
|
||||||
export const isSearchEnabled = writable<boolean>(false);
|
export const isSearchEnabled = writable<boolean>(false);
|
||||||
export const preventRaceConditionSearchBar = writable<boolean>(false);
|
export const preventRaceConditionSearchBar = writable<boolean>(false);
|
||||||
|
export const searchQuery = writable<SmartSearchDto | MetadataSearchDto | undefined>(undefined);
|
||||||
|
@ -50,3 +50,20 @@ export function splitBucketIntoDateGroups(
|
|||||||
);
|
);
|
||||||
return sortBy(grouped, (group) => assets.indexOf(group[0]));
|
return sortBy(grouped, (group) => assets.indexOf(group[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LayoutBox = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function calculateWidth(boxes: LayoutBox[]): number {
|
||||||
|
let width = 0;
|
||||||
|
for (const box of boxes) {
|
||||||
|
if (box.top < 100) {
|
||||||
|
width = box.left + box.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
@ -20,25 +20,30 @@
|
|||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { preventRaceConditionSearchBar } from '$lib/stores/search.store';
|
import { preventRaceConditionSearchBar, searchQuery } from '$lib/stores/search.store';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
import { search, type AssetResponseDto, type SearchResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto, type SearchResponseDto, searchSmart, searchMetadata, getPerson } from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
const MAX_ASSET_COUNT = 5000;
|
const MAX_ASSET_COUNT = 5000;
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
const viewport: Viewport = { width: 0, height: 0 };
|
||||||
|
|
||||||
// The GalleryViewer pushes it's own history state, which causes weird
|
// The GalleryViewer pushes it's own history state, which causes weird
|
||||||
// behavior for history.back(). To prevent that we store the previous page
|
// behavior for history.back(). To prevent that we store the previous page
|
||||||
// manually and navigate back to that.
|
// manually and navigate back to that.
|
||||||
let previousRoute = AppRoute.EXPLORE as string;
|
let previousRoute = AppRoute.EXPLORE as string;
|
||||||
$: curPage = data.results?.assets.nextPage;
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
let terms: any;
|
||||||
|
$: currentPage = data.results?.assets.nextPage;
|
||||||
$: albums = data.results?.albums.items;
|
$: albums = data.results?.albums.items;
|
||||||
|
|
||||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||||
@ -87,16 +92,9 @@
|
|||||||
if (from?.route.id === '/(user)/albums/[albumId]') {
|
if (from?.route.id === '/(user)/albums/[albumId]') {
|
||||||
previousRoute = AppRoute.EXPLORE;
|
previousRoute = AppRoute.EXPLORE;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$: term = (() => {
|
updateInformationChip();
|
||||||
let term = $page.url.searchParams.get(QueryParameter.SEARCH_TERM) || data.term || '';
|
});
|
||||||
const isMetadataSearch = $page.url.searchParams.get(QueryParameter.SMART_SEARCH) === 'false';
|
|
||||||
if (isMetadataSearch && term !== '') {
|
|
||||||
term = `m:${term}`;
|
|
||||||
}
|
|
||||||
return term;
|
|
||||||
})();
|
|
||||||
|
|
||||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||||
@ -111,32 +109,120 @@
|
|||||||
selectedAssets = new Set(searchResultAssets);
|
selectedAssets = new Set(searchResultAssets);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateInformationChip() {
|
||||||
|
let query = $page.url.searchParams.get(QueryParameter.SEARCH_TERM) || data.term || '';
|
||||||
|
terms = JSON.parse(query);
|
||||||
|
}
|
||||||
|
|
||||||
export const loadNextPage = async () => {
|
export const loadNextPage = async () => {
|
||||||
if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
|
if (currentPage == null || !terms || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await authenticate();
|
await authenticate();
|
||||||
let results: SearchResponseDto | null = null;
|
let results: SearchResponseDto | null = null;
|
||||||
$page.url.searchParams.set('page', curPage.toString());
|
$page.url.searchParams.set(QueryParameter.PAGE, currentPage.toString());
|
||||||
const res = await search({ ...$page.url.searchParams });
|
const payload = $searchQuery;
|
||||||
|
let responses: SearchResponseDto;
|
||||||
|
|
||||||
|
responses =
|
||||||
|
payload && 'query' in payload
|
||||||
|
? await searchSmart({
|
||||||
|
smartSearchDto: { ...payload, page: Number.parseInt(currentPage), withExif: true },
|
||||||
|
})
|
||||||
|
: await searchMetadata({
|
||||||
|
metadataSearchDto: { ...payload, page: Number.parseInt(currentPage), withExif: true },
|
||||||
|
});
|
||||||
|
|
||||||
if (searchResultAssets) {
|
if (searchResultAssets) {
|
||||||
searchResultAssets.push(...res.assets.items);
|
searchResultAssets.push(...responses.assets.items);
|
||||||
} else {
|
} else {
|
||||||
searchResultAssets = res.assets.items;
|
searchResultAssets = responses.assets.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets = {
|
const assets = {
|
||||||
...res.assets,
|
...responses.assets,
|
||||||
items: searchResultAssets,
|
items: searchResultAssets,
|
||||||
};
|
};
|
||||||
results = {
|
results = {
|
||||||
assets,
|
assets,
|
||||||
albums: res.albums,
|
albums: responses.albums,
|
||||||
};
|
};
|
||||||
|
|
||||||
data.results = results;
|
data.results = results;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getHumanReadableDate(date: string) {
|
||||||
|
const d = new Date(date);
|
||||||
|
return d.toLocaleDateString($locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHumanReadableSearchKey(key: string): string {
|
||||||
|
switch (key) {
|
||||||
|
case 'takenAfter': {
|
||||||
|
return 'Start date';
|
||||||
|
}
|
||||||
|
case 'takenBefore': {
|
||||||
|
return 'End date';
|
||||||
|
}
|
||||||
|
case 'isArchived': {
|
||||||
|
return 'In archive';
|
||||||
|
}
|
||||||
|
case 'isFavorite': {
|
||||||
|
return 'Favorite';
|
||||||
|
}
|
||||||
|
case 'isNotInAlbum': {
|
||||||
|
return 'Not in any album';
|
||||||
|
}
|
||||||
|
case 'type': {
|
||||||
|
return 'Media type';
|
||||||
|
}
|
||||||
|
case 'query': {
|
||||||
|
return 'Context';
|
||||||
|
}
|
||||||
|
case 'city': {
|
||||||
|
return 'City';
|
||||||
|
}
|
||||||
|
case 'country': {
|
||||||
|
return 'Country';
|
||||||
|
}
|
||||||
|
case 'state': {
|
||||||
|
return 'State';
|
||||||
|
}
|
||||||
|
case 'make': {
|
||||||
|
return 'Camera brand';
|
||||||
|
}
|
||||||
|
case 'model': {
|
||||||
|
return 'Camera model';
|
||||||
|
}
|
||||||
|
case 'personIds': {
|
||||||
|
return 'People';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPersonName(personIds: string[]) {
|
||||||
|
const personNames = await Promise.all(
|
||||||
|
personIds.map(async (personId) => {
|
||||||
|
const person = await getPerson({ id: personId });
|
||||||
|
|
||||||
|
if (person.name == '') {
|
||||||
|
return 'No Name';
|
||||||
|
}
|
||||||
|
|
||||||
|
return person.name;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return personNames.join(', ');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@ -161,15 +247,53 @@
|
|||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="fixed z-[100] top-0 left-0 w-full">
|
||||||
<ControlAppBar on:close={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
<ControlAppBar on:close={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||||
<div class="w-full flex-1 pl-4">
|
<div class="w-full flex-1 pl-4">
|
||||||
<SearchBar grayTheme={false} value={term} />
|
<SearchBar grayTheme={false} />
|
||||||
</div>
|
</div>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="relative mb-12 bg-immich-bg pt-32 dark:bg-immich-dark-bg">
|
{#if terms}
|
||||||
|
<section
|
||||||
|
id="search-chips"
|
||||||
|
class="mt-24 text-center w-full flex gap-5 place-content-center place-items-center flex-wrap px-24"
|
||||||
|
>
|
||||||
|
{#each Object.keys(terms) as key, index (index)}
|
||||||
|
<div class="flex place-content-center place-items-center text-xs">
|
||||||
|
<div
|
||||||
|
class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
|
||||||
|
{terms[key] === true ? 'rounded-full' : 'rounded-tl-full rounded-bl-full'}"
|
||||||
|
>
|
||||||
|
{getHumanReadableSearchKey(key)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if terms[key] !== true}
|
||||||
|
<div class="bg-gray-300 py-2 px-4 dark:bg-gray-800 dark:text-white rounded-tr-full rounded-br-full">
|
||||||
|
{#if key === 'takenAfter' || key === 'takenBefore'}
|
||||||
|
{getHumanReadableDate(terms[key])}
|
||||||
|
{:else if key === 'personIds'}
|
||||||
|
{#await getPersonName(terms[key]) then personName}
|
||||||
|
{personName}
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
{terms[key]}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="relative mb-12 bg-immich-bg dark:bg-immich-dark-bg m-4"
|
||||||
|
bind:clientHeight={viewport.height}
|
||||||
|
bind:clientWidth={viewport.width}
|
||||||
|
>
|
||||||
<section class="immich-scrollbar relative overflow-y-auto">
|
<section class="immich-scrollbar relative overflow-y-auto">
|
||||||
{#if albums && albums.length > 0}
|
{#if albums && albums.length > 0}
|
||||||
<section>
|
<section>
|
||||||
@ -193,14 +317,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||||
{#if searchResultAssets && searchResultAssets.length > 0}
|
{#if searchResultAssets && searchResultAssets.length > 0}
|
||||||
<div class="pl-4">
|
|
||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
assets={searchResultAssets}
|
assets={searchResultAssets}
|
||||||
bind:selectedAssets
|
bind:selectedAssets
|
||||||
on:intersected={loadNextPage}
|
on:intersected={loadNextPage}
|
||||||
showArchiveIcon={true}
|
showArchiveIcon={true}
|
||||||
|
{viewport}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||||
<div class="flex flex-col content-center items-center text-center">
|
<div class="flex flex-col content-center items-center text-center">
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { QueryParameter } from '$lib/constants';
|
import { QueryParameter } from '$lib/constants';
|
||||||
|
import { searchQuery } from '$lib/stores/search.store';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { search, type AssetResponseDto, type SearchResponseDto } from '@immich/sdk';
|
import {
|
||||||
|
searchMetadata,
|
||||||
|
searchSmart,
|
||||||
|
type MetadataSearchDto,
|
||||||
|
type SearchResponseDto,
|
||||||
|
type SmartSearchDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async (data) => {
|
export const load = (async (data) => {
|
||||||
@ -10,22 +17,13 @@ export const load = (async (data) => {
|
|||||||
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
|
url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined;
|
||||||
let results: SearchResponseDto | null = null;
|
let results: SearchResponseDto | null = null;
|
||||||
if (term) {
|
if (term) {
|
||||||
let params = {};
|
const payload = JSON.parse(term) as SmartSearchDto | MetadataSearchDto;
|
||||||
for (const [key, value] of data.url.searchParams) {
|
searchQuery.set(payload);
|
||||||
params = { ...params, [key]: value };
|
|
||||||
}
|
results =
|
||||||
const response = await search({ ...params });
|
payload && 'query' in payload
|
||||||
let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items;
|
? await searchSmart({ smartSearchDto: { ...payload, withExif: true } })
|
||||||
if (items) {
|
: await searchMetadata({ metadataSearchDto: { ...payload, withExif: true } });
|
||||||
items.push(...response.assets.items);
|
|
||||||
} else {
|
|
||||||
items = response.assets.items;
|
|
||||||
}
|
|
||||||
const assets = { ...response.assets, items };
|
|
||||||
results = {
|
|
||||||
assets,
|
|
||||||
albums: response.albums,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user