mirror of
https://github.com/immich-app/immich.git
synced 2025-12-14 22:19:18 +03:00
feat: location favorites
This commit is contained in:
@@ -1112,6 +1112,8 @@
|
||||
"failed_to_load_assets": "Failed to load assets",
|
||||
"failed_to_load_folder": "Failed to load folder",
|
||||
"favorite": "Favorite",
|
||||
"favorite_locations": "Favorite Locations",
|
||||
"favorite_locations_not_found": "No favorite locations saved",
|
||||
"favorite_action_prompt": "{count} added to Favorites",
|
||||
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
|
||||
"favorites": "Favorites",
|
||||
|
||||
7
mobile/openapi/README.md
generated
7
mobile/openapi/README.md
generated
@@ -163,8 +163,12 @@ Class | Method | HTTP request | Description
|
||||
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
|
||||
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
|
||||
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
||||
*MapApi* | [**createFavoriteLocation**](doc//MapApi.md#createfavoritelocation) | **POST** /map/favorite-locations | Create favorite location
|
||||
*MapApi* | [**deleteFavoriteLocation**](doc//MapApi.md#deletefavoritelocation) | **DELETE** /map/favorite-locations/{id} | Delete favorite location
|
||||
*MapApi* | [**getFavoriteLocations**](doc//MapApi.md#getfavoritelocations) | **GET** /map/favorite-locations | Get favorite locations
|
||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
||||
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | Reverse geocode coordinates
|
||||
*MapApi* | [**updateFavoriteLocation**](doc//MapApi.md#updatefavoritelocation) | **PUT** /map/favorite-locations/{id} | Update favorite location
|
||||
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | Add assets to a memory
|
||||
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories | Create a memory
|
||||
*MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} | Delete a memory
|
||||
@@ -384,6 +388,7 @@ Class | Method | HTTP request | Description
|
||||
- [Colorspace](doc//Colorspace.md)
|
||||
- [ContributorCountResponseDto](doc//ContributorCountResponseDto.md)
|
||||
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
||||
- [CreateFavoriteLocationDto](doc//CreateFavoriteLocationDto.md)
|
||||
- [CreateLibraryDto](doc//CreateLibraryDto.md)
|
||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
|
||||
@@ -399,6 +404,7 @@ Class | Method | HTTP request | Description
|
||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||
- [FaceDto](doc//FaceDto.md)
|
||||
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
||||
- [FavoriteLocationResponseDto](doc//FavoriteLocationResponseDto.md)
|
||||
- [FoldersResponse](doc//FoldersResponse.md)
|
||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||
- [ImageFormat](doc//ImageFormat.md)
|
||||
@@ -613,6 +619,7 @@ Class | Method | HTTP request | Description
|
||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
- [UpdateFavoriteLocationDto](doc//UpdateFavoriteLocationDto.md)
|
||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
|
||||
|
||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@@ -136,6 +136,7 @@ part 'model/check_existing_assets_response_dto.dart';
|
||||
part 'model/colorspace.dart';
|
||||
part 'model/contributor_count_response_dto.dart';
|
||||
part 'model/create_album_dto.dart';
|
||||
part 'model/create_favorite_location_dto.dart';
|
||||
part 'model/create_library_dto.dart';
|
||||
part 'model/create_profile_image_response_dto.dart';
|
||||
part 'model/database_backup_config.dart';
|
||||
@@ -151,6 +152,7 @@ part 'model/email_notifications_update.dart';
|
||||
part 'model/exif_response_dto.dart';
|
||||
part 'model/face_dto.dart';
|
||||
part 'model/facial_recognition_config.dart';
|
||||
part 'model/favorite_location_response_dto.dart';
|
||||
part 'model/folders_response.dart';
|
||||
part 'model/folders_update.dart';
|
||||
part 'model/image_format.dart';
|
||||
@@ -365,6 +367,7 @@ part 'model/trash_response_dto.dart';
|
||||
part 'model/update_album_dto.dart';
|
||||
part 'model/update_album_user_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
part 'model/update_favorite_location_dto.dart';
|
||||
part 'model/update_library_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_admin_create_dto.dart';
|
||||
|
||||
217
mobile/openapi/lib/api/map_api.dart
generated
217
mobile/openapi/lib/api/map_api.dart
generated
@@ -16,6 +16,162 @@ class MapApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Create favorite location
|
||||
///
|
||||
/// Create a new favorite location for the user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [CreateFavoriteLocationDto] createFavoriteLocationDto (required):
|
||||
Future<Response> createFavoriteLocationWithHttpInfo(CreateFavoriteLocationDto createFavoriteLocationDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/favorite-locations';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = createFavoriteLocationDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create favorite location
|
||||
///
|
||||
/// Create a new favorite location for the user.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [CreateFavoriteLocationDto] createFavoriteLocationDto (required):
|
||||
Future<FavoriteLocationResponseDto?> createFavoriteLocation(CreateFavoriteLocationDto createFavoriteLocationDto,) async {
|
||||
final response = await createFavoriteLocationWithHttpInfo(createFavoriteLocationDto,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FavoriteLocationResponseDto',) as FavoriteLocationResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Delete favorite location
|
||||
///
|
||||
/// Delete a favorite location by its ID.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteFavoriteLocationWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/favorite-locations/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete favorite location
|
||||
///
|
||||
/// Delete a favorite location by its ID.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deleteFavoriteLocation(String id,) async {
|
||||
final response = await deleteFavoriteLocationWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get favorite locations
|
||||
///
|
||||
/// Retrieve a list of user's favorite locations.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getFavoriteLocationsWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/favorite-locations';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get favorite locations
|
||||
///
|
||||
/// Retrieve a list of user's favorite locations.
|
||||
Future<List<FavoriteLocationResponseDto>?> getFavoriteLocations() async {
|
||||
final response = await getFavoriteLocationsWithHttpInfo();
|
||||
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<FavoriteLocationResponseDto>') as List)
|
||||
.cast<FavoriteLocationResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieve map markers
|
||||
///
|
||||
/// Retrieve a list of latitude and longitude coordinates for every asset with location data.
|
||||
@@ -179,4 +335,65 @@ class MapApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Update favorite location
|
||||
///
|
||||
/// Update an existing favorite location.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [UpdateFavoriteLocationDto] updateFavoriteLocationDto (required):
|
||||
Future<Response> updateFavoriteLocationWithHttpInfo(String id, UpdateFavoriteLocationDto updateFavoriteLocationDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/map/favorite-locations/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = updateFavoriteLocationDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Update favorite location
|
||||
///
|
||||
/// Update an existing favorite location.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [UpdateFavoriteLocationDto] updateFavoriteLocationDto (required):
|
||||
Future<FavoriteLocationResponseDto?> updateFavoriteLocation(String id, UpdateFavoriteLocationDto updateFavoriteLocationDto,) async {
|
||||
final response = await updateFavoriteLocationWithHttpInfo(id, updateFavoriteLocationDto,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'FavoriteLocationResponseDto',) as FavoriteLocationResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@@ -320,6 +320,8 @@ class ApiClient {
|
||||
return ContributorCountResponseDto.fromJson(value);
|
||||
case 'CreateAlbumDto':
|
||||
return CreateAlbumDto.fromJson(value);
|
||||
case 'CreateFavoriteLocationDto':
|
||||
return CreateFavoriteLocationDto.fromJson(value);
|
||||
case 'CreateLibraryDto':
|
||||
return CreateLibraryDto.fromJson(value);
|
||||
case 'CreateProfileImageResponseDto':
|
||||
@@ -350,6 +352,8 @@ class ApiClient {
|
||||
return FaceDto.fromJson(value);
|
||||
case 'FacialRecognitionConfig':
|
||||
return FacialRecognitionConfig.fromJson(value);
|
||||
case 'FavoriteLocationResponseDto':
|
||||
return FavoriteLocationResponseDto.fromJson(value);
|
||||
case 'FoldersResponse':
|
||||
return FoldersResponse.fromJson(value);
|
||||
case 'FoldersUpdate':
|
||||
@@ -778,6 +782,8 @@ class ApiClient {
|
||||
return UpdateAlbumUserDto.fromJson(value);
|
||||
case 'UpdateAssetDto':
|
||||
return UpdateAssetDto.fromJson(value);
|
||||
case 'UpdateFavoriteLocationDto':
|
||||
return UpdateFavoriteLocationDto.fromJson(value);
|
||||
case 'UpdateLibraryDto':
|
||||
return UpdateLibraryDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
|
||||
115
mobile/openapi/lib/model/create_favorite_location_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/create_favorite_location_dto.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// 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 CreateFavoriteLocationDto {
|
||||
/// Returns a new [CreateFavoriteLocationDto] instance.
|
||||
CreateFavoriteLocationDto({
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
num latitude;
|
||||
|
||||
num longitude;
|
||||
|
||||
String name;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CreateFavoriteLocationDto &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.name == name;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(latitude.hashCode) +
|
||||
(longitude.hashCode) +
|
||||
(name.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateFavoriteLocationDto[latitude=$latitude, longitude=$longitude, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'latitude'] = this.latitude;
|
||||
json[r'longitude'] = this.longitude;
|
||||
json[r'name'] = this.name;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [CreateFavoriteLocationDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateFavoriteLocationDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "CreateFavoriteLocationDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return CreateFavoriteLocationDto(
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CreateFavoriteLocationDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CreateFavoriteLocationDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CreateFavoriteLocationDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CreateFavoriteLocationDto> mapFromJson(dynamic json) {
|
||||
final map = <String, CreateFavoriteLocationDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CreateFavoriteLocationDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CreateFavoriteLocationDto-objects as value to a dart map
|
||||
static Map<String, List<CreateFavoriteLocationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CreateFavoriteLocationDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = CreateFavoriteLocationDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'latitude',
|
||||
'longitude',
|
||||
'name',
|
||||
};
|
||||
}
|
||||
|
||||
135
mobile/openapi/lib/model/favorite_location_response_dto.dart
generated
Normal file
135
mobile/openapi/lib/model/favorite_location_response_dto.dart
generated
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// 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 FavoriteLocationResponseDto {
|
||||
/// Returns a new [FavoriteLocationResponseDto] instance.
|
||||
FavoriteLocationResponseDto({
|
||||
required this.id,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
num? latitude;
|
||||
|
||||
num? longitude;
|
||||
|
||||
String name;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is FavoriteLocationResponseDto &&
|
||||
other.id == id &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.name == name;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode) +
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(name.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'FavoriteLocationResponseDto[id=$id, latitude=$latitude, longitude=$longitude, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
if (this.latitude != null) {
|
||||
json[r'latitude'] = this.latitude;
|
||||
} else {
|
||||
// json[r'latitude'] = null;
|
||||
}
|
||||
if (this.longitude != null) {
|
||||
json[r'longitude'] = this.longitude;
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
json[r'name'] = this.name;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [FavoriteLocationResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static FavoriteLocationResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "FavoriteLocationResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return FavoriteLocationResponseDto(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
latitude: json[r'latitude'] == null
|
||||
? null
|
||||
: num.parse('${json[r'latitude']}'),
|
||||
longitude: json[r'longitude'] == null
|
||||
? null
|
||||
: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<FavoriteLocationResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <FavoriteLocationResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = FavoriteLocationResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, FavoriteLocationResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, FavoriteLocationResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = FavoriteLocationResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of FavoriteLocationResponseDto-objects as value to a dart map
|
||||
static Map<String, List<FavoriteLocationResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<FavoriteLocationResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = FavoriteLocationResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'name',
|
||||
};
|
||||
}
|
||||
|
||||
142
mobile/openapi/lib/model/update_favorite_location_dto.dart
generated
Normal file
142
mobile/openapi/lib/model/update_favorite_location_dto.dart
generated
Normal file
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// 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 UpdateFavoriteLocationDto {
|
||||
/// Returns a new [UpdateFavoriteLocationDto] instance.
|
||||
UpdateFavoriteLocationDto({
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.name,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? latitude;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? name;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateFavoriteLocationDto &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.name == name;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateFavoriteLocationDto[latitude=$latitude, longitude=$longitude, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.latitude != null) {
|
||||
json[r'latitude'] = this.latitude;
|
||||
} else {
|
||||
// json[r'latitude'] = null;
|
||||
}
|
||||
if (this.longitude != null) {
|
||||
json[r'longitude'] = this.longitude;
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UpdateFavoriteLocationDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UpdateFavoriteLocationDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "UpdateFavoriteLocationDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UpdateFavoriteLocationDto(
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UpdateFavoriteLocationDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateFavoriteLocationDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateFavoriteLocationDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UpdateFavoriteLocationDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UpdateFavoriteLocationDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateFavoriteLocationDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UpdateFavoriteLocationDto-objects as value to a dart map
|
||||
static Map<String, List<UpdateFavoriteLocationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UpdateFavoriteLocationDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UpdateFavoriteLocationDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5592,6 +5592,216 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/map/favorite-locations": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of user's favorite locations.",
|
||||
"operationId": "getFavoriteLocations",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/FavoriteLocationResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get favorite locations",
|
||||
"tags": [
|
||||
"Map"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
},
|
||||
"post": {
|
||||
"description": "Create a new favorite location for the user.",
|
||||
"operationId": "createFavoriteLocation",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateFavoriteLocationDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FavoriteLocationResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Create favorite location",
|
||||
"tags": [
|
||||
"Map"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/map/favorite-locations/{id}": {
|
||||
"delete": {
|
||||
"description": "Delete a favorite location by its ID.",
|
||||
"operationId": "deleteFavoriteLocation",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete favorite location",
|
||||
"tags": [
|
||||
"Map"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
},
|
||||
"put": {
|
||||
"description": "Update an existing favorite location.",
|
||||
"operationId": "updateFavoriteLocation",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateFavoriteLocationDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FavoriteLocationResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Update favorite location",
|
||||
"tags": [
|
||||
"Map"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/map/markers": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of latitude and longitude coordinates for every asset with location data.",
|
||||
@@ -16150,6 +16360,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateFavoriteLocationDto": {
|
||||
"properties": {
|
||||
"latitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"latitude",
|
||||
"longitude",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CreateLibraryDto": {
|
||||
"properties": {
|
||||
"exclusionPatterns": {
|
||||
@@ -16554,6 +16783,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FavoriteLocationResponseDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FoldersResponse": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -22593,6 +22847,20 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateFavoriteLocationDto": {
|
||||
"properties": {
|
||||
"latitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateLibraryDto": {
|
||||
"properties": {
|
||||
"exclusionPatterns": {
|
||||
|
||||
@@ -790,6 +790,22 @@ export type ValidateLibraryImportPathResponseDto = {
|
||||
export type ValidateLibraryResponseDto = {
|
||||
importPaths?: ValidateLibraryImportPathResponseDto[];
|
||||
};
|
||||
export type FavoriteLocationResponseDto = {
|
||||
id: string;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
name: string;
|
||||
};
|
||||
export type CreateFavoriteLocationDto = {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
};
|
||||
export type UpdateFavoriteLocationDto = {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
name?: string;
|
||||
};
|
||||
export type MapMarkerResponseDto = {
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
@@ -3082,6 +3098,59 @@ export function validate({ id, validateLibraryDto }: {
|
||||
body: validateLibraryDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Get favorite locations
|
||||
*/
|
||||
export function getFavoriteLocations(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: FavoriteLocationResponseDto[];
|
||||
}>("/map/favorite-locations", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Create favorite location
|
||||
*/
|
||||
export function createFavoriteLocation({ createFavoriteLocationDto }: {
|
||||
createFavoriteLocationDto: CreateFavoriteLocationDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: FavoriteLocationResponseDto;
|
||||
}>("/map/favorite-locations", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: createFavoriteLocationDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete favorite location
|
||||
*/
|
||||
export function deleteFavoriteLocation({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/map/favorite-locations/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Update favorite location
|
||||
*/
|
||||
export function updateFavoriteLocation({ id, updateFavoriteLocationDto }: {
|
||||
id: string;
|
||||
updateFavoriteLocationDto: UpdateFavoriteLocationDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: FavoriteLocationResponseDto;
|
||||
}>(`/map/favorite-locations/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: updateFavoriteLocationDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Retrieve map markers
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
CreateFavoriteLocationDto,
|
||||
FavoriteLocationResponseDto,
|
||||
UpdateFavoriteLocationDto,
|
||||
} from 'src/dtos/favorite-location.dto';
|
||||
import {
|
||||
MapMarkerDto,
|
||||
MapMarkerResponseDto,
|
||||
@@ -39,4 +44,59 @@ export class MapController {
|
||||
reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> {
|
||||
return this.service.reverseGeocode(dto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('favorite-locations')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Endpoint({
|
||||
summary: 'Get favorite locations',
|
||||
description: "Retrieve a list of user's favorite locations.",
|
||||
history: new HistoryBuilder().added('v2').stable('v2'),
|
||||
})
|
||||
getFavoriteLocations(@Auth() auth: AuthDto): Promise<FavoriteLocationResponseDto[]> {
|
||||
return this.service.getFavoriteLocations(auth);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('favorite-locations')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Endpoint({
|
||||
summary: 'Create favorite location',
|
||||
description: 'Create a new favorite location for the user.',
|
||||
history: new HistoryBuilder().added('v2').stable('v2'),
|
||||
})
|
||||
createFavoriteLocation(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CreateFavoriteLocationDto,
|
||||
): Promise<FavoriteLocationResponseDto> {
|
||||
return this.service.createFavoriteLocation(auth, dto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Put('favorite-locations/:id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Endpoint({
|
||||
summary: 'Update favorite location',
|
||||
description: 'Update an existing favorite location.',
|
||||
history: new HistoryBuilder().added('v2').stable('v2'),
|
||||
})
|
||||
updateFavoriteLocation(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateFavoriteLocationDto,
|
||||
): Promise<FavoriteLocationResponseDto> {
|
||||
return this.service.updateFavoriteLocation(auth, id, dto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete('favorite-locations/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Delete favorite location',
|
||||
description: 'Delete a favorite location by its ID.',
|
||||
history: new HistoryBuilder().added('v2').stable('v2'),
|
||||
})
|
||||
deleteFavoriteLocation(@Param('id') id: string) {
|
||||
return this.service.deleteFavoriteLocation(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +274,16 @@ export type AssetFace = {
|
||||
updateId: string;
|
||||
};
|
||||
|
||||
export type FavoriteLocation = {
|
||||
id: string;
|
||||
name: string;
|
||||
userId: string;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type Plugin = Selectable<PluginTable>;
|
||||
|
||||
export type PluginFilter = Selectable<PluginFilterTable> & {
|
||||
|
||||
34
server/src/dtos/favorite-location.dto.ts
Normal file
34
server/src/dtos/favorite-location.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IsLatitude, IsLongitude, IsString } from 'class-validator';
|
||||
import { Optional } from 'src/validation';
|
||||
|
||||
export class CreateFavoriteLocationDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsLatitude()
|
||||
latitude!: number;
|
||||
|
||||
@IsLongitude()
|
||||
longitude!: number;
|
||||
}
|
||||
|
||||
export class UpdateFavoriteLocationDto {
|
||||
@Optional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@Optional()
|
||||
@IsLatitude()
|
||||
latitude?: number;
|
||||
|
||||
@Optional()
|
||||
@IsLongitude()
|
||||
longitude?: number;
|
||||
}
|
||||
|
||||
export class FavoriteLocationResponseDto {
|
||||
id!: string;
|
||||
name!: string;
|
||||
latitude!: number | null;
|
||||
longitude!: number | null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getName } from 'i18n-iso-countries';
|
||||
import { Expression, Insertable, Kysely, NotNull, sql, SqlBool } from 'kysely';
|
||||
import { Expression, Insertable, Kysely, NotNull, sql, SqlBool, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
@@ -12,6 +12,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { FavoriteLocationTable } from 'src/schema/tables/favorite-location.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||
|
||||
@@ -138,6 +139,42 @@ export class MapRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFavoriteLocations(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('favorite_location')
|
||||
.selectAll()
|
||||
.where('userId', '=', userId)
|
||||
.orderBy('name', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async createFavoriteLocation(entity: Insertable<FavoriteLocationTable>) {
|
||||
const inserted = await this.db
|
||||
.insertInto('favorite_location')
|
||||
.values(entity)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return inserted;
|
||||
}
|
||||
|
||||
async updateFavoriteLocation(id: string, userId: string, updates: Updateable<FavoriteLocationTable>) {
|
||||
const updated = await this.db
|
||||
.updateTable('favorite_location')
|
||||
.set(updates)
|
||||
.where('id', '=', id)
|
||||
.where('userId', '=', userId)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteFavoriteLocation(id: string) {
|
||||
await this.db.deleteFrom('favorite_location').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { FavoriteLocationTable } from 'src/schema/tables/favorite-location.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
|
||||
@@ -97,6 +98,7 @@ export class ImmichDatabase {
|
||||
AuditTable,
|
||||
AssetExifTable,
|
||||
FaceSearchTable,
|
||||
FavoriteLocationTable,
|
||||
GeodataPlacesTable,
|
||||
LibraryTable,
|
||||
MemoryTable,
|
||||
@@ -192,6 +194,7 @@ export interface DB {
|
||||
audit: AuditTable;
|
||||
|
||||
face_search: FaceSearchTable;
|
||||
favorite_location: FavoriteLocationTable;
|
||||
|
||||
geodata_places: GeodataPlacesTable;
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TABLE "favorite_location" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision,
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "favorite_location_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "favorite_location_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "favorite_location_userId_idx" ON "favorite_location" ("userId");`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE "favorite_location";`.execute(db);
|
||||
}
|
||||
35
server/src/schema/tables/favorite-location.table.ts
Normal file
35
server/src/schema/tables/favorite-location.table.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('favorite_location')
|
||||
export class FavoriteLocationTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'character varying', nullable: false })
|
||||
name!: string;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', index: true })
|
||||
userId!: string;
|
||||
|
||||
@Column({ type: 'double precision', nullable: true })
|
||||
latitude!: number | null;
|
||||
|
||||
@Column({ type: 'double precision', nullable: true })
|
||||
longitude!: number | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CreateFavoriteLocationDto, UpdateFavoriteLocationDto } from 'src/dtos/favorite-location.dto';
|
||||
import { MapService } from 'src/services/map.service';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@@ -93,4 +94,71 @@ describe(MapService.name, () => {
|
||||
expect(mocks.map.reverseGeocode).toHaveBeenCalledWith({ latitude: 42, longitude: 69 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFavoriteLocations', () => {
|
||||
it('should return favorite locations for the user', async () => {
|
||||
const favoriteLocation = {
|
||||
id: 'loc1',
|
||||
userId: authStub.user1.user.id,
|
||||
name: 'Home',
|
||||
latitude: 12.34,
|
||||
longitude: 56.78,
|
||||
};
|
||||
|
||||
mocks.map.getFavoriteLocations.mockResolvedValue([favoriteLocation]);
|
||||
|
||||
const result = await sut.getFavoriteLocations(authStub.user1);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual(favoriteLocation);
|
||||
expect(mocks.map.getFavoriteLocations).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFavoriteLocation', () => {
|
||||
it('should create a new favorite location', async () => {
|
||||
const dto: CreateFavoriteLocationDto = { name: 'Work', latitude: 1, longitude: 2 };
|
||||
const created = { id: 'loc2', userId: authStub.user1.user.id, ...dto };
|
||||
|
||||
mocks.map.createFavoriteLocation.mockResolvedValue(created);
|
||||
|
||||
const result = await sut.createFavoriteLocation(authStub.user1, dto);
|
||||
|
||||
expect(result).toEqual(created);
|
||||
expect(mocks.map.createFavoriteLocation).toHaveBeenCalledWith({
|
||||
userId: authStub.user1.user.id,
|
||||
name: dto.name,
|
||||
latitude: dto.latitude,
|
||||
longitude: dto.longitude,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFavoriteLocation', () => {
|
||||
it('should update an existing favorite location', async () => {
|
||||
const dto: UpdateFavoriteLocationDto = { name: 'Gym' };
|
||||
const updated = { id: 'loc3', userId: authStub.user1.user.id, name: 'Gym', latitude: null, longitude: null };
|
||||
|
||||
mocks.map.updateFavoriteLocation.mockResolvedValue(updated);
|
||||
|
||||
const result = await sut.updateFavoriteLocation(authStub.user1, 'loc3', dto);
|
||||
|
||||
expect(result).toEqual(updated);
|
||||
expect(mocks.map.updateFavoriteLocation).toHaveBeenCalledWith('loc3', authStub.user1.user.id, {
|
||||
id: 'loc3',
|
||||
userId: authStub.user1.user.id,
|
||||
name: 'Gym',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFavoriteLocation', () => {
|
||||
it('should call repository to delete a location by id', async () => {
|
||||
mocks.map.deleteFavoriteLocation.mockResolvedValue(undefined);
|
||||
|
||||
await sut.deleteFavoriteLocation('loc4');
|
||||
|
||||
expect(mocks.map.deleteFavoriteLocation).toHaveBeenCalledWith('loc4');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
CreateFavoriteLocationDto,
|
||||
FavoriteLocationResponseDto,
|
||||
UpdateFavoriteLocationDto,
|
||||
} from 'src/dtos/favorite-location.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
@@ -32,4 +37,38 @@ export class MapService extends BaseService {
|
||||
const result = await this.mapRepository.reverseGeocode({ latitude, longitude });
|
||||
return result ? [result] : [];
|
||||
}
|
||||
|
||||
async getFavoriteLocations(auth: AuthDto): Promise<FavoriteLocationResponseDto[]> {
|
||||
return this.mapRepository.getFavoriteLocations(auth.user.id);
|
||||
}
|
||||
|
||||
async createFavoriteLocation(auth: AuthDto, dto: CreateFavoriteLocationDto): Promise<FavoriteLocationResponseDto> {
|
||||
const entity = {
|
||||
userId: auth.user.id,
|
||||
name: dto.name,
|
||||
latitude: dto.latitude,
|
||||
longitude: dto.longitude,
|
||||
};
|
||||
|
||||
return this.mapRepository.createFavoriteLocation(entity);
|
||||
}
|
||||
|
||||
async updateFavoriteLocation(
|
||||
auth: AuthDto,
|
||||
id: string,
|
||||
dto: UpdateFavoriteLocationDto,
|
||||
): Promise<FavoriteLocationResponseDto> {
|
||||
const entity = {
|
||||
userId: auth.user.id,
|
||||
id,
|
||||
...(dto.name !== undefined && { name: dto.name }),
|
||||
...(dto.latitude !== undefined && { latitude: dto.latitude }),
|
||||
...(dto.longitude !== undefined && { longitude: dto.longitude }),
|
||||
};
|
||||
return this.mapRepository.updateFavoriteLocation(id, auth.user.id, entity);
|
||||
}
|
||||
|
||||
async deleteFavoriteLocation(id: string) {
|
||||
await this.mapRepository.deleteFavoriteLocation(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,18 @@
|
||||
import { lastChosenLocation } from '$lib/stores/asset-editor.store';
|
||||
import { delay } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
|
||||
import { ConfirmModal, LoadingSpinner } from '@immich/ui';
|
||||
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
|
||||
import {
|
||||
createFavoriteLocation,
|
||||
deleteFavoriteLocation,
|
||||
getFavoriteLocations,
|
||||
searchPlaces,
|
||||
type AssetResponseDto,
|
||||
type FavoriteLocationResponseDto,
|
||||
type PlacesResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Button, ConfirmModal, IconButton, Input, LoadingSpinner } from '@immich/ui';
|
||||
import { mdiDelete, mdiMapMarkerMultipleOutline } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
interface Point {
|
||||
@@ -45,6 +54,22 @@
|
||||
|
||||
let zoom = $derived(mapLat && mapLng ? 12.5 : 1);
|
||||
|
||||
let favoriteLocations: FavoriteLocationResponseDto[] = $state([]);
|
||||
let newFavoriteName = $state('');
|
||||
let savingFavorite = $state(false);
|
||||
|
||||
const loadFavoriteLocations = async () => {
|
||||
try {
|
||||
favoriteLocations = await getFavoriteLocations();
|
||||
} catch (err) {
|
||||
handleError(err, 'Failed to load favorite locations');
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
loadFavoriteLocations();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (mapElement && initialPoint) {
|
||||
mapElement.addClipMapMarker(initialPoint.lng, initialPoint.lat);
|
||||
@@ -68,6 +93,39 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFavorite = async () => {
|
||||
if (newFavoriteName.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
savingFavorite = true;
|
||||
try {
|
||||
const newLocation: FavoriteLocationResponseDto = await createFavoriteLocation({
|
||||
createFavoriteLocationDto: {
|
||||
name: newFavoriteName,
|
||||
latitude: point!.lat,
|
||||
longitude: point!.lng,
|
||||
},
|
||||
});
|
||||
favoriteLocations = [...favoriteLocations, newLocation];
|
||||
favoriteLocations = favoriteLocations.sort((a, b) => a.name.localeCompare(b.name));
|
||||
newFavoriteName = '';
|
||||
} catch (err) {
|
||||
handleError(err, 'Failed to save favorite location');
|
||||
} finally {
|
||||
savingFavorite = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFavorite = async (locationId: string) => {
|
||||
try {
|
||||
await deleteFavoriteLocation({ id: locationId });
|
||||
favoriteLocations = favoriteLocations.filter((loc) => loc.id !== locationId);
|
||||
} catch (err) {
|
||||
handleError(err, 'Failed to delete favorite location');
|
||||
}
|
||||
};
|
||||
|
||||
const getLocation = (name: string, admin1Name?: string, admin2Name?: string): string => {
|
||||
return `${name}${admin1Name ? ', ' + admin1Name : ''}${admin2Name ? ', ' + admin2Name : ''}`;
|
||||
};
|
||||
@@ -106,10 +164,13 @@
|
||||
latestSearchTimeout = searchTimeout;
|
||||
};
|
||||
|
||||
const handleUseSuggested = (latitude: number, longitude: number) => {
|
||||
const handleUseSuggested = (latitude: number, longitude: number, setZoom?: number) => {
|
||||
hideSuggestion = true;
|
||||
point = { lng: longitude, lat: latitude };
|
||||
mapElement?.addClipMapMarker(longitude, latitude);
|
||||
if (setZoom) {
|
||||
zoom = setZoom;
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = (lat: number, lng: number) => {
|
||||
@@ -206,6 +267,57 @@
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm text-start mt-4">
|
||||
<CoordinatesInput lat={point ? point.lat : assetLat} lng={point ? point.lng : assetLng} {onUpdate} />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between items-center gap-2 mb-2">
|
||||
<p>{$t('favorite_locations')}</p>
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Input placeholder={$t('name')} size="tiny" bind:value={newFavoriteName} />
|
||||
<Button
|
||||
onclick={handleSaveFavorite}
|
||||
disabled={newFavoriteName.trim() === '' || savingFavorite || point === null}
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
class="shrink-0">{$t('save')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-40 overflow-y-auto border border-gray-300 dark:border-immich-dark-gray rounded-md p-2">
|
||||
{#if favoriteLocations.length === 0}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{$t('favorite_locations_not_found')}</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each favoriteLocations as location (location.id)}
|
||||
<li>
|
||||
<button
|
||||
class="w-full"
|
||||
onclick={() => handleUseSuggested(location.latitude!, location.longitude!, 14)}
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center p-2 bg-gray-100 dark:bg-gray-800 rounded hover:bg-gray-200 hover:dark:bg-gray-700"
|
||||
>
|
||||
{location.name}
|
||||
<IconButton
|
||||
icon={mdiDelete}
|
||||
shape="round"
|
||||
variant="outline"
|
||||
size="medium"
|
||||
color="danger"
|
||||
aria-label={$t('delete')}
|
||||
onclick={(e: Event) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteFavorite(location.id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ConfirmModal>
|
||||
|
||||
Reference in New Issue
Block a user