Compare commits

..

3 Commits

Author SHA1 Message Date
idubnori
4568e55f3f feat: replace heart icons to thumbs-up across activity 2025-12-16 00:06:57 +09:00
Yaros
35eda735c8 fix(web): recent search doesn't use search type (#24578)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-12-15 12:44:00 +01:00
Diogo Correia
8f7a71d1cf fix(web): download panel being hidden by admin sidebar (#24583) 2025-12-15 12:29:18 +01:00
31 changed files with 49 additions and 1464 deletions

View File

@@ -1113,8 +1113,6 @@
"failed_to_load_folder": "Failed to load folder",
"favorite": "Favorite",
"favorite_action_prompt": "{count} added to Favorites",
"favorite_locations": "Favorite Locations",
"favorite_locations_not_found": "No favorite locations saved",
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites",
"favorites_page_no_favorites": "No favorite assets found",

View File

@@ -47,7 +47,7 @@ class LikeActivityActionButton extends ConsumerWidget {
return BaseActionButton(
maxWidth: 60,
iconData: liked != null ? Icons.favorite : Icons.favorite_border,
iconData: liked != null ? Icons.thumb_up : Icons.thumb_up_off_alt,
label: "like".t(context: context),
onPressed: () => onTap(liked),
iconOnly: iconOnly,
@@ -57,7 +57,7 @@ class LikeActivityActionButton extends ConsumerWidget {
// default to empty heart during loading
loading: () => BaseActionButton(
iconData: Icons.favorite_border,
iconData: Icons.thumb_up_off_alt,
label: "like".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem,

View File

@@ -68,11 +68,11 @@ class ActivityTextField extends HookConsumerWidget {
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
icon: Icon(liked ? Icons.favorite_rounded : Icons.favorite_border_rounded),
icon: Icon(liked ? Icons.thumb_up : Icons.thumb_up_off_alt),
onPressed: liked ? removeLike : addLike,
),
),
suffixIconColor: liked ? Colors.red[700] : null,
suffixIconColor: liked ? Colors.blue[700] : null,
hintText: !isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(),
hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]),
),

View File

@@ -37,7 +37,7 @@ class ActivityTile extends HookConsumerWidget {
? Container(
width: isBottomSheet ? 30 : 44,
alignment: Alignment.center,
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
child: Icon(Icons.thumb_up, color: Colors.blue[700]),
)
: isBottomSheet
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)

View File

@@ -68,7 +68,7 @@ class CommentBubble extends ConsumerWidget {
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
child: Icon(Icons.thumb_up, color: Colors.blue[600], size: 18),
),
),
],
@@ -82,7 +82,7 @@ class CommentBubble extends ConsumerWidget {
likes = Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.7), shape: BoxShape.circle),
child: Icon(Icons.favorite, color: Colors.red[600], size: 18),
child: Icon(Icons.thumb_up, color: Colors.blue[600], size: 18),
);
}

View File

@@ -163,12 +163,8 @@ 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
@@ -388,7 +384,6 @@ 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)
@@ -404,7 +399,6 @@ 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)
@@ -619,7 +613,6 @@ 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)

View File

@@ -136,7 +136,6 @@ 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';
@@ -152,7 +151,6 @@ 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';
@@ -367,7 +365,6 @@ 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';

View File

@@ -16,162 +16,6 @@ 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.
@@ -335,65 +179,4 @@ 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;
}
}

View File

@@ -320,8 +320,6 @@ 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':
@@ -352,8 +350,6 @@ 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':
@@ -782,8 +778,6 @@ 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':

View File

@@ -1,115 +0,0 @@
//
// 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',
};
}

View File

@@ -1,135 +0,0 @@
//
// 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',
};
}

View File

@@ -1,142 +0,0 @@
//
// 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>{
};
}

View File

@@ -77,15 +77,15 @@ void main() {
overrides: overrides,
);
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsOneWidget);
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsNothing);
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget);
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing);
});
testWidgets('Bordered icon if likedId == null', (tester) async {
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
expect(find.widgetWithIcon(IconButton, Icons.favorite_border_rounded), findsOneWidget);
expect(find.widgetWithIcon(IconButton, Icons.favorite_rounded), findsNothing);
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget);
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing);
});
testWidgets('Adds new like', (tester) async {

View File

@@ -91,17 +91,17 @@ void main() {
group('Like Activity', () {
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
testWidgets('Like contains filled heart as leading', (tester) async {
testWidgets('Like contains filled thumbs-up as leading', (tester) async {
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
// Leading widget should not be null
final listTile = tester.widget<ListTile>(find.byType(ListTile));
expect(listTile.leading, isNotNull);
// And should have a favorite icon
final favoIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.favorite_rounded);
// And should have a thumb_up icon
final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up);
expect(favoIconFinder, findsOneWidget);
expect(thumbUpIconFinder, findsOneWidget);
});
testWidgets('Like title is center aligned', (tester) async {

View File

@@ -5592,216 +5592,6 @@
"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.",
@@ -16360,25 +16150,6 @@
],
"type": "object"
},
"CreateFavoriteLocationDto": {
"properties": {
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
},
"name": {
"type": "string"
}
},
"required": [
"latitude",
"longitude",
"name"
],
"type": "object"
},
"CreateLibraryDto": {
"properties": {
"exclusionPatterns": {
@@ -16783,31 +16554,6 @@
],
"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": {
@@ -22847,20 +22593,6 @@
},
"type": "object"
},
"UpdateFavoriteLocationDto": {
"properties": {
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
},
"name": {
"type": "string"
}
},
"type": "object"
},
"UpdateLibraryDto": {
"properties": {
"exclusionPatterns": {

View File

@@ -790,22 +790,6 @@ 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;
@@ -3098,59 +3082,6 @@ 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
*/

View File

@@ -1,12 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { Controller, Get, HttpCode, HttpStatus, 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,
@@ -44,59 +39,4 @@ 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);
}
}

View File

@@ -274,16 +274,6 @@ 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> & {

View File

@@ -1,34 +0,0 @@
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;
}

View File

@@ -29,13 +29,3 @@ where
)
order by
"fileCreatedAt" desc
-- MapRepository.getFavoriteLocations
select
*
from
"favorite_location"
where
"userId" = $1
order by
"name" asc

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { getName } from 'i18n-iso-countries';
import { Expression, Insertable, Kysely, NotNull, sql, SqlBool, Updateable } from 'kysely';
import { Expression, Insertable, Kysely, NotNull, sql, SqlBool } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { createReadStream, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
@@ -12,7 +12,6 @@ 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';
@@ -139,42 +138,6 @@ 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}`);

View File

@@ -39,7 +39,6 @@ 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';
@@ -98,7 +97,6 @@ export class ImmichDatabase {
AuditTable,
AssetExifTable,
FaceSearchTable,
FavoriteLocationTable,
GeodataPlacesTable,
LibraryTable,
MemoryTable,
@@ -194,7 +192,6 @@ export interface DB {
audit: AuditTable;
face_search: FaceSearchTable;
favorite_location: FavoriteLocationTable;
geodata_places: GeodataPlacesTable;

View File

@@ -1,20 +0,0 @@
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);
}

View File

@@ -1,35 +0,0 @@
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>;
}

View File

@@ -1,4 +1,3 @@
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';
@@ -94,91 +93,4 @@ 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,
createdAt: new Date(),
updatedAt: new Date(),
};
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,
name: 'Work',
latitude: 1,
longitude: 2,
createdAt: new Date(),
updatedAt: new Date(),
};
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,
createdAt: new Date(),
updatedAt: new Date(),
};
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();
await sut.deleteFavoriteLocation('loc4');
expect(mocks.map.deleteFavoriteLocation).toHaveBeenCalledWith('loc4');
});
});
});

View File

@@ -1,10 +1,5 @@
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';
@@ -37,38 +32,4 @@ 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);
}
}

View File

@@ -2,7 +2,7 @@
import { locale } from '$lib/stores/preferences.store';
import type { ActivityResponseDto } from '@immich/sdk';
import { Icon } from '@immich/ui';
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
import { mdiCommentOutline, mdiThumbUp, mdiThumbUpOutline } from '@mdi/js';
interface Props {
isLiked: ActivityResponseDto | null;
@@ -19,7 +19,7 @@
<div class="w-full flex p-4 items-center justify-center rounded-full gap-5 bg-subtle border bg-opacity-60">
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} onclick={onFavorite} {disabled}>
<div class="flex gap-2 items-center justify-center">
<Icon icon={isLiked ? mdiHeart : mdiHeartOutline} size="24" class={isLiked ? 'text-red-400' : 'text-fg'} />
<Icon icon={isLiked ? mdiThumbUp : mdiThumbUpOutline} size="24" class={isLiked ? 'text-blue-400' : 'text-fg'} />
{#if numberOfLikes}
<div class="text-l">{numberOfLikes.toLocaleString($locale)}</div>
{/if}

View File

@@ -13,7 +13,7 @@
import { isTenMinutesApart } from '$lib/utils/timesince';
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum, type UserResponseDto } from '@immich/sdk';
import { Icon, IconButton, LoadingSpinner, toastManager } from '@immich/ui';
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js';
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiSend, mdiThumbUp } from '@mdi/js';
import * as luxon from 'luxon';
import { t } from 'svelte-i18n';
import UserAvatar from '../shared-components/user-avatar.svelte';
@@ -181,7 +181,7 @@
{:else if reaction.type === ReactionType.Like}
<div class="relative">
<div class="flex py-3 ps-3 mt-3 gap-4 items-center text-sm">
<div class="text-red-600"><Icon icon={mdiHeart} size="20" /></div>
<div class="text-blue-600"><Icon icon={mdiThumbUp} size="20" /></div>
<div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}>
{$t('user_liked', {

View File

@@ -16,7 +16,7 @@
{#if downloadManager.isDownloading}
<div
transition:fly={{ x: -100, duration: 350 }}
class="fixed bottom-10 start-2 max-h-67.5 w-79 rounded-2xl border dark:border-white/10 p-4 shadow-lg bg-subtle"
class="fixed bottom-10 start-2 max-h-67.5 w-79 z-60 rounded-2xl border dark:border-white/10 p-4 shadow-lg bg-subtle"
>
<Heading size="tiny">{$t('downloading')}</Heading>
<div class="my-2 mb-2 flex max-h-50 flex-col overflow-y-auto text-sm">

View File

@@ -8,18 +8,9 @@
import { lastChosenLocation } from '$lib/stores/asset-editor.store';
import { delay } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
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 { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
import { ConfirmModal, LoadingSpinner } from '@immich/ui';
import { mdiMapMarkerMultipleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
interface Point {
@@ -54,22 +45,6 @@
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 (error) {
handleError(error, 'Failed to load favorite locations');
}
};
onMount(async () => {
await loadFavoriteLocations();
});
$effect(() => {
if (mapElement && initialPoint) {
mapElement.addClipMapMarker(initialPoint.lng, initialPoint.lat);
@@ -93,39 +68,6 @@
}
};
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 (error) {
handleError(error, '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 (error) {
handleError(error, 'Failed to delete favorite location');
}
};
const getLocation = (name: string, admin1Name?: string, admin2Name?: string): string => {
return `${name}${admin1Name ? ', ' + admin1Name : ''}${admin2Name ? ', ' + admin2Name : ''}`;
};
@@ -164,13 +106,10 @@
latestSearchTimeout = searchTimeout;
};
const handleUseSuggested = (latitude: number, longitude: number, setZoom?: number) => {
const handleUseSuggested = (latitude: number, longitude: number) => {
hideSuggestion = true;
point = { lng: longitude, lat: latitude };
mapElement?.addClipMapMarker(longitude, latitude);
if (setZoom) {
zoom = setZoom;
}
};
const onUpdate = (lat: number, lng: number) => {
@@ -267,58 +206,6 @@
<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
type="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={async (e: Event) => {
e.stopPropagation();
await handleDeleteFavorite(location.id);
}}
/>
</div>
</button>
</li>
{/each}
</ul>
{/if}
</div>
</div>
</div>
{/snippet}
</ConfirmModal>

View File

@@ -79,10 +79,30 @@
searchStore.isSearchEnabled = false;
};
const buildSearchPayload = (term: string): SmartSearchDto | MetadataSearchDto => {
const searchType = getSearchType();
switch (searchType) {
case 'smart': {
return { query: term };
}
case 'metadata': {
return { originalFileName: term };
}
case 'description': {
return { description: term };
}
case 'ocr': {
return { ocr: term };
}
default: {
return { query: term };
}
}
};
const onHistoryTermClick = async (searchTerm: string) => {
value = searchTerm;
const searchPayload = { query: searchTerm };
await handleSearch(searchPayload);
await handleSearch(buildSearchPayload(searchTerm));
};
const onFilterClick = async () => {
@@ -112,29 +132,7 @@
};
const onSubmit = () => {
const searchType = getSearchType();
let payload = {} as SmartSearchDto | MetadataSearchDto;
switch (searchType) {
case 'smart': {
payload = { query: value } as SmartSearchDto;
break;
}
case 'metadata': {
payload = { originalFileName: value } as MetadataSearchDto;
break;
}
case 'description': {
payload = { description: value } as MetadataSearchDto;
break;
}
case 'ocr': {
payload = { ocr: value } as MetadataSearchDto;
break;
}
}
handlePromiseError(handleSearch(payload));
handlePromiseError(handleSearch(buildSearchPayload(value)));
saveSearchTerm(value);
};