mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web,server): explore (#1926)
* feat: explore * chore: generate open api * styling explore page * styling no result page * style overlay * style: bluring text on thumbnail card for readability * explore page tweaks * fix(web): search urls * feat(web): use objects for things * feat(server): filter by motion, sort by createdAt * More styling * better navigation --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									1f631eafce
								
							
						
					
					
						commit
						2ca560ebf8
					
				
							
								
								
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							@ -66,6 +66,8 @@ doc/SearchApi.md
 | 
				
			|||||||
doc/SearchAssetDto.md
 | 
					doc/SearchAssetDto.md
 | 
				
			||||||
doc/SearchAssetResponseDto.md
 | 
					doc/SearchAssetResponseDto.md
 | 
				
			||||||
doc/SearchConfigResponseDto.md
 | 
					doc/SearchConfigResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchExploreItem.md
 | 
				
			||||||
 | 
					doc/SearchExploreResponseDto.md
 | 
				
			||||||
doc/SearchFacetCountResponseDto.md
 | 
					doc/SearchFacetCountResponseDto.md
 | 
				
			||||||
doc/SearchFacetResponseDto.md
 | 
					doc/SearchFacetResponseDto.md
 | 
				
			||||||
doc/SearchResponseDto.md
 | 
					doc/SearchResponseDto.md
 | 
				
			||||||
@ -179,6 +181,8 @@ lib/model/search_album_response_dto.dart
 | 
				
			|||||||
lib/model/search_asset_dto.dart
 | 
					lib/model/search_asset_dto.dart
 | 
				
			||||||
lib/model/search_asset_response_dto.dart
 | 
					lib/model/search_asset_response_dto.dart
 | 
				
			||||||
lib/model/search_config_response_dto.dart
 | 
					lib/model/search_config_response_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_explore_item.dart
 | 
				
			||||||
 | 
					lib/model/search_explore_response_dto.dart
 | 
				
			||||||
lib/model/search_facet_count_response_dto.dart
 | 
					lib/model/search_facet_count_response_dto.dart
 | 
				
			||||||
lib/model/search_facet_response_dto.dart
 | 
					lib/model/search_facet_response_dto.dart
 | 
				
			||||||
lib/model/search_response_dto.dart
 | 
					lib/model/search_response_dto.dart
 | 
				
			||||||
@ -273,6 +277,8 @@ test/search_api_test.dart
 | 
				
			|||||||
test/search_asset_dto_test.dart
 | 
					test/search_asset_dto_test.dart
 | 
				
			||||||
test/search_asset_response_dto_test.dart
 | 
					test/search_asset_response_dto_test.dart
 | 
				
			||||||
test/search_config_response_dto_test.dart
 | 
					test/search_config_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_explore_item_test.dart
 | 
				
			||||||
 | 
					test/search_explore_response_dto_test.dart
 | 
				
			||||||
test/search_facet_count_response_dto_test.dart
 | 
					test/search_facet_count_response_dto_test.dart
 | 
				
			||||||
test/search_facet_response_dto_test.dart
 | 
					test/search_facet_response_dto_test.dart
 | 
				
			||||||
test/search_response_dto_test.dart
 | 
					test/search_response_dto_test.dart
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@ -121,6 +121,7 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link | 
 | 
					*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link | 
 | 
				
			||||||
*OAuthApi* | [**mobileRedirect**](doc//OAuthApi.md#mobileredirect) | **GET** /oauth/mobile-redirect | 
 | 
					*OAuthApi* | [**mobileRedirect**](doc//OAuthApi.md#mobileredirect) | **GET** /oauth/mobile-redirect | 
 | 
				
			||||||
*OAuthApi* | [**unlink**](doc//OAuthApi.md#unlink) | **POST** /oauth/unlink | 
 | 
					*OAuthApi* | [**unlink**](doc//OAuthApi.md#unlink) | **POST** /oauth/unlink | 
 | 
				
			||||||
 | 
					*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
				
			||||||
*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
					*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
				
			||||||
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 | 
					*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
					*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
				
			||||||
@ -210,6 +211,8 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
					 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
				
			||||||
 - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
 | 
					 - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
 | 
				
			||||||
 - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md)
 | 
					 - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md)
 | 
				
			||||||
 | 
					 - [SearchExploreItem](doc//SearchExploreItem.md)
 | 
				
			||||||
 | 
					 - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md)
 | 
				
			||||||
 - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
 | 
					 - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
 | 
				
			||||||
 - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
 | 
					 - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
 | 
				
			||||||
 - [SearchResponseDto](doc//SearchResponseDto.md)
 | 
					 - [SearchResponseDto](doc//SearchResponseDto.md)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										58
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
								
							@ -9,10 +9,60 @@ All URIs are relative to */api*
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Method | HTTP request | Description
 | 
					Method | HTTP request | Description
 | 
				
			||||||
------------- | ------------- | -------------
 | 
					------------- | ------------- | -------------
 | 
				
			||||||
 | 
					[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | 
 | 
				
			||||||
[**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
					[**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
				
			||||||
[**search**](SearchApi.md#search) | **GET** /search | 
 | 
					[**search**](SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **getExploreData**
 | 
				
			||||||
 | 
					> List<SearchExploreResponseDto> getExploreData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					// TODO Configure HTTP Bearer authorization: bearer
 | 
				
			||||||
 | 
					// Case 1. Use String Token
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
 | 
				
			||||||
 | 
					// Case 2. Use Function which generate token.
 | 
				
			||||||
 | 
					// String yourTokenGeneratorFunction() { ... }
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: cookie
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final api_instance = SearchApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.getExploreData();
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling SearchApi->getExploreData: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					This endpoint does not need any parameter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**List<SearchExploreResponseDto>**](SearchExploreResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **Accept**: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **getSearchConfig**
 | 
					# **getSearchConfig**
 | 
				
			||||||
> SearchConfigResponseDto getSearchConfig()
 | 
					> SearchConfigResponseDto getSearchConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -63,7 +113,7 @@ This endpoint does not need any parameter.
 | 
				
			|||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# **search**
 | 
					# **search**
 | 
				
			||||||
> SearchResponseDto search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags)
 | 
					> SearchResponseDto search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -94,9 +144,11 @@ final exifInfoPeriodMake = exifInfoPeriodMake_example; // String |
 | 
				
			|||||||
final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
 | 
					final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
 | 
				
			||||||
final smartInfoPeriodObjects = []; // List<String> | 
 | 
					final smartInfoPeriodObjects = []; // List<String> | 
 | 
				
			||||||
final smartInfoPeriodTags = []; // List<String> | 
 | 
					final smartInfoPeriodTags = []; // List<String> | 
 | 
				
			||||||
 | 
					final recent = true; // bool | 
 | 
				
			||||||
 | 
					final motion = true; // bool | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
    final result = api_instance.search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags);
 | 
					    final result = api_instance.search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion);
 | 
				
			||||||
    print(result);
 | 
					    print(result);
 | 
				
			||||||
} catch (e) {
 | 
					} catch (e) {
 | 
				
			||||||
    print('Exception when calling SearchApi->search: $e\n');
 | 
					    print('Exception when calling SearchApi->search: $e\n');
 | 
				
			||||||
@ -117,6 +169,8 @@ Name | Type | Description  | Notes
 | 
				
			|||||||
 **exifInfoPeriodModel** | **String**|  | [optional] 
 | 
					 **exifInfoPeriodModel** | **String**|  | [optional] 
 | 
				
			||||||
 **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
					 **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
				
			||||||
 **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
					 **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
				
			||||||
 | 
					 **recent** | **bool**|  | [optional] 
 | 
				
			||||||
 | 
					 **motion** | **bool**|  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Return type
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SearchExploreItem.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SearchExploreItem.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchExploreItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**value** | **String** |  | 
 | 
				
			||||||
 | 
					**data** | [**AssetResponseDto**](AssetResponseDto.md) |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SearchExploreResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SearchExploreResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchExploreResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**fieldName** | **String** |  | 
 | 
				
			||||||
 | 
					**items** | [**List<SearchExploreItem>**](SearchExploreItem.md) |  | [default to const []]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							@ -97,6 +97,8 @@ part 'model/search_album_response_dto.dart';
 | 
				
			|||||||
part 'model/search_asset_dto.dart';
 | 
					part 'model/search_asset_dto.dart';
 | 
				
			||||||
part 'model/search_asset_response_dto.dart';
 | 
					part 'model/search_asset_response_dto.dart';
 | 
				
			||||||
part 'model/search_config_response_dto.dart';
 | 
					part 'model/search_config_response_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_explore_item.dart';
 | 
				
			||||||
 | 
					part 'model/search_explore_response_dto.dart';
 | 
				
			||||||
part 'model/search_facet_count_response_dto.dart';
 | 
					part 'model/search_facet_count_response_dto.dart';
 | 
				
			||||||
part 'model/search_facet_response_dto.dart';
 | 
					part 'model/search_facet_response_dto.dart';
 | 
				
			||||||
part 'model/search_response_dto.dart';
 | 
					part 'model/search_response_dto.dart';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										67
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										67
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							@ -16,6 +16,53 @@ class SearchApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final ApiClient apiClient;
 | 
					  final ApiClient apiClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Note: This method returns the HTTP [Response].
 | 
				
			||||||
 | 
					  Future<Response> getExploreDataWithHttpInfo() async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/search/explore';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'GET',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  Future<List<SearchExploreResponseDto>?> getExploreData() async {
 | 
				
			||||||
 | 
					    final response = await getExploreDataWithHttpInfo();
 | 
				
			||||||
 | 
					    if (response.statusCode >= HttpStatus.badRequest) {
 | 
				
			||||||
 | 
					      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // When a remote server returns no body with a status of 204, we shall not decode it.
 | 
				
			||||||
 | 
					    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
 | 
				
			||||||
 | 
					    // FormatException when trying to decode an empty string.
 | 
				
			||||||
 | 
					    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
 | 
				
			||||||
 | 
					      final responseBody = await _decodeBodyBytes(response);
 | 
				
			||||||
 | 
					      return (await apiClient.deserializeAsync(responseBody, 'List<SearchExploreResponseDto>') as List)
 | 
				
			||||||
 | 
					        .cast<SearchExploreResponseDto>()
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// 
 | 
					  /// 
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// Note: This method returns the HTTP [Response].
 | 
					  /// Note: This method returns the HTTP [Response].
 | 
				
			||||||
@ -85,7 +132,11 @@ class SearchApi {
 | 
				
			|||||||
  /// * [List<String>] smartInfoPeriodObjects:
 | 
					  /// * [List<String>] smartInfoPeriodObjects:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [List<String>] smartInfoPeriodTags:
 | 
					  /// * [List<String>] smartInfoPeriodTags:
 | 
				
			||||||
  Future<Response> searchWithHttpInfo({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, }) async {
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [bool] recent:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [bool] motion:
 | 
				
			||||||
 | 
					  Future<Response> searchWithHttpInfo({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
 | 
				
			||||||
    // ignore: prefer_const_declarations
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
    final path = r'/search';
 | 
					    final path = r'/search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -126,6 +177,12 @@ class SearchApi {
 | 
				
			|||||||
    if (smartInfoPeriodTags != null) {
 | 
					    if (smartInfoPeriodTags != null) {
 | 
				
			||||||
      queryParams.addAll(_queryParams('multi', 'smartInfo.tags', smartInfoPeriodTags));
 | 
					      queryParams.addAll(_queryParams('multi', 'smartInfo.tags', smartInfoPeriodTags));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (recent != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'recent', recent));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (motion != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'motion', motion));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const contentTypes = <String>[];
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -164,8 +221,12 @@ class SearchApi {
 | 
				
			|||||||
  /// * [List<String>] smartInfoPeriodObjects:
 | 
					  /// * [List<String>] smartInfoPeriodObjects:
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
  /// * [List<String>] smartInfoPeriodTags:
 | 
					  /// * [List<String>] smartInfoPeriodTags:
 | 
				
			||||||
  Future<SearchResponseDto?> search({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, }) async {
 | 
					  ///
 | 
				
			||||||
    final response = await searchWithHttpInfo( query: query, type: type, isFavorite: isFavorite, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, );
 | 
					  /// * [bool] recent:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [bool] motion:
 | 
				
			||||||
 | 
					  Future<SearchResponseDto?> search({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
 | 
				
			||||||
 | 
					    final response = await searchWithHttpInfo( query: query, type: type, isFavorite: isFavorite, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, recent: recent, motion: motion, );
 | 
				
			||||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
					    if (response.statusCode >= HttpStatus.badRequest) {
 | 
				
			||||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
					      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							@ -302,6 +302,10 @@ class ApiClient {
 | 
				
			|||||||
          return SearchAssetResponseDto.fromJson(value);
 | 
					          return SearchAssetResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SearchConfigResponseDto':
 | 
					        case 'SearchConfigResponseDto':
 | 
				
			||||||
          return SearchConfigResponseDto.fromJson(value);
 | 
					          return SearchConfigResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchExploreItem':
 | 
				
			||||||
 | 
					          return SearchExploreItem.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchExploreResponseDto':
 | 
				
			||||||
 | 
					          return SearchExploreResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SearchFacetCountResponseDto':
 | 
					        case 'SearchFacetCountResponseDto':
 | 
				
			||||||
          return SearchFacetCountResponseDto.fromJson(value);
 | 
					          return SearchFacetCountResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SearchFacetResponseDto':
 | 
					        case 'SearchFacetResponseDto':
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/search_explore_item.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/search_explore_item.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of openapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchExploreItem {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchExploreItem] instance.
 | 
				
			||||||
 | 
					  SearchExploreItem({
 | 
				
			||||||
 | 
					    required this.value,
 | 
				
			||||||
 | 
					    required this.data,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AssetResponseDto data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchExploreItem &&
 | 
				
			||||||
 | 
					     other.value == value &&
 | 
				
			||||||
 | 
					     other.data == data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (value.hashCode) +
 | 
				
			||||||
 | 
					    (data.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchExploreItem[value=$value, data=$data]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'value'] = this.value;
 | 
				
			||||||
 | 
					      json[r'data'] = this.data;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchExploreItem] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchExploreItem? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchExploreItem[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchExploreItem[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchExploreItem(
 | 
				
			||||||
 | 
					        value: mapValueOfType<String>(json, r'value')!,
 | 
				
			||||||
 | 
					        data: AssetResponseDto.fromJson(json[r'data'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchExploreItem>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchExploreItem>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchExploreItem.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchExploreItem> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchExploreItem>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchExploreItem.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchExploreItem-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchExploreItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchExploreItem>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchExploreItem.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'value',
 | 
				
			||||||
 | 
					    'data',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/search_explore_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/search_explore_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of openapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchExploreResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchExploreResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchExploreResponseDto({
 | 
				
			||||||
 | 
					    required this.fieldName,
 | 
				
			||||||
 | 
					    this.items = const [],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String fieldName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<SearchExploreItem> items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchExploreResponseDto &&
 | 
				
			||||||
 | 
					     other.fieldName == fieldName &&
 | 
				
			||||||
 | 
					     other.items == items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (fieldName.hashCode) +
 | 
				
			||||||
 | 
					    (items.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchExploreResponseDto[fieldName=$fieldName, items=$items]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'fieldName'] = this.fieldName;
 | 
				
			||||||
 | 
					      json[r'items'] = this.items;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchExploreResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchExploreResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchExploreResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchExploreResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchExploreResponseDto(
 | 
				
			||||||
 | 
					        fieldName: mapValueOfType<String>(json, r'fieldName')!,
 | 
				
			||||||
 | 
					        items: SearchExploreItem.listFromJson(json[r'items'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchExploreResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchExploreResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchExploreResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchExploreResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchExploreResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchExploreResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchExploreResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchExploreResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchExploreResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchExploreResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'fieldName',
 | 
				
			||||||
 | 
					    'items',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
								
							@ -17,6 +17,13 @@ void main() {
 | 
				
			|||||||
  // final instance = SearchApi();
 | 
					  // final instance = SearchApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  group('tests for SearchApi', () {
 | 
					  group('tests for SearchApi', () {
 | 
				
			||||||
 | 
					    // 
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    //Future<List<SearchExploreResponseDto>> getExploreData() async
 | 
				
			||||||
 | 
					    test('test getExploreData', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 
 | 
					    // 
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    //Future<SearchConfigResponseDto> getSearchConfig() async
 | 
					    //Future<SearchConfigResponseDto> getSearchConfig() async
 | 
				
			||||||
@ -26,7 +33,7 @@ void main() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // 
 | 
					    // 
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    //Future<SearchResponseDto> search({ String query, String type, bool isFavorite, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags }) async
 | 
					    //Future<SearchResponseDto> search({ String query, String type, bool isFavorite, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
 | 
				
			||||||
    test('test search', () async {
 | 
					    test('test search', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/search_explore_item_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/search_explore_item_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchExploreItem
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchExploreItem();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchExploreItem', () {
 | 
				
			||||||
 | 
					    // String value
 | 
				
			||||||
 | 
					    test('to test the property `value`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // AssetResponseDto data
 | 
				
			||||||
 | 
					    test('to test the property `data`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/search_explore_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/search_explore_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, unused_import
 | 
				
			||||||
 | 
					// ignore_for_file: always_put_required_named_parameters_first
 | 
				
			||||||
 | 
					// ignore_for_file: constant_identifier_names
 | 
				
			||||||
 | 
					// ignore_for_file: lines_longer_than_80_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchExploreResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchExploreResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchExploreResponseDto', () {
 | 
				
			||||||
 | 
					    // String fieldName
 | 
				
			||||||
 | 
					    test('to test the property `fieldName`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<SearchExploreItem> items (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `items`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,4 +1,11 @@
 | 
				
			|||||||
import { AuthUserDto, SearchConfigResponseDto, SearchDto, SearchResponseDto, SearchService } from '@app/domain';
 | 
					import {
 | 
				
			||||||
 | 
					  AuthUserDto,
 | 
				
			||||||
 | 
					  SearchConfigResponseDto,
 | 
				
			||||||
 | 
					  SearchDto,
 | 
				
			||||||
 | 
					  SearchExploreResponseDto,
 | 
				
			||||||
 | 
					  SearchResponseDto,
 | 
				
			||||||
 | 
					  SearchService,
 | 
				
			||||||
 | 
					} from '@app/domain';
 | 
				
			||||||
import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
 | 
					import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
 | 
				
			||||||
import { ApiTags } from '@nestjs/swagger';
 | 
					import { ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
 | 
					import { GetAuthUser } from '../decorators/auth-user.decorator';
 | 
				
			||||||
@ -10,7 +17,6 @@ import { Authenticated } from '../decorators/authenticated.decorator';
 | 
				
			|||||||
export class SearchController {
 | 
					export class SearchController {
 | 
				
			||||||
  constructor(private readonly searchService: SearchService) {}
 | 
					  constructor(private readonly searchService: SearchService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Authenticated()
 | 
					 | 
				
			||||||
  @Get()
 | 
					  @Get()
 | 
				
			||||||
  async search(
 | 
					  async search(
 | 
				
			||||||
    @GetAuthUser() authUser: AuthUserDto,
 | 
					    @GetAuthUser() authUser: AuthUserDto,
 | 
				
			||||||
@ -19,9 +25,13 @@ export class SearchController {
 | 
				
			|||||||
    return this.searchService.search(authUser, dto);
 | 
					    return this.searchService.search(authUser, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Authenticated()
 | 
					 | 
				
			||||||
  @Get('config')
 | 
					  @Get('config')
 | 
				
			||||||
  getSearchConfig(): SearchConfigResponseDto {
 | 
					  getSearchConfig(): SearchConfigResponseDto {
 | 
				
			||||||
    return this.searchService.getConfig();
 | 
					    return this.searchService.getConfig();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get('explore')
 | 
				
			||||||
 | 
					  getExploreData(@GetAuthUser() authUser: AuthUserDto): Promise<SearchExploreResponseDto[]> {
 | 
				
			||||||
 | 
					    return this.searchService.getExploreData(authUser) as Promise<SearchExploreResponseDto[]>;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,8 @@ import {
 | 
				
			|||||||
  AssetCore,
 | 
					  AssetCore,
 | 
				
			||||||
  IAssetRepository,
 | 
					  IAssetRepository,
 | 
				
			||||||
  IAssetUploadedJob,
 | 
					  IAssetUploadedJob,
 | 
				
			||||||
 | 
					  IJobRepository,
 | 
				
			||||||
  IReverseGeocodingJob,
 | 
					  IReverseGeocodingJob,
 | 
				
			||||||
  ISearchRepository,
 | 
					 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
  QueueName,
 | 
					  QueueName,
 | 
				
			||||||
} from '@app/domain';
 | 
					} from '@app/domain';
 | 
				
			||||||
@ -86,14 +86,14 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) searchRepository: ISearchRepository,
 | 
					    @Inject(IJobRepository) jobRepository: IJobRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @InjectRepository(ExifEntity)
 | 
					    @InjectRepository(ExifEntity)
 | 
				
			||||||
    private exifRepository: Repository<ExifEntity>,
 | 
					    private exifRepository: Repository<ExifEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    configService: ConfigService,
 | 
					    configService: ConfigService,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.assetCore = new AssetCore(assetRepository, searchRepository);
 | 
					    this.assetCore = new AssetCore(assetRepository, jobRepository);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!configService.get('DISABLE_REVERSE_GEOCODING')) {
 | 
					    if (!configService.get('DISABLE_REVERSE_GEOCODING')) {
 | 
				
			||||||
      this.logger.log('Initializing Reverse Geocoding');
 | 
					      this.logger.log('Initializing Reverse Geocoding');
 | 
				
			||||||
 | 
				
			|||||||
@ -640,6 +640,22 @@
 | 
				
			|||||||
                "type": "string"
 | 
					                "type": "string"
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "recent",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "boolean"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "motion",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "boolean"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "responses": {
 | 
					        "responses": {
 | 
				
			||||||
@ -658,12 +674,6 @@
 | 
				
			|||||||
          "Search"
 | 
					          "Search"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        "security": [
 | 
					        "security": [
 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "bearer": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            "cookie": []
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "bearer": []
 | 
					            "bearer": []
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -699,7 +709,34 @@
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "cookie": []
 | 
					            "cookie": []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/search/explore": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "operationId": "getExploreData",
 | 
				
			||||||
 | 
					        "description": "",
 | 
				
			||||||
 | 
					        "parameters": [],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "description": "",
 | 
				
			||||||
 | 
					            "content": {
 | 
				
			||||||
 | 
					              "application/json": {
 | 
				
			||||||
 | 
					                "schema": {
 | 
				
			||||||
 | 
					                  "type": "array",
 | 
				
			||||||
 | 
					                  "items": {
 | 
				
			||||||
 | 
					                    "$ref": "#/components/schemas/SearchExploreResponseDto"
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "Search"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "security": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "bearer": []
 | 
					            "bearer": []
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -4149,6 +4186,39 @@
 | 
				
			|||||||
          "enabled"
 | 
					          "enabled"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchExploreItem": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "value": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "data": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/AssetResponseDto"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "value",
 | 
				
			||||||
 | 
					          "data"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchExploreResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "fieldName": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/SearchExploreItem"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "fieldName",
 | 
				
			||||||
 | 
					          "items"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "SharedLinkType": {
 | 
					      "SharedLinkType": {
 | 
				
			||||||
        "type": "string",
 | 
					        "type": "string",
 | 
				
			||||||
        "enum": [
 | 
					        "enum": [
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,21 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
import { ISearchRepository, SearchCollection } from '../search/search.repository';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
import { AssetSearchOptions, IAssetRepository } from './asset.repository';
 | 
					import { AssetSearchOptions, IAssetRepository } from './asset.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AssetCore {
 | 
					export class AssetCore {
 | 
				
			||||||
  constructor(private repository: IAssetRepository, private searchRepository: ISearchRepository) {}
 | 
					  constructor(private assetRepository: IAssetRepository, private jobRepository: IJobRepository) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAll(options: AssetSearchOptions) {
 | 
					  getAll(options: AssetSearchOptions) {
 | 
				
			||||||
    return this.repository.getAll(options);
 | 
					    return this.assetRepository.getAll(options);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async save(asset: Partial<AssetEntity>) {
 | 
					  async save(asset: Partial<AssetEntity>) {
 | 
				
			||||||
    const _asset = await this.repository.save(asset);
 | 
					    const _asset = await this.assetRepository.save(asset);
 | 
				
			||||||
    await this.searchRepository.index(SearchCollection.ASSETS, _asset);
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { asset: _asset } });
 | 
				
			||||||
    return _asset;
 | 
					    return _asset;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null> {
 | 
					  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null> {
 | 
				
			||||||
    return this.repository.findLivePhotoMatch(livePhotoCID, otherAssetId, type);
 | 
					    return this.assetRepository.findLivePhotoMatch(livePhotoCID, otherAssetId, type);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,12 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
import { assetEntityStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
 | 
					import { assetEntityStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
 | 
				
			||||||
import { newSearchRepositoryMock } from '../../test/search.repository.mock';
 | 
					 | 
				
			||||||
import { AssetService, IAssetRepository } from '../asset';
 | 
					import { AssetService, IAssetRepository } from '../asset';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
import { ISearchRepository } from '../search';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(AssetService.name, () => {
 | 
					describe(AssetService.name, () => {
 | 
				
			||||||
  let sut: AssetService;
 | 
					  let sut: AssetService;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
  let searchMock: jest.Mocked<ISearchRepository>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
    expect(sut).toBeDefined();
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
@ -18,8 +15,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    searchMock = newSearchRepositoryMock();
 | 
					    sut = new AssetService(assetMock, jobMock);
 | 
				
			||||||
    sut = new AssetService(assetMock, jobMock, searchMock);
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe(`handle asset upload`, () => {
 | 
					  describe(`handle asset upload`, () => {
 | 
				
			||||||
@ -56,7 +52,10 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
      await sut.save(assetEntityStub.image);
 | 
					      await sut.save(assetEntityStub.image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith(assetEntityStub.image);
 | 
					      expect(assetMock.save).toHaveBeenCalledWith(assetEntityStub.image);
 | 
				
			||||||
      expect(searchMock.index).toHaveBeenCalledWith('assets', assetEntityStub.image);
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.SEARCH_INDEX_ASSET,
 | 
				
			||||||
 | 
					        data: { asset: assetEntityStub.image },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
import { Inject } from '@nestjs/common';
 | 
					import { Inject } from '@nestjs/common';
 | 
				
			||||||
import { IAssetUploadedJob, IJobRepository, JobName } from '../job';
 | 
					import { IAssetUploadedJob, IJobRepository, JobName } from '../job';
 | 
				
			||||||
import { ISearchRepository } from '../search';
 | 
					 | 
				
			||||||
import { AssetCore } from './asset.core';
 | 
					import { AssetCore } from './asset.core';
 | 
				
			||||||
import { IAssetRepository } from './asset.repository';
 | 
					import { IAssetRepository } from './asset.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,9 +10,8 @@ export class AssetService {
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) searchRepository: ISearchRepository,
 | 
					 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.assetCore = new AssetCore(assetRepository, searchRepository);
 | 
					    this.assetCore = new AssetCore(assetRepository, jobRepository);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleAssetUpload(data: IAssetUploadedJob) {
 | 
					  async handleAssetUpload(data: IAssetUploadedJob) {
 | 
				
			||||||
 | 
				
			|||||||
@ -54,4 +54,14 @@ export class SearchDto {
 | 
				
			|||||||
  @IsOptional()
 | 
					  @IsOptional()
 | 
				
			||||||
  @Transform(({ value }) => value.split(','))
 | 
					  @Transform(({ value }) => value.split(','))
 | 
				
			||||||
  'smartInfo.tags'?: string[];
 | 
					  'smartInfo.tags'?: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  @Transform(toBoolean)
 | 
				
			||||||
 | 
					  recent?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  @Transform(toBoolean)
 | 
				
			||||||
 | 
					  motion?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,3 @@
 | 
				
			|||||||
export * from './search-config-response.dto';
 | 
					export * from './search-config-response.dto';
 | 
				
			||||||
 | 
					export * from './search-explore.response.dto';
 | 
				
			||||||
export * from './search-response.dto';
 | 
					export * from './search-response.dto';
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { AssetResponseDto } from '../../asset';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchExploreItem {
 | 
				
			||||||
 | 
					  value!: string;
 | 
				
			||||||
 | 
					  data!: AssetResponseDto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SearchExploreResponseDto {
 | 
				
			||||||
 | 
					  fieldName!: string;
 | 
				
			||||||
 | 
					  items!: SearchExploreItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -17,6 +17,8 @@ export interface SearchFilter {
 | 
				
			|||||||
  model?: string;
 | 
					  model?: string;
 | 
				
			||||||
  objects?: string[];
 | 
					  objects?: string[];
 | 
				
			||||||
  tags?: string[];
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  recent?: boolean;
 | 
				
			||||||
 | 
					  motion?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SearchResult<T> {
 | 
					export interface SearchResult<T> {
 | 
				
			||||||
@ -39,6 +41,14 @@ export interface SearchFacet {
 | 
				
			|||||||
  }>;
 | 
					  }>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchExploreItem<T> {
 | 
				
			||||||
 | 
					  fieldName: string;
 | 
				
			||||||
 | 
					  items: Array<{
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					    data: T;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SearchCollectionIndexStatus = Record<SearchCollection, boolean>;
 | 
					export type SearchCollectionIndexStatus = Record<SearchCollection, boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ISearchRepository = 'ISearchRepository';
 | 
					export const ISearchRepository = 'ISearchRepository';
 | 
				
			||||||
@ -57,4 +67,6 @@ export interface ISearchRepository {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  search(collection: SearchCollection.ASSETS, query: string, filters: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
					  search(collection: SearchCollection.ASSETS, query: string, filters: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
				
			||||||
  search(collection: SearchCollection.ALBUMS, query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
					  search(collection: SearchCollection.ALBUMS, query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  explore(userId: string): Promise<SearchExploreItem<AssetEntity>[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { AssetEntity } from '@app/infra/db/entities';
 | 
				
			||||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
					import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import { IAlbumRepository } from '../album/album.repository';
 | 
					import { IAlbumRepository } from '../album/album.repository';
 | 
				
			||||||
@ -6,7 +7,7 @@ import { AuthUserDto } from '../auth';
 | 
				
			|||||||
import { IAlbumJob, IAssetJob, IDeleteJob, IJobRepository, JobName } from '../job';
 | 
					import { IAlbumJob, IAssetJob, IDeleteJob, IJobRepository, JobName } from '../job';
 | 
				
			||||||
import { SearchDto } from './dto';
 | 
					import { SearchDto } from './dto';
 | 
				
			||||||
import { SearchConfigResponseDto, SearchResponseDto } from './response-dto';
 | 
					import { SearchConfigResponseDto, SearchResponseDto } from './response-dto';
 | 
				
			||||||
import { ISearchRepository, SearchCollection } from './search.repository';
 | 
					import { ISearchRepository, SearchCollection, SearchExploreItem } from './search.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class SearchService {
 | 
					export class SearchService {
 | 
				
			||||||
@ -52,11 +53,14 @@ export class SearchService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
 | 
					  async getExploreData(authUser: AuthUserDto): Promise<SearchExploreItem<AssetEntity>[]> {
 | 
				
			||||||
    if (!this.enabled) {
 | 
					    this.assertEnabled();
 | 
				
			||||||
      throw new BadRequestException('Search is disabled');
 | 
					    return this.searchRepository.explore(authUser.id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
 | 
				
			||||||
 | 
					    this.assertEnabled();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const query = dto.query || '*';
 | 
					    const query = dto.query || '*';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@ -83,6 +87,7 @@ export class SearchService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      this.logger.log(`Indexing ${assets.length} assets`);
 | 
					      this.logger.log(`Indexing ${assets.length} assets`);
 | 
				
			||||||
      await this.searchRepository.import(SearchCollection.ASSETS, assets, true);
 | 
					      await this.searchRepository.import(SearchCollection.ASSETS, assets, true);
 | 
				
			||||||
 | 
					      this.logger.debug('Finished re-indexing all assets');
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      this.logger.error(`Unable to index all assets`, error?.stack);
 | 
					      this.logger.error(`Unable to index all assets`, error?.stack);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -94,6 +99,9 @@ export class SearchService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { asset } = data;
 | 
					    const { asset } = data;
 | 
				
			||||||
 | 
					    if (!asset.isVisible) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.searchRepository.index(SearchCollection.ASSETS, asset);
 | 
					      await this.searchRepository.index(SearchCollection.ASSETS, asset);
 | 
				
			||||||
@ -111,6 +119,7 @@ export class SearchService {
 | 
				
			|||||||
      const albums = await this.albumRepository.getAll();
 | 
					      const albums = await this.albumRepository.getAll();
 | 
				
			||||||
      this.logger.log(`Indexing ${albums.length} albums`);
 | 
					      this.logger.log(`Indexing ${albums.length} albums`);
 | 
				
			||||||
      await this.searchRepository.import(SearchCollection.ALBUMS, albums, true);
 | 
					      await this.searchRepository.import(SearchCollection.ALBUMS, albums, true);
 | 
				
			||||||
 | 
					      this.logger.debug('Finished re-indexing all albums');
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      this.logger.error(`Unable to index all albums`, error?.stack);
 | 
					      this.logger.error(`Unable to index all albums`, error?.stack);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -151,4 +160,10 @@ export class SearchService {
 | 
				
			|||||||
      this.logger.error(`Unable to remove ${collection}: ${id}`, error?.stack);
 | 
					      this.logger.error(`Unable to remove ${collection}: ${id}`, error?.stack);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private assertEnabled() {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Search is disabled');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,5 +8,6 @@ export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
 | 
				
			|||||||
    import: jest.fn(),
 | 
					    import: jest.fn(),
 | 
				
			||||||
    search: jest.fn(),
 | 
					    search: jest.fn(),
 | 
				
			||||||
    delete: jest.fn(),
 | 
					    delete: jest.fn(),
 | 
				
			||||||
 | 
					    explore: jest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
					import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const assetSchemaVersion = 1;
 | 
					export const assetSchemaVersion = 2;
 | 
				
			||||||
export const assetSchema: CollectionCreateSchema = {
 | 
					export const assetSchema: CollectionCreateSchema = {
 | 
				
			||||||
  name: `assets-v${assetSchemaVersion}`,
 | 
					  name: `assets-v${assetSchemaVersion}`,
 | 
				
			||||||
  fields: [
 | 
					  fields: [
 | 
				
			||||||
@ -22,7 +22,6 @@ export const assetSchema: CollectionCreateSchema = {
 | 
				
			|||||||
    { name: 'exifInfo.state', type: 'string', facet: true, optional: true },
 | 
					    { name: 'exifInfo.state', type: 'string', facet: true, optional: true },
 | 
				
			||||||
    { name: 'exifInfo.description', type: 'string', facet: false, optional: true },
 | 
					    { name: 'exifInfo.description', type: 'string', facet: false, optional: true },
 | 
				
			||||||
    { name: 'exifInfo.imageName', type: 'string', facet: false, optional: true },
 | 
					    { name: 'exifInfo.imageName', type: 'string', facet: false, optional: true },
 | 
				
			||||||
    { name: 'geo', type: 'geopoint', facet: false, optional: true },
 | 
					 | 
				
			||||||
    { name: 'exifInfo.make', type: 'string', facet: true, optional: true },
 | 
					    { name: 'exifInfo.make', type: 'string', facet: true, optional: true },
 | 
				
			||||||
    { name: 'exifInfo.model', type: 'string', facet: true, optional: true },
 | 
					    { name: 'exifInfo.model', type: 'string', facet: true, optional: true },
 | 
				
			||||||
    { name: 'exifInfo.orientation', type: 'string', optional: true },
 | 
					    { name: 'exifInfo.orientation', type: 'string', optional: true },
 | 
				
			||||||
@ -30,6 +29,10 @@ export const assetSchema: CollectionCreateSchema = {
 | 
				
			|||||||
    // smart info
 | 
					    // smart info
 | 
				
			||||||
    { name: 'smartInfo.objects', type: 'string[]', facet: true, optional: true },
 | 
					    { name: 'smartInfo.objects', type: 'string[]', facet: true, optional: true },
 | 
				
			||||||
    { name: 'smartInfo.tags', type: 'string[]', facet: true, optional: true },
 | 
					    { name: 'smartInfo.tags', type: 'string[]', facet: true, optional: true },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // computed
 | 
				
			||||||
 | 
					    { name: 'geo', type: 'geopoint', facet: false, optional: true },
 | 
				
			||||||
 | 
					    { name: 'motion', type: 'bool', facet: true },
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  token_separators: ['.'],
 | 
					  token_separators: ['.'],
 | 
				
			||||||
  enable_nested_fields: true,
 | 
					  enable_nested_fields: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,13 @@ import {
 | 
				
			|||||||
  ISearchRepository,
 | 
					  ISearchRepository,
 | 
				
			||||||
  SearchCollection,
 | 
					  SearchCollection,
 | 
				
			||||||
  SearchCollectionIndexStatus,
 | 
					  SearchCollectionIndexStatus,
 | 
				
			||||||
 | 
					  SearchExploreItem,
 | 
				
			||||||
  SearchFilter,
 | 
					  SearchFilter,
 | 
				
			||||||
  SearchResult,
 | 
					  SearchResult,
 | 
				
			||||||
} from '@app/domain';
 | 
					} from '@app/domain';
 | 
				
			||||||
import { Injectable, Logger } from '@nestjs/common';
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import _, { Dictionary } from 'lodash';
 | 
					import _, { Dictionary } from 'lodash';
 | 
				
			||||||
 | 
					import { filter, firstValueFrom, from, map, mergeMap, toArray } from 'rxjs';
 | 
				
			||||||
import { Client } from 'typesense';
 | 
					import { Client } from 'typesense';
 | 
				
			||||||
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
					import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
				
			||||||
import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
 | 
					import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
 | 
				
			||||||
@ -14,8 +16,9 @@ import { AlbumEntity, AssetEntity } from '../db';
 | 
				
			|||||||
import { albumSchema } from './schemas/album.schema';
 | 
					import { albumSchema } from './schemas/album.schema';
 | 
				
			||||||
import { assetSchema } from './schemas/asset.schema';
 | 
					import { assetSchema } from './schemas/asset.schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface GeoAssetEntity extends AssetEntity {
 | 
					interface CustomAssetEntity extends AssetEntity {
 | 
				
			||||||
  geo?: [number, number];
 | 
					  geo?: [number, number];
 | 
				
			||||||
 | 
					  motion?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function removeNil<T extends Dictionary<any>>(item: T): Partial<T> {
 | 
					function removeNil<T extends Dictionary<any>>(item: T): Partial<T> {
 | 
				
			||||||
@ -85,6 +88,12 @@ export class TypesenseRepository implements ISearchRepository {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async setup(): Promise<void> {
 | 
					  async setup(): Promise<void> {
 | 
				
			||||||
 | 
					    const collections = await this.client.collections().retrieve();
 | 
				
			||||||
 | 
					    for (const collection of collections) {
 | 
				
			||||||
 | 
					      this.logger.debug(`${collection.name} => ${collection.num_documents}`);
 | 
				
			||||||
 | 
					      // await this.client.collections(collection.name).delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // upsert collections
 | 
					    // upsert collections
 | 
				
			||||||
    for (const [collectionName, schema] of schemas) {
 | 
					    for (const [collectionName, schema] of schemas) {
 | 
				
			||||||
      const collection = await this.client
 | 
					      const collection = await this.client
 | 
				
			||||||
@ -172,6 +181,59 @@ export class TypesenseRepository implements ISearchRepository {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async explore(userId: string): Promise<SearchExploreItem<AssetEntity>[]> {
 | 
				
			||||||
 | 
					    const alias = await this.client.aliases(SearchCollection.ASSETS).retrieve();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const common = {
 | 
				
			||||||
 | 
					      q: '*',
 | 
				
			||||||
 | 
					      filter_by: `ownerId:${userId}`,
 | 
				
			||||||
 | 
					      per_page: 100,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const asset$ = this.client.collections<AssetEntity>(alias.collection_name).documents();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { facet_counts: facets } = await asset$.search({
 | 
				
			||||||
 | 
					      ...common,
 | 
				
			||||||
 | 
					      query_by: 'exifInfo.imageName',
 | 
				
			||||||
 | 
					      facet_by: this.getFacetFieldNames(SearchCollection.ASSETS),
 | 
				
			||||||
 | 
					      max_facet_values: 50,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return firstValueFrom(
 | 
				
			||||||
 | 
					      from(facets || []).pipe(
 | 
				
			||||||
 | 
					        mergeMap(
 | 
				
			||||||
 | 
					          (facet) =>
 | 
				
			||||||
 | 
					            from(facet.counts).pipe(
 | 
				
			||||||
 | 
					              mergeMap(
 | 
				
			||||||
 | 
					                (count) =>
 | 
				
			||||||
 | 
					                  from(
 | 
				
			||||||
 | 
					                    asset$.search({
 | 
				
			||||||
 | 
					                      ...common,
 | 
				
			||||||
 | 
					                      query_by: 'exifInfo.imageName',
 | 
				
			||||||
 | 
					                      filter_by: `${facet.field_name}:${count.value}`,
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                  ).pipe(
 | 
				
			||||||
 | 
					                    map((result) => ({
 | 
				
			||||||
 | 
					                      value: count.value,
 | 
				
			||||||
 | 
					                      data: result.hits?.[0]?.document as AssetEntity,
 | 
				
			||||||
 | 
					                    })),
 | 
				
			||||||
 | 
					                    filter((item) => !!item.data),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                5,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              toArray(),
 | 
				
			||||||
 | 
					              map((items) => ({
 | 
				
			||||||
 | 
					                fieldName: facet.field_name as string,
 | 
				
			||||||
 | 
					                items,
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          3,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        toArray(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  search(collection: SearchCollection.ASSETS, query: string, filter: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
					  search(collection: SearchCollection.ASSETS, query: string, filter: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
				
			||||||
  search(collection: SearchCollection.ALBUMS, query: string, filter: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
					  search(collection: SearchCollection.ALBUMS, query: string, filter: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
				
			||||||
  async search(collection: SearchCollection, query: string, filters: SearchFilter) {
 | 
					  async search(collection: SearchCollection, query: string, filters: SearchFilter) {
 | 
				
			||||||
@ -213,10 +275,8 @@ export class TypesenseRepository implements ISearchRepository {
 | 
				
			|||||||
          ].join(','),
 | 
					          ].join(','),
 | 
				
			||||||
          filter_by: _filters.join(' && '),
 | 
					          filter_by: _filters.join(' && '),
 | 
				
			||||||
          per_page: 250,
 | 
					          per_page: 250,
 | 
				
			||||||
          facet_by: (assetSchema.fields || [])
 | 
					          sort_by: filters.recent ? 'createdAt:desc' : undefined,
 | 
				
			||||||
            .filter((field) => field.facet)
 | 
					          facet_by: this.getFacetFieldNames(SearchCollection.ASSETS),
 | 
				
			||||||
            .map((field) => field.name)
 | 
					 | 
				
			||||||
            .join(','),
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return this.asResponse(results);
 | 
					      return this.asResponse(results);
 | 
				
			||||||
@ -313,13 +373,24 @@ export class TypesenseRepository implements ISearchRepository {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private patchAsset(asset: AssetEntity): GeoAssetEntity {
 | 
					  private patchAsset(asset: AssetEntity): CustomAssetEntity {
 | 
				
			||||||
 | 
					    let custom = asset as CustomAssetEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const lat = asset.exifInfo?.latitude;
 | 
					    const lat = asset.exifInfo?.latitude;
 | 
				
			||||||
    const lng = asset.exifInfo?.longitude;
 | 
					    const lng = asset.exifInfo?.longitude;
 | 
				
			||||||
    if (lat && lng && lat !== 0 && lng !== 0) {
 | 
					    if (lat && lng && lat !== 0 && lng !== 0) {
 | 
				
			||||||
      return { ...asset, geo: [lat, lng] };
 | 
					      custom = { ...custom, geo: [lat, lng] };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return asset;
 | 
					    custom = { ...custom, motion: !!asset.livePhotoVideoId };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return custom;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getFacetFieldNames(collection: SearchCollection) {
 | 
				
			||||||
 | 
					    return (schemaMap[collection].fields || [])
 | 
				
			||||||
 | 
					      .filter((field) => field.facet)
 | 
				
			||||||
 | 
					      .map((field) => field.name)
 | 
				
			||||||
 | 
					      .join(',');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										130
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										130
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -1539,6 +1539,44 @@ export interface SearchConfigResponseDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'enabled': boolean;
 | 
					    'enabled': boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchExploreItem
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchExploreItem {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SearchExploreItem
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'value': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {AssetResponseDto}
 | 
				
			||||||
 | 
					     * @memberof SearchExploreItem
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'data': AssetResponseDto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchExploreResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchExploreResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SearchExploreResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'fieldName': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<SearchExploreItem>}
 | 
				
			||||||
 | 
					     * @memberof SearchExploreResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'items': Array<SearchExploreItem>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -6629,6 +6667,41 @@ export class OAuthApi extends BaseAPI {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export const SearchApiAxiosParamCreator = function (configuration?: Configuration) {
 | 
					export const SearchApiAxiosParamCreator = function (configuration?: Configuration) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getExploreData: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            const localVarPath = `/search/explore`;
 | 
				
			||||||
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
 | 
					            let baseOptions;
 | 
				
			||||||
 | 
					            if (configuration) {
 | 
				
			||||||
 | 
					                baseOptions = configuration.baseOptions;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
				
			||||||
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication bearer required
 | 
				
			||||||
 | 
					            // http bearer authentication required
 | 
				
			||||||
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication cookie required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6676,10 +6749,12 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
 | 
				
			|||||||
         * @param {string} [exifInfoModel] 
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoObjects] 
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoTags] 
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {boolean} [recent] 
 | 
				
			||||||
 | 
					         * @param {boolean} [motion] 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        search: async (query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
					        search: async (query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
            const localVarPath = `/search`;
 | 
					            const localVarPath = `/search`;
 | 
				
			||||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
@ -6738,6 +6813,14 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
 | 
				
			|||||||
                localVarQueryParameter['smartInfo.tags'] = smartInfoTags;
 | 
					                localVarQueryParameter['smartInfo.tags'] = smartInfoTags;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (recent !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['recent'] = recent;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (motion !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['motion'] = motion;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
@ -6759,6 +6842,15 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
 | 
				
			|||||||
export const SearchApiFp = function(configuration?: Configuration) {
 | 
					export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			||||||
    const localVarAxiosParamCreator = SearchApiAxiosParamCreator(configuration)
 | 
					    const localVarAxiosParamCreator = SearchApiAxiosParamCreator(configuration)
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async getExploreData(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SearchExploreResponseDto>>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getExploreData(options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6780,11 +6872,13 @@ export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
         * @param {string} [exifInfoModel] 
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoObjects] 
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoTags] 
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {boolean} [recent] 
 | 
				
			||||||
 | 
					         * @param {boolean} [motion] 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        async search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
 | 
					        async search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
 | 
				
			||||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options);
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options);
 | 
				
			||||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -6797,6 +6891,14 @@ export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			|||||||
export const SearchApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 | 
					export const SearchApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 | 
				
			||||||
    const localVarFp = SearchApiFp(configuration)
 | 
					    const localVarFp = SearchApiFp(configuration)
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getExploreData(options?: any): AxiosPromise<Array<SearchExploreResponseDto>> {
 | 
				
			||||||
 | 
					            return localVarFp.getExploreData(options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 
 | 
					         * 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6817,11 +6919,13 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
 | 
				
			|||||||
         * @param {string} [exifInfoModel] 
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoObjects] 
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
         * @param {Array<string>} [smartInfoTags] 
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {boolean} [recent] 
 | 
				
			||||||
 | 
					         * @param {boolean} [motion] 
 | 
				
			||||||
         * @param {*} [options] Override http request option.
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
         * @throws {RequiredError}
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: any): AxiosPromise<SearchResponseDto> {
 | 
					        search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
 | 
				
			||||||
            return localVarFp.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options).then((request) => request(axios, basePath));
 | 
					            return localVarFp.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath));
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -6833,6 +6937,16 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
 | 
				
			|||||||
 * @extends {BaseAPI}
 | 
					 * @extends {BaseAPI}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export class SearchApi extends BaseAPI {
 | 
					export class SearchApi extends BaseAPI {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof SearchApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public getExploreData(options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return SearchApiFp(this.configuration).getExploreData(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
@ -6855,12 +6969,14 @@ export class SearchApi extends BaseAPI {
 | 
				
			|||||||
     * @param {string} [exifInfoModel] 
 | 
					     * @param {string} [exifInfoModel] 
 | 
				
			||||||
     * @param {Array<string>} [smartInfoObjects] 
 | 
					     * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
     * @param {Array<string>} [smartInfoTags] 
 | 
					     * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					     * @param {boolean} [recent] 
 | 
				
			||||||
 | 
					     * @param {boolean} [motion] 
 | 
				
			||||||
     * @param {*} [options] Override http request option.
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
     * @throws {RequiredError}
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
     * @memberof SearchApi
 | 
					     * @memberof SearchApi
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: AxiosRequestConfig) {
 | 
					    public search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) {
 | 
				
			||||||
        return SearchApiFp(this.configuration).search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options).then((request) => request(this.axios, this.basePath));
 | 
					        return SearchApiFp(this.configuration).search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@
 | 
				
			|||||||
	export let format: ThumbnailFormat = ThumbnailFormat.Webp;
 | 
						export let format: ThumbnailFormat = ThumbnailFormat.Webp;
 | 
				
			||||||
	export let selected = false;
 | 
						export let selected = false;
 | 
				
			||||||
	export let disabled = false;
 | 
						export let disabled = false;
 | 
				
			||||||
 | 
						export let readonly = false;
 | 
				
			||||||
	export let publicSharedKey = '';
 | 
						export let publicSharedKey = '';
 | 
				
			||||||
	export let isRoundedCorner = false;
 | 
						export let isRoundedCorner = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -56,6 +57,7 @@
 | 
				
			|||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const parseVideoDuration = (duration: string) => {
 | 
						const parseVideoDuration = (duration: string) => {
 | 
				
			||||||
 | 
							duration = duration || '0:00:00.00000';
 | 
				
			||||||
		const timePart = duration.split(':');
 | 
							const timePart = duration.split(':');
 | 
				
			||||||
		const hours = timePart[0];
 | 
							const hours = timePart[0];
 | 
				
			||||||
		const minutes = timePart[1];
 | 
							const minutes = timePart[1];
 | 
				
			||||||
@ -118,7 +120,7 @@
 | 
				
			|||||||
		} else if (disabled) {
 | 
							} else if (disabled) {
 | 
				
			||||||
			return 'border-[20px] border-gray-300';
 | 
								return 'border-[20px] border-gray-300';
 | 
				
			||||||
		} else if (isRoundedCorner) {
 | 
							} else if (isRoundedCorner) {
 | 
				
			||||||
			return 'rounded-[20px]';
 | 
								return 'rounded-lg';
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return '';
 | 
								return '';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -157,7 +159,7 @@
 | 
				
			|||||||
		on:click={thumbnailClickedHandler}
 | 
							on:click={thumbnailClickedHandler}
 | 
				
			||||||
		on:keydown={thumbnailClickedHandler}
 | 
							on:keydown={thumbnailClickedHandler}
 | 
				
			||||||
	>
 | 
						>
 | 
				
			||||||
		{#if mouseOver || selected || disabled}
 | 
							{#if (mouseOver || selected || disabled) && !readonly}
 | 
				
			||||||
			<div
 | 
								<div
 | 
				
			||||||
				in:fade={{ duration: 200 }}
 | 
									in:fade={{ duration: 200 }}
 | 
				
			||||||
				class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
 | 
									class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@
 | 
				
			|||||||
	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 | 
						import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 | 
				
			||||||
	import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 | 
						import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 | 
				
			||||||
	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
						import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
				
			||||||
 | 
						import Magnify from 'svelte-material-icons/Magnify.svelte';
 | 
				
			||||||
	import StarOutline from 'svelte-material-icons/StarOutline.svelte';
 | 
						import StarOutline from 'svelte-material-icons/StarOutline.svelte';
 | 
				
			||||||
	import { AppRoute } from '../../../constants';
 | 
						import { AppRoute } from '../../../constants';
 | 
				
			||||||
	import LoadingSpinner from '../loading-spinner.svelte';
 | 
						import LoadingSpinner from '../loading-spinner.svelte';
 | 
				
			||||||
@ -62,6 +63,18 @@
 | 
				
			|||||||
			</svelte:fragment>
 | 
								</svelte:fragment>
 | 
				
			||||||
		</SideBarButton>
 | 
							</SideBarButton>
 | 
				
			||||||
	</a>
 | 
						</a>
 | 
				
			||||||
 | 
						<a
 | 
				
			||||||
 | 
							data-sveltekit-preload-data="hover"
 | 
				
			||||||
 | 
							data-sveltekit-noscroll
 | 
				
			||||||
 | 
							href={AppRoute.EXPLORE}
 | 
				
			||||||
 | 
							draggable="false"
 | 
				
			||||||
 | 
						>
 | 
				
			||||||
 | 
							<SideBarButton
 | 
				
			||||||
 | 
								title="Explore"
 | 
				
			||||||
 | 
								logo={Magnify}
 | 
				
			||||||
 | 
								isSelected={$page.route.id === '/(user)/explore'}
 | 
				
			||||||
 | 
							/>
 | 
				
			||||||
 | 
						</a>
 | 
				
			||||||
	<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
 | 
						<a data-sveltekit-preload-data="hover" href={AppRoute.SHARING} draggable="false">
 | 
				
			||||||
		<SideBarButton
 | 
							<SideBarButton
 | 
				
			||||||
			title="Sharing"
 | 
								title="Sharing"
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ export enum AppRoute {
 | 
				
			|||||||
	ALBUMS = '/albums',
 | 
						ALBUMS = '/albums',
 | 
				
			||||||
	FAVORITES = '/favorites',
 | 
						FAVORITES = '/favorites',
 | 
				
			||||||
	PHOTOS = '/photos',
 | 
						PHOTOS = '/photos',
 | 
				
			||||||
 | 
						EXPLORE = '/explore',
 | 
				
			||||||
	SHARING = '/sharing',
 | 
						SHARING = '/sharing',
 | 
				
			||||||
 | 
					 | 
				
			||||||
	AUTH_LOGIN = '/auth/login'
 | 
						AUTH_LOGIN = '/auth/login'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								web/src/routes/(user)/explore/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/routes/(user)/explore/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load = (async ({ locals, parent }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { data: items } = await locals.api.searchApi.getExploreData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return { user, items };
 | 
				
			||||||
 | 
					}) satisfies PageServerLoad;
 | 
				
			||||||
							
								
								
									
										173
									
								
								web/src/routes/(user)/explore/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								web/src/routes/(user)/explore/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
 | 
				
			||||||
 | 
						import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
 | 
				
			||||||
 | 
						import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
 | 
				
			||||||
 | 
						import { AppRoute } from '$lib/constants';
 | 
				
			||||||
 | 
						import { AssetTypeEnum, SearchExploreItem } from '@api';
 | 
				
			||||||
 | 
						import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
 | 
				
			||||||
 | 
						import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
 | 
				
			||||||
 | 
						import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
 | 
				
			||||||
 | 
						import StarOutline from 'svelte-material-icons/StarOutline.svelte';
 | 
				
			||||||
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						enum Field {
 | 
				
			||||||
 | 
							CITY = 'exifInfo.city',
 | 
				
			||||||
 | 
							TAGS = 'smartInfo.tags',
 | 
				
			||||||
 | 
							OBJECTS = 'smartInfo.objects'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const MAX_ITEMS = 12;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let things: SearchExploreItem[] = [];
 | 
				
			||||||
 | 
						let places: SearchExploreItem[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (const item of data.items) {
 | 
				
			||||||
 | 
							switch (item.fieldName) {
 | 
				
			||||||
 | 
								case Field.OBJECTS:
 | 
				
			||||||
 | 
									things = item.items;
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case Field.CITY:
 | 
				
			||||||
 | 
									places = item.items;
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						things = things.slice(0, MAX_ITEMS);
 | 
				
			||||||
 | 
						places = places.slice(0, MAX_ITEMS);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section>
 | 
				
			||||||
 | 
						<NavigationBar user={data.user} shouldShowUploadButton={false} />
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section
 | 
				
			||||||
 | 
						class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg dark:bg-immich-dark-bg"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<SideBar />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="overflow-y-auto relative immich-scrollbar">
 | 
				
			||||||
 | 
							<section
 | 
				
			||||||
 | 
								id="album-content"
 | 
				
			||||||
 | 
								class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								<!-- Main Section -->
 | 
				
			||||||
 | 
								<div class="px-4 flex justify-between place-items-center dark:text-immich-dark-fg">
 | 
				
			||||||
 | 
									<div>
 | 
				
			||||||
 | 
										<p class="font-medium">Explore</p>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div class="my-4">
 | 
				
			||||||
 | 
									<hr class="dark:border-immich-dark-gray" />
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div class="mx-4 flex flex-col">
 | 
				
			||||||
 | 
									{#if places.length > 0}
 | 
				
			||||||
 | 
										<div class="mb-6 mt-2">
 | 
				
			||||||
 | 
											<div>
 | 
				
			||||||
 | 
												<p class="mb-4 dark:text-immich-dark-fg font-medium">Places</p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
											<div class="flex flex-row flex-wrap gap-4">
 | 
				
			||||||
 | 
												{#each places as item}
 | 
				
			||||||
 | 
													<a class="relative" href="/search?{Field.CITY}={item.value}" draggable="false">
 | 
				
			||||||
 | 
														<div class="filter brightness-75 rounded-xl overflow-hidden">
 | 
				
			||||||
 | 
															<ImmichThumbnail
 | 
				
			||||||
 | 
																isRoundedCorner={true}
 | 
				
			||||||
 | 
																thumbnailSize={156}
 | 
				
			||||||
 | 
																asset={item.data}
 | 
				
			||||||
 | 
																readonly={true}
 | 
				
			||||||
 | 
															/>
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
														<span
 | 
				
			||||||
 | 
															class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
 | 
				
			||||||
 | 
														>
 | 
				
			||||||
 | 
															{item.value}
 | 
				
			||||||
 | 
														</span>
 | 
				
			||||||
 | 
													</a>
 | 
				
			||||||
 | 
												{/each}
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									{#if things.length > 0}
 | 
				
			||||||
 | 
										<div class="mb-6 mt-2">
 | 
				
			||||||
 | 
											<div>
 | 
				
			||||||
 | 
												<p class="mb-4 dark:text-immich-dark-fg font-medium">Things</p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
											<div class="flex flex-row flex-wrap gap-4">
 | 
				
			||||||
 | 
												{#each things as item}
 | 
				
			||||||
 | 
													<a class="relative" href="/search?{Field.OBJECTS}={item.value}" draggable="false">
 | 
				
			||||||
 | 
														<div class="filter brightness-75 rounded-xl overflow-hidden">
 | 
				
			||||||
 | 
															<ImmichThumbnail
 | 
				
			||||||
 | 
																isRoundedCorner={true}
 | 
				
			||||||
 | 
																thumbnailSize={156}
 | 
				
			||||||
 | 
																asset={item.data}
 | 
				
			||||||
 | 
																readonly={true}
 | 
				
			||||||
 | 
															/>
 | 
				
			||||||
 | 
														</div>
 | 
				
			||||||
 | 
														<span
 | 
				
			||||||
 | 
															class="capitalize absolute bottom-2 w-full text-center text-sm font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
 | 
				
			||||||
 | 
														>
 | 
				
			||||||
 | 
															{item.value}
 | 
				
			||||||
 | 
														</span>
 | 
				
			||||||
 | 
													</a>
 | 
				
			||||||
 | 
												{/each}
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<hr class="dark:border-immich-dark-gray mb-4" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div
 | 
				
			||||||
 | 
										class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-8"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
 | 
				
			||||||
 | 
											<p class="text-sm">YOUR ACTIVITY</p>
 | 
				
			||||||
 | 
											<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
 | 
				
			||||||
 | 
												<a
 | 
				
			||||||
 | 
													href={AppRoute.FAVORITES}
 | 
				
			||||||
 | 
													class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
 | 
				
			||||||
 | 
													draggable="false"
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
													<StarOutline size={24} />
 | 
				
			||||||
 | 
													<span>Favorites</span>
 | 
				
			||||||
 | 
												</a>
 | 
				
			||||||
 | 
												<a
 | 
				
			||||||
 | 
													href="/search?recent=true"
 | 
				
			||||||
 | 
													class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary content-center gap-2"
 | 
				
			||||||
 | 
													draggable="false"
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
													<ClockOutline size={24} />
 | 
				
			||||||
 | 
													<span>Recently added</span>
 | 
				
			||||||
 | 
												</a>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="flex flex-col gap-6 dark:text-immich-dark-fg">
 | 
				
			||||||
 | 
											<p class="text-sm">CATEGORIES</p>
 | 
				
			||||||
 | 
											<div class="flex flex-col gap-4 dark:text-immich-dark-fg/80">
 | 
				
			||||||
 | 
												<a
 | 
				
			||||||
 | 
													href="/search?type={AssetTypeEnum.Video}"
 | 
				
			||||||
 | 
													class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
													<PlayCircleOutline size={24} />
 | 
				
			||||||
 | 
													<span>Videos</span>
 | 
				
			||||||
 | 
												</a>
 | 
				
			||||||
 | 
												<div>
 | 
				
			||||||
 | 
													<a
 | 
				
			||||||
 | 
														href="/search?motion=true"
 | 
				
			||||||
 | 
														class="w-full flex text-sm font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary items-center gap-2"
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														<MotionPlayOutline size={24} />
 | 
				
			||||||
 | 
														<span>Motion photos</span>
 | 
				
			||||||
 | 
													</a>
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
@ -8,7 +8,6 @@ export const load = (async ({ locals, parent, url }) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const term = url.searchParams.get('q') || undefined;
 | 
						const term = url.searchParams.get('q') || undefined;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const { data: results } = await locals.api.searchApi.search(
 | 
						const { data: results } = await locals.api.searchApi.search(
 | 
				
			||||||
		term,
 | 
							term,
 | 
				
			||||||
		undefined,
 | 
							undefined,
 | 
				
			||||||
@ -20,6 +19,8 @@ export const load = (async ({ locals, parent, url }) => {
 | 
				
			|||||||
		undefined,
 | 
							undefined,
 | 
				
			||||||
		undefined,
 | 
							undefined,
 | 
				
			||||||
		undefined,
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
		{ params: url.searchParams }
 | 
							{ params: url.searchParams }
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
	return { user, term, results };
 | 
						return { user, term, results };
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,34 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { page } from '$app/stores';
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						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 NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
 | 
					 | 
				
			||||||
	import type { PageData } from './$types';
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
 | 
						import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 | 
				
			||||||
 | 
						import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte';
 | 
				
			||||||
 | 
						import { afterNavigate, goto } from '$app/navigation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let data: PageData;
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
						const term = $page.url.searchParams.get('q') || data.term || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const term = $page.url.searchParams.get('q') || '';
 | 
						let goBackRoute = '/explore';
 | 
				
			||||||
 | 
						afterNavigate((r) => {
 | 
				
			||||||
 | 
							if (r.from) {
 | 
				
			||||||
 | 
								goBackRoute = r.from.url.href;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section>
 | 
					<section>
 | 
				
			||||||
	<NavigationBar {term} user={data.user} shouldShowUploadButton={false} />
 | 
						<ControlAppBar on:close-button-click={() => goto(goBackRoute)} backIcon={ArrowLeft}>
 | 
				
			||||||
 | 
							<svelte:fragment slot="leading">
 | 
				
			||||||
 | 
								<p class="text-xl capitalize">
 | 
				
			||||||
 | 
									Search
 | 
				
			||||||
 | 
									{#if term}
 | 
				
			||||||
 | 
										- {term}
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
								</p>
 | 
				
			||||||
 | 
							</svelte:fragment>
 | 
				
			||||||
 | 
						</ControlAppBar>
 | 
				
			||||||
</section>
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section class="relative pt-[72px] h-screen bg-immich-bg  dark:bg-immich-dark-bg">
 | 
					<section class="relative pt-[72px] h-screen bg-immich-bg  dark:bg-immich-dark-bg">
 | 
				
			||||||
@ -19,8 +37,16 @@
 | 
				
			|||||||
			id="search-content"
 | 
								id="search-content"
 | 
				
			||||||
			class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
 | 
								class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
 | 
				
			||||||
		>
 | 
							>
 | 
				
			||||||
			{#if data.results?.assets?.items}
 | 
								{#if data.results?.assets?.items.length != 0}
 | 
				
			||||||
				<GalleryViewer assets={data.results.assets.items} />
 | 
									<GalleryViewer assets={data.results.assets.items} />
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									<div class="w-full text-center dark:text-white ">
 | 
				
			||||||
 | 
										<div class="mt-60 flex flex-col place-content-center place-items-center">
 | 
				
			||||||
 | 
											<ImageOffOutline size="56" />
 | 
				
			||||||
 | 
											<p class="font-medium text-3xl mt-5">No results</p>
 | 
				
			||||||
 | 
											<p class="text-base font-normal">Try a synonym or more general keyword</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
		</section>
 | 
							</section>
 | 
				
			||||||
	</section>
 | 
						</section>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user