mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +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/MemoryLaneResponseDto.md
|
||||
doc/MergePersonDto.md
|
||||
doc/MetadataSearchDto.md
|
||||
doc/ModelType.md
|
||||
doc/OAuthApi.md
|
||||
doc/OAuthAuthorizeResponseDto.md
|
||||
@ -137,6 +138,7 @@ doc/SharedLinkResponseDto.md
|
||||
doc/SharedLinkType.md
|
||||
doc/SignUpDto.md
|
||||
doc/SmartInfoResponseDto.md
|
||||
doc/SmartSearchDto.md
|
||||
doc/SystemConfigApi.md
|
||||
doc/SystemConfigDto.md
|
||||
doc/SystemConfigFFmpegDto.md
|
||||
@ -288,6 +290,7 @@ lib/model/map_marker_response_dto.dart
|
||||
lib/model/map_theme.dart
|
||||
lib/model/memory_lane_response_dto.dart
|
||||
lib/model/merge_person_dto.dart
|
||||
lib/model/metadata_search_dto.dart
|
||||
lib/model/model_type.dart
|
||||
lib/model/o_auth_authorize_response_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/sign_up_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_f_fmpeg_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/memory_lane_response_dto_test.dart
|
||||
test/merge_person_dto_test.dart
|
||||
test/metadata_search_dto_test.dart
|
||||
test/model_type_test.dart
|
||||
test/o_auth_api_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/sign_up_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_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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isNotInAlbum",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isOffline",
|
||||
"required": false,
|
||||
@ -2345,6 +2353,17 @@
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "personIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "resizePath",
|
||||
"required": false,
|
||||
@ -4526,350 +4545,21 @@
|
||||
}
|
||||
},
|
||||
"/search/metadata": {
|
||||
"get": {
|
||||
"post": {
|
||||
"operationId": "searchMetadata",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "checksum",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/MetadataSearchDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "city",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"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": "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": {
|
||||
"200": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -4949,269 +4639,21 @@
|
||||
}
|
||||
},
|
||||
"/search/smart": {
|
||||
"get": {
|
||||
"post": {
|
||||
"operationId": "searchSmart",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "city",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/SmartSearchDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "country",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"200": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -8805,6 +8247,153 @@
|
||||
],
|
||||
"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": {
|
||||
"enum": [
|
||||
"facial-recognition",
|
||||
@ -9760,6 +9349,116 @@
|
||||
},
|
||||
"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": {
|
||||
"properties": {
|
||||
"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;
|
||||
}
|
||||
|
||||
export interface SearchUserIDOptions {
|
||||
export interface SearchUserIdOptions {
|
||||
deviceId?: string;
|
||||
libraryId?: string;
|
||||
ownerId?: string;
|
||||
userIds?: string[];
|
||||
}
|
||||
|
||||
export type SearchIDOptions = SearchAssetIDOptions & SearchUserIDOptions;
|
||||
export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
|
||||
|
||||
export interface SearchStatusOptions {
|
||||
isArchived?: boolean;
|
||||
@ -83,6 +84,7 @@ export interface SearchStatusOptions {
|
||||
isOffline?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isVisible?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
type?: AssetType;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
@ -132,6 +134,10 @@ export interface SearchEmbeddingOptions {
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchPeopleOptions {
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOrderOptions {
|
||||
orderDirection?: 'ASC' | 'DESC';
|
||||
}
|
||||
@ -142,12 +148,14 @@ export interface SearchPaginationOptions {
|
||||
}
|
||||
|
||||
export type AssetSearchOptions = SearchDateOptions &
|
||||
SearchIDOptions &
|
||||
SearchIdOptions &
|
||||
SearchExifOptions &
|
||||
SearchOrderOptions &
|
||||
SearchPathOptions &
|
||||
SearchRelationOptions &
|
||||
SearchStatusOptions;
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions;
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
@ -156,7 +164,8 @@ export type SmartSearchOptions = SearchDateOptions &
|
||||
SearchExifOptions &
|
||||
SearchOneToOneRelationOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIDOptions;
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions;
|
||||
|
||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
hasPerson?: boolean;
|
||||
|
@ -169,6 +169,12 @@ export class MetadataSearchDto extends BaseSearchDto {
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
order?: AssetOrder;
|
||||
|
||||
@QueryBoolean({ optional: true })
|
||||
isNotInAlbum?: boolean;
|
||||
|
||||
@Optional()
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export class SmartSearchDto extends BaseSearchDto {
|
||||
|
@ -60,6 +60,7 @@ export class SearchService {
|
||||
|
||||
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
let checksum: Buffer | undefined;
|
||||
const userIds = await this.getUserIdsToSearch(auth);
|
||||
|
||||
if (dto.checksum) {
|
||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||
@ -74,7 +75,7 @@ export class SearchService {
|
||||
{
|
||||
...dto,
|
||||
checksum,
|
||||
ownerId: auth.user.id,
|
||||
userIds,
|
||||
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
|
||||
},
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
SmartSearchDto,
|
||||
} from '@app/domain';
|
||||
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 { Auth, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
@ -22,13 +22,13 @@ import { UseValidation } from '../app.utils';
|
||||
export class SearchController {
|
||||
constructor(private service: SearchService) {}
|
||||
|
||||
@Get('metadata')
|
||||
searchMetadata(@Auth() auth: AuthDto, @Query() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
@Post('metadata')
|
||||
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||
return this.service.searchMetadata(auth, dto);
|
||||
}
|
||||
|
||||
@Get('smart')
|
||||
searchSmart(@Auth() auth: AuthDto, @Query() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
@Post('smart')
|
||||
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||
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);
|
||||
if (Object.keys(exifInfo).length > 0) {
|
||||
builder.leftJoin(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
const hasExifQuery = Object.keys(exifInfo).length > 0;
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId', 'ownerId']);
|
||||
const id = _.pick(options, ['checksum', 'deviceAssetId', 'deviceId', 'id', 'libraryId']);
|
||||
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']);
|
||||
builder.andWhere(_.omitBy(path, _.isUndefined));
|
||||
|
||||
@ -164,8 +177,8 @@ export function searchAssetBuilder(
|
||||
),
|
||||
);
|
||||
|
||||
if (options.withExif) {
|
||||
builder.leftJoinAndSelect(`${builder.alias}.exifInfo`, 'exifInfo');
|
||||
if (options.isNotInAlbum) {
|
||||
builder.leftJoin(`${builder.alias}.albums`, 'albums').andWhere('albums.id IS NULL');
|
||||
}
|
||||
|
||||
if (options.withFaces || options.withPeople) {
|
||||
@ -180,6 +193,18 @@ export function searchAssetBuilder(
|
||||
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) {
|
||||
builder
|
||||
.leftJoinAndSelect(`${builder.alias}.stack`, 'stack')
|
||||
|
@ -58,6 +58,7 @@ export class SearchRepository implements ISearchRepository {
|
||||
ownerId: DummyValue.UUID,
|
||||
withStacked: true,
|
||||
isFavorite: true,
|
||||
ownerIds: [DummyValue.UUID],
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -66,7 +67,6 @@ export class SearchRepository implements ISearchRepository {
|
||||
builder = searchAssetBuilder(builder, options);
|
||||
|
||||
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
|
||||
|
||||
return paginatedBuilder<AssetEntity>(builder, {
|
||||
mode: PaginationMode.SKIP_TAKE,
|
||||
skip: (pagination.page - 1) * pagination.size,
|
||||
|
@ -77,9 +77,9 @@ FROM
|
||||
(
|
||||
"asset"."fileCreatedAt" >= $1
|
||||
AND "exifInfo"."lensModel" = $2
|
||||
AND "asset"."ownerId" = $3
|
||||
AND 1 = 1
|
||||
AND "asset"."isFavorite" = $4
|
||||
AND 1 = 1
|
||||
AND "asset"."isFavorite" = $3
|
||||
AND (
|
||||
"stack"."primaryAssetId" = "asset"."id"
|
||||
OR "asset"."stackId" IS NULL
|
||||
|
@ -38,6 +38,7 @@ module.exports = {
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/no-nested-ternary': 'off',
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
|
@ -7,6 +7,7 @@
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
import { memoryStore } from '$lib/stores/memory.store';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||
@ -34,6 +35,7 @@
|
||||
$: canGoForward = !!(nextMemory || nextAsset);
|
||||
$: canGoBack = !!(previousMemory || previousAsset);
|
||||
|
||||
const viewport: Viewport = { width: 0, height: 0 };
|
||||
const toNextMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex + 1}`);
|
||||
const toPreviousMemory = () => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex - 1}`);
|
||||
|
||||
@ -251,7 +253,7 @@
|
||||
|
||||
<!-- GALERY VIEWER -->
|
||||
|
||||
<section class="bg-immich-dark-gray pl-4">
|
||||
<section class="bg-immich-dark-gray m-4">
|
||||
<div
|
||||
class="sticky mb-10 mt-4 flex place-content-center place-items-center transition-all"
|
||||
class:opacity-0={galleryInView}
|
||||
@ -268,8 +270,13 @@
|
||||
on:hidden={() => (galleryInView = false)}
|
||||
bottom={-200}
|
||||
>
|
||||
<div id="gallery-memory" bind:this={memoryGallery}>
|
||||
<GalleryViewer assets={currentMemory.assets} />
|
||||
<div
|
||||
id="gallery-memory"
|
||||
bind:this={memoryGallery}
|
||||
bind:clientHeight={viewport.height}
|
||||
bind:clientWidth={viewport.width}
|
||||
>
|
||||
<GalleryViewer assets={currentMemory.assets} {viewport} />
|
||||
</div>
|
||||
</IntersectionObserver>
|
||||
</section>
|
||||
|
@ -5,7 +5,12 @@
|
||||
import type { AssetStore, Viewport } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
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 { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
@ -36,12 +41,6 @@
|
||||
let actualBucketHeight: number;
|
||||
let hoveredDateGroup = '';
|
||||
|
||||
interface LayoutBox {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
$: assetsGroupByDate = splitBucketIntoDateGroups(assets, $locale);
|
||||
|
||||
$: 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) => {
|
||||
if (isSelectionMode || $isMultiSelectState) {
|
||||
assetSelectHandler(asset, assetsInDateGroup, groupTitle);
|
||||
|
@ -16,10 +16,12 @@
|
||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
|
||||
const viewport: Viewport = { width: 0, height: 0 };
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
$: assets = sharedLink.assets;
|
||||
@ -97,7 +99,7 @@
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<GalleryViewer {assets} bind:selectedAssets />
|
||||
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
|
||||
<GalleryViewer {assets} bind:selectedAssets {viewport} />
|
||||
</section>
|
||||
</section>
|
||||
|
@ -51,7 +51,7 @@
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full z-[100] bg-transparent">
|
||||
<div
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
|
@ -2,13 +2,14 @@
|
||||
import { page } from '$app/stores';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
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 { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
import { ThumbnailFormat, type AssetResponseDto } from '@immich/sdk';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
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 } }>();
|
||||
|
||||
@ -16,14 +17,12 @@
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
export let disableAssetSelect = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let viewport: Viewport;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
let selectedAsset: AssetResponseDto;
|
||||
let currentViewAssetIndex = 0;
|
||||
let viewWidth: number;
|
||||
$: thumbnailSize = getThumbnailSize(assets.length, viewWidth);
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
const viewAssetHandler = (event: CustomEvent) => {
|
||||
@ -86,23 +85,45 @@
|
||||
onDestroy(() => {
|
||||
$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>
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div class="flex w-full flex-wrap gap-1 pb-20" bind:clientWidth={viewWidth}>
|
||||
{#each assets as asset, i (asset.id)}
|
||||
<div animate:flip={{ duration: 500 }}>
|
||||
<div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
|
||||
{#each assets as asset, i (i)}
|
||||
<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
|
||||
{asset}
|
||||
{thumbnailSize}
|
||||
readonly={disableAssetSelect}
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
on:intersected={(event) =>
|
||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||
selected={selectedAssets.has(asset)}
|
||||
{showArchiveIcon}
|
||||
thumbnailWidth={geometry.boxes[i].width}
|
||||
thumbnailHeight={geometry.boxes[i].height}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -2,12 +2,18 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
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 { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||
import IconButton from '$lib/components/elements/buttons/icon-button.svelte';
|
||||
import SearchHistoryBox from './search-history-box.svelte';
|
||||
import SearchFilterBox from './search-filter-box.svelte';
|
||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
export let value = '';
|
||||
export let grayTheme: boolean;
|
||||
|
||||
@ -17,28 +23,17 @@
|
||||
let showFilter = false;
|
||||
$: showClearIcon = value.length > 0;
|
||||
|
||||
function onSearch() {
|
||||
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 onSearch = (payload: SmartSearchDto | MetadataSearchDto) => {
|
||||
const parameters = new URLSearchParams({
|
||||
q: searchValue,
|
||||
smart: smartSearch,
|
||||
take: '100',
|
||||
query: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
showHistory = false;
|
||||
showFilter = false;
|
||||
$isSearchEnabled = false;
|
||||
$searchQuery = payload;
|
||||
goto(`${AppRoute.SEARCH}?${parameters}`, { invalidateAll: true });
|
||||
}
|
||||
};
|
||||
|
||||
const clearSearchTerm = (searchTerm: string) => {
|
||||
input.focus();
|
||||
@ -70,6 +65,26 @@
|
||||
|
||||
showHistory = 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>
|
||||
|
||||
@ -80,7 +95,7 @@
|
||||
class="relative select-text text-sm"
|
||||
action={AppRoute.SEARCH}
|
||||
on:reset={() => (value = '')}
|
||||
on:submit|preventDefault={() => onSearch()}
|
||||
on:submit|preventDefault={onSubmit}
|
||||
>
|
||||
<label>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-6">
|
||||
@ -107,9 +122,9 @@
|
||||
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">
|
||||
<IconButton on:click={() => (showFilter = !showFilter)} title="Show search options">
|
||||
<IconButton on:click={onFilterClick} title="Show search options">
|
||||
<Icon path={mdiTune} size="1.5em" />
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -131,15 +146,12 @@
|
||||
<SearchHistoryBox
|
||||
on:clearAllSearchTerms={clearAllSearchTerms}
|
||||
on:clearSearchTerm={({ detail: searchTerm }) => clearSearchTerm(searchTerm)}
|
||||
on:selectSearchTerm={({ detail: searchTerm }) => {
|
||||
value = searchTerm;
|
||||
onSearch();
|
||||
}}
|
||||
on:selectSearchTerm={({ detail: searchTerm }) => onHistoryTermClick(searchTerm)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showFilter}
|
||||
<SearchFilterBox />
|
||||
<SearchFilterBox on:search={({ detail }) => onSearch(detail)} />
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -4,12 +4,20 @@
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
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 { mdiArrowRight, mdiClose } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Combobox, { type ComboBoxOption } from '../combobox.svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { searchQuery } from '$lib/stores/search.store';
|
||||
|
||||
enum MediaType {
|
||||
All = 'all',
|
||||
@ -22,8 +30,8 @@
|
||||
country: ComboBoxOption[];
|
||||
state: ComboBoxOption[];
|
||||
city: ComboBoxOption[];
|
||||
cameraMake: ComboBoxOption[];
|
||||
cameraModel: ComboBoxOption[];
|
||||
make: ComboBoxOption[];
|
||||
model: ComboBoxOption[];
|
||||
};
|
||||
|
||||
type SearchParams = {
|
||||
@ -49,14 +57,14 @@
|
||||
model?: ComboBoxOption;
|
||||
};
|
||||
|
||||
dateRange: {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
date: {
|
||||
takenAfter?: string;
|
||||
takenBefore?: string;
|
||||
};
|
||||
|
||||
inArchive?: boolean;
|
||||
inFavorite?: boolean;
|
||||
notInAlbum?: boolean;
|
||||
isArchive?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
|
||||
mediaType: MediaType;
|
||||
};
|
||||
@ -66,8 +74,8 @@
|
||||
country: [],
|
||||
state: [],
|
||||
city: [],
|
||||
cameraMake: [],
|
||||
cameraModel: [],
|
||||
make: [],
|
||||
model: [],
|
||||
};
|
||||
|
||||
let filter: SearchFilter = {
|
||||
@ -82,21 +90,23 @@
|
||||
make: undefined,
|
||||
model: undefined,
|
||||
},
|
||||
dateRange: {
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
date: {
|
||||
takenAfter: undefined,
|
||||
takenBefore: undefined,
|
||||
},
|
||||
inArchive: undefined,
|
||||
inFavorite: undefined,
|
||||
notInAlbum: undefined,
|
||||
isArchive: undefined,
|
||||
isFavorite: undefined,
|
||||
isNotInAlbum: undefined,
|
||||
mediaType: MediaType.All,
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher<{ search: SmartSearchDto | MetadataSearchDto }>();
|
||||
let showAllPeople = false;
|
||||
$: peopleList = showAllPeople ? suggestions.people : suggestions.people.slice(0, 11);
|
||||
|
||||
onMount(() => {
|
||||
getPeople();
|
||||
populateExistingFilters();
|
||||
});
|
||||
|
||||
const showSelectedPeopleFirst = () => {
|
||||
@ -141,7 +151,7 @@
|
||||
}
|
||||
|
||||
if (type === SearchSuggestionType.CameraMake || type === SearchSuggestionType.CameraModel) {
|
||||
suggestions = { ...suggestions, cameraMake: [], cameraModel: [] };
|
||||
suggestions = { ...suggestions, make: [], model: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
@ -178,14 +188,14 @@
|
||||
|
||||
case SearchSuggestionType.CameraMake: {
|
||||
for (const make of data) {
|
||||
suggestions.cameraMake = [...suggestions.cameraMake, { label: make, value: make }];
|
||||
suggestions.make = [...suggestions.make, { label: make, value: make }];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SearchSuggestionType.CameraModel: {
|
||||
for (const model of data) {
|
||||
suggestions.cameraModel = [...suggestions.cameraModel, { label: model, value: model }];
|
||||
suggestions.model = [...suggestions.model, { label: model, value: model }];
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -208,18 +218,99 @@
|
||||
make: undefined,
|
||||
model: undefined,
|
||||
},
|
||||
dateRange: {
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
date: {
|
||||
takenAfter: undefined,
|
||||
takenBefore: undefined,
|
||||
},
|
||||
inArchive: undefined,
|
||||
inFavorite: undefined,
|
||||
notInAlbum: undefined,
|
||||
isArchive: undefined,
|
||||
isFavorite: undefined,
|
||||
isNotInAlbum: undefined,
|
||||
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>
|
||||
|
||||
<div
|
||||
@ -347,7 +438,7 @@
|
||||
<div class="w-full">
|
||||
<p class="text-sm text-black dark:text-white">Make</p>
|
||||
<Combobox
|
||||
options={suggestions.cameraMake}
|
||||
options={suggestions.make}
|
||||
bind:selectedOption={filter.camera.make}
|
||||
placeholder="Search camera make..."
|
||||
on:click={() =>
|
||||
@ -358,7 +449,7 @@
|
||||
<div class="w-full">
|
||||
<p class="text-sm text-black dark:text-white">Model</p>
|
||||
<Combobox
|
||||
options={suggestions.cameraModel}
|
||||
options={suggestions.model}
|
||||
bind:selectedOption={filter.camera.model}
|
||||
placeholder="Search camera model..."
|
||||
on:click={() =>
|
||||
@ -379,7 +470,7 @@
|
||||
type="date"
|
||||
id="start-date"
|
||||
name="start-date"
|
||||
bind:value={filter.dateRange.startDate}
|
||||
bind:value={filter.date.takenAfter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -391,7 +482,7 @@
|
||||
id="end-date"
|
||||
name="end-date"
|
||||
placeholder=""
|
||||
bind:value={filter.dateRange.endDate}
|
||||
bind:value={filter.date.takenBefore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -450,17 +541,17 @@
|
||||
|
||||
<div class="flex gap-5 mt-3">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -16,15 +16,6 @@
|
||||
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"
|
||||
>
|
||||
<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}
|
||||
<div class="flex items-center justify-between px-5 pt-5 text-xs">
|
||||
<p>RECENT SEARCHES</p>
|
||||
|
@ -71,6 +71,7 @@ export enum QueryParameter {
|
||||
SEARCHED_PEOPLE = 'searchedPeople',
|
||||
SEARCH_TERM = 'q',
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
PAGE = 'page',
|
||||
}
|
||||
|
||||
export enum OpenSettingQueryParameterValue {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
import { persisted } from 'svelte-local-storage-store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const savedSearchTerms = persisted<string[]>('search-terms', [], {});
|
||||
export const isSearchEnabled = 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]));
|
||||
}
|
||||
|
||||
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 { AppRoute, QueryParameter } from '$lib/constants';
|
||||
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 { 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 { onDestroy, onMount } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import type { PageData } from './$types';
|
||||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const MAX_ASSET_COUNT = 5000;
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
const viewport: Viewport = { width: 0, height: 0 };
|
||||
|
||||
// The GalleryViewer pushes it's own history state, which causes weird
|
||||
// behavior for history.back(). To prevent that we store the previous page
|
||||
// manually and navigate back to that.
|
||||
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;
|
||||
|
||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||
@ -87,16 +92,9 @@
|
||||
if (from?.route.id === '/(user)/albums/[albumId]') {
|
||||
previousRoute = AppRoute.EXPLORE;
|
||||
}
|
||||
});
|
||||
|
||||
$: term = (() => {
|
||||
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;
|
||||
})();
|
||||
updateInformationChip();
|
||||
});
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
@ -111,32 +109,120 @@
|
||||
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 () => {
|
||||
if (curPage == null || !term || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
|
||||
if (currentPage == null || !terms || (searchResultAssets && searchResultAssets.length >= MAX_ASSET_COUNT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await authenticate();
|
||||
let results: SearchResponseDto | null = null;
|
||||
$page.url.searchParams.set('page', curPage.toString());
|
||||
const res = await search({ ...$page.url.searchParams });
|
||||
$page.url.searchParams.set(QueryParameter.PAGE, currentPage.toString());
|
||||
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) {
|
||||
searchResultAssets.push(...res.assets.items);
|
||||
searchResultAssets.push(...responses.assets.items);
|
||||
} else {
|
||||
searchResultAssets = res.assets.items;
|
||||
searchResultAssets = responses.assets.items;
|
||||
}
|
||||
|
||||
const assets = {
|
||||
...res.assets,
|
||||
...responses.assets,
|
||||
items: searchResultAssets,
|
||||
};
|
||||
results = {
|
||||
assets,
|
||||
albums: res.albums,
|
||||
albums: responses.albums,
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<section>
|
||||
@ -161,15 +247,53 @@
|
||||
</AssetSelectControlBar>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="fixed z-[100] top-0 left-0 w-full">
|
||||
<ControlAppBar on:close={() => goto(previousRoute)} backIcon={mdiArrowLeft}>
|
||||
<div class="w-full flex-1 pl-4">
|
||||
<SearchBar grayTheme={false} value={term} />
|
||||
<SearchBar grayTheme={false} />
|
||||
</div>
|
||||
</ControlAppBar>
|
||||
</div>
|
||||
{/if}
|
||||
</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">
|
||||
{#if albums && albums.length > 0}
|
||||
<section>
|
||||
@ -193,14 +317,13 @@
|
||||
{/if}
|
||||
<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if searchResultAssets && searchResultAssets.length > 0}
|
||||
<div class="pl-4">
|
||||
<GalleryViewer
|
||||
assets={searchResultAssets}
|
||||
bind:selectedAssets
|
||||
on:intersected={loadNextPage}
|
||||
showArchiveIcon={true}
|
||||
{viewport}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<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">
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { searchQuery } from '$lib/stores/search.store';
|
||||
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';
|
||||
|
||||
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;
|
||||
let results: SearchResponseDto | null = null;
|
||||
if (term) {
|
||||
let params = {};
|
||||
for (const [key, value] of data.url.searchParams) {
|
||||
params = { ...params, [key]: value };
|
||||
}
|
||||
const response = await search({ ...params });
|
||||
let items: AssetResponseDto[] = (data as unknown as { results: SearchResponseDto }).results?.assets.items;
|
||||
if (items) {
|
||||
items.push(...response.assets.items);
|
||||
} else {
|
||||
items = response.assets.items;
|
||||
}
|
||||
const assets = { ...response.assets, items };
|
||||
results = {
|
||||
assets,
|
||||
albums: response.albums,
|
||||
};
|
||||
const payload = JSON.parse(term) as SmartSearchDto | MetadataSearchDto;
|
||||
searchQuery.set(payload);
|
||||
|
||||
results =
|
||||
payload && 'query' in payload
|
||||
? await searchSmart({ smartSearchDto: { ...payload, withExif: true } })
|
||||
: await searchMetadata({ metadataSearchDto: { ...payload, withExif: true } });
|
||||
}
|
||||
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user